From a5ec81b51f4579414351d9426c772a8f9b1268b3 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:18:49 -0600 Subject: [PATCH] Created basic MultiSelect Custom Menu --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 136 +++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 MiraAPI/Hud/CustomMultiSelectMenu.cs diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs new file mode 100644 index 0000000..323bc08 --- /dev/null +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -0,0 +1,136 @@ +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Patches.Stubs; +using MiraAPI.Utilities.Assets; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using UnityEngine; +using UnityEngine.Events; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. +#pragma warning disable S2365 // Properties should not make collection or array copies + +namespace MiraAPI.Hud; + +/// +/// Multi-select using the as a base. +/// +/// Used by Il2Cpp. Do not use constructor, this is a . +/// The type of object each entry represents. +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")] +public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) + : CustomPhoneMenu.MenuEntry>(il2CppPtr) where TEntry : class +{ + private int totalSelections; + private bool shouldConfirm; + private readonly List selectedEntries = []; + + private LoadableAsset? hoverSelectSprite; + private LoadableAsset? hoverDeselectSprite; + private Color? activeColor; + private Color? hoverSelectColor; + private Color? hoverDeselectColor; + + public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry; + + protected static TMenu Create( + Color? activeColor = null, + LoadableAsset? hoverSelectSprite = null, + Color? hoverSelectColor = null, + LoadableAsset? hoverDeselectSprite = null, + Color? hoverDeselectColor = null, + PanelButtonOnMouse? onMouseOut = null, + PanelButtonOnMouse? onMouseOver = null + ) where TMenu : CustomMultiSelectMenu + { + TMenu customMenu = Create(onMouseOut, onMouseOver); + + customMenu.activeColor = activeColor; + + customMenu.hoverSelectSprite = hoverSelectSprite; + customMenu.hoverSelectColor = hoverSelectColor; + + customMenu.hoverDeselectSprite = hoverDeselectSprite ?? hoverSelectSprite; + customMenu.hoverDeselectColor = hoverDeselectColor ?? hoverSelectColor; + + return customMenu; + } + + protected abstract void SetupPanelEntry(ShapeshifterPanel panel, int i, TEntry entry, Action onClick); + + /// + /// Begins/opens the custom multi-select menu. + /// + /// All entries to give the custom menu. + /// Function called when all selections are made/confirmed. + /// The number of selections required. + /// Wheter the entire selection should be confirmed manually. + [HideFromIl2Cpp] + protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm) + { + MinigameStubs.Begin(this, null); + + this.totalSelections = totalSelections; + this.shouldConfirm = shouldConfirm; + var back = backButton.GetComponent(); + back.OnClick.AddListener((UnityAction)(() => + { + onClick(null); + })); + + DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType); + var list2 = new Il2CppSystem.Collections.Generic.List(); + RegisterPanels( + entries, + (shapeshifterPanel, i, entry) => + { + var num = i % 3; + var num2 = i / 3; + shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); + SetupPanelEntry(shapeshifterPanel, i, entry, () => OnEntryClick(entry, onClick)); + list2.Add(shapeshifterPanel.Button); + }, + (p, e) => new MenuEntry(p, e)); + ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); + } + + private void OnEntryClick(TEntry entry, Action> onClick) + { + MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry); + + if (selectedEntries.Remove(menuEntry)) // Unselect previous choice + { + SetNameplateAppearance(menuEntry, false); + return; + } + + selectedEntries.Add(menuEntry); + if (selectedEntries.Count < totalSelections) // Add choice to list of selections + { + SetNameplateAppearance(menuEntry, true); + return; + } + + // Total selections reached + onClick(selectedEntries.Select(se => se.Entry).ToList()); + selectedEntries.Clear(); + } + + protected override bool IsEntrySelected(IMenuEntry entry) + { + return selectedEntries.Any(e => e.Panel == entry.Panel); + } + + [HideFromIl2Cpp] + protected void SetNameplateAppearance(IMenuEntry menuEntry, bool isSelected) + { + SetNameplateAppearance( + menuEntry, + isSelected ? hoverDeselectSprite : hoverSelectSprite, + isSelected ? hoverDeselectColor : hoverSelectColor, + isSelected ? activeColor : Color.clear); + } +}