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);
+ }
+}