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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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 df4b32b46239c9281c678a9ecc36d6f5e39587a4 Mon Sep 17 00:00:00 2001
From: Fault
Date: Sun, 13 Sep 2026 12:42:52 -0600
Subject: [PATCH 37/39] Added localization changes for menus and Modifiers mini
button in-game
Reused the Modifiers string
---
.../ModifierDisplay/ModifierDisplayComponent.cs | 4 ++--
MiraAPI/Patches/Options/MenuState.cs | 9 +++++----
MiraAPI/Resources/Locale/en_US.xml | 3 +++
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs
index 73922f3..4a35f32 100644
--- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs
+++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs
@@ -175,7 +175,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
_pagination.gameObject.SetActive(true);
if (_pageText != null)
{
- _pageText.text = $"Page {_currentPage + 1}/{Mathf.Max(totalPages, 1)}";
+ _pageText.text = $"{MiraLocaleManager.Get("MiraApi.ModifierDisplay.Page", "Page")} {_currentPage + 1}/{Mathf.Max(totalPages, 1)}";
}
_nextButton.gameObject.SetActive(true);
@@ -212,7 +212,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
_modifiers.Remove(mod.Key);
}
- _toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})";
+ _toggleBtnText.text = $"{MiraLocaleManager.Get("Modifiers", "Modifiers")} ({filteredModifiers.Count})";
_toggleButton.gameObject.SetActive(filteredModifiers.Count != 0);
diff --git a/MiraAPI/Patches/Options/MenuState.cs b/MiraAPI/Patches/Options/MenuState.cs
index 44be9a5..10eded9 100644
--- a/MiraAPI/Patches/Options/MenuState.cs
+++ b/MiraAPI/Patches/Options/MenuState.cs
@@ -262,7 +262,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr)
var smC1Btn = Instantiate(smModBtn, smModBtn.transform.parent);
_smallButtons[MenuButton.CustomOne] = smC1Btn;
smC1Btn.name = "SmallCustomOneButton";
- smC1Btn.buttonText.text = "Custom 1";
+ smC1Btn.buttonText.text = "MiraApi.CustomCategory1".Translate();
smC1Btn.OnClick = new Button.ButtonClickedEvent();
smC1Btn.OnClick.AddListener(
(UnityAction)(() =>
@@ -280,7 +280,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr)
var smC2Btn = Instantiate(smModBtn, smModBtn.transform.parent);
_smallButtons[MenuButton.CustomTwo] = smC2Btn;
smC2Btn.name = "SmallCustomTwoButton";
- smC2Btn.buttonText.text = "Custom 2";
+ smC2Btn.buttonText.text = "MiraApi.CustomCategory2".Translate();
smC2Btn.OnClick = new Button.ButtonClickedEvent();
smC2Btn.OnClick.AddListener(
(UnityAction)(() =>
@@ -382,15 +382,16 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr)
private void UpdateUi()
{
+ var pageText = "MiraApi.Page".Translate();
// Update mod text
if (CurrentModIdx == 0)
{
- _text.text = $"(Page 1/{ModCount + 1})\nMain";
+ _text.text = $"({pageText} 1/{ModCount + 1})\n{"MiraApi.Main".Translate()}";
}
else
{
var modName = CurrentMod.MiraPlugin.OptionsTitleText.Translate();
- _text.text = $"(Page {CurrentModIdx + 1}/{ModCount + 1})\n" +
+ _text.text = $"({pageText} {CurrentModIdx + 1}/{ModCount + 1})\n" +
modName[..Math.Min(modName.Length, 25)];
}
diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml
index a00952f..7cd2c1c 100644
--- a/MiraAPI/Resources/Locale/en_US.xml
+++ b/MiraAPI/Resources/Locale/en_US.xml
@@ -40,4 +40,7 @@ Outlive the timer or eliminate the crew.
Neut
Neutral Roles
Other
+ Page
+ Page
+ Main
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 38/39] 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 39/39] 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.");
+ }
}
///