From 5d34f78fe0d7b31deb0448f7eeb477239a792ec0 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:04:11 -0600 Subject: [PATCH 01/38] Created parent class for phone menus --- MiraAPI/Hud/CustomPhoneMenu.cs | 76 +++++++++++++++++++++++++++++++++ MiraAPI/Hud/CustomPlayerMenu.cs | 49 +-------------------- 2 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 MiraAPI/Hud/CustomPhoneMenu.cs diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs new file mode 100644 index 0000000..f05b4a3 --- /dev/null +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -0,0 +1,76 @@ +using AmongUs.GameOptions; +using Reactor.Utilities.Extensions; +using System; +using System.Diagnostics.CodeAnalysis; +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. + +namespace MiraAPI.Hud; + +/// +/// Custom Phone Menu using the as a base. +/// +/// Used by Il2Cpp. Do not use constructor, this is a . +[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 CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) +{ + public float xStart = -0.8f; + public float yStart = 2.15f; + public float xOffset = 1.95f; + public float yOffset = -0.65f; + + public ShapeshifterPanel panelPrefab; + public UiElement backButton; + public UiElement defaultButtonSelected; + + /// + /// Creates a . + /// + /// The type of . + /// New object. + protected static TMenu Create() where TMenu : CustomPhoneMenu + { + var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter); + + var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu; + var newMenu = Instantiate(ogMenu); + var customMenu = newMenu.gameObject.AddComponent(); + + customMenu.panelPrefab = newMenu.PanelPrefab; + customMenu.xStart = newMenu.XStart; + customMenu.yStart = newMenu.YStart; + customMenu.xOffset = newMenu.XOffset; + customMenu.yOffset = newMenu.YOffset; + customMenu.defaultButtonSelected = newMenu.DefaultButtonSelected; + customMenu.backButton = newMenu.BackButton; + + var back = customMenu.backButton.GetComponent(); + back.OnClick.RemoveAllListeners(); + back.OnClick.AddListener((UnityAction)customMenu.Close); + + customMenu.CloseSound = newMenu.CloseSound; + customMenu.logger = newMenu.logger; + customMenu.OpenSound = newMenu.OpenSound; + + newMenu.DestroyImmediate(); + + customMenu.transform.SetParent(Camera.main!.transform, false); + customMenu.transform.localPosition = new Vector3(0f, 0f, -50f); + return customMenu; + } + + private void OnDisable() + { + ControllerManager.Instance.CloseOverlayMenu(name); + } + + /// + public override sealed void Begin(PlayerTask task) + { + throw new NotImplementedException("Use the other Begin method."); + } +} diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index fb27f14..7c13ba8 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -22,15 +22,8 @@ namespace MiraAPI.Hud; [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 class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) +public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) { - public ShapeshifterPanel panelPrefab; - public float xStart = -0.8f; - public float yStart = 2.15f; - public float xOffset = 1.95f; - public float yOffset = -0.65f; - public UiElement backButton; - public UiElement defaultButtonSelected; public List potentialVictims; /// @@ -39,45 +32,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) /// New object. public static CustomPlayerMenu Create() { - var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter); - - var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu; - var newMenu = Instantiate(ogMenu); - var customMenu = newMenu.gameObject.AddComponent(); - - customMenu.panelPrefab = newMenu.PanelPrefab; - customMenu.xStart = newMenu.XStart; - customMenu.yStart = newMenu.YStart; - customMenu.xOffset = newMenu.XOffset; - customMenu.yOffset = newMenu.YOffset; - customMenu.backButton = newMenu.BackButton; - var back = customMenu.backButton.GetComponent(); - back.OnClick.RemoveAllListeners(); - back.OnClick.AddListener((UnityAction)(() => - { - Instance.Close(); - })); - - customMenu.CloseSound = newMenu.CloseSound; - customMenu.logger = newMenu.logger; - customMenu.OpenSound = newMenu.OpenSound; - - newMenu.DestroyImmediate(); - - customMenu.transform.SetParent(Camera.main!.transform, false); - customMenu.transform.localPosition = new Vector3(0f, 0f, -50f); - return customMenu; - } - - private void OnDisable() - { - ControllerManager.Instance.CloseOverlayMenu(name); - } - - /// - public override void Begin(PlayerTask task) - { - throw new NotImplementedException("Use the other Begin method."); + return Create(); } /// From 71234128d021e35dc50864e0c8e180d72aa984d3 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:22:50 -0600 Subject: [PATCH 02/38] Added protected methods to add panels to custom Phone Menu --- MiraAPI/Hud/CustomPhoneMenu.cs | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index f05b4a3..dcdcf27 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -1,7 +1,9 @@ using AmongUs.GameOptions; using Reactor.Utilities.Extensions; 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 @@ -18,6 +20,18 @@ namespace MiraAPI.Hud; [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")] public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) { + public interface IMenuEntry + { + ShapeshifterPanel Panel { get; } + } + + protected record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry + { + public static implicit operator ShapeshifterPanel(BasicEntry entry) => entry.Panel; + } + + public List menuEntries; + public float xStart = -0.8f; public float yStart = 2.15f; public float xOffset = 1.95f; @@ -73,4 +87,38 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) { throw new NotImplementedException("Use the other Begin method."); } + + protected void RegisterPanels( + IEnumerable entries, + Action entrySetter, + Func? menuConfig = null) + { + menuEntries ??= []; + + int currentEntries = menuEntries.Count; + + var list = entries.ToList(); + + for (var i = 0; i < list.Count; i++) + { + var index = currentEntries + i; + var entry = list[i]; + + var shapeshifterPanel = Instantiate(panelPrefab, transform); + shapeshifterPanel!.transform.localPosition = new Vector3(0f, 0f, -1f); + entrySetter(shapeshifterPanel, index, entry); + var entryMenuConfig = menuConfig ?? ((s, _) => new BasicEntry(s)); + var menuEntry = entryMenuConfig(shapeshifterPanel, entry); + menuEntries.Add(menuEntry); + } + } + + protected void RegisterPanels( + IEnumerable entries, + Action onEntryClick, + Action entrySetter, + Func? menuConfig = null) + { + RegisterPanels(entries, (p, i, e) => entrySetter(p, i, e, () => onEntryClick(e)), menuConfig); + } } From 98a0d3e2a6339c16519b158c806a8423961df70d Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:21:47 -0600 Subject: [PATCH 03/38] Changed to use Register Panels --- MiraAPI/Hud/CustomPlayerMenu.cs | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 7c13ba8..22c8d74 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -2,11 +2,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; using MiraAPI.Patches.Stubs; using Reactor.Utilities.Attributes; -using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -52,23 +50,20 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) })); DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType); - var list = PlayerControl.AllPlayerControls.ToArray().Where(playerMatch).ToList(); - potentialVictims = []; var list2 = new Il2CppSystem.Collections.Generic.List(); - - for (var i = 0; i < list.Count; i++) - { - var player = list[i]; - var num = i % 3; - var num2 = i / 3; - var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; - var shapeshifterPanel = Instantiate(panelPrefab, transform); - shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); - shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); })); - shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white; - potentialVictims.Add(shapeshifterPanel); - list2.Add(shapeshifterPanel.Button); - } + RegisterPanels( + PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), + (shapeshifterPanel, i, player) => + { + var num = i % 3; + var num2 = i / 3; + var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; + shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); + shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); })); + shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white; + list2.Add(shapeshifterPanel.Button); + }); + potentialVictims = menuEntries.Cast().ToList(); ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); } } From 3f952da5b24b5aa05a37accbb62b9c2552e2082d Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 12:42:42 -0600 Subject: [PATCH 04/38] Added documentation and reverse implicit operator to BasicEntry --- MiraAPI/Hud/CustomPhoneMenu.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index dcdcf27..6393e88 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -25,9 +25,15 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) ShapeshifterPanel Panel { get; } } + /// + /// Menu Entry used when specifically only the Panel itself is required. + /// + /// The instance. protected record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry { public static implicit operator ShapeshifterPanel(BasicEntry entry) => entry.Panel; + + public static implicit operator BasicEntry(ShapeshifterPanel panel) => new(panel); } public List menuEntries; From 3f2a3286ed3bb7c1efdc4da53c1071d951531a9f Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:38:23 -0600 Subject: [PATCH 05/38] Added MenuDepth property --- MiraAPI/Hud/CustomPhoneMenu.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 6393e88..37b0f67 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -47,6 +47,8 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) public UiElement backButton; public UiElement defaultButtonSelected; + protected virtual float MenuDepth => -50f; + /// /// Creates a . /// @@ -79,7 +81,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) newMenu.DestroyImmediate(); customMenu.transform.SetParent(Camera.main!.transform, false); - customMenu.transform.localPosition = new Vector3(0f, 0f, -50f); + customMenu.transform.localPosition = new Vector3(0f, 0f, customMenu.MenuDepth); return customMenu; } From 1660dd72dcea1147f7af0fac7f009cd55668013c Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 14:36:49 -0600 Subject: [PATCH 06/38] Renamed variables and removed variable --- MiraAPI/Hud/CustomPhoneMenu.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 37b0f67..c2407d4 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -98,8 +98,8 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) protected void RegisterPanels( IEnumerable entries, - Action entrySetter, - Func? menuConfig = null) + Action entryPanelConfig, + Func? menuEntryMaker = null) { menuEntries ??= []; @@ -114,9 +114,10 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) var shapeshifterPanel = Instantiate(panelPrefab, transform); shapeshifterPanel!.transform.localPosition = new Vector3(0f, 0f, -1f); - entrySetter(shapeshifterPanel, index, entry); - var entryMenuConfig = menuConfig ?? ((s, _) => new BasicEntry(s)); - var menuEntry = entryMenuConfig(shapeshifterPanel, entry); + entryPanelConfig(shapeshifterPanel, index, entry); + + menuEntryMaker ??= (s, _) => new BasicEntry(s); + var menuEntry = menuEntryMaker(shapeshifterPanel, entry); menuEntries.Add(menuEntry); } } @@ -124,9 +125,9 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) protected void RegisterPanels( IEnumerable entries, Action onEntryClick, - Action entrySetter, - Func? menuConfig = null) + Action entryPanelConfig, + Func? menuEntryConfig = null) { - RegisterPanels(entries, (p, i, e) => entrySetter(p, i, e, () => onEntryClick(e)), menuConfig); + RegisterPanels(entries, (p, i, e) => entryPanelConfig(p, i, e, () => onEntryClick(e)), menuEntryConfig); } } From 48112c6a42d543d35143e7b247942686b93b7473 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:06:44 -0600 Subject: [PATCH 07/38] Added on mouse actions --- MiraAPI/Hud/CustomPhoneMenu.cs | 38 ++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index c2407d4..f8a351f 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -47,14 +47,23 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) public UiElement backButton; public UiElement defaultButtonSelected; + // These are the Highlight, Icon, and IsSelected variable respectively. + protected Action? onMouseOverAction; + protected Action? onMouseOutAction; + protected virtual float MenuDepth => -50f; /// /// Creates a . /// /// The type of . + /// Function that can optionally be run when the mouse is moved outside a menu panel. + /// Function that can optionally be run when the mouse is moved over a menu panel. /// New object. - protected static TMenu Create() where TMenu : CustomPhoneMenu + protected static TMenu Create( + Action? onMouseOut = null, + Action? onMouseOver = null + ) where TMenu : CustomPhoneMenu { var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter); @@ -82,6 +91,10 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) customMenu.transform.SetParent(Camera.main!.transform, false); customMenu.transform.localPosition = new Vector3(0f, 0f, customMenu.MenuDepth); + + customMenu.onMouseOverAction = onMouseOver; + customMenu.onMouseOutAction = onMouseOut; + return customMenu; } @@ -113,12 +126,31 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) var entry = list[i]; var shapeshifterPanel = Instantiate(panelPrefab, transform); - shapeshifterPanel!.transform.localPosition = new Vector3(0f, 0f, -1f); + shapeshifterPanel.transform.localPosition = new Vector3(0f, 0f, -1f); entryPanelConfig(shapeshifterPanel, index, entry); menuEntryMaker ??= (s, _) => new BasicEntry(s); var menuEntry = menuEntryMaker(shapeshifterPanel, entry); menuEntries.Add(menuEntry); + + var nameplate = shapeshifterPanel.gameObject.transform.FindChild("Nameplate"); + var highlight = nameplate.FindChild("Highlight").GetComponent(); + var icon = highlight.transform.GetChild(0).GetComponent(); + + if (onMouseOverAction != null) + { + shapeshifterPanel.Button.OnMouseOver.RemoveAllListeners(); + shapeshifterPanel.Button.OnMouseOver = new UnityEvent(); + shapeshifterPanel.Button.OnMouseOver.AddListener((UnityAction) + (() => onMouseOverAction(highlight, icon, IsEntrySelected(menuEntry)))); + } + if (onMouseOutAction != null) + { + shapeshifterPanel.Button.OnMouseOut.RemoveAllListeners(); + shapeshifterPanel.Button.OnMouseOut = new UnityEvent(); + shapeshifterPanel.Button.OnMouseOut.AddListener((UnityAction) + (() => onMouseOutAction(highlight, icon, IsEntrySelected(menuEntry)))); + } } } @@ -130,4 +162,6 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) { RegisterPanels(entries, (p, i, e) => entryPanelConfig(p, i, e, () => onEntryClick(e)), menuEntryConfig); } + + protected abstract bool IsEntrySelected(IMenuEntry entry); } From 17db947003e90e00aa5c57c8eb6bd0a8b5225aea Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:07:19 -0600 Subject: [PATCH 08/38] Added base nameplate appearance method --- MiraAPI/Hud/CustomPhoneMenu.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index f8a351f..3a9915c 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -1,4 +1,6 @@ using AmongUs.GameOptions; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; using System; using System.Collections.Generic; @@ -164,4 +166,28 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) } protected abstract bool IsEntrySelected(IMenuEntry entry); + + [HideFromIl2Cpp] + protected static void SetNameplateAppearance( + IMenuEntry menuEntry, LoadableAsset? sprite, Color? overColor, Color? unselectedColor) + { + ShapeshifterPanel panel = menuEntry.Panel; + + var nameplate = panel.gameObject.transform.FindChild("Nameplate"); + var highlight = nameplate.FindChild("Highlight").GetComponent(); + var icon = highlight.transform.GetChild(0).GetComponent(); + if (sprite != null) + { + icon.sprite = sprite.LoadAsset(); + } + var button = nameplate.GetComponent(); + if (overColor is { } oColor) + { + button.OverColor = oColor; + } + if (unselectedColor is { } uColor) + { + button.UnselectedColor = uColor; + } + } } From 59db03f39dd407ca06aec06d335c8e3e01325f38 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:10:13 -0600 Subject: [PATCH 09/38] Made Entry Selection check default to false --- MiraAPI/Hud/CustomPhoneMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 3a9915c..b82a703 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -165,7 +165,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) RegisterPanels(entries, (p, i, e) => entryPanelConfig(p, i, e, () => onEntryClick(e)), menuEntryConfig); } - protected abstract bool IsEntrySelected(IMenuEntry entry); + protected virtual bool IsEntrySelected(IMenuEntry entry) => false; [HideFromIl2Cpp] protected static void SetNameplateAppearance( From 1719bbf1b7fceb939e060ed0dbf3b17df963748e Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:25:10 -0600 Subject: [PATCH 10/38] Added local variable, properties, and delegate type --- MiraAPI/Hud/CustomPhoneMenu.cs | 29 ++++++++++++++++++----------- MiraAPI/Hud/CustomPlayerMenu.cs | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index b82a703..d070c24 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -10,6 +10,7 @@ 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; @@ -40,6 +41,8 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) public List menuEntries; + public List EntryPanels => menuEntries.Select(e => e.Panel).ToList(); + public float xStart = -0.8f; public float yStart = 2.15f; public float xOffset = 1.95f; @@ -49,9 +52,12 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) public UiElement backButton; public UiElement defaultButtonSelected; - // These are the Highlight, Icon, and IsSelected variable respectively. - protected Action? onMouseOverAction; - protected Action? onMouseOutAction; + public Transform PhoneUI => transform.FindChild("PhoneUI"); + + public delegate void PanelButtonOnMouse(SpriteRenderer highlight, SpriteRenderer icon, bool isSelected); + + protected PanelButtonOnMouse? onMouseOverAction; + protected PanelButtonOnMouse? onMouseOutAction; protected virtual float MenuDepth => -50f; @@ -63,8 +69,8 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) /// Function that can optionally be run when the mouse is moved over a menu panel. /// New object. protected static TMenu Create( - Action? onMouseOut = null, - Action? onMouseOver = null + PanelButtonOnMouse? onMouseOut = null, + PanelButtonOnMouse? onMouseOver = null ) where TMenu : CustomPhoneMenu { var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter); @@ -135,22 +141,23 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) var menuEntry = menuEntryMaker(shapeshifterPanel, entry); menuEntries.Add(menuEntry); + var button = shapeshifterPanel.Button; var nameplate = shapeshifterPanel.gameObject.transform.FindChild("Nameplate"); var highlight = nameplate.FindChild("Highlight").GetComponent(); var icon = highlight.transform.GetChild(0).GetComponent(); if (onMouseOverAction != null) { - shapeshifterPanel.Button.OnMouseOver.RemoveAllListeners(); - shapeshifterPanel.Button.OnMouseOver = new UnityEvent(); - shapeshifterPanel.Button.OnMouseOver.AddListener((UnityAction) + button.OnMouseOver.RemoveAllListeners(); + button.OnMouseOver = new UnityEvent(); + button.OnMouseOver.AddListener((UnityAction) (() => onMouseOverAction(highlight, icon, IsEntrySelected(menuEntry)))); } if (onMouseOutAction != null) { - shapeshifterPanel.Button.OnMouseOut.RemoveAllListeners(); - shapeshifterPanel.Button.OnMouseOut = new UnityEvent(); - shapeshifterPanel.Button.OnMouseOut.AddListener((UnityAction) + button.OnMouseOut.RemoveAllListeners(); + button.OnMouseOut = new UnityEvent(); + button.OnMouseOut.AddListener((UnityAction) (() => onMouseOutAction(highlight, icon, IsEntrySelected(menuEntry)))); } } diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 22c8d74..0317dfc 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -63,7 +63,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white; list2.Add(shapeshifterPanel.Button); }); - potentialVictims = menuEntries.Cast().ToList(); + potentialVictims = EntryPanels; ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); } } From 86912d75e45873a6e7470a4926d060801a50479f Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:49:00 -0600 Subject: [PATCH 11/38] Made menuEntries protected with initial value and BasicEntry sealed --- MiraAPI/Hud/CustomPhoneMenu.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index d070c24..1f07601 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -32,14 +32,14 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) /// Menu Entry used when specifically only the Panel itself is required. /// /// The instance. - protected record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry + protected sealed record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry { public static implicit operator ShapeshifterPanel(BasicEntry entry) => entry.Panel; public static implicit operator BasicEntry(ShapeshifterPanel panel) => new(panel); } - public List menuEntries; + protected List menuEntries = []; public List EntryPanels => menuEntries.Select(e => e.Panel).ToList(); @@ -122,8 +122,6 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) Action entryPanelConfig, Func? menuEntryMaker = null) { - menuEntries ??= []; - int currentEntries = menuEntries.Count; var list = entries.ToList(); From 04329e1c87348538fb7372c88d98737f5ac6462a Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:22:55 -0600 Subject: [PATCH 12/38] Added generic to only have certain types of entries --- MiraAPI/Hud/CustomPhoneMenu.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 1f07601..ade867e 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -39,9 +39,9 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) public static implicit operator BasicEntry(ShapeshifterPanel panel) => new(panel); } - protected List menuEntries = []; + protected List MenuEntries { get; set; } = []; - public List EntryPanels => menuEntries.Select(e => e.Panel).ToList(); + public List EntryPanels => MenuEntries.Select(e => e.Panel).ToList(); public float xStart = -0.8f; public float yStart = 2.15f; @@ -122,7 +122,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) Action entryPanelConfig, Func? menuEntryMaker = null) { - int currentEntries = menuEntries.Count; + int currentEntries = MenuEntries.Count; var list = entries.ToList(); @@ -137,7 +137,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) menuEntryMaker ??= (s, _) => new BasicEntry(s); var menuEntry = menuEntryMaker(shapeshifterPanel, entry); - menuEntries.Add(menuEntry); + MenuEntries.Add(menuEntry); var button = shapeshifterPanel.Button; var nameplate = shapeshifterPanel.gameObject.transform.FindChild("Nameplate"); @@ -196,3 +196,14 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) } } } + +/// +/// The type of menu entries. +public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) where TMenu : CustomPhoneMenu.IMenuEntry +{ + protected new List MenuEntries + { + get => base.MenuEntries.Cast().ToList(); + set => base.MenuEntries = value.Cast().ToList(); + } +} From d65203692be0fcd51d1b4452294a4bfe83502c9a Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:31:53 -0600 Subject: [PATCH 13/38] Created Paginable Menu subclass based on TOU GuesserMenu --- MiraAPI/Hud/CustomPaginableMenu.cs | 367 +++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 MiraAPI/Hud/CustomPaginableMenu.cs diff --git a/MiraAPI/Hud/CustomPaginableMenu.cs b/MiraAPI/Hud/CustomPaginableMenu.cs new file mode 100644 index 0000000..4ef1df6 --- /dev/null +++ b/MiraAPI/Hud/CustomPaginableMenu.cs @@ -0,0 +1,367 @@ +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; +#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; + +/// +/// Paginable using the as a base. +/// +/// Used by Il2Cpp. Do not use constructor, this is a . +[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 CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) +{ + protected abstract string Name { get; } + + protected int currentPage; + + protected abstract TextBoxTMP? PrefabTextbox { get; } + + private TextBoxTMP? searchTextbox; + private string searchText = string.Empty; + private TextMeshPro? noResultsText; + + private const int ItemsPerPage = 15; + + protected override float MenuDepth => -60f; + + public record MenuEntry(ShapeshifterPanel Panel, string SortKey) : IMenuEntry; + + /// + /// Creates a . + /// + /// The type of . + /// Function that can optionally be run when the mouse is moved outside a menu panel. + /// Function that can optionally be run when the mouse is moved over a menu panel. + /// New object. + protected static new TMenu Create( + PanelButtonOnMouse? onMouseOut = null, + PanelButtonOnMouse? onMouseOver = null + ) where TMenu : CustomPaginableMenu + { + TMenu customMenu = CustomPhoneMenu.Create(onMouseOut, onMouseOver); + + var nextButton = Instantiate(customMenu.backButton, customMenu.transform).gameObject; + nextButton.transform.localPosition = new Vector3(1.85f, -2.185f, customMenu.MenuDepth); + nextButton.transform.localScale = new Vector3(0.65f, 0.65f, 1); + nextButton.name = "RightArrowButton"; + nextButton.GetComponent().sprite = MiraAssets.NextButton.LoadAsset(); + nextButton.gameObject.GetComponent().DestroyImmediate(); + + var passiveButton = nextButton.gameObject.GetComponent(); + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.OnClick.AddListener((UnityAction)customMenu.NextPage); + + var backButton = Instantiate(nextButton, customMenu.transform).gameObject; + backButton.transform.localPosition = new Vector3(-1.85f, -2.185f, customMenu.MenuDepth); + backButton.name = "LeftArrowButton"; + backButton.gameObject.GetComponent().Destroy(); + backButton.GetComponent().flipX = true; + var prevPassive = backButton.gameObject.GetComponent(); + prevPassive.OnClick.AddListener((UnityAction)customMenu.PreviousPage); + + customMenu.PhoneUI.GetChild(0).GetComponent().material = + PlayerControl.LocalPlayer.cosmetics.currentBodySprite.BodySprite.material; + customMenu.PhoneUI.GetChild(1).GetComponent().material = + PlayerControl.LocalPlayer.cosmetics.currentBodySprite.BodySprite.material; + + return customMenu; + } + + private static string NormalizeForSearch(string? text) + { + return string.IsNullOrWhiteSpace(text) + ? string.Empty + : text.Trim().ToLowerInvariant(); + } + + [HideFromIl2Cpp] + private List GetFilteredEntries() + { + var query = NormalizeForSearch(searchText); + if (string.IsNullOrEmpty(query)) + { + return MenuEntries; + } + + return MenuEntries + .Where(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(e => e.SortKey.Equals(query, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(e => e.SortKey.StartsWith(query, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase)) + .ThenBy(e => e.SortKey, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static int GetTotalPages(int itemCount) + { + return Mathf.Max(1, Mathf.CeilToInt(itemCount / (float)ItemsPerPage)); + } + + private void RefreshControllerOverlay(Il2CppSystem.Collections.Generic.List list) + { + if (ControllerManager.Instance && backButton != null) + { + ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list); + } + } + + [HideFromIl2Cpp] + private IEnumerator CoRestoreFocus() + { + yield return null; + searchTextbox?.GiveFocus(); + } + + private void NextPage() + { + var filtered = GetFilteredEntries(); + var pages = GetTotalPages(filtered.Count); + currentPage = (currentPage + 1) % pages; + var list = ShowPage(); + RefreshControllerOverlay(list); + } + + private void PreviousPage() + { + var filtered = GetFilteredEntries(); + var pages = GetTotalPages(filtered.Count); + currentPage = (currentPage - 1 + pages) % pages; + var list = ShowPage(); + RefreshControllerOverlay(list); + } + + public Il2CppSystem.Collections.Generic.List ShowPage() + { + foreach (var entry in MenuEntries) + { + entry.Panel.gameObject.SetActive(false); + } + + var filtered = GetFilteredEntries(); + noResultsText?.gameObject.SetActive(filtered.Count == 0 && !string.IsNullOrWhiteSpace(searchText)); + var totalPages = GetTotalPages(filtered.Count); + currentPage = Mathf.Clamp(currentPage, 0, totalPages - 1); + + var list = filtered.Skip(currentPage * ItemsPerPage).Take(ItemsPerPage).ToList(); + var list2 = new Il2CppSystem.Collections.Generic.List(); + + for (var i = 0; i < list.Count; i++) + { + var entry = list[i]; + var num = i % 3; + var num2 = i / 3 % 5; + entry.Panel.transform.localPosition = + new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); + entry.Panel.gameObject.SetActive(true); + list2.Add(entry.Panel.Button); + } + + return list2; + } + + [HideFromIl2Cpp] + protected void EnsureSearchUi() + { + if (searchTextbox != null) + { + return; + } + + var gridCenterX = xStart + xOffset; + var desiredSearchBarCenterY = yStart + 0.55f; + + if (PrefabTextbox == null) + { + return; + } + + var searchRoot = PrefabTextbox.transform.parent != null ? PrefabTextbox.transform.parent.gameObject : PrefabTextbox.gameObject; + var searchObj = Instantiate(searchRoot, transform); + searchObj.name = $"{Name}SearchBar"; + + foreach (var aspect in searchObj.GetComponentsInChildren(true)) + { + aspect.DestroyImmediate(); + } + + searchTextbox = searchObj.GetComponentInChildren(true); + if (searchTextbox == null) + { + return; + } + + try + { + var placeholder = searchTextbox.transform.parent.GetChild(2).GetComponent(); + placeholder?.gameObject.SetActive(false); + } + catch + { + foreach (var tmp in searchObj.GetComponentsInChildren(true)) + { + if (tmp != searchTextbox.outputText && + (tmp.text.Contains("Search", StringComparison.OrdinalIgnoreCase) || + tmp.text.Contains("Here", StringComparison.OrdinalIgnoreCase))) + { + tmp.gameObject.SetActive(false); + } + } + } + + searchObj.transform.localScale = new Vector3(0.7f, 0.7f, 1f); + searchObj.transform.localPosition = new Vector3(gridCenterX, desiredSearchBarCenterY, -1f); + + var searchBounds = CalcSpriteBoundsInParentSpace(transform, searchObj); + var deltaX = gridCenterX - searchBounds.center.x; + var deltaY = desiredSearchBarCenterY - searchBounds.center.y; + searchObj.transform.localPosition += new Vector3(deltaX, deltaY, 0f); + searchBounds = CalcSpriteBoundsInParentSpace(transform, searchObj); + + var wikiClickSound = HudManager.Instance?.MapButton?.ClickSound; + + var searchFocusButton = searchTextbox.gameObject.GetComponent() + ?? searchTextbox.gameObject.AddComponent(); + if (wikiClickSound != null) + { + searchFocusButton.ClickSound = wikiClickSound; + } + searchFocusButton.OnClick.RemoveAllListeners(); + searchFocusButton.OnClick.AddListener((UnityAction)(Action)(() => + { + searchTextbox.GiveFocus(); + })); + + searchTextbox.SetText(string.Empty); + searchTextbox.OnChange.RemoveAllListeners(); + searchTextbox.OnChange.AddListener((UnityAction)(Action)(() => + { + searchText = searchTextbox.outputText.text ?? string.Empty; + currentPage = 0; + var list = ShowPage(); + RefreshControllerOverlay(list); + + if (searchTextbox != null) + { + Coroutines.Start(CoRestoreFocus()); + } + })); + + var label = Instantiate(HudManager.Instance?.TaskPanel.taskText, transform); + if (label != null) + { + label.name = $"{Name}SearchLabel"; + label.text = "Search"; + label.alignment = TextAlignmentOptions.Center; + label.fontSize = label.fontSizeMin = label.fontSizeMax = 2.1f; + label.color = Color.white; + label.transform.localPosition = new Vector3(gridCenterX, searchBounds.max.y + 0.18f, -1f); + + if (searchTextbox.outputText != null) + { + label.font = searchTextbox.outputText.font; + label.fontMaterial = searchTextbox.outputText.fontMaterial; + } + } + + noResultsText = Instantiate(HudManager.Instance?.TaskPanel.taskText, transform); + if (noResultsText != null) + { + noResultsText.name = $"{Name}NoResultsText"; + noResultsText.text = "No results"; + noResultsText.alignment = TextAlignmentOptions.Center; + noResultsText.fontSize = noResultsText.fontSizeMin = noResultsText.fontSizeMax = 2.25f; + noResultsText.color = Color.white; + noResultsText.transform.localPosition = new Vector3(gridCenterX, yStart + 0.1f, -1f); + noResultsText.gameObject.SetActive(false); + } + + if (backButton != null && searchTextbox != null) + { + var clearButtonX = searchBounds.max.x + 0.2f; + var clearButtonY = searchBounds.center.y; + + var clearObj = Instantiate(backButton.gameObject, transform); + clearObj.name = "ClearSearchButton"; + clearObj.transform.localScale = new Vector3(0.35f, 0.35f, 1f); + clearObj.transform.localPosition = new Vector3(clearButtonX, clearButtonY, -1f); + + clearObj.GetComponent()?.DestroyImmediate(); + clearObj.GetComponent()?.DestroyImmediate(); + + var clearSearchButton = clearObj.GetComponent(); + if (clearSearchButton != null) + { + if (wikiClickSound != null) + { + clearSearchButton.ClickSound = wikiClickSound; + } + + clearSearchButton.OnClick.RemoveAllListeners(); + clearSearchButton.OnClick = new Button.ButtonClickedEvent(); + clearSearchButton.OnClick.AddListener((UnityAction)(() => + { + if (searchTextbox == null) + { + return; + } + + searchTextbox.SetText(string.Empty); + searchText = string.Empty; + currentPage = 0; + var list = ShowPage(); + RefreshControllerOverlay(list); + })); + } + } + } + + [HideFromIl2Cpp] + private static Bounds CalcSpriteBoundsInParentSpace(Transform parent, GameObject root) + { + var first = true; + var bounds = default(Bounds); + + foreach (var r in root.GetComponentsInChildren(true)) + { + if (r == null || r.sprite == null) continue; + + var b = r.bounds; + var c = b.center; + var e = b.extents; + + var p1 = parent.InverseTransformPoint(new Vector3(c.x - e.x, c.y - e.y, c.z)); + var p2 = parent.InverseTransformPoint(new Vector3(c.x - e.x, c.y + e.y, c.z)); + var p3 = parent.InverseTransformPoint(new Vector3(c.x + e.x, c.y - e.y, c.z)); + var p4 = parent.InverseTransformPoint(new Vector3(c.x + e.x, c.y + e.y, c.z)); + + if (first) + { + bounds = new Bounds(p1, Vector3.zero); + first = false; + } + + bounds.Encapsulate(p1); + bounds.Encapsulate(p2); + bounds.Encapsulate(p3); + bounds.Encapsulate(p4); + } + + return bounds; + } +} From 6175628fdff7c1a7116121e543b38e5a83e08b09 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:53:12 -0600 Subject: [PATCH 14/38] Moved interface out of class --- MiraAPI/Hud/CustomPhoneMenu.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index ade867e..26e5cf2 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -14,6 +14,17 @@ using UnityEngine.Events; namespace MiraAPI.Hud; +/// +/// Defines an entry in a with a Panel. +/// +public interface IMenuEntry +{ + /// + /// Gets the base Panel of the menu entry. + /// + ShapeshifterPanel Panel { get; } +} + /// /// Custom Phone Menu using the as a base. /// @@ -23,11 +34,6 @@ namespace MiraAPI.Hud; [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")] public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) { - public interface IMenuEntry - { - ShapeshifterPanel Panel { get; } - } - /// /// Menu Entry used when specifically only the Panel itself is required. /// @@ -199,7 +205,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) /// /// The type of menu entries. -public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) where TMenu : CustomPhoneMenu.IMenuEntry +public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) where TMenu : IMenuEntry { protected new List MenuEntries { From 49e5061d8803d2a9a01e164aaa6c68715feb2a5c Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:00:37 -0600 Subject: [PATCH 15/38] Created interfaces to define entries stored within the menu --- MiraAPI/Hud/CustomPhoneMenu.cs | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 26e5cf2..6514672 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -25,6 +25,32 @@ public interface IMenuEntry ShapeshifterPanel Panel { get; } } +/// +/// Defines a custom menu with a list of entries. +/// +public interface ICustomMenu +{ + /// + /// Gets all registered s. + /// + List MenuEntries { get; } +} + +/// +/// Defines a custom menu with a list of only entries. +/// +/// Only implement this if you want to define a single new type of menu entries explicitly. +/// Must reference the member of the superclass that is being hidden by this one to work. +/// +/// The type of menu entry. +public interface ICustomMenu : ICustomMenu where TMenu : IMenuEntry +{ + /// + /// Gets all registered s. + /// + new List MenuEntries { get; } +} + /// /// Custom Phone Menu using the as a base. /// @@ -32,7 +58,7 @@ public interface IMenuEntry [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 CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) +public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), ICustomMenu { /// /// Menu Entry used when specifically only the Panel itself is required. @@ -45,7 +71,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) public static implicit operator BasicEntry(ShapeshifterPanel panel) => new(panel); } - protected List MenuEntries { get; set; } = []; + public List MenuEntries { get; protected set; } = []; public List EntryPanels => MenuEntries.Select(e => e.Panel).ToList(); @@ -205,11 +231,11 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) /// /// The type of menu entries. -public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) where TMenu : IMenuEntry +public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr), ICustomMenu where TMenu : IMenuEntry { - protected new List MenuEntries + public new List MenuEntries { get => base.MenuEntries.Cast().ToList(); - set => base.MenuEntries = value.Cast().ToList(); + protected set => base.MenuEntries = value.Cast().ToList(); } } From 88e17c4f586aaeafb03ed05b4f3051e57f7c8002 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:15:00 -0600 Subject: [PATCH 16/38] Marked method as Obsolete to prevent it from being used. --- MiraAPI/Hud/CustomPhoneMenu.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 6514672..141538f 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -143,11 +143,14 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I ControllerManager.Instance.CloseOverlayMenu(name); } - /// +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member + [Obsolete("Will always fail. Call or define another Begin method that calls MinigameStubs.Begin", true)] + [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Overrides an unusable method.")] public override sealed void Begin(PlayerTask task) { throw new NotImplementedException("Use the other Begin method."); } +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member protected void RegisterPanels( IEnumerable entries, 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 17/38] 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); + } +} From e6621bd0369d031730847a950900aaf492842c61 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:07:23 -0600 Subject: [PATCH 18/38] Added overload for single selections --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 323bc08..646ea2a 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -97,6 +97,17 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); } + /// + /// Begins/opens the custom menu, only requiring a single selection. + /// + /// All entries to give the custom menu. + /// Function called when the selection is made. + [HideFromIl2Cpp] + protected void Begin(IEnumerable entries, Action onClick) + { + Begin(entries, list => onClick(list?[0]), 1, false); + } + private void OnEntryClick(TEntry entry, Action> onClick) { MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry); From 810facf50e5fb5fbcd8dd43702060f655d70552f Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:12:56 -0600 Subject: [PATCH 19/38] Added canRepeat parameter --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 646ea2a..5fd597c 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -26,6 +26,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) { private int totalSelections; private bool shouldConfirm; + private bool canRepeat; private readonly List selectedEntries = []; private LoadableAsset? hoverSelectSprite; @@ -68,13 +69,15 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// Function called when all selections are made/confirmed. /// The number of selections required. /// Wheter the entire selection should be confirmed manually. + /// If the same entry can be selected multiple times, else unselect entry on click. [HideFromIl2Cpp] - protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm) + protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm, bool canRepeat = false) { MinigameStubs.Begin(this, null); this.totalSelections = totalSelections; this.shouldConfirm = shouldConfirm; + this.canRepeat = canRepeat; var back = backButton.GetComponent(); back.OnClick.AddListener((UnityAction)(() => { @@ -112,7 +115,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) { MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry); - if (selectedEntries.Remove(menuEntry)) // Unselect previous choice + if (!canRepeat && selectedEntries.Remove(menuEntry)) // Unselect previous choice { SetNameplateAppearance(menuEntry, false); return; From 4fb9897cc696b1c36e6cb2f42f13e9124e6ada13 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:19:59 -0600 Subject: [PATCH 20/38] Made CustomPlayerMenu extend MultiSelectMenu --- MiraAPI/Hud/CustomPlayerMenu.cs | 35 +++++++++------------------------ 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 0317dfc..b060631 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Patches.Stubs; using Reactor.Utilities.Attributes; 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. @@ -20,7 +18,7 @@ namespace MiraAPI.Hud; [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 class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) +public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu(il2CppPtr) { public List potentialVictims; @@ -41,29 +39,14 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) [HideFromIl2Cpp] public void Begin(Func playerMatch, Action onClick) { - MinigameStubs.Begin(this, null); - - 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( - PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), - (shapeshifterPanel, i, player) => - { - var num = i % 3; - var num2 = i / 3; - var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; - shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); - shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); })); - shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white; - list2.Add(shapeshifterPanel.Button); - }); + Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick); potentialVictims = EntryPanels; - ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); + } + + protected override void SetupPanelEntry(ShapeshifterPanel panel, int i, PlayerControl player, Action onClick) + { + var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; + panel.SetPlayer(i, player.Data, (Il2CppSystem.Action)onClick); + panel.NameText.color = flag ? player.Data.Role.NameColor : Color.white; } } From 54f594f94733a3a3145a49fa0f3fb87e40a6ebbe Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:28:11 -0600 Subject: [PATCH 21/38] Added 2 selections overload --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 13 +++++++++++++ MiraAPI/Hud/CustomPlayerMenu.cs | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 5fd597c..3383bd1 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -111,6 +111,19 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) Begin(entries, list => onClick(list?[0]), 1, false); } + /// + /// Begins/opens the custom menu, requiring two selections. + /// + /// All entries to give the custom menu. + /// Function called when both selections are made. + /// Wheter the set of both selections should be confirmed manually. + /// If the same entry can be selected both times, else unselect entry on click. + [HideFromIl2Cpp] + protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm, bool canRepeat = false) + { + Begin(entries, list => onClick(list?[0], list?[1]), 2, shouldConfirm, canRepeat); + } + private void OnEntryClick(TEntry entry, Action> onClick) { MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry); diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index b060631..59e2803 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -43,6 +43,20 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu + /// Begins/opens the custom player menu. + /// + /// Function to determine if player should show in the custom menu. + /// action for the two-player selection. + /// Wheter the set of both selections should be confirmed manually. + /// If the same entry can be selected both times, else unselect entry on click. + [HideFromIl2Cpp] + public void Begin(Func playerMatch, Action onClick, bool shouldConfirm = false, bool canRepeat = false) + { + Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick, shouldConfirm, canRepeat); + potentialVictims = EntryPanels; + } + protected override void SetupPanelEntry(ShapeshifterPanel panel, int i, PlayerControl player, Action onClick) { var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; From 9cfd8d6857575e66cbdc444984c31e2d39b62b85 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:32:03 -0600 Subject: [PATCH 22/38] Clear selections on clicking back --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 3383bd1..4145501 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -81,6 +81,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) var back = backButton.GetComponent(); back.OnClick.AddListener((UnityAction)(() => { + selectedEntries.Clear(); onClick(null); })); From b3501b47c4365ba616bdc8179bdde730646b12ad Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:33:34 -0600 Subject: [PATCH 23/38] Added TODO comments for confirming multi-selection --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 4145501..e1f1f12 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -49,6 +49,8 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) { TMenu customMenu = Create(onMouseOut, onMouseOver); + // TODO: create/add confirm button with click listener + customMenu.activeColor = activeColor; customMenu.hoverSelectSprite = hoverSelectSprite; @@ -131,6 +133,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) if (!canRepeat && selectedEntries.Remove(menuEntry)) // Unselect previous choice { + // TODO: if confirm button is enabled, disable it SetNameplateAppearance(menuEntry, false); return; } @@ -143,6 +146,12 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) } // Total selections reached + if (shouldConfirm) + { + // TODO: enable confirm button with click listener to call onClick + return; + } + onClick(selectedEntries.Select(se => se.Entry).ToList()); selectedEntries.Clear(); } From dc909037e807182a632a5c0b06d1f9012efc80f7 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:35:54 -0600 Subject: [PATCH 24/38] Added shouldConfirm to single selection Begins with default false --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 5 +++-- MiraAPI/Hud/CustomPlayerMenu.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index e1f1f12..d95c460 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -108,10 +108,11 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// /// All entries to give the custom menu. /// Function called when the selection is made. + /// Wheter the set of both selections should be confirmed manually. [HideFromIl2Cpp] - protected void Begin(IEnumerable entries, Action onClick) + protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm) { - Begin(entries, list => onClick(list?[0]), 1, false); + Begin(entries, list => onClick(list?[0]), 1, shouldConfirm); } /// diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 59e2803..3da194b 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -36,10 +36,11 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu /// Function to determine if player should show in the custom menu. /// action for player. + /// Wheter the set of both selections should be confirmed manually. [HideFromIl2Cpp] - public void Begin(Func playerMatch, Action onClick) + public void Begin(Func playerMatch, Action onClick, bool shouldConfirm = false) { - Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick); + Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick, shouldConfirm); potentialVictims = EntryPanels; } From 8700a2bf92d43a8fe64f205b1a6300b6093df355 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:39:04 -0600 Subject: [PATCH 25/38] Added onSelection field to use earlier --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index d95c460..d940da7 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -35,6 +35,8 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) private Color? hoverSelectColor; private Color? hoverDeselectColor; + private Action>? onSelection; + public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry; protected static TMenu Create( @@ -80,6 +82,8 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) this.totalSelections = totalSelections; this.shouldConfirm = shouldConfirm; this.canRepeat = canRepeat; + this.onSelection = onClick; + var back = backButton.GetComponent(); back.OnClick.AddListener((UnityAction)(() => { @@ -96,7 +100,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) 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)); + SetupPanelEntry(shapeshifterPanel, i, entry, () => OnEntryClick(entry)); list2.Add(shapeshifterPanel.Button); }, (p, e) => new MenuEntry(p, e)); @@ -128,7 +132,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) Begin(entries, list => onClick(list?[0], list?[1]), 2, shouldConfirm, canRepeat); } - private void OnEntryClick(TEntry entry, Action> onClick) + private void OnEntryClick(TEntry entry) { MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry); @@ -153,7 +157,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) return; } - onClick(selectedEntries.Select(se => se.Entry).ToList()); + onSelection!(selectedEntries.Select(se => se.Entry).ToList()); selectedEntries.Clear(); } From 7f023eb2c9a475a41783b7e0e939d66248faa6f1 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:55:03 -0600 Subject: [PATCH 26/38] Created UiElement for confirm button --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index d940da7..21b4998 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -35,6 +35,8 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) private Color? hoverSelectColor; private Color? hoverDeselectColor; + public UiElement confirmButton; + private Action>? onSelection; public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry; @@ -51,7 +53,11 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) { TMenu customMenu = Create(onMouseOut, onMouseOver); - // TODO: create/add confirm button with click listener + customMenu.confirmButton = null!; // TODO: create/add confirm button + + var button = customMenu.confirmButton.GetComponent(); + button.OnClick.RemoveAllListeners(); + button.OnClick.AddListener((UnityAction)customMenu.OnCompleteSelection); customMenu.activeColor = activeColor; @@ -153,14 +159,19 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) // Total selections reached if (shouldConfirm) { - // TODO: enable confirm button with click listener to call onClick + // TODO: enable confirm button return; } - onSelection!(selectedEntries.Select(se => se.Entry).ToList()); + OnCompleteSelection(); selectedEntries.Clear(); } + private void OnCompleteSelection() + { + onSelection!(selectedEntries.Select(se => se.Entry).ToList()); + } + protected override bool IsEntrySelected(IMenuEntry entry) { return selectedEntries.Any(e => e.Panel == entry.Panel); From 24443c94cab2b98f9475f0bfcd0121c6ce6d72c5 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:13:19 -0600 Subject: [PATCH 27/38] Renamed parameter to match overload --- MiraAPI/Hud/CustomPhoneMenu.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 141538f..bf85b8e 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -200,9 +200,9 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I IEnumerable entries, Action onEntryClick, Action entryPanelConfig, - Func? menuEntryConfig = null) + Func? menuEntryMaker = null) { - RegisterPanels(entries, (p, i, e) => entryPanelConfig(p, i, e, () => onEntryClick(e)), menuEntryConfig); + RegisterPanels(entries, (p, i, e) => entryPanelConfig(p, i, e, () => onEntryClick(e)), menuEntryMaker); } protected virtual bool IsEntrySelected(IMenuEntry entry) => false; From bae3ff8cea7b4a934de4e8d50e9a492a2cbccf4c Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:34:54 -0600 Subject: [PATCH 28/38] Added documentation to new classes and methods --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 23 +++++++++++++++++++++++ MiraAPI/Hud/CustomPhoneMenu.cs | 26 ++++++++++++++++++++++++++ MiraAPI/Hud/CustomPlayerMenu.cs | 1 + 3 files changed, 50 insertions(+) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 21b4998..8f829cb 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -41,6 +41,18 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry; + /// + /// Creates a . + /// + /// The type of . + /// The to use when an entry is selected but not hovered over. + /// The to use when an entry is hovered over while not selected. + /// The to use when an entry is hovered over while not selected. + /// The to use when an entry is hovered over while selected. + /// The to use when an entry is hovered over while selected. + /// Function that can optionally be run when the mouse is moved outside a menu panel. + /// Function that can optionally be run when the mouse is moved over a menu panel. + /// New object. protected static TMenu Create( Color? activeColor = null, LoadableAsset? hoverSelectSprite = null, @@ -70,6 +82,13 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) return customMenu; } + /// + /// Setup the given once created, given its index, entry, and onClick action. + /// + /// The panel to configure. + /// The 's index. + /// The 's entry. + /// Action to perform when the is clicked on. protected abstract void SetupPanelEntry(ShapeshifterPanel panel, int i, TEntry entry, Action onClick); /// @@ -172,11 +191,15 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) onSelection!(selectedEntries.Select(se => se.Entry).ToList()); } + /// protected override bool IsEntrySelected(IMenuEntry entry) { return selectedEntries.Any(e => e.Panel == entry.Panel); } + /// + /// + /// Whether the menu entry is actively selected. [HideFromIl2Cpp] protected void SetNameplateAppearance(IMenuEntry menuEntry, bool isSelected) { diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index bf85b8e..29677b7 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -152,6 +152,14 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I } #pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + /// + /// Register new menu panels given a set of entries. + /// + /// The type of the entry. + /// The entries to create panels for. + /// Function to configure the menu's panel once created, given its index and entry. + /// Function to create a for a given panel and it's entry. + /// If , it will create s, acting as wrappers for the s. protected void RegisterPanels( IEnumerable entries, Action entryPanelConfig, @@ -196,6 +204,12 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I } } + /// + /// + /// Action to perform when a given entry is clicked on. + /// Argument will be if the back button was clicked instead. + /// Function to configure the menu's panel once created, given its index, entry, and onClick action. + /// protected void RegisterPanels( IEnumerable entries, Action onEntryClick, @@ -205,8 +219,20 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I RegisterPanels(entries, (p, i, e) => entryPanelConfig(p, i, e, () => onEntryClick(e)), menuEntryMaker); } + /// + /// Determines if a given entry is currently selected, not just hovered over. + /// + /// The to check. + /// if the is selected, else . protected virtual bool IsEntrySelected(IMenuEntry entry) => false; + /// + /// Set the icon, over color, and unselected color for a given entry. + /// + /// The menu entry. + /// The to use as the icon, if any. + /// The to use when hovering over an entry. + /// The to use when not hovering over an entry. [HideFromIl2Cpp] protected static void SetNameplateAppearance( IMenuEntry menuEntry, LoadableAsset? sprite, Color? overColor, Color? unselectedColor) diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 3da194b..80a231f 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -58,6 +58,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu protected override void SetupPanelEntry(ShapeshifterPanel panel, int i, PlayerControl player, Action onClick) { var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; From 8246e58f00ea060f0a2c044f165eaf008b129bae Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:44:51 -0600 Subject: [PATCH 29/38] Added base Begin for Paginable menu --- MiraAPI/Hud/CustomPaginableMenu.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/MiraAPI/Hud/CustomPaginableMenu.cs b/MiraAPI/Hud/CustomPaginableMenu.cs index 4ef1df6..26142df 100644 --- a/MiraAPI/Hud/CustomPaginableMenu.cs +++ b/MiraAPI/Hud/CustomPaginableMenu.cs @@ -1,4 +1,5 @@ using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Patches.Stubs; using MiraAPI.Utilities.Assets; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -364,4 +365,25 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu + /// Begins/opens the custom player menu. After registering panels, it will prepare the search, pages, and open the menu. + /// + /// Function where all panels should be registered. + [HideFromIl2Cpp] + protected void Begin(Action registerEntryPanels) + { + MinigameStubs.Begin(this, null); + + searchText = string.Empty; + currentPage = 0; + + registerEntryPanels(); + + EnsureSearchUi(); + + var list2 = ShowPage(); + + ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); + } } From a776edc1f6527044cba41069ce6b55bd23bb9d0a Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:51:13 -0600 Subject: [PATCH 30/38] Added case for selecting a panel past the max selections --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 8f829cb..211aaf5 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -168,6 +168,11 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) return; } + if (selectedEntries.Count >= totalSelections) // Do not go past total selections. + { + return; + } + selectedEntries.Add(menuEntry); if (selectedEntries.Count < totalSelections) // Add choice to list of selections { From d603f5c953edfc8ec4f843a2833259487c94c687 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:52:13 -0600 Subject: [PATCH 31/38] Called SetNameplateAppearance on total selections equal case as well to account for confirming --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 211aaf5..1ff3e7a 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -174,9 +174,12 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) } selectedEntries.Add(menuEntry); - if (selectedEntries.Count < totalSelections) // Add choice to list of selections + if (selectedEntries.Count <= totalSelections) // Add choice to list of selections { SetNameplateAppearance(menuEntry, true); + } + if (selectedEntries.Count < totalSelections) + { return; } From 638db813357cf57d514936ee2e692b7bd7b7502c Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:56:29 -0600 Subject: [PATCH 32/38] Renamed parameters to match/improve documentation --- MiraAPI/Hud/CustomPhoneMenu.cs | 6 +++--- MiraAPI/Hud/CustomPlayerMenu.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 29677b7..9dbcb2b 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -208,15 +208,15 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I /// /// Action to perform when a given entry is clicked on. /// Argument will be if the back button was clicked instead. - /// Function to configure the menu's panel once created, given its index, entry, and onClick action. + /// Function to configure the menu's panel once created, given its index, entry, and onClick action. /// protected void RegisterPanels( IEnumerable entries, Action onEntryClick, - Action entryPanelConfig, + Action entryPanelActionConfig, Func? menuEntryMaker = null) { - RegisterPanels(entries, (p, i, e) => entryPanelConfig(p, i, e, () => onEntryClick(e)), menuEntryMaker); + RegisterPanels(entries, (p, i, e) => entryPanelActionConfig(p, i, e, () => onEntryClick(e)), menuEntryMaker); } /// diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 80a231f..d4c4fe1 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -59,10 +59,10 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu - protected override void SetupPanelEntry(ShapeshifterPanel panel, int i, PlayerControl player, Action onClick) + protected override void SetupPanelEntry(ShapeshifterPanel panel, int i, PlayerControl entry, Action onClick) { - var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; - panel.SetPlayer(i, player.Data, (Il2CppSystem.Action)onClick); - panel.NameText.color = flag ? player.Data.Role.NameColor : Color.white; + var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == entry.Data.Role.NameColor; + panel.SetPlayer(i, entry.Data, (Il2CppSystem.Action)onClick); + panel.NameText.color = flag ? entry.Data.Role.NameColor : Color.white; } } From 6ca0b2567416dc6373b1d2c8c06f58b66d826f2f Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:02:17 -0600 Subject: [PATCH 33/38] Fixed documentation typo lol --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 6 +++--- MiraAPI/Hud/CustomPlayerMenu.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index 1ff3e7a..aabd1f2 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -97,7 +97,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// 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. + /// Whether the entire selection should be confirmed manually. /// If the same entry can be selected multiple times, else unselect entry on click. [HideFromIl2Cpp] protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm, bool canRepeat = false) @@ -137,7 +137,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// /// All entries to give the custom menu. /// Function called when the selection is made. - /// Wheter the set of both selections should be confirmed manually. + /// Whether the set of both selections should be confirmed manually. [HideFromIl2Cpp] protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm) { @@ -149,7 +149,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// /// All entries to give the custom menu. /// Function called when both selections are made. - /// Wheter the set of both selections should be confirmed manually. + /// Whether the set of both selections should be confirmed manually. /// If the same entry can be selected both times, else unselect entry on click. [HideFromIl2Cpp] protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm, bool canRepeat = false) diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index d4c4fe1..461a3e0 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -36,7 +36,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu /// Function to determine if player should show in the custom menu. /// action for player. - /// Wheter the set of both selections should be confirmed manually. + /// Whether the set of both selections should be confirmed manually. [HideFromIl2Cpp] public void Begin(Func playerMatch, Action onClick, bool shouldConfirm = false) { @@ -49,7 +49,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu /// Function to determine if player should show in the custom menu. /// action for the two-player selection. - /// Wheter the set of both selections should be confirmed manually. + /// Whether the set of both selections should be confirmed manually. /// If the same entry can be selected both times, else unselect entry on click. [HideFromIl2Cpp] public void Begin(Func playerMatch, Action onClick, bool shouldConfirm = false, bool canRepeat = false) From 695bbf8ea762d08e6c570be54265f9d0812d95e5 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:18:01 -0600 Subject: [PATCH 34/38] Registered menu types in Il2Cpp --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 2 ++ MiraAPI/Hud/CustomPaginableMenu.cs | 2 ++ MiraAPI/Hud/CustomPhoneMenu.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index aabd1f2..b58a44b 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -1,6 +1,7 @@ using Il2CppInterop.Runtime.Attributes; using MiraAPI.Patches.Stubs; using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Attributes; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -18,6 +19,7 @@ namespace MiraAPI.Hud; /// /// Used by Il2Cpp. Do not use constructor, this is a . /// The type of object each entry represents. +[RegisterInIl2Cpp] [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")] diff --git a/MiraAPI/Hud/CustomPaginableMenu.cs b/MiraAPI/Hud/CustomPaginableMenu.cs index 26142df..b3ef876 100644 --- a/MiraAPI/Hud/CustomPaginableMenu.cs +++ b/MiraAPI/Hud/CustomPaginableMenu.cs @@ -2,6 +2,7 @@ using MiraAPI.Patches.Stubs; using MiraAPI.Utilities.Assets; using Reactor.Utilities; +using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using System; using System.Collections; @@ -22,6 +23,7 @@ namespace MiraAPI.Hud; /// Paginable using the as a base. /// /// Used by Il2Cpp. Do not use constructor, this is a . +[RegisterInIl2Cpp] [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")] diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 9dbcb2b..8bda766 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -1,6 +1,7 @@ using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using System; using System.Collections.Generic; @@ -55,6 +56,7 @@ public interface ICustomMenu : ICustomMenu where TMenu : IMenuEntry /// Custom Phone Menu using the as a base. /// /// Used by Il2Cpp. Do not use constructor, this is a . +[RegisterInIl2Cpp] [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")] From 34a4fcf69ff39e58b30c4fa4413985d507d1088f Mon Sep 17 00:00:00 2001 From: Az <56699741+AlchlcDvl@users.noreply.github.com> Date: Sat, 12 Sep 2026 01:23:01 -0400 Subject: [PATCH 35/38] change custom menu behaviour to wrap a script instead --- MiraAPI/Hud/CustomMultiSelectMenu.cs | 38 +++--- MiraAPI/Hud/CustomPaginableMenu.cs | 168 ++++++++++++++------------- MiraAPI/Hud/CustomPhoneMenu.cs | 164 +++++++++++++++++--------- MiraAPI/Hud/CustomPlayerMenu.cs | 19 ++- 4 files changed, 220 insertions(+), 169 deletions(-) diff --git a/MiraAPI/Hud/CustomMultiSelectMenu.cs b/MiraAPI/Hud/CustomMultiSelectMenu.cs index b58a44b..0e87ade 100644 --- a/MiraAPI/Hud/CustomMultiSelectMenu.cs +++ b/MiraAPI/Hud/CustomMultiSelectMenu.cs @@ -1,30 +1,22 @@ -using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Patches.Stubs; +using MiraAPI.Patches.Stubs; using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Attributes; 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. -[RegisterInIl2Cpp] -[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 +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity convention.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")] +public abstract class CustomMultiSelectMenu : CustomPhoneMenu.MenuEntry> where TEntry : class { private int totalSelections; private bool shouldConfirm; @@ -37,10 +29,18 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) private Color? hoverSelectColor; private Color? hoverDeselectColor; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + [SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "Unity convention.")] public UiElement confirmButton; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member private Action>? onSelection; + /// + /// Menu Entry used when multiple entries are at play. + /// + /// The panel of the entry. + /// The entry itself. public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry; /// @@ -63,7 +63,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) Color? hoverDeselectColor = null, PanelButtonOnMouse? onMouseOut = null, PanelButtonOnMouse? onMouseOver = null - ) where TMenu : CustomMultiSelectMenu + ) where TMenu : CustomMultiSelectMenu, new() { TMenu customMenu = Create(onMouseOut, onMouseOver); @@ -101,10 +101,9 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// The number of selections required. /// Whether the entire selection should be confirmed manually. /// If the same entry can be selected multiple times, else unselect entry on click. - [HideFromIl2Cpp] protected void Begin(IEnumerable entries, Action?> onClick, int totalSelections, bool shouldConfirm, bool canRepeat = false) { - MinigameStubs.Begin(this, null); + MinigameStubs.Begin(Component, null); this.totalSelections = totalSelections; this.shouldConfirm = shouldConfirm; @@ -118,7 +117,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) onClick(null); })); - DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType); + DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, Component.TaskType); var list2 = new Il2CppSystem.Collections.Generic.List(); RegisterPanels( entries, @@ -140,7 +139,6 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// All entries to give the custom menu. /// Function called when the selection is made. /// Whether the set of both selections should be confirmed manually. - [HideFromIl2Cpp] protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm) { Begin(entries, list => onClick(list?[0]), 1, shouldConfirm); @@ -153,7 +151,6 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// Function called when both selections are made. /// Whether the set of both selections should be confirmed manually. /// If the same entry can be selected both times, else unselect entry on click. - [HideFromIl2Cpp] protected void Begin(IEnumerable entries, Action onClick, bool shouldConfirm, bool canRepeat = false) { Begin(entries, list => onClick(list?[0], list?[1]), 2, shouldConfirm, canRepeat); @@ -198,7 +195,7 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) private void OnCompleteSelection() { - onSelection!(selectedEntries.Select(se => se.Entry).ToList()); + onSelection!([.. selectedEntries.Select(se => se.Entry)]); } /// @@ -210,7 +207,6 @@ public abstract class CustomMultiSelectMenu(IntPtr il2CppPtr) /// /// /// Whether the menu entry is actively selected. - [HideFromIl2Cpp] protected void SetNameplateAppearance(IMenuEntry menuEntry, bool isSelected) { SetNameplateAppearance( diff --git a/MiraAPI/Hud/CustomPaginableMenu.cs b/MiraAPI/Hud/CustomPaginableMenu.cs index b3ef876..9c6b7b6 100644 --- a/MiraAPI/Hud/CustomPaginableMenu.cs +++ b/MiraAPI/Hud/CustomPaginableMenu.cs @@ -1,8 +1,6 @@ -using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Patches.Stubs; +using MiraAPI.Patches.Stubs; using MiraAPI.Utilities.Assets; using Reactor.Utilities; -using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using System; using System.Collections; @@ -13,38 +11,49 @@ using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; -#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 +using CppCollections = Il2CppSystem.Collections.Generic; +using Object = UnityEngine.Object; namespace MiraAPI.Hud; /// /// Paginable using the as a base. /// -/// Used by Il2Cpp. Do not use constructor, this is a . -[RegisterInIl2Cpp] -[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 CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr) +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity convention.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")] +public abstract class CustomPaginableMenu : CustomPhoneMenu { + /// + /// Menu Entry used for when multiple pages are needed. + /// + /// The panel. + /// The key by which the panels are sorted. + public record MenuEntry(ShapeshifterPanel Panel, string SortKey) : IMenuEntry; + + /// + /// Gets the name of the menu. + /// protected abstract string Name { get; } - protected int currentPage; - + /// + /// Gets the prefab used to generate the search field. + /// protected abstract TextBoxTMP? PrefabTextbox { get; } + /// + protected override float MenuDepth => -60f; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + protected int currentPage; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + private TextBoxTMP? searchTextbox; private string searchText = string.Empty; private TextMeshPro? noResultsText; private const int ItemsPerPage = 15; - protected override float MenuDepth => -60f; - - public record MenuEntry(ShapeshifterPanel Panel, string SortKey) : IMenuEntry; - /// /// Creates a . /// @@ -52,14 +61,11 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenuFunction that can optionally be run when the mouse is moved outside a menu panel. /// Function that can optionally be run when the mouse is moved over a menu panel. /// New object. - protected static new TMenu Create( - PanelButtonOnMouse? onMouseOut = null, - PanelButtonOnMouse? onMouseOver = null - ) where TMenu : CustomPaginableMenu + protected static new TMenu Create(PanelButtonOnMouse? onMouseOut = null, PanelButtonOnMouse? onMouseOver = null) where TMenu : CustomPaginableMenu, new() { TMenu customMenu = CustomPhoneMenu.Create(onMouseOut, onMouseOver); - var nextButton = Instantiate(customMenu.backButton, customMenu.transform).gameObject; + var nextButton = Object.Instantiate(customMenu.backButton, customMenu.transform).gameObject; nextButton.transform.localPosition = new Vector3(1.85f, -2.185f, customMenu.MenuDepth); nextButton.transform.localScale = new Vector3(0.65f, 0.65f, 1); nextButton.name = "RightArrowButton"; @@ -70,7 +76,7 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu().Destroy(); @@ -93,22 +99,19 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu GetFilteredEntries() { var query = NormalizeForSearch(searchText); - if (string.IsNullOrEmpty(query)) - { - return MenuEntries; - } - return MenuEntries + if (string.IsNullOrEmpty(query)) + return MenuEntries; + + return [.. MenuEntries .Where(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(e => e.SortKey.Equals(query, StringComparison.OrdinalIgnoreCase)) .ThenByDescending(e => e.SortKey.StartsWith(query, StringComparison.OrdinalIgnoreCase)) .ThenByDescending(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase)) - .ThenBy(e => e.SortKey, StringComparer.OrdinalIgnoreCase) - .ToList(); + .ThenBy(e => e.SortKey, StringComparer.OrdinalIgnoreCase)]; } private static int GetTotalPages(int itemCount) @@ -116,7 +119,7 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu list) + private void RefreshControllerOverlay(CppCollections.List list) { if (ControllerManager.Instance && backButton != null) { @@ -124,7 +127,6 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu ShowPage() + /// + /// Shows the current page. + /// + /// The Il2CPP list of ui elements displayed on the page. + public CppCollections.List ShowPage() { foreach (var entry in MenuEntries) { @@ -162,7 +168,7 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu(); + var list2 = new CppCollections.List(); for (var i = 0; i < list.Count; i++) { @@ -178,7 +184,9 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu + /// Ensures that the search UI exists. + /// protected void EnsureSearchUi() { if (searchTextbox != null) @@ -195,7 +203,7 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu(true)) @@ -265,7 +273,7 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu()?.DestroyImmediate(); + clearObj.GetComponent()?.DestroyImmediate(); + + var clearSearchButton = clearObj.GetComponent(); + if (clearSearchButton == null) { - var clearButtonX = searchBounds.max.x + 0.2f; - var clearButtonY = searchBounds.center.y; - - var clearObj = Instantiate(backButton.gameObject, transform); - clearObj.name = "ClearSearchButton"; - clearObj.transform.localScale = new Vector3(0.35f, 0.35f, 1f); - clearObj.transform.localPosition = new Vector3(clearButtonX, clearButtonY, -1f); - - clearObj.GetComponent()?.DestroyImmediate(); - clearObj.GetComponent()?.DestroyImmediate(); - - var clearSearchButton = clearObj.GetComponent(); - if (clearSearchButton != null) - { - if (wikiClickSound != null) - { - clearSearchButton.ClickSound = wikiClickSound; - } - - clearSearchButton.OnClick.RemoveAllListeners(); - clearSearchButton.OnClick = new Button.ButtonClickedEvent(); - clearSearchButton.OnClick.AddListener((UnityAction)(() => - { - if (searchTextbox == null) - { - return; - } - - searchTextbox.SetText(string.Empty); - searchText = string.Empty; - currentPage = 0; - var list = ShowPage(); - RefreshControllerOverlay(list); - })); - } + return; } + if (wikiClickSound != null) + { + clearSearchButton.ClickSound = wikiClickSound; + } + + clearSearchButton.OnClick.RemoveAllListeners(); + clearSearchButton.OnClick = new Button.ButtonClickedEvent(); + clearSearchButton.OnClick.AddListener((UnityAction)(() => + { + if (searchTextbox == null) + { + return; + } + + searchTextbox.SetText(string.Empty); + searchText = string.Empty; + currentPage = 0; + var list = ShowPage(); + RefreshControllerOverlay(list); + })); } - [HideFromIl2Cpp] private static Bounds CalcSpriteBoundsInParentSpace(Transform parent, GameObject root) { var first = true; @@ -342,7 +350,8 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu(true)) { - if (r == null || r.sprite == null) continue; + if (r == null || r.sprite == null) + continue; var b = r.bounds; var c = b.center; @@ -372,10 +381,9 @@ public abstract class CustomPaginableMenu(IntPtr il2CppPtr) : CustomPhoneMenu /// Function where all panels should be registered. - [HideFromIl2Cpp] protected void Begin(Action registerEntryPanels) { - MinigameStubs.Begin(this, null); + MinigameStubs.Begin(Component, null); searchText = string.Empty; currentPage = 0; diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 8bda766..51f508f 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -1,5 +1,4 @@ using AmongUs.GameOptions; -using Il2CppInterop.Runtime.Attributes; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; @@ -9,12 +8,26 @@ 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 +using Object = UnityEngine.Object; namespace MiraAPI.Hud; +/// +/// Component registered in Il2Cpp domain to handle Unity events for custom menus. +/// +[RegisterInIl2Cpp] +public class CustomPhoneMenuComponent(IntPtr cppPtr) : Minigame(cppPtr) +{ + [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] + private void OnDisable() + { + if (ControllerManager.Instance) + { + ControllerManager.Instance.CloseOverlayMenu(name); + } + } +} + /// /// Defines an entry in a with a Panel. /// @@ -53,14 +66,13 @@ public interface ICustomMenu : ICustomMenu where TMenu : IMenuEntry } /// -/// Custom Phone Menu using the as a base. +/// Custom Phone Menu logic using the as a base. /// -/// Used by Il2Cpp. Do not use constructor, this is a . -[RegisterInIl2Cpp] -[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 CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), ICustomMenu +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")] +[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Proxy for Unity properties.")] +public abstract class CustomPhoneMenu : ICustomMenu { /// /// Menu Entry used when specifically only the Panel itself is required. @@ -68,14 +80,51 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I /// The instance. protected sealed record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry { - public static implicit operator ShapeshifterPanel(BasicEntry entry) => entry.Panel; + /// + /// Implicitly converts to . + /// + /// The entry that contains the reference to the panel. + public static implicit operator ShapeshifterPanel(BasicEntry entry) + { + return entry.Panel; + } - public static implicit operator BasicEntry(ShapeshifterPanel panel) => new(panel); + /// + /// Implicitly converts to an instance of . + /// + /// The panel instance to make an entry off of. + public static implicit operator BasicEntry(ShapeshifterPanel panel) + { + return new(panel); + } } + /// + /// A delegate that is invoked when the mouse interacts with a panel. + /// + /// The panel's highlight that the delegate modifies. + /// The panel's icon that the delegate modifies. + /// A flag that indicates if the current panel is selected. + public delegate void PanelButtonOnMouse(SpriteRenderer highlight, SpriteRenderer icon, bool isSelected); + + /// + /// Gets the wrapped component that the menu modifies. + /// + public CustomPhoneMenuComponent Component { get; internal set; } = null!; + + /// public List MenuEntries { get; protected set; } = []; - public List EntryPanels => MenuEntries.Select(e => e.Panel).ToList(); + /// + /// Gets all of the panels of the menu. + /// + public List EntryPanels => [.. MenuEntries.Select(e => e.Panel)]; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Unity convention.) +#pragma warning disable S1104 // Fields should not have public accessibility (Justification: Read above.) + public Transform transform => Component.transform; + public GameObject gameObject => Component.gameObject; + public string name => Component.name; public float xStart = -0.8f; public float yStart = 2.15f; @@ -86,15 +135,29 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I public UiElement backButton; public UiElement defaultButtonSelected; - public Transform PhoneUI => transform.FindChild("PhoneUI"); - - public delegate void PanelButtonOnMouse(SpriteRenderer highlight, SpriteRenderer icon, bool isSelected); - protected PanelButtonOnMouse? onMouseOverAction; protected PanelButtonOnMouse? onMouseOutAction; +#pragma warning restore S1104 // Fields should not have public accessibility +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + /// + /// Gets the transform of the menu's Phone UI. + /// + public Transform PhoneUI => transform.FindChild("PhoneUI"); + + /// + /// Gets the z-axis depth that the menu is instantiated at. + /// protected virtual float MenuDepth => -50f; + /// + /// Closes the menu. + /// + public void Close() + { + Component.Close(); + } + /// /// Creates a . /// @@ -102,32 +165,32 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I /// Function that can optionally be run when the mouse is moved outside a menu panel. /// Function that can optionally be run when the mouse is moved over a menu panel. /// New object. - protected static TMenu Create( - PanelButtonOnMouse? onMouseOut = null, - PanelButtonOnMouse? onMouseOver = null - ) where TMenu : CustomPhoneMenu + protected static TMenu Create(PanelButtonOnMouse? onMouseOut = null, PanelButtonOnMouse? onMouseOver = null) where TMenu : CustomPhoneMenu, new() { var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter); var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu; - var newMenu = Instantiate(ogMenu); - var customMenu = newMenu.gameObject.AddComponent(); - - customMenu.panelPrefab = newMenu.PanelPrefab; - customMenu.xStart = newMenu.XStart; - customMenu.yStart = newMenu.YStart; - customMenu.xOffset = newMenu.XOffset; - customMenu.yOffset = newMenu.YOffset; - customMenu.defaultButtonSelected = newMenu.DefaultButtonSelected; - customMenu.backButton = newMenu.BackButton; + var newMenu = Object.Instantiate(ogMenu); + var component = newMenu.gameObject.AddComponent(); + var customMenu = new TMenu + { + Component = component, + panelPrefab = newMenu.PanelPrefab, + xStart = newMenu.XStart, + yStart = newMenu.YStart, + xOffset = newMenu.XOffset, + yOffset = newMenu.YOffset, + defaultButtonSelected = newMenu.DefaultButtonSelected, + backButton = newMenu.BackButton, + }; var back = customMenu.backButton.GetComponent(); back.OnClick.RemoveAllListeners(); back.OnClick.AddListener((UnityAction)customMenu.Close); - customMenu.CloseSound = newMenu.CloseSound; - customMenu.logger = newMenu.logger; - customMenu.OpenSound = newMenu.OpenSound; + component.CloseSound = newMenu.CloseSound; + component.logger = newMenu.logger; + component.OpenSound = newMenu.OpenSound; newMenu.DestroyImmediate(); @@ -140,20 +203,6 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I return customMenu; } - private void OnDisable() - { - ControllerManager.Instance.CloseOverlayMenu(name); - } - -#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member - [Obsolete("Will always fail. Call or define another Begin method that calls MinigameStubs.Begin", true)] - [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Overrides an unusable method.")] - public override sealed void Begin(PlayerTask task) - { - throw new NotImplementedException("Use the other Begin method."); - } -#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member - /// /// Register new menu panels given a set of entries. /// @@ -176,7 +225,7 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I var index = currentEntries + i; var entry = list[i]; - var shapeshifterPanel = Instantiate(panelPrefab, transform); + var shapeshifterPanel = Object.Instantiate(panelPrefab, transform); shapeshifterPanel.transform.localPosition = new Vector3(0f, 0f, -1f); entryPanelConfig(shapeshifterPanel, index, entry); @@ -226,7 +275,10 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I /// /// The to check. /// if the is selected, else . - protected virtual bool IsEntrySelected(IMenuEntry entry) => false; + protected virtual bool IsEntrySelected(IMenuEntry entry) + { + return false; + } /// /// Set the icon, over color, and unselected color for a given entry. @@ -235,7 +287,6 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I /// The to use as the icon, if any. /// The to use when hovering over an entry. /// The to use when not hovering over an entry. - [HideFromIl2Cpp] protected static void SetNameplateAppearance( IMenuEntry menuEntry, LoadableAsset? sprite, Color? overColor, Color? unselectedColor) { @@ -260,13 +311,14 @@ public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr), I } } -/// +/// /// The type of menu entries. -public abstract class CustomPhoneMenu(IntPtr il2CppPtr) : CustomPhoneMenu(il2CppPtr), ICustomMenu where TMenu : IMenuEntry +public abstract class CustomPhoneMenu : CustomPhoneMenu, ICustomMenu where TMenu : IMenuEntry { + /// public new List MenuEntries { - get => base.MenuEntries.Cast().ToList(); - protected set => base.MenuEntries = value.Cast().ToList(); + get => [.. base.MenuEntries.Cast()]; + protected set => base.MenuEntries = [.. value.Cast()]; } } diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 461a3e0..a0b57a2 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -2,25 +2,22 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Il2CppInterop.Runtime.Attributes; -using Reactor.Utilities.Attributes; using UnityEngine; -#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. namespace MiraAPI.Hud; /// /// Custom Player Menu using the as a base. /// -/// Used by Il2Cpp. Do not use constructor, this is a . -[RegisterInIl2Cpp] -[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 class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu(il2CppPtr) +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")] +public class CustomPlayerMenu : CustomMultiSelectMenu { + [SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "Read above.")] +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public List potentialVictims; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// /// Creates a . @@ -37,7 +34,6 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenuFunction to determine if player should show in the custom menu. /// action for player. /// Whether the set of both selections should be confirmed manually. - [HideFromIl2Cpp] public void Begin(Func playerMatch, Action onClick, bool shouldConfirm = false) { Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick, shouldConfirm); @@ -51,7 +47,6 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : CustomMultiSelectMenu action for the two-player selection. /// Whether the set of both selections should be confirmed manually. /// If the same entry can be selected both times, else unselect entry on click. - [HideFromIl2Cpp] public void Begin(Func playerMatch, Action onClick, bool shouldConfirm = false, bool canRepeat = false) { Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick, shouldConfirm, canRepeat); From f62513a3b4abb87c944a0bf7fa4ce473d004e780 Mon Sep 17 00:00:00 2001 From: Az <56699741+AlchlcDvl@users.noreply.github.com> Date: Sat, 12 Sep 2026 01:27:16 -0400 Subject: [PATCH 36/38] forgot shapeshifter's menu is a minigame itself --- MiraAPI/Hud/CustomPhoneMenu.cs | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 51f508f..8873aac 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -12,22 +12,6 @@ using Object = UnityEngine.Object; namespace MiraAPI.Hud; -/// -/// Component registered in Il2Cpp domain to handle Unity events for custom menus. -/// -[RegisterInIl2Cpp] -public class CustomPhoneMenuComponent(IntPtr cppPtr) : Minigame(cppPtr) -{ - [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] - private void OnDisable() - { - if (ControllerManager.Instance) - { - ControllerManager.Instance.CloseOverlayMenu(name); - } - } -} - /// /// Defines an entry in a with a Panel. /// @@ -110,7 +94,7 @@ public abstract class CustomPhoneMenu : ICustomMenu /// /// Gets the wrapped component that the menu modifies. /// - public CustomPhoneMenuComponent Component { get; internal set; } = null!; + public ShapeshifterMinigame Component { get; internal set; } = null!; /// public List MenuEntries { get; protected set; } = []; @@ -171,10 +155,9 @@ public abstract class CustomPhoneMenu : ICustomMenu var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu; var newMenu = Object.Instantiate(ogMenu); - var component = newMenu.gameObject.AddComponent(); var customMenu = new TMenu { - Component = component, + Component = newMenu, panelPrefab = newMenu.PanelPrefab, xStart = newMenu.XStart, yStart = newMenu.YStart, @@ -188,10 +171,6 @@ public abstract class CustomPhoneMenu : ICustomMenu back.OnClick.RemoveAllListeners(); back.OnClick.AddListener((UnityAction)customMenu.Close); - component.CloseSound = newMenu.CloseSound; - component.logger = newMenu.logger; - component.OpenSound = newMenu.OpenSound; - newMenu.DestroyImmediate(); customMenu.transform.SetParent(Camera.main!.transform, false); From 2c08e89e307b4a9f3c6f7a242494a5ae8404d48c Mon Sep 17 00:00:00 2001 From: Az <56699741+AlchlcDvl@users.noreply.github.com> Date: Sun, 13 Sep 2026 16:41:19 -0400 Subject: [PATCH 37/38] Revert "forgot shapeshifter's menu is a minigame itself" This reverts commit f62513a3b4abb87c944a0bf7fa4ce473d004e780. --- MiraAPI/Hud/CustomPhoneMenu.cs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 8873aac..51f508f 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -12,6 +12,22 @@ using Object = UnityEngine.Object; namespace MiraAPI.Hud; +/// +/// Component registered in Il2Cpp domain to handle Unity events for custom menus. +/// +[RegisterInIl2Cpp] +public class CustomPhoneMenuComponent(IntPtr cppPtr) : Minigame(cppPtr) +{ + [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] + private void OnDisable() + { + if (ControllerManager.Instance) + { + ControllerManager.Instance.CloseOverlayMenu(name); + } + } +} + /// /// Defines an entry in a with a Panel. /// @@ -94,7 +110,7 @@ public abstract class CustomPhoneMenu : ICustomMenu /// /// Gets the wrapped component that the menu modifies. /// - public ShapeshifterMinigame Component { get; internal set; } = null!; + public CustomPhoneMenuComponent Component { get; internal set; } = null!; /// public List MenuEntries { get; protected set; } = []; @@ -155,9 +171,10 @@ public abstract class CustomPhoneMenu : ICustomMenu var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu; var newMenu = Object.Instantiate(ogMenu); + var component = newMenu.gameObject.AddComponent(); var customMenu = new TMenu { - Component = newMenu, + Component = component, panelPrefab = newMenu.PanelPrefab, xStart = newMenu.XStart, yStart = newMenu.YStart, @@ -171,6 +188,10 @@ public abstract class CustomPhoneMenu : ICustomMenu back.OnClick.RemoveAllListeners(); back.OnClick.AddListener((UnityAction)customMenu.Close); + component.CloseSound = newMenu.CloseSound; + component.logger = newMenu.logger; + component.OpenSound = newMenu.OpenSound; + newMenu.DestroyImmediate(); customMenu.transform.SetParent(Camera.main!.transform, false); From 8b83dcbf1f333ad838556e28feecd41381c56656 Mon Sep 17 00:00:00 2001 From: Az <56699741+AlchlcDvl@users.noreply.github.com> Date: Sun, 13 Sep 2026 16:44:33 -0400 Subject: [PATCH 38/38] re-add obsolete begin --- MiraAPI/Hud/CustomPhoneMenu.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MiraAPI/Hud/CustomPhoneMenu.cs b/MiraAPI/Hud/CustomPhoneMenu.cs index 51f508f..38edd89 100644 --- a/MiraAPI/Hud/CustomPhoneMenu.cs +++ b/MiraAPI/Hud/CustomPhoneMenu.cs @@ -26,6 +26,17 @@ public class CustomPhoneMenuComponent(IntPtr cppPtr) : Minigame(cppPtr) ControllerManager.Instance.CloseOverlayMenu(name); } } + + [Obsolete("Should not be called, see the respective Begin methods instead.", true)] + [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Retained.")] +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member (Justification: Intentional to avoid mods calling Begin when they are not supposed to.) +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Base game code.) + public override void Begin(PlayerTask task) +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + { + throw new NotSupportedException("Should not be called, see the respective Begin methods instead."); + } } ///