From 713a7a2bcc2f3b178d032049d60ef80c0459d226 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 18 Oct 2025 12:19:15 -0500
Subject: [PATCH 01/36] Work on adding custom game tabs
---
MiraAPI/GameOptions/AbstractOptionGroup.cs | 37 +++-
MiraAPI/MiraApiPlugin.cs | 11 +
.../Patches/Options/GameOptionsMenuPatch.cs | 65 +++++-
.../Patches/Options/GameSettingMenuPatches.cs | 200 ++++++++++++++++--
4 files changed, 291 insertions(+), 22 deletions(-)
diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs
index cab7761..51e18bd 100644
--- a/MiraAPI/GameOptions/AbstractOptionGroup.cs
+++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using MiraAPI.Modifiers;
using UnityEngine;
namespace MiraAPI.GameOptions;
@@ -28,11 +29,16 @@ public abstract class AbstractOptionGroup
public virtual Type? OptionableType => null;
///
- /// Gets a value indicating whether the group should be shown in the modifiers menu.
+ /// Gets a value indicating whether the group should be shown in the modifiers menu. This is deprecated, please use ParentMenu!
///
// TODO: make this not a boolean
public virtual bool ShowInModifiersMenu => false;
+ ///
+ /// Gets a value indicating which menu the group is in.
+ ///
+ public virtual MenuCategory ParentMenu => MenuCategory.Roles;
+
///
/// Gets the function that determines whether the group should be visible or not.
///
@@ -62,4 +68,33 @@ public abstract class AbstractOptionGroup : AbstractOptionGroup where T : IOp
{
///
public override Type OptionableType => typeof(T);
+
+ ///
+ /// Gets a value indicating which menu the group is in.
+ ///
+ public override MenuCategory ParentMenu
+ {
+ get
+ {
+ var type = typeof(T);
+ if (type == typeof(BaseModifier))
+ {
+ return MenuCategory.Modifiers;
+ }
+ else if (type == typeof(RoleBehaviour))
+ {
+ return MenuCategory.Roles;
+ }
+ return MenuCategory.Game;
+ }
+ }
+}
+
+public enum MenuCategory
+{
+ Game,
+ Roles,
+ Modifiers,
+ CustomOne,
+ CustomTwo
}
diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs
index 469ffd2..74e972a 100644
--- a/MiraAPI/MiraApiPlugin.cs
+++ b/MiraAPI/MiraApiPlugin.cs
@@ -31,6 +31,17 @@ public partial class MiraApiPlugin : BasePlugin
///
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
+ ///
+ /// Gets the name for the first custom category in the game options menu, if any.
+ ///
+ public static string CustomCategoryMenuNameOne { get; } = "Custom Category 1";
+
+ ///
+ /// Gets the name for the second custom category in the game options menu, if any.
+ ///
+ public static string CustomCategoryMenuNameTwo { get; } = "Custom Category 2";
+
+
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
index 155b50d..218c6a5 100644
--- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
+++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
@@ -32,11 +32,20 @@ internal static class GameOptionsMenuPatch
{
ModifiersUpdate(ref num);
}
+ else if (__instance.name == "CUSTOM TAB 1")
+ {
+ CustomMenuOneUpdate(ref num);
+ }
+ else if (__instance.name == "CUSTOM TAB 2")
+ {
+ CustomMenuTwoUpdate(ref num);
+ }
else
{
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
- .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
+ .Where(x => x.OptionableType == null &&
+ !x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
{
@@ -99,6 +108,48 @@ internal static class GameOptionsMenuPatch
}
}
+ private static void CustomMenuOneUpdate(ref float num)
+ {
+ var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
+ .Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
+
+ foreach (var modGroup in groups)
+ {
+ UpdateGroup(modGroup, ref num);
+ }
+ }
+
+ private static void CustomMenuOneCreate(GameOptionsMenu menu)
+ {
+ var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
+ .Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
+ foreach (var group in groups)
+ {
+ CreateGroup(menu, group);
+ }
+ }
+
+ private static void CustomMenuTwoUpdate(ref float num)
+ {
+ var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
+ .Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
+
+ foreach (var modGroup in groups)
+ {
+ UpdateGroup(modGroup, ref num);
+ }
+ }
+
+ private static void CustomMenuTwoCreate(GameOptionsMenu menu)
+ {
+ var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
+ .Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
+ foreach (var group in groups)
+ {
+ CreateGroup(menu, group);
+ }
+ }
+
private static void ModifiersUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
@@ -137,9 +188,19 @@ internal static class GameOptionsMenuPatch
ModifiersCreate(__instance);
return false;
}
+ else if (__instance.name == "CUSTOM TAB 1")
+ {
+ CustomMenuOneCreate(__instance);
+ return false;
+ }
+ else if (__instance.name == "CUSTOM TAB 2")
+ {
+ CustomMenuTwoCreate(__instance);
+ return false;
+ }
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
- .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
+ .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
{
diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
index fd9c78b..72bf586 100644
--- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
+using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
@@ -27,13 +28,21 @@ internal static class GameSettingMenuPatches
private static Vector3 _roleBtnOgPos;
private static Vector3 _smallRoleBtnOgPos;
private static Vector3 _modifierBtnOgPos;
+ private static Vector3 _customOneBtnOgPos;
+ private static Vector3 _customTwoBtnOgPos;
private static GameOptionsMenu? _modifiersTab;
private static PassiveButton? _modifiersButton;
private static PassiveButton? _smallRolesButton;
+ private static GameOptionsMenu? _customOneTab;
+ private static PassiveButton? _customOneButton;
+ private static GameOptionsMenu? _customTwoTab;
+ private static PassiveButton? _customTwoButton;
private static Dictionary OptionsPositions { get; } = [];
private static Dictionary ModifiersPositions { get; } = [];
+ private static Dictionary CustomOnePositions { get; } = [];
+ private static Dictionary CustomTwoPositions { get; } = [];
private static void SaveScrollPositions(GameSettingMenu gameSettingMenu)
{
@@ -41,6 +50,14 @@ internal static class GameSettingMenuPatches
{
ModifiersPositions[SelectedModIdx] = _modifiersTab!.scrollBar.Inner.localPosition;
}
+ else if (_customOneTab)
+ {
+ CustomOnePositions[SelectedModIdx] = _customOneTab!.scrollBar.Inner.localPosition;
+ }
+ else if (_customTwoTab)
+ {
+ CustomTwoPositions[SelectedModIdx] = _customTwoTab!.scrollBar.Inner.localPosition;
+ }
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = gameSettingMenu.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionsPositions[SelectedModIdx] = gameSettingMenu.GameSettingsTab.scrollBar.Inner.localPosition;
@@ -65,9 +82,21 @@ internal static class GameSettingMenuPatches
_modifiersTab!.gameObject.SetActive(tabNum == 3);
}
+ if (_customOneTab)
+ {
+ _customOneTab!.gameObject.SetActive(tabNum == 4);
+ }
+
+ if (_customTwoTab)
+ {
+ _customTwoTab!.gameObject.SetActive(tabNum == 5);
+ }
+
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
- if (tabNum == 3)
+ _customOneButton?.SelectButton(tabNum == 4);
+ _customTwoButton?.SelectButton(tabNum == 5);
+ if (tabNum >= 3 && tabNum <= 5)
{
if (__instance.RoleSettingsButton.gameObject.active)
{
@@ -75,7 +104,18 @@ internal static class GameSettingMenuPatches
}
else
{
- _modifiersButton?.SelectButton(true);
+ switch (tabNum)
+ {
+ case 3:
+ _modifiersButton?.SelectButton(true);
+ break;
+ case 4:
+ _customOneButton?.SelectButton(true);
+ break;
+ case 5:
+ _customTwoButton?.SelectButton(true);
+ break;
+ }
}
}
}
@@ -87,19 +127,30 @@ internal static class GameSettingMenuPatches
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
+ _customOneButton?.SelectButton(tabNum == 4);
+ _customTwoButton?.SelectButton(tabNum == 5);
- if (tabNum != 3)
+ if (tabNum >= 3 && tabNum <= 5)
{
- return;
- }
-
- if (__instance.RoleSettingsButton.gameObject.active)
- {
- __instance.RoleSettingsButton.SelectButton(true);
- }
- else
- {
- _modifiersButton?.SelectButton(true);
+ if (__instance.RoleSettingsButton.gameObject.active)
+ {
+ __instance.RoleSettingsButton.SelectButton(true);
+ }
+ else
+ {
+ switch (tabNum)
+ {
+ case 3:
+ _modifiersButton?.SelectButton(true);
+ break;
+ case 4:
+ _customOneButton?.SelectButton(true);
+ break;
+ case 5:
+ _customTwoButton?.SelectButton(true);
+ break;
+ }
+ }
}
}
@@ -174,6 +225,61 @@ internal static class GameSettingMenuPatches
_modifiersTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
_modifiersTab.name = "MODIFIERS TAB";
+ // clone game settings tab for both custom tabs
+ _customOneTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
+ _customOneTab.name = "CUSTOM TAB 1";
+ _customTwoTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
+ _customTwoTab.name = "CUSTOM TAB 2";
+
+ // create button for the first custom category
+ var customPos = new Vector3(
+ __instance.RoleSettingsButton.transform.localPosition.x,
+ __instance.RoleSettingsButton.transform.localPosition.y,
+ __instance.RoleSettingsButton.transform.localPosition.z);
+ _customOneButton = Object.Instantiate(__instance.RoleSettingsButton, __instance.RoleSettingsButton.transform.parent);
+ _customOneButton.buttonText.gameObject.GetComponent().Destroy();
+ _customOneButton.OnClick = new ButtonClickedEvent();
+ _customOneButton.OnClick.AddListener(
+ (UnityAction)(() =>
+ {
+ __instance.ChangeTab(4, false);
+ }));
+ _customOneButton.OnMouseOver = new UnityEvent();
+ _customOneButton.OnMouseOver.AddListener(
+ (UnityAction)(() =>
+ {
+ __instance.ChangeTab(4, true);
+ }));
+
+ _customOneButton.buttonText.text = MiraApiPlugin.CustomCategoryMenuNameOne;
+ customPos.y += 0.5f;
+ _customOneButton.transform.localPosition = customPos;
+ _customOneButton.name = "CustomOneButton";
+
+ _customOneBtnOgPos = _customOneButton.transform.localPosition;
+
+ // create button for the first custom category
+ _customTwoButton = Object.Instantiate(_customOneButton, _customOneButton.transform.parent);
+ _customTwoButton.OnClick = new ButtonClickedEvent();
+ _customTwoButton.OnClick.AddListener(
+ (UnityAction)(() =>
+ {
+ __instance.ChangeTab(5, false);
+ }));
+ _customTwoButton.OnMouseOver = new UnityEvent();
+ _customTwoButton.OnMouseOver.AddListener(
+ (UnityAction)(() =>
+ {
+ __instance.ChangeTab(5, true);
+ }));
+
+ _customTwoButton.buttonText.text = MiraApiPlugin.CustomCategoryMenuNameTwo;
+ customPos.y += 0.5f;
+ _customTwoButton.transform.localPosition = customPos;
+ _customTwoButton.name = "CustomTwoButton";
+
+ _customTwoBtnOgPos = _customTwoButton.transform.localPosition;
+
// create button for modifiers
__instance.RoleSettingsButton.buttonText.gameObject.GetComponent().Destroy();
__instance.RoleSettingsButton.gameObject.SetActive(false);
@@ -304,21 +410,53 @@ internal static class GameSettingMenuPatches
bool replaceWithModifiers = true;
_smallRolesButton!.transform.localPosition = _smallRoleBtnOgPos;
_modifiersButton!.transform.localPosition = _modifierBtnOgPos;
+ _customOneButton!.transform.localPosition = _customOneBtnOgPos;
+ _customTwoButton!.transform.localPosition = _customTwoBtnOgPos;
menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos;
if (SelectedModIdx != 0)
{
var modHasRoles = SelectedMod!.InternalRoles.Count != 0;
+ var modHasCustomOne = SelectedMod.InternalOptionGroups.Exists(
+ x => x.ParentMenu == MenuCategory.CustomOne);
+ var modHasCustomTwo = SelectedMod.InternalOptionGroups.Exists(
+ x => x.ParentMenu == MenuCategory.CustomTwo);
var modHasModifiers = SelectedMod.InternalOptionGroups.Exists(
- x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
+ x => x.ShowInModifiersMenu || x.ParentMenu == MenuCategory.Modifiers || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
var modHasOptions =
- SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu);
+ SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && (!x.ShowInModifiersMenu ||
+ (x.ParentMenu != MenuCategory.Modifiers && x.ParentMenu != MenuCategory.Roles)));
_modifiersButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(true);
+ _customOneButton.gameObject.SetActive(true);
+ _customTwoButton.gameObject.SetActive(true);
menu.GameSettingsButton.gameObject.SetActive(true);
menu.RoleSettingsButton.gameObject.SetActive(false);
+ // If the mod has no primary custom tab, we can go ahead and hide the button.
+ if (!modHasCustomOne)
+ {
+ _customOneButton.gameObject.SetActive(false);
+ menu.RoleSettingsButton.gameObject.SetActive(true);
+
+ if (_customOneTab!.gameObject.active)
+ {
+ menu.RoleSettingsButton.SelectButton(true);
+ }
+ }
+ // If the mod has no secondary custom tab, we can go ahead and hide the button.
+ if (!modHasCustomTwo)
+ {
+ _customTwoButton.gameObject.SetActive(false);
+ menu.RoleSettingsButton.gameObject.SetActive(true);
+
+ if (_customTwoTab!.gameObject.active)
+ {
+ menu.RoleSettingsButton.SelectButton(true);
+ }
+ }
+
// If there are no registered custom roles in the selected mod, hide the button.
if (!modHasRoles)
{
@@ -394,11 +532,13 @@ internal static class GameSettingMenuPatches
{
_modifiersButton.gameObject.SetActive(false);
_smallRolesButton.gameObject.SetActive(false);
+ _customOneButton.gameObject.SetActive(false);
+ _customTwoButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(true);
menu.GameSettingsButton.gameObject.SetActive(true);
replaceWithModifiers = false;
- if (_modifiersTab!.gameObject.active)
+ if (_modifiersTab!.gameObject.active || _customOneTab!.gameObject.active || _customTwoTab!.gameObject.active)
{
menu.ChangeTab(0, false);
}
@@ -446,7 +586,17 @@ internal static class GameSettingMenuPatches
if (_modifiersTab?.Children?.Count > 0)
{
- CleanupSettings(_modifiersTab, true);
+ CleanupSettings(_modifiersTab, MenuCategory.Modifiers);
+ }
+
+ if (_customOneTab?.Children?.Count > 0)
+ {
+ CleanupSettings(_customOneTab, MenuCategory.CustomOne);
+ }
+
+ if (_customTwoTab?.Children?.Count > 0)
+ {
+ CleanupSettings(_customTwoTab, MenuCategory.CustomTwo);
}
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
@@ -484,7 +634,7 @@ internal static class GameSettingMenuPatches
}
}
- void CleanupSettings(GameOptionsMenu gameOptMenu, bool modifiers = false)
+ void CleanupSettings(GameOptionsMenu gameOptMenu, MenuCategory menuCategory = MenuCategory.Roles)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
@@ -495,7 +645,19 @@ internal static class GameSettingMenuPatches
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
- var positions = modifiers ? ModifiersPositions : OptionsPositions;
+ var positions = OptionsPositions;
+ switch (menuCategory)
+ {
+ case MenuCategory.Modifiers:
+ positions = ModifiersPositions;
+ break;
+ case MenuCategory.CustomOne:
+ positions = CustomOnePositions;
+ break;
+ case MenuCategory.CustomTwo:
+ positions = CustomTwoPositions;
+ break;
+ }
if (positions.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
From dde7e704062e5ac9c15d51dd23be16f26cd1a4ab Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 18 Oct 2025 12:32:34 -0500
Subject: [PATCH 02/36] Fix category names
---
MiraAPI/MiraApiPlugin.cs | 11 -----------
MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 6 ++++--
MiraAPI/PluginLoading/IMiraPlugin.cs | 10 ++++++++++
3 files changed, 14 insertions(+), 13 deletions(-)
diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs
index 74e972a..469ffd2 100644
--- a/MiraAPI/MiraApiPlugin.cs
+++ b/MiraAPI/MiraApiPlugin.cs
@@ -31,17 +31,6 @@ public partial class MiraApiPlugin : BasePlugin
///
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
- ///
- /// Gets the name for the first custom category in the game options menu, if any.
- ///
- public static string CustomCategoryMenuNameOne { get; } = "Custom Category 1";
-
- ///
- /// Gets the name for the second custom category in the game options menu, if any.
- ///
- public static string CustomCategoryMenuNameTwo { get; } = "Custom Category 2";
-
-
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
index 72bf586..1a3e055 100644
--- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
@@ -251,7 +251,7 @@ internal static class GameSettingMenuPatches
__instance.ChangeTab(4, true);
}));
- _customOneButton.buttonText.text = MiraApiPlugin.CustomCategoryMenuNameOne;
+ _customOneButton.buttonText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuNameOne : "Custom Category 1";
customPos.y += 0.5f;
_customOneButton.transform.localPosition = customPos;
_customOneButton.name = "CustomOneButton";
@@ -273,7 +273,7 @@ internal static class GameSettingMenuPatches
__instance.ChangeTab(5, true);
}));
- _customTwoButton.buttonText.text = MiraApiPlugin.CustomCategoryMenuNameTwo;
+ _customTwoButton.buttonText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuNameTwo : "Custom Category 2";
customPos.y += 0.5f;
_customTwoButton.transform.localPosition = customPos;
_customTwoButton.name = "CustomTwoButton";
@@ -416,6 +416,8 @@ internal static class GameSettingMenuPatches
if (SelectedModIdx != 0)
{
+ _customOneButton.buttonText.text = SelectedMod!.MiraPlugin.CustomOptionMenuNameOne;
+ _customTwoButton.buttonText.text = SelectedMod!.MiraPlugin.CustomOptionMenuNameTwo;
var modHasRoles = SelectedMod!.InternalRoles.Count != 0;
var modHasCustomOne = SelectedMod.InternalOptionGroups.Exists(
x => x.ParentMenu == MenuCategory.CustomOne);
diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs
index af3d3d5..8e2f2b6 100644
--- a/MiraAPI/PluginLoading/IMiraPlugin.cs
+++ b/MiraAPI/PluginLoading/IMiraPlugin.cs
@@ -12,6 +12,16 @@ public interface IMiraPlugin
///
string OptionsTitleText { get; }
+ ///
+ /// Gets the name for the first custom category in the game options menu, if any.
+ ///
+ public virtual string CustomOptionMenuNameOne => "Custom Category 1";
+
+ ///
+ /// Gets the name for the second custom category in the game options menu, if any.
+ ///
+ public virtual string CustomOptionMenuNameTwo => "Custom Category 2";
+
///
/// Gets the BepInEx configuration file for the plugin.
///
From 448e2f9152a253e625d4f4a1c6cce61fbc1788a7 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 18 Oct 2025 13:27:33 -0500
Subject: [PATCH 03/36] Adjust size and scale
---
.../Patches/Options/GameSettingMenuPatches.cs | 50 +++++++++----------
1 file changed, 25 insertions(+), 25 deletions(-)
diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
index 1a3e055..bec1281 100644
--- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
@@ -252,7 +252,7 @@ internal static class GameSettingMenuPatches
}));
_customOneButton.buttonText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuNameOne : "Custom Category 1";
- customPos.y += 0.5f;
+ customPos.y -= 0.637f;
_customOneButton.transform.localPosition = customPos;
_customOneButton.name = "CustomOneButton";
@@ -274,7 +274,7 @@ internal static class GameSettingMenuPatches
}));
_customTwoButton.buttonText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuNameTwo : "Custom Category 2";
- customPos.y += 0.5f;
+ customPos.y -= 0.637f;
_customTwoButton.transform.localPosition = customPos;
_customTwoButton.name = "CustomTwoButton";
@@ -435,29 +435,7 @@ internal static class GameSettingMenuPatches
_customTwoButton.gameObject.SetActive(true);
menu.GameSettingsButton.gameObject.SetActive(true);
menu.RoleSettingsButton.gameObject.SetActive(false);
-
- // If the mod has no primary custom tab, we can go ahead and hide the button.
- if (!modHasCustomOne)
- {
- _customOneButton.gameObject.SetActive(false);
- menu.RoleSettingsButton.gameObject.SetActive(true);
-
- if (_customOneTab!.gameObject.active)
- {
- menu.RoleSettingsButton.SelectButton(true);
- }
- }
- // If the mod has no secondary custom tab, we can go ahead and hide the button.
- if (!modHasCustomTwo)
- {
- _customTwoButton.gameObject.SetActive(false);
- menu.RoleSettingsButton.gameObject.SetActive(true);
-
- if (_customTwoTab!.gameObject.active)
- {
- menu.RoleSettingsButton.SelectButton(true);
- }
- }
+ var defaultButton = menu.RoleSettingsButton;
// If there are no registered custom roles in the selected mod, hide the button.
if (!modHasRoles)
@@ -474,6 +452,7 @@ internal static class GameSettingMenuPatches
{
_modifiersButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(true);
+ defaultButton = _modifiersButton;
if (_modifiersTab!.gameObject.active)
{
@@ -496,6 +475,7 @@ internal static class GameSettingMenuPatches
// If the mod has roles, we can enable the bigger role button and disable the small one.
if (modHasRoles)
{
+ defaultButton = menu.RoleSettingsButton;
menu.RoleSettingsButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(false);
}
@@ -510,6 +490,7 @@ internal static class GameSettingMenuPatches
{
menu.ChangeTab(0, false);
}
+ defaultButton = menu.RoleSettingsButton;
// If the mod has roles and modifiers, we can move their buttons to the game settings button position, since nothing is there.
if (menu.RoleSettingsButton.gameObject.active)
@@ -529,6 +510,25 @@ internal static class GameSettingMenuPatches
_smallRolesButton.transform.localPosition.z);
}
}
+
+ // If the mod has no primary custom tab, we can go ahead and hide the button.
+ if (!modHasCustomOne)
+ {
+ _customOneButton.gameObject.SetActive(false);
+ if (_customOneTab!.gameObject.active)
+ {
+ defaultButton.SelectButton(true);
+ }
+ }
+ // If the mod has no secondary custom tab, we can go ahead and hide the button.
+ if (!modHasCustomTwo)
+ {
+ _customTwoButton.gameObject.SetActive(false);
+ if (_customTwoTab!.gameObject.active)
+ {
+ defaultButton.SelectButton(true);
+ }
+ }
}
else
{
From f31349f6e8e68ce7f07262278582e9c5af74763a Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Mon, 20 Oct 2025 11:17:51 -0500
Subject: [PATCH 04/36] Add shift-intervals for options
---
.../Attributes/ModdedNumberOptionAttribute.cs | 10 ++++++-
.../GameOptions/MiraNumberOptionComponent.cs | 28 +++++++++++++++++
.../OptionTypes/ModdedEnumOption.cs | 4 +++
.../OptionTypes/ModdedNumberOption.cs | 13 ++++++++
.../OptionTypes/ModdedPlayerOption.cs | 2 ++
.../OptionTypes/ModdedToggleOption.cs | 2 ++
MiraAPI/Hud/CustomActionButton.cs | 2 +-
.../Patches/Options/GameSettingMenuPatches.cs | 30 ++++++++++---------
MiraAPI/Patches/Options/OptionsPatches.cs | 19 ++++++++++++
9 files changed, 94 insertions(+), 16 deletions(-)
create mode 100644 MiraAPI/GameOptions/MiraNumberOptionComponent.cs
diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs
index 07b5cfc..63dc82f 100644
--- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs
+++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs
@@ -21,7 +21,15 @@ public class ModdedNumberOptionAttribute(
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
- return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity);
+ return new ModdedNumberOption(
+ Title,
+ (float)(value ?? min + increment),
+ min,
+ max,
+ increment,
+ suffixType,
+ formatString,
+ zeroInfinity);
}
///
diff --git a/MiraAPI/GameOptions/MiraNumberOptionComponent.cs b/MiraAPI/GameOptions/MiraNumberOptionComponent.cs
new file mode 100644
index 0000000..a00274f
--- /dev/null
+++ b/MiraAPI/GameOptions/MiraNumberOptionComponent.cs
@@ -0,0 +1,28 @@
+using System;
+using MiraAPI.GameOptions.OptionTypes;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace MiraAPI.GameOptions;
+
+///
+/// Component added onto options generated via MiraAPI. This allows developers to hook patches onto options in general, without using a list.
+///
+[RegisterInIl2Cpp]
+public class MiraNumberOptionComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
+{
+ ///
+ /// Gets or sets the modded option associated with the object.
+ ///
+ public ModdedNumberOption NumberOption { get; set; }
+
+ ///
+ /// Gets or sets the default increment.
+ ///
+ public float DefaultIncrement { get; set; }
+
+ ///
+ /// Gets or sets the value of whether or not holding shift will split the increment in half.
+ ///
+ public bool ShiftIncrementToggle { get; set; }
+}
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs
index 0bec22f..48f7451 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs
@@ -47,6 +47,8 @@ public class ModdedEnumOption : ModdedOption
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
+ stringOption.name =
+ $"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged;
@@ -138,6 +140,8 @@ public class ModdedEnumOption : ModdedOption where T : Enum
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
+ stringOption.name =
+ $"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged;
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
index 53378e1..0cca206 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
@@ -37,6 +37,11 @@ public class ModdedNumberOption : ModdedOption
///
public bool ZeroInfinity { get; }
+ ///
+ /// Gets a value indicating whether holding shift will divide the increment.
+ ///
+ public bool ShiftIncrement { get; }
+
///
/// Initializes a new instance of the class.
///
@@ -49,6 +54,7 @@ public class ModdedNumberOption : ModdedOption
/// Optional format string for the option screen.
/// Whether zero is infinity or not.
/// Whether to include this option in the preset or not.
+ /// Whether holding shift will divide the increment or not.
public ModdedNumberOption(
string title,
float defaultValue,
@@ -65,6 +71,7 @@ public class ModdedNumberOption : ModdedOption
Increment = increment;
SuffixType = suffixType;
ZeroInfinity = zeroInfinity;
+ ShiftIncrement = increment != 1f;
Value = Mathf.Clamp(defaultValue, min, max);
@@ -97,6 +104,12 @@ public class ModdedNumberOption : ModdedOption
Transform container)
{
var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container);
+ numberOption.name =
+ $"{ParentMod!.OptionsTitleText}.NumberOption.{TranslationController.Instance.GetString(StringName)}";
+ var optionComponent = numberOption.gameObject.AddComponent();
+ optionComponent.NumberOption = this;
+ optionComponent.DefaultIncrement = Increment;
+ optionComponent.ShiftIncrementToggle = ShiftIncrement;
numberOption.SetUpFromData(Data, 20);
numberOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged;
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs
index 6069a2c..dd2f772 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs
@@ -55,6 +55,8 @@ public class ModdedPlayerOption : ModdedOption
Transform container)
{
var playerOption = Object.Instantiate(playerOpt, container);
+ playerOption.name =
+ $"{ParentMod!.OptionsTitleText}.PlayerOption.{TranslationController.Instance.GetString(StringName)}";
var filteredList = GetFilteredPlayers();
Values = filteredList.ToIl2CppList();
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs
index 2cdd0e1..0b13f97 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs
@@ -29,6 +29,8 @@ public class ModdedToggleOption : ModdedOption
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container);
+ toggleOption.name =
+ $"{ParentMod!.OptionsTitleText}.ToggleOption.{TranslationController.Instance.GetString(StringName)}";
toggleOption.SetUpFromData(Data, 20);
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 3e4bd9d..5d6b357 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -494,7 +494,7 @@ public abstract class CustomActionButton
if (Keybind != null && KeybindText != null)
{
KeybindText.text = Keybind.CurrentKey.ToString();
- KeybindIcon?.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
+ KeybindIcon?.SetActive(ActiveInputManager.currentControlType is ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton.Instance.ShowKeybinds.Value &&
Keybind.CurrentKey != KeyboardKeyCode.None);
}
diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
index bec1281..273cef2 100644
--- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
@@ -38,6 +38,8 @@ internal static class GameSettingMenuPatches
private static PassiveButton? _customOneButton;
private static GameOptionsMenu? _customTwoTab;
private static PassiveButton? _customTwoButton;
+ private static GameObject? _nextModButton;
+ private static GameObject? _previousModButton;
private static Dictionary OptionsPositions { get; } = [];
private static Dictionary ModifiersPositions { get; } = [];
@@ -178,16 +180,16 @@ internal static class GameSettingMenuPatches
_text.alignment = TextAlignmentOptions.Center;
- var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject;
- nextButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f);
- nextButton.name = "RightArrowButton";
- nextButton.transform.FindChild("Inactive").gameObject.GetComponent().sprite =
+ _nextModButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject;
+ _nextModButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f);
+ _nextModButton.name = "RightArrowButton";
+ _nextModButton.transform.FindChild("Inactive").gameObject.GetComponent().sprite =
MiraAssets.NextButton.LoadAsset();
- nextButton.transform.FindChild("Active").gameObject.GetComponent().sprite =
+ _nextModButton.transform.FindChild("Active").gameObject.GetComponent().sprite =
MiraAssets.NextButtonActive.LoadAsset();
- nextButton.gameObject.GetComponent().DestroyImmediate();
+ _nextModButton.gameObject.GetComponent().DestroyImmediate();
- var passiveButton = nextButton.gameObject.GetComponent();
+ var passiveButton = _nextModButton.gameObject.GetComponent();
passiveButton.OnClick = new ButtonClickedEvent();
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
@@ -202,13 +204,13 @@ internal static class GameSettingMenuPatches
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
- var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject;
- backButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f);
- backButton.name = "LeftArrowButton";
- backButton.gameObject.GetComponent().Destroy();
- backButton.transform.FindChild("Active").gameObject.GetComponent().flipX =
- backButton.transform.FindChild("Inactive").gameObject.GetComponent().flipX = true;
- backButton.gameObject.GetComponent().OnClick.AddListener(
+ _previousModButton = Object.Instantiate(_nextModButton, __instance.BackButton.transform.parent).gameObject;
+ _previousModButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f);
+ _previousModButton.name = "LeftArrowButton";
+ _previousModButton.gameObject.GetComponent().Destroy();
+ _previousModButton.transform.FindChild("Active").gameObject.GetComponent().flipX =
+ _previousModButton.transform.FindChild("Inactive").gameObject.GetComponent().flipX = true;
+ _previousModButton.gameObject.GetComponent().OnClick.AddListener(
(UnityAction)(() =>
{
SaveScrollPositions(__instance);
diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs
index 469a053..2cdb35f 100644
--- a/MiraAPI/Patches/Options/OptionsPatches.cs
+++ b/MiraAPI/Patches/Options/OptionsPatches.cs
@@ -14,6 +14,25 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch]
public static class OptionsPatches
{
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increment), MethodType.Getter)]
+ public static bool FloatIncrementPatch(NumberOption __instance, ref float __result)
+ {
+ if (!__instance.TryGetComponent(out var miraNumber) || !miraNumber.ShiftIncrementToggle)
+ {
+ return true;
+ }
+
+ var increment = miraNumber.DefaultIncrement;
+
+ if (Input.GetKey(KeyCode.LeftShift))
+ {
+ increment /= 2;
+ }
+
+ __result = increment;
+ return false;
+ }
[HarmonyPrefix]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseChance))]
public static bool RoleIncreaseChancePrefix(RoleOptionSetting __instance)
From ee22b776ebd522b307040ac375247c762f2badb4 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Mon, 20 Oct 2025 18:23:13 -0500
Subject: [PATCH 05/36] Fix Vanilla view pane
---
.../Patches/Options/LobbyViewPanePatches.cs | 104 +++++++++++++++++-
1 file changed, 103 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
index 901d21c..f309b6f 100644
--- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
+++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
@@ -163,7 +164,108 @@ public static class LobbyViewPanePatches
{
if (SelectedModIdx == 0)
{
- return true;
+ float num = 0.95f;
+ float num2 = -6.53f;
+ CategoryHeaderMasked categoryHeaderMasked =
+ Object.Instantiate(__instance.categoryHeaderOrigin, __instance.settingsContainer);
+ categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61);
+ categoryHeaderMasked.transform.localScale = Vector3.one;
+ categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, 1.26f, -2f);
+ __instance.settingsInfo.Add(categoryHeaderMasked.gameObject);
+ var list = new List();
+ for (int i = 0; i < 2; i++)
+ {
+ var categoryHeaderRoleVariant =
+ Object.Instantiate(__instance.categoryHeaderRoleOrigin, __instance.settingsContainer);
+ categoryHeaderRoleVariant.SetHeader(
+ (i == 0) ? StringNames.CrewmateRolesHeader : StringNames.ImpostorRolesHeader,
+ 61);
+ categoryHeaderRoleVariant.transform.localScale = Vector3.one;
+ categoryHeaderRoleVariant.transform.localPosition = new Vector3(0.09f, num, -2f);
+ __instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject);
+ num -= 0.696f;
+ var roles = RoleManager.Instance.AllRoles.ToArray().Where(x =>
+ x is not ICustomRole && x.Role != RoleTypes.Crewmate && x.Role != RoleTypes.Impostor &&
+ x.Role != RoleTypes.CrewmateGhost && x.Role != RoleTypes.ImpostorGhost).ToList();
+ for (int j = 0; j < roles.Count; j++)
+ {
+ var roleBehaviour = roles[j];
+ int chancePerGame =
+ GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(
+ roleBehaviour.Role);
+ int numPerGame =
+ GameOptionsManager.Instance.CurrentGameOptions.RoleOptions
+ .GetNumPerGame(roleBehaviour.Role);
+ bool flag = numPerGame == 0;
+ var viewSettingsInfoPanelRoleVariant =
+ Object.Instantiate(
+ __instance.infoPanelRoleOrigin,
+ __instance.settingsContainer);
+ viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
+ viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
+ if (!flag)
+ {
+ list.Add(roleBehaviour);
+ }
+
+ viewSettingsInfoPanelRoleVariant.SetInfo(
+ roleBehaviour.NiceName,
+ numPerGame,
+ chancePerGame,
+ 61,
+ (i == 0) ? Palette.CrewmateRoleBlue : Palette.ImpostorRoleRed,
+ roleBehaviour.RoleIconSolid,
+ i == 0,
+ flag);
+ __instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
+ num -= 0.664f;
+ }
+ }
+
+ if (list.Count > 0)
+ {
+ var categoryHeaderMasked2 =
+ Object.Instantiate(__instance.categoryHeaderOrigin, __instance.settingsContainer);
+ categoryHeaderMasked2.SetHeader(StringNames.RoleSettingsLabel, 61);
+ categoryHeaderMasked2.transform.localScale = Vector3.one;
+ categoryHeaderMasked2.transform.localPosition = new Vector3(-9.77f, num, -2f);
+ __instance.settingsInfo.Add(categoryHeaderMasked2.gameObject);
+ num -= 2.1f;
+ float num3 = 0f;
+ for (int k = 0; k < list.Count; k++)
+ {
+ float num4;
+ if (k % 2 == 0)
+ {
+ num4 = -5.8f;
+ if (k > 0)
+ {
+ num -= num3 + 0.85f;
+ num3 = 0f;
+ }
+ }
+ else
+ {
+ num4 = 0.14999962f;
+ }
+
+ var advancedRoleViewPanel =
+ Object.Instantiate(__instance.advancedRolePanelOrigin, __instance.settingsContainer);
+ advancedRoleViewPanel.transform.localScale = Vector3.one;
+ advancedRoleViewPanel.transform.localPosition = new Vector3(num4, num, -2f);
+ float num5 = advancedRoleViewPanel.SetUp(list[k], 0.85f, 61);
+ if (num5 > num3)
+ {
+ num3 = num5;
+ }
+
+ __instance.settingsInfo.Add(advancedRoleViewPanel.gameObject);
+ }
+ }
+
+ __instance.scrollBar.SetYBoundsMax(-num);
+
+ return false;
}
if (__instance.currentTab == ModifiersTabName)
From 43f92d4dc28fde13db2a9af874bedef864466f76 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 25 Oct 2025 12:17:44 -0500
Subject: [PATCH 06/36] Standadize button position
---
MiraAPI/Patches/HudManagerPatches.cs | 2 ++
MiraAPI/Patches/PlayerControlPatches.cs | 6 ++++--
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs
index 03657d0..adc0fc5 100644
--- a/MiraAPI/Patches/HudManagerPatches.cs
+++ b/MiraAPI/Patches/HudManagerPatches.cs
@@ -112,6 +112,8 @@ public static class HudManagerPatches
Error($"Failed to create custom button {button.GetType().Name}: {e}");
}
}
+ __instance.ImpostorVentButton.transform.SetParent(null);
+ __instance.ImpostorVentButton.transform.SetParent(BottomRight.transform);
gridArrange.Start();
gridArrange.ArrangeChilds();
diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs
index 954fd45..659c52f 100644
--- a/MiraAPI/Patches/PlayerControlPatches.cs
+++ b/MiraAPI/Patches/PlayerControlPatches.cs
@@ -127,17 +127,19 @@ internal static class PlayerControlPatches
return;
}
+ var role = __instance.Data?.Role;
foreach (var button in CustomButtonManager.CustomButtons)
{
- if (__instance.Data?.Role == null)
+ if (role == null)
{
continue;
}
try
{
- if (!button.Enabled(__instance.Data?.Role))
+ if (!button.Enabled(role))
{
+ button.SetActive(false, role);
continue;
}
From 7fa75946fda344bfb9204eda11f388ac8196ca89 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 25 Oct 2025 18:51:14 -0500
Subject: [PATCH 07/36] Make ResetCooldowns callable
---
MiraAPI/Patches/Hud/ButtonResetPatches.cs | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/MiraAPI/Patches/Hud/ButtonResetPatches.cs b/MiraAPI/Patches/Hud/ButtonResetPatches.cs
index 902e140..5e46048 100644
--- a/MiraAPI/Patches/Hud/ButtonResetPatches.cs
+++ b/MiraAPI/Patches/Hud/ButtonResetPatches.cs
@@ -9,6 +9,11 @@ namespace MiraAPI.Patches.Hud;
public static class ButtonResetPatches
{
public static void Postfix()
+ {
+ ResetCooldowns();
+ }
+
+ public static void ResetCooldowns()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
From 4ce24c2a00c3b36a484bea4c066164812de7ce13 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 26 Oct 2025 12:20:02 -0500
Subject: [PATCH 08/36] Fix warning on epic games
---
MiraAPI/Patches/Options/OptionsPatches.cs | 41 +++++++++++------------
1 file changed, 20 insertions(+), 21 deletions(-)
diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs
index 2cdb35f..0f99691 100644
--- a/MiraAPI/Patches/Options/OptionsPatches.cs
+++ b/MiraAPI/Patches/Options/OptionsPatches.cs
@@ -14,25 +14,6 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch]
public static class OptionsPatches
{
- [HarmonyPrefix]
- [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increment), MethodType.Getter)]
- public static bool FloatIncrementPatch(NumberOption __instance, ref float __result)
- {
- if (!__instance.TryGetComponent(out var miraNumber) || !miraNumber.ShiftIncrementToggle)
- {
- return true;
- }
-
- var increment = miraNumber.DefaultIncrement;
-
- if (Input.GetKey(KeyCode.LeftShift))
- {
- increment /= 2;
- }
-
- __result = increment;
- return false;
- }
[HarmonyPrefix]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseChance))]
public static bool RoleIncreaseChancePrefix(RoleOptionSetting __instance)
@@ -235,7 +216,16 @@ public static class OptionsPatches
return true;
}
- __instance.Value += __instance.Increment;
+ var increment = __instance.Increment;
+
+ if (Input.GetKey(KeyCode.LeftShift) &&
+ __instance.TryGetComponent(out var miraNumber) &&
+ miraNumber.ShiftIncrementToggle)
+ {
+ increment /= 2;
+ }
+
+ __instance.Value += increment;
if (__instance.Value > __instance.ValidRange.max)
{
__instance.Value = __instance.ValidRange.min;
@@ -254,7 +244,16 @@ public static class OptionsPatches
return true;
}
- __instance.Value -= __instance.Increment;
+ var increment = __instance.Increment;
+
+ if (Input.GetKey(KeyCode.LeftShift) &&
+ __instance.TryGetComponent(out var miraNumber) &&
+ miraNumber.ShiftIncrementToggle)
+ {
+ increment /= 2;
+ }
+
+ __instance.Value -= increment;
if (__instance.Value < __instance.ValidRange.min)
{
__instance.Value = __instance.ValidRange.max;
From 7d1976f7729893e4671eb2668cf5a70488cea7a5 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 26 Oct 2025 13:15:20 -0500
Subject: [PATCH 09/36] Relocale keybinds toggle to local options for epic
---
MiraAPI/MiraApiSettings.cs | 2 ++
MiraAPI/Patches/Keybinds/ControlMapperPatches.cs | 5 +++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs
index 59eb6a4..9ad9e3a 100644
--- a/MiraAPI/MiraApiSettings.cs
+++ b/MiraAPI/MiraApiSettings.cs
@@ -30,7 +30,9 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
///
/// Gets whether to show keybinds in the control mapper.
///
+ [LocalToggleSetting]
public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true);
+ // This would be placed in the keybinds menu, but it crashes for Epic Games users. - Atony
///
/// Gets whether to apply cosmetic changes to the TaskAdder.
diff --git a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs
index ae46734..f0c51c0 100644
--- a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs
+++ b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs
@@ -15,7 +15,8 @@ namespace MiraAPI.Patches.Keybinds;
[HarmonyPatch(typeof(ControlMapper))]
public static class ControlMapperPatches
{
- [HarmonyPrefix]
+ // Patching this crashes for Epic Games users
+ /*[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Start))]
private static void StartPrefix(ControlMapper __instance)
{
@@ -37,7 +38,7 @@ public static class ControlMapperPatches
entry.Value = !entry.Value;
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
}));
- }
+ }*/
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Update))]
From baf80618bb12561eca9c7380ea5f27a24a225834 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 26 Oct 2025 13:19:22 -0500
Subject: [PATCH 10/36] Rename keybinds option
---
MiraAPI/MiraApiSettings.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs
index 9ad9e3a..71426ab 100644
--- a/MiraAPI/MiraApiSettings.cs
+++ b/MiraAPI/MiraApiSettings.cs
@@ -31,7 +31,7 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
/// Gets whether to show keybinds in the control mapper.
///
[LocalToggleSetting]
- public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true);
+ public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds on Buttons", true);
// This would be placed in the keybinds menu, but it crashes for Epic Games users. - Atony
///
From bf9b34e728c44e00dc6ed14d59e5cdabaf10bd4c Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 26 Oct 2025 13:36:35 -0500
Subject: [PATCH 11/36] Fix modded keybinds not being hidden on buttons
---
MiraAPI/Hud/CustomActionButton.cs | 1 +
MiraAPI/MiraApiSettings.cs | 2 +-
MiraAPI/Patches/HudManagerPatches.cs | 12 ++++++++++--
3 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 5d6b357..43c11df 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -217,6 +217,7 @@ public abstract class CustomActionButton
new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f)
);
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent();
+ HudManagerPatches.moddedKeybindIcons.Add(KeybindText);
Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f);
}
}
diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs
index 71426ab..397caa6 100644
--- a/MiraAPI/MiraApiSettings.cs
+++ b/MiraAPI/MiraApiSettings.cs
@@ -28,7 +28,7 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false);
///
- /// Gets whether to show keybinds in the control mapper.
+ /// Gets whether to show keybinds on buttons.
///
[LocalToggleSetting]
public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds on Buttons", true);
diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs
index adc0fc5..17fb2bd 100644
--- a/MiraAPI/Patches/HudManagerPatches.cs
+++ b/MiraAPI/Patches/HudManagerPatches.cs
@@ -27,6 +27,8 @@ public static class HudManagerPatches
private static Dictionary vanillaKeybindIcons = new();
+ internal static List moddedKeybindIcons = new();
+
/*
///
/// Trigger hudstart on current custom gamemode
@@ -88,6 +90,7 @@ public static class HudManagerPatches
fakeButton.ToggleVisible(true);
fakeButton.Destroy();
}
+ moddedKeybindIcons = [];
foreach (var button in CustomButtonManager.CustomButtons)
{
@@ -182,11 +185,16 @@ public static class HudManagerPatches
[HarmonyPostfix]
public static void UpdatePostfix()
{
+ var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
+ LocalSettingsTabSingleton.Instance.ShowKeybinds.Value;
foreach (var btnIcon in vanillaKeybindIcons)
{
btnIcon.Key.text = KeybindUtils.GetKeycodeByActionId(btnIcon.Value).ToString();
- btnIcon.Key.transform.parent.gameObject.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
- LocalSettingsTabSingleton.Instance.ShowKeybinds.Value);
+ btnIcon.Key.transform.parent.gameObject.SetActive(canSeeBinds);
+ }
+ foreach (var btnIcon in moddedKeybindIcons)
+ {
+ btnIcon.transform.parent.gameObject.SetActive(canSeeBinds);
}
var player = ReInput.players.GetPlayer(0);
From e1ca50be3ec9588637e17df5bf69aaefaf8bbd78 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 1 Nov 2025 10:00:42 -0500
Subject: [PATCH 12/36] Vanilla roles no longer bypass murder cancels
---
MiraAPI/Networking/CustomMurderRpc.cs | 2 +-
MiraAPI/Patches/Roles/KillButtonPatches.cs | 7 +------
2 files changed, 2 insertions(+), 7 deletions(-)
diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs
index f94223b..710791a 100644
--- a/MiraAPI/Networking/CustomMurderRpc.cs
+++ b/MiraAPI/Networking/CustomMurderRpc.cs
@@ -87,11 +87,11 @@ public static class CustomMurderRpc
bool playKillSound = true)
{
source.isKilling = false;
- var data = target.Data;
if (resultFlags.HasFlag(MurderResultFlags.FailedError))
{
return;
}
+ var data = target.Data;
if (resultFlags.HasFlag(MurderResultFlags.FailedProtected) ||
(resultFlags.HasFlag(MurderResultFlags.DecisionByHost) && target.protectedByGuardianId > -1))
diff --git a/MiraAPI/Patches/Roles/KillButtonPatches.cs b/MiraAPI/Patches/Roles/KillButtonPatches.cs
index 902a9e6..4001425 100644
--- a/MiraAPI/Patches/Roles/KillButtonPatches.cs
+++ b/MiraAPI/Patches/Roles/KillButtonPatches.cs
@@ -46,7 +46,7 @@ public static class KillButtonPatches
}
///
- /// Use Custom Murder if player is custom role.
+ /// Use Custom Murder instead of Murder Player from vanilla.
///
[HarmonyPrefix]
[HarmonyPatch(nameof(KillButton.DoClick))]
@@ -58,11 +58,6 @@ public static class KillButtonPatches
return false;
}
- if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole)
- {
- return true;
- }
-
PlayerControl.LocalPlayer.RpcCustomMurder(__instance.currentTarget);
__instance.SetTarget(null);
From fdc715dac207788287c38df7f0b9385136d22b97 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Fri, 7 Nov 2025 17:26:06 -0600
Subject: [PATCH 13/36] Expose data for option names
---
MiraAPI/GameOptions/IModdedOption.cs | 4 ++--
.../OptionTypes/ModdedNumberOption.cs | 1 -
.../GameOptions/OptionTypes/ModdedOption.cs | 2 +-
MiraAPI/Hud/CustomActionButton.cs | 2 +-
MiraAPI/Keybinds/VanillaKeybind.cs | 2 +-
MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 23 ++++++++++---------
MiraAPI/Patches/HudManagerPatches.cs | 6 ++---
MiraAPI/Roles/CustomRoleConfiguration.cs | 3 ++-
8 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs
index 947b6ff..5a3c3f8 100644
--- a/MiraAPI/GameOptions/IModdedOption.cs
+++ b/MiraAPI/GameOptions/IModdedOption.cs
@@ -17,9 +17,9 @@ public interface IModdedOption
uint Id { get; }
///
- /// Gets the title of the option.
+ /// Gets or sets the title of the option.
///
- string Title { get; }
+ string Title { get; set; }
///
/// Gets the StringName for the option, used for localization.
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
index 0cca206..04cfd09 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
@@ -54,7 +54,6 @@ public class ModdedNumberOption : ModdedOption
/// Optional format string for the option screen.
/// Whether zero is infinity or not.
/// Whether to include this option in the preset or not.
- /// Whether holding shift will divide the increment or not.
public ModdedNumberOption(
string title,
float defaultValue,
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
index 392c76f..206894d 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
@@ -21,7 +21,7 @@ public abstract class ModdedOption : IModdedOption
public uint Id { get; }
///
- public string Title { get; }
+ public string Title { get; set; }
///
public StringNames StringName { get; }
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 43c11df..2fd5c63 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -217,7 +217,7 @@ public abstract class CustomActionButton
new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f)
);
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent();
- HudManagerPatches.moddedKeybindIcons.Add(KeybindText);
+ HudManagerPatches.ModdedKeybindIcons.Add(KeybindText);
Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f);
}
}
diff --git a/MiraAPI/Keybinds/VanillaKeybind.cs b/MiraAPI/Keybinds/VanillaKeybind.cs
index c04b83b..8911b49 100644
--- a/MiraAPI/Keybinds/VanillaKeybind.cs
+++ b/MiraAPI/Keybinds/VanillaKeybind.cs
@@ -19,6 +19,6 @@ public class VanillaKeybind : BaseKeybind
public VanillaKeybind(int id)
{
RewiredInputAction = KeybindUtils.GetInputActionById(id);
- Id = RewiredInputAction?.name;
+ Id = RewiredInputAction != null ? RewiredInputAction.name : id.ToString();
}
}
diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
index 78fa445..5bee1ae 100644
--- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
+++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
@@ -21,10 +21,14 @@ using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Freeplay;
[HarmonyPatch(typeof(TaskAdderGame))]
-internal static class TaskAdderPatches
+public static class TaskAdderPatches
{
private static Scroller _scroller = null!;
private static Dictionary folders = new();
+ public static string CrewmateName => TranslationController.Instance.GetString(StringNames.Crewmate);
+ public static string ImpostorName => TranslationController.Instance.GetString(StringNames.Impostor);
+ public static string NeutralName => "Neutral";
+ public static string ModifiersName => "Modifiers";
[HarmonyPostfix]
[HarmonyPatch(nameof(TaskAdderGame.Begin))]
@@ -66,19 +70,16 @@ internal static class TaskAdderPatches
__instance.RootFolderPrefab.GetComponent().ClickMask = collider;
__instance.RootFolderPrefab.gameObject.SetActive(false);
__instance.TaskParent = inner.transform;
- var crewLocale = TranslationController.Instance.GetString(StringNames.Crewmate);
- var impLocale = TranslationController.Instance.GetString(StringNames.Impostor);
-
- var crewmateFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == crewLocale)!;
- var impostorFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == impLocale)!;
+ var crewmateFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == CrewmateName)!;
+ var impostorFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == ImpostorName)!;
//var neutralFolder = __instance.CreateFolder("Neutral", __instance.Root, 2, Color.gray);
- var modifiersFolder = __instance.CreateFolder("Modifiers", __instance.Root, 0, Color.blue);
+ var modifiersFolder = __instance.CreateFolder(ModifiersName, __instance.Root, 0, Color.blue);
folders.Clear();
folders.Add(crewmateFolder.FolderName, crewmateFolder);
folders.Add(impostorFolder.FolderName, impostorFolder);
//folders.Add("Neutrals", neutralFolder);
- folders.Add("Modifiers", modifiersFolder);
+ folders.Add(ModifiersName, modifiersFolder);
int folderIdx = 2;
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
@@ -137,8 +138,8 @@ internal static class TaskAdderPatches
Object.Instantiate(__instance.RootFolderPrefab, __instance.transform);
impFolder.gameObject.SetActive(false);
impFolder.SetFolderColor(TaskFolder.FolderColor.Red);
- crewFolder.FolderName = TranslationController.Instance.GetString(StringNames.Crewmate);
- impFolder.FolderName = TranslationController.Instance.GetString(StringNames.Impostor);
+ crewFolder.FolderName = CrewmateName;
+ impFolder.FolderName = ImpostorName;
rootFolder.SubFolders.Insert(0, crewFolder);
rootFolder.SubFolders.Insert(0, impFolder);
Il2CppSystem.Collections.Generic.List impRoles = new();
@@ -440,7 +441,7 @@ internal static class TaskAdderPatches
}
}
- if (folders.TryGetValue("Modifiers", out var modifiersFolder) && taskFolder.IsChildOf(modifiersFolder))
+ if (folders.TryGetValue(ModifiersName, out var modifiersFolder) && taskFolder.IsChildOf(modifiersFolder))
{
var plugin = MiraPluginManager.GetPluginByGuid(taskFolder.name.Replace("(Clone)", string.Empty));
if (plugin != null)
diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs
index 17fb2bd..8a40b85 100644
--- a/MiraAPI/Patches/HudManagerPatches.cs
+++ b/MiraAPI/Patches/HudManagerPatches.cs
@@ -27,7 +27,7 @@ public static class HudManagerPatches
private static Dictionary vanillaKeybindIcons = new();
- internal static List moddedKeybindIcons = new();
+ internal static List ModdedKeybindIcons = new();
/*
///
@@ -90,7 +90,7 @@ public static class HudManagerPatches
fakeButton.ToggleVisible(true);
fakeButton.Destroy();
}
- moddedKeybindIcons = [];
+ ModdedKeybindIcons = [];
foreach (var button in CustomButtonManager.CustomButtons)
{
@@ -192,7 +192,7 @@ public static class HudManagerPatches
btnIcon.Key.text = KeybindUtils.GetKeycodeByActionId(btnIcon.Value).ToString();
btnIcon.Key.transform.parent.gameObject.SetActive(canSeeBinds);
}
- foreach (var btnIcon in moddedKeybindIcons)
+ foreach (var btnIcon in ModdedKeybindIcons)
{
btnIcon.transform.parent.gameObject.SetActive(canSeeBinds);
}
diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs
index d93fbf2..b1dfae1 100644
--- a/MiraAPI/Roles/CustomRoleConfiguration.cs
+++ b/MiraAPI/Roles/CustomRoleConfiguration.cs
@@ -1,4 +1,5 @@
using AmongUs.GameOptions;
+using MiraAPI.Patches.Freeplay;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -36,7 +37,7 @@ public record struct CustomRoleConfiguration
{
ModdedRoleTeams.Crewmate => TranslationController.Instance.GetString(StringNames.Crewmate),
ModdedRoleTeams.Impostor => TranslationController.Instance.GetString(StringNames.Impostor),
- _ => "Neutral",
+ _ => TaskAdderPatches.NeutralName,
};
IntroSound = role.Team is ModdedRoleTeams.Crewmate
? CustomRoleManager.CrewmateIntroSound
From 8b23021b2a318becf74c1a35e509eaa9ff0714b1 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 16 Nov 2025 08:41:29 -0600
Subject: [PATCH 14/36] Fix null error for player without assigned role
---
MiraAPI/Patches/Roles/NameTagPatch.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/Patches/Roles/NameTagPatch.cs b/MiraAPI/Patches/Roles/NameTagPatch.cs
index 7974351..f373bdf 100644
--- a/MiraAPI/Patches/Roles/NameTagPatch.cs
+++ b/MiraAPI/Patches/Roles/NameTagPatch.cs
@@ -20,7 +20,7 @@ public static class NameTagPatch
return false;
}
- if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && otherPlayerRole.IsImpostor)
+ if (PlayerControl.LocalPlayer.Data?.Role?.IsImpostor == true && otherPlayerRole.IsImpostor)
{
return true;
}
@@ -30,7 +30,7 @@ public static class NameTagPatch
return true;
}
- if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data || !PlayerControl.LocalPlayer.Data.Role || !otherPlayerRole)
+ if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data || !PlayerControl.LocalPlayer.Data?.Role || !otherPlayerRole)
{
__result = Color.white;
}
From fe1580bb7ed97da5948be78b8b3b37c9321797ad Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Mon, 17 Nov 2025 18:45:32 -0600
Subject: [PATCH 15/36] Fix double round events on Airship & Submerged
---
MiraAPI/Patches/Events/SpawnInMinigamePatch.cs | 4 +++-
MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs | 4 +++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs b/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs
index 65768d4..2344097 100644
--- a/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs
+++ b/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs
@@ -1,4 +1,5 @@
-using HarmonyLib;
+/*
+using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
@@ -18,3 +19,4 @@ internal static class SpawnInMinigamePatch
MiraEventManager.InvokeEvent(@event2);
}
}
+*/
diff --git a/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs b/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs
index 07c358a..3a06abb 100644
--- a/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs
+++ b/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+/*
+using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
@@ -34,3 +35,4 @@ internal static class SubmergedSpawnGamePatch
MiraEventManager.InvokeEvent(@event2);
}
}
+*/
From 3f60bcae2ccef0fe57f3ec05cc909834e923443b Mon Sep 17 00:00:00 2001
From: XtraCube <72575280+XtraCube@users.noreply.github.com>
Date: Thu, 20 Nov 2025 10:18:39 -0500
Subject: [PATCH 16/36] default constructor not supported for custom role
config
---
MiraAPI/Roles/CustomRoleConfiguration.cs | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs
index d93fbf2..1470aa5 100644
--- a/MiraAPI/Roles/CustomRoleConfiguration.cs
+++ b/MiraAPI/Roles/CustomRoleConfiguration.cs
@@ -1,4 +1,5 @@
-using AmongUs.GameOptions;
+using System;
+using AmongUs.GameOptions;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -9,6 +10,16 @@ namespace MiraAPI.Roles;
///
public record struct CustomRoleConfiguration
{
+#pragma warning disable S1133
+ [Obsolete("Default constructor is not supported. Please use the constructor that takes an ICustomRole parameter.")]
+#pragma warning restore S1133
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ public CustomRoleConfiguration()
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+ {
+ throw new NotImplementedException("Default constructor is not supported. Please use the constructor that takes an ICustomRole parameter.");
+ }
+
///
/// Initializes a new instance of the struct.
///
From 1fd2dd3539fd0a988eababade5b499165ffdb50b Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Thu, 20 Nov 2025 21:39:45 -0600
Subject: [PATCH 17/36] Allow mods to blacklist vanilla roles
---
MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 3 ++-
.../Patches/Options/RoleSettingMenuPatches.cs | 4 ++--
MiraAPI/Utilities/Helpers.cs | 16 ++++++++++++++++
3 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
index f309b6f..79164cc 100644
--- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
+++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
@@ -185,7 +185,8 @@ public static class LobbyViewPanePatches
__instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject);
num -= 0.696f;
var roles = RoleManager.Instance.AllRoles.ToArray().Where(x =>
- x is not ICustomRole && x.Role != RoleTypes.Crewmate && x.Role != RoleTypes.Impostor &&
+ x is not ICustomRole && !x.IsRoleBlacklisted() && x.Role != RoleTypes.Crewmate &&
+ x.Role != RoleTypes.Impostor &&
x.Role != RoleTypes.CrewmateGhost && x.Role != RoleTypes.ImpostorGhost).ToList();
for (int j = 0; j < roles.Count; j++)
{
diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
index 78e720c..17913d5 100644
--- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
@@ -74,10 +74,10 @@ public static class RoleSettingMenuPatches
var num = 0.662f;
var list = CustomRoleManager.AllRoles.Where(
- r => !r.IsCustomRole() && r.TeamType == RoleTeamTypes.Crewmate && r.Role != RoleTypes.Crewmate &&
+ r => !r.IsCustomRole() && !r.IsRoleBlacklisted() && r.TeamType == RoleTeamTypes.Crewmate && r.Role != RoleTypes.Crewmate &&
r.Role != RoleTypes.CrewmateGhost).ToList();
var list2 = CustomRoleManager.AllRoles.Where(
- r => !r.IsCustomRole() && r.TeamType == RoleTeamTypes.Impostor && r.Role != RoleTypes.Impostor &&
+ r => !r.IsCustomRole() && !r.IsRoleBlacklisted() && r.TeamType == RoleTeamTypes.Impostor && r.Role != RoleTypes.Impostor &&
r.Role != RoleTypes.ImpostorGhost).ToList();
if (__instance.roleTabs == null || __instance.roleTabs.Count == 0)
diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs
index 3b007a6..c89fc74 100644
--- a/MiraAPI/Utilities/Helpers.cs
+++ b/MiraAPI/Utilities/Helpers.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Roles;
@@ -536,4 +537,19 @@ public static class Helpers
}
return role.NiceName;
}
+
+ ///
+ /// Returns whether the role is blacklisted from appearing and spawning. (Only applicable to vanilla roles)
+ ///
+ /// The role to check.
+ /// The role's blacklist status.
+ public static bool IsRoleBlacklisted(this RoleBehaviour role)
+ {
+ // This should be patchable by mods when a vanilla role is meant to be replaced by a custom role.
+ if (role.Role is RoleTypes.Engineer)
+ {
+ return true;
+ }
+ return false;
+ }
}
From 7e3a279b56d9e18b83dde276455ee1d02679447b Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Thu, 20 Nov 2025 21:51:39 -0600
Subject: [PATCH 18/36] Fix imps and crew being shown twice in view pane
---
MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
index 79164cc..13f1f5a 100644
--- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
+++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
@@ -186,7 +186,8 @@ public static class LobbyViewPanePatches
num -= 0.696f;
var roles = RoleManager.Instance.AllRoles.ToArray().Where(x =>
x is not ICustomRole && !x.IsRoleBlacklisted() && x.Role != RoleTypes.Crewmate &&
- x.Role != RoleTypes.Impostor &&
+ x.Role != RoleTypes.Impostor && ((i == 0 && x.TeamType is RoleTeamTypes.Crewmate) ||
+ (i == 1 && x.TeamType is RoleTeamTypes.Impostor)) &&
x.Role != RoleTypes.CrewmateGhost && x.Role != RoleTypes.ImpostorGhost).ToList();
for (int j = 0; j < roles.Count; j++)
{
From 259b9b7097fbe4311cf45eb33149c351db6257c1 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Fri, 21 Nov 2025 18:23:08 -0600
Subject: [PATCH 19/36] Add logic for modifiers & roles in hide and seek
---
MiraAPI/Modifiers/ModifierManager.cs | 3 +-
MiraAPI/Modifiers/Types/GameModifier.cs | 6 +
.../Roles/LogicRoleSelectionHnsPatch.cs | 179 ++++++++++++++++++
.../Roles/LogicRoleSelectionNormalPatch.cs | 86 +++++++++
MiraAPI/Patches/Roles/RoleManagerPatches.cs | 1 +
MiraAPI/PluginLoading/MiraPluginInfo.cs | 2 +-
MiraAPI/Presets/PresetManager.cs | 64 ++++++-
MiraAPI/Roles/CustomRoleUtils.cs | 59 +++++-
MiraAPI/Roles/ICustomRole.cs | 6 +
9 files changed, 402 insertions(+), 4 deletions(-)
create mode 100644 MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs
create mode 100644 MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs
diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs
index 71c0dc7..2456af0 100644
--- a/MiraAPI/Modifiers/ModifierManager.cs
+++ b/MiraAPI/Modifiers/ModifierManager.cs
@@ -179,6 +179,7 @@ public static class ModifierManager
private static bool IsGameModifierValid(PlayerControl player, GameModifier modifier, uint modifierId)
{
return (player.Data.Role is not ICustomRole role || role.IsModifierApplicable(modifier)) &&
- modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifier(modifierId);
+ modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifier(modifierId) &&
+ modifier.CanSpawnOnCurrentMode();
}
}
diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs
index 13f6754..144ff51 100644
--- a/MiraAPI/Modifiers/Types/GameModifier.cs
+++ b/MiraAPI/Modifiers/Types/GameModifier.cs
@@ -39,4 +39,10 @@ public abstract class GameModifier : BaseModifier
/// The reason why the game ended.
/// True if the player won, false if they lost. Return null to use the player's default win condition.
public virtual bool? DidWin(GameOverReason reason) => null;
+
+ ///
+ /// Determines whether the modifier can spawn in general, accounting for gamemodes and everything else.
+ ///
+ /// True if the modifier is able to spawn, otherwise false.
+ public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek();
}
diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs
new file mode 100644
index 0000000..7789180
--- /dev/null
+++ b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs
@@ -0,0 +1,179 @@
+using AmongUs.GameOptions;
+using HarmonyLib;
+using Il2CppSystem.Collections.Generic;
+using System.Linq;
+using MiraAPI.Roles;
+
+namespace MiraAPI.Patches.Roles;
+
+[HarmonyPatch(typeof(LogicRoleSelectionHnS))]
+public static class LogicRoleSelectionHnsPatch
+{
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(LogicRoleSelectionHnS.AssignRolesForTeam))]
+ public static bool AssignRolesForTeam(
+ LogicRoleSelectionHnS __instance,
+ List players,
+ IGameOptions opts,
+ RoleTeamTypes team,
+ int teamMax,
+ Il2CppSystem.Nullable defaultRole)
+ {
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}");
+ int num = 0;
+ IRoleOptionsCollection roleOptions = opts.RoleOptions;
+ var source = RoleManager.Instance.AllRoles.ToArray()
+ .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) &&
+ CustomRoleUtils.CanSpawnOnCurrentMode(role));
+ var assignmentData = source.Where(x => !x.IsDead).Select(role =>
+ new RoleManager.RoleAssignmentData(
+ role,
+ roleOptions.GetNumPerGame(role.Role),
+ roleOptions.GetChancePerGame(role.Role))).ToList();
+ var source2 = CustomRoleUtils.GetPossibleRoles(assignmentData, x => x.Chance == 100);
+ var guaranteedRoles = source.Where(x => source2.Contains(((ushort)x.Role, 100)));
+ List list = new List();
+ if (team == RoleTeamTypes.Crewmate)
+ {
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment");
+ foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
+ new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
+ {
+ for (;;)
+ {
+ RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
+ int count = roleAssignmentData2.Count;
+ roleAssignmentData2.Count = count - 1;
+ if (count <= 0)
+ {
+ break;
+ }
+
+ list.Add(roleAssignmentData.Role.Role);
+ }
+ }
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment");
+ AssignRolesFromList(players, teamMax, list, ref num);
+
+ var list2 = source.Where(x => !x.IsDead).Select(role =>
+ new RoleManager.RoleAssignmentData(
+ role,
+ roleOptions.GetNumPerGame(role.Role),
+ roleOptions.GetChancePerGame(role.Role))).ToList();
+
+ list.Clear();
+ foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2)
+ {
+ for (int i = 0; i < roleAssignmentData3.Count; i++)
+ {
+ if (HashRandom.Next(101) < roleAssignmentData3.Chance)
+ {
+ list.Add(roleAssignmentData3.Role.Role);
+ }
+ }
+ }
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Potential Assignment");
+
+ AssignRolesFromList(players, teamMax, list, ref num);
+ var basicRole = RoleTypes.Engineer;
+ while (list.Count < players.Count && list.Count + num < teamMax)
+ {
+ list.Add(basicRole);
+ }
+
+ AssignRolesFromList(players, teamMax, list, ref num);
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Fallback Assignment");
+ }
+ else if (team == RoleTeamTypes.Impostor)
+ {
+ var newImpostors = new List();
+ // Specified Seeker
+ if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() &&
+ __instance.hnsManager.LogicOptionsHnS.ValidateImpostorPlayerID(players) &&
+ !AmongUsClient.Instance.IsGamePublic)
+ {
+ NetworkedPlayerInfo networkedPlayerInfo = players.ToArray()
+ .First(p => p.PlayerId == __instance.hnsManager.LogicOptionsHnS.ImpostorPlayerID());
+ players.Remove(networkedPlayerInfo);
+ newImpostors.Add(networkedPlayerInfo);
+ }
+ // Random Seeker
+ else
+ {
+ int num2 = 0;
+ while (num2 < teamMax && players.Count > 0)
+ {
+ PseudoRandomList pseudoRandomList = new PseudoRandomList(AmongUsClient.Instance.GameId);
+ players._items.Do(x => pseudoRandomList.Add(x));
+ for (int i = 0; i < GameData.RoundsPlayedInSession; i++)
+ {
+ pseudoRandomList.PickRandom();
+ }
+ NetworkedPlayerInfo networkedPlayerInfo = pseudoRandomList.PickRandom();
+ players.Remove(networkedPlayerInfo);
+ newImpostors.Add(networkedPlayerInfo);
+ num2++;
+ }
+ }
+ foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
+ new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
+ {
+ for (;;)
+ {
+ RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
+ int count = roleAssignmentData2.Count;
+ roleAssignmentData2.Count = count - 1;
+ if (count <= 0)
+ {
+ break;
+ }
+
+ list.Add(roleAssignmentData.Role.Role);
+ }
+ }
+ AssignRolesFromList(players, teamMax, list, ref num);
+
+ var list2 = source.Where(x => !x.IsDead).Select(role =>
+ new RoleManager.RoleAssignmentData(
+ role,
+ roleOptions.GetNumPerGame(role.Role),
+ roleOptions.GetChancePerGame(role.Role))).ToList();
+
+ list.Clear();
+ foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2)
+ {
+ for (int i = 0; i < roleAssignmentData3.Count; i++)
+ {
+ if (HashRandom.Next(101) < roleAssignmentData3.Chance)
+ {
+ list.Add(roleAssignmentData3.Role.Role);
+ }
+ }
+ }
+
+ AssignRolesFromList(players, teamMax, list, ref num);
+ var basicRole = RoleTypes.Impostor;
+ while (list.Count < players.Count && list.Count + num < teamMax)
+ {
+ list.Add(basicRole);
+ }
+
+ AssignRolesFromList(players, teamMax, list, ref num);
+ }
+ return false;
+ }
+
+ public static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned)
+ {
+ while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax)
+ {
+ int index = HashRandom.FastNext(roleList.Count);
+ RoleTypes roleType = roleList[index];
+ roleList.RemoveAt(index);
+ int index2 = HashRandom.FastNext(players.Count);
+ players[index2].Object.RpcSetRole(roleType, false);
+ players.RemoveAt(index2);
+ rolesAssigned++;
+ }
+ }
+}
diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs
new file mode 100644
index 0000000..d32f0dd
--- /dev/null
+++ b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs
@@ -0,0 +1,86 @@
+using AmongUs.GameOptions;
+using HarmonyLib;
+using Il2CppSystem.Collections.Generic;
+using System.Linq;
+using MiraAPI.Roles;
+
+namespace MiraAPI.Patches.Roles;
+
+[HarmonyPatch(typeof(LogicRoleSelectionNormal))]
+public static class LogicRoleSelectionNormalPatch
+{
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(LogicRoleSelectionNormal.AssignRolesForTeam))]
+ public static bool AssignRolesForTeam(
+ LogicRoleSelectionNormal __instance,
+ List players,
+ IGameOptions opts,
+ RoleTeamTypes team,
+ int teamMax,
+ Il2CppSystem.Nullable defaultRole)
+ {
+ int num = 0;
+ var source = RoleManager.Instance.AllRoles.ToArray()
+ .Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) &&
+ CustomRoleUtils.CanSpawnOnCurrentMode(role));
+ List list = new List();
+ IRoleOptionsCollection roleOptions = opts.RoleOptions;
+
+ var assignmentData = source.Where(x => !x.IsDead).Select(role =>
+ new RoleManager.RoleAssignmentData(
+ role,
+ roleOptions.GetNumPerGame(role.Role),
+ roleOptions.GetChancePerGame(role.Role))).ToList();
+ var source2 = CustomRoleUtils.GetPossibleRoles(assignmentData, x => x.Chance == 100);
+ var guaranteedRoles = source.Where(x => source2.Contains(((ushort)x.Role, 100)));
+ foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
+ new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
+ {
+ for (;;)
+ {
+ RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
+ int count = roleAssignmentData2.Count;
+ roleAssignmentData2.Count = count - 1;
+ if (count <= 0)
+ {
+ break;
+ }
+
+ list.Add(roleAssignmentData.Role.Role);
+ }
+ }
+
+ __instance.AssignRolesFromList(players, teamMax, list, ref num);
+
+ var list2 = source.Where(x => !x.IsDead).Select(role =>
+ new RoleManager.RoleAssignmentData(
+ role,
+ roleOptions.GetNumPerGame(role.Role),
+ roleOptions.GetChancePerGame(role.Role))).ToList();
+
+ list.Clear();
+ foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2)
+ {
+ for (int i = 0; i < roleAssignmentData3.Count; i++)
+ {
+ if (HashRandom.Next(101) < roleAssignmentData3.Chance)
+ {
+ list.Add(roleAssignmentData3.Role.Role);
+ }
+ }
+ }
+
+ __instance.AssignRolesFromList(players, teamMax, list, ref num);
+ if (defaultRole != null)
+ {
+ while (list.Count < players.Count && list.Count + num < teamMax)
+ {
+ list.Add(defaultRole.Value);
+ }
+
+ __instance.AssignRolesFromList(players, teamMax, list, ref num);
+ }
+
+ return false;
+ }
+}
diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs
index 8d13595..713ffdf 100644
--- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs
+++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs
@@ -1,6 +1,7 @@
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
+using InnerNet;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Modifiers;
diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs
index 891c3ba..7168a70 100644
--- a/MiraAPI/PluginLoading/MiraPluginInfo.cs
+++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs
@@ -51,7 +51,7 @@ public class MiraPluginInfo
public IReadOnlyCollection Buttons { get; private set; } = null!;
///
- /// Gets a read only dictionary of this plugin's custom game modes.
+ /// Gets a read only dictionary of this plugin's custom presets.
///
public IReadOnlyCollection Presets { get; internal set; } = null!;
diff --git a/MiraAPI/Presets/PresetManager.cs b/MiraAPI/Presets/PresetManager.cs
index 7fa83a2..9204e42 100644
--- a/MiraAPI/Presets/PresetManager.cs
+++ b/MiraAPI/Presets/PresetManager.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System.Collections.Generic;
+using System.IO;
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.PluginLoading;
@@ -18,6 +19,13 @@ public static class PresetManager
///
public static string PresetDirectory { get; } = Path.GetFullPath("mira_presets", Application.persistentDataPath);
+ internal static List InternalMasterPresets { get; } = [];
+
+ ///
+ /// Gets a read only dictionary of this plugin's custom presets.
+ ///
+ public static IReadOnlyCollection MasterPresets { get; internal set; } = null!;
+
internal static void CreateDefaultPreset(MiraPluginInfo plugin)
{
var presetPath = Path.Combine(PresetDirectory, plugin.PluginId);
@@ -93,4 +101,58 @@ public static class PresetManager
}
plugin.Presets = [..plugin.InternalPresets];
}
+
+ ///
+ /// Loads the master presets by reading the configuration files from the preset directory.
+ ///
+ public static void LoadMasterPreset()
+ {
+ /*foreach (var btn in plugin.InternalPresets.Select(x=>x.PresetButton))
+ {
+ if (btn != null)
+ {
+ Object.DestroyImmediate(btn);
+ }
+ }
+
+ plugin.InternalPresets.Clear();*/
+ if (!Directory.Exists(PresetDirectory))
+ {
+ Directory.CreateDirectory(PresetDirectory);
+ }
+
+ var pluginPresetPath = Path.Combine(PresetDirectory, "MasterPresets");
+ if (!Directory.Exists(pluginPresetPath))
+ {
+ Directory.CreateDirectory(pluginPresetPath);
+ }
+
+ foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
+ {
+ var fileName = Path.GetFileName(file);
+ Info($"Loading preset file {fileName}");
+ var presetName = Path.GetFileNameWithoutExtension(file);
+ var presetConfig = new ConfigFile(file, false)
+ {
+ SaveOnConfigSet = false,
+ };
+ foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
+ {
+ foreach (var option in plugin.InternalOptions.Where(x=>x.IncludeInPreset))
+ {
+ option.Bind(presetConfig);
+ }
+
+ foreach (var role in plugin.InternalRoles.Values.OfType().Where(x=>!x.Configuration.HideSettings))
+ {
+ role.BindConfig(presetConfig);
+ }
+ }
+
+ presetConfig.Save();
+
+ // InternalMasterPresets.Add(new OptionPreset(presetName, plugin, presetConfig));
+ }
+ MasterPresets = [..InternalMasterPresets];
+ }
}
diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs
index cb4dbfb..fba91bf 100644
--- a/MiraAPI/Roles/CustomRoleUtils.cs
+++ b/MiraAPI/Roles/CustomRoleUtils.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
@@ -13,6 +14,62 @@ namespace MiraAPI.Roles;
///
public static class CustomRoleUtils
{
+ ///
+ /// Determines whether the specified role can spawn in general, accounting for gamemodes and everything else.
+ ///
+ /// The role you would like to check for.
+ /// True if the role is able to spawn, otherwise false.
+ public static bool CanSpawnOnCurrentMode(RoleBehaviour role)
+ {
+ if (role is ICustomRole custom)
+ {
+ return custom.CanSpawnOnCurrentMode();
+ }
+
+ if (GameManager.Instance.IsHideAndSeek())
+ {
+ return role.Role is RoleTypes.Engineer || role.Role is RoleTypes.Impostor;
+ }
+ return true;
+ }
+
+ public static List<(ushort RoleType, int Chance)> GetPossibleRoles(
+ List assignmentData,
+ Func? predicate = null)
+ {
+ var roles = new List<(ushort, int)>();
+
+ assignmentData.Where(x => predicate == null || predicate(x)).ToList().ForEach(x =>
+ {
+ for (var i = 0; i < x.Count; i++)
+ {
+ roles.Add(((ushort)x.Role.Role, x.Chance));
+ }
+ });
+
+ return roles;
+ }
+
+ public static RoleManager.RoleAssignmentData GetAssignData(RoleTypes roleType)
+ {
+ var currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions;
+ var roleOptions = currentGameOptions.RoleOptions;
+
+ var role = GetRegisteredRole(roleType);
+ var assignmentData = new RoleManager.RoleAssignmentData(role, roleOptions.GetNumPerGame(role!.Role),
+ roleOptions.GetChancePerGame(role.Role));
+
+ return assignmentData;
+ }
+
+ public static RoleBehaviour? GetRegisteredRole(RoleTypes roleType)
+ {
+ // we want to prioritize the custom roles because the role has the right RoleColour/TeamColor
+ var role = CustomRoleManager.AllRoles.FirstOrDefault(x => x.Role == roleType);
+
+ return role;
+ }
+
///
/// Gets all active in-game roles.
///
diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs
index d0cfe90..9ed860d 100644
--- a/MiraAPI/Roles/ICustomRole.cs
+++ b/MiraAPI/Roles/ICustomRole.cs
@@ -239,4 +239,10 @@ public interface ICustomRole : IOptionable
{
return true;
}
+
+ ///
+ /// Determines whether the role can spawn in general, accounting for gamemodes and everything else.
+ ///
+ /// True if the role is able to spawn, otherwise false.
+ public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek();
}
From 11ba564eeda1be5c350d9fca7fc0bddf09d1fa23 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:18:41 -0600
Subject: [PATCH 20/36] Fix seeker role assignment
---
MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs
index 7789180..4a9547b 100644
--- a/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs
+++ b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs
@@ -86,6 +86,7 @@ public static class LogicRoleSelectionHnsPatch
}
else if (team == RoleTeamTypes.Impostor)
{
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment");
var newImpostors = new List();
// Specified Seeker
if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() &&
@@ -96,6 +97,7 @@ public static class LogicRoleSelectionHnsPatch
.First(p => p.PlayerId == __instance.hnsManager.LogicOptionsHnS.ImpostorPlayerID());
players.Remove(networkedPlayerInfo);
newImpostors.Add(networkedPlayerInfo);
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}");
}
// Random Seeker
else
@@ -113,8 +115,10 @@ public static class LogicRoleSelectionHnsPatch
players.Remove(networkedPlayerInfo);
newImpostors.Add(networkedPlayerInfo);
num2++;
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}");
}
}
+ Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment");
foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
{
@@ -131,7 +135,7 @@ public static class LogicRoleSelectionHnsPatch
list.Add(roleAssignmentData.Role.Role);
}
}
- AssignRolesFromList(players, teamMax, list, ref num);
+ AssignRolesFromList(newImpostors, teamMax, list, ref num);
var list2 = source.Where(x => !x.IsDead).Select(role =>
new RoleManager.RoleAssignmentData(
@@ -151,14 +155,14 @@ public static class LogicRoleSelectionHnsPatch
}
}
- AssignRolesFromList(players, teamMax, list, ref num);
+ AssignRolesFromList(newImpostors, teamMax, list, ref num);
var basicRole = RoleTypes.Impostor;
- while (list.Count < players.Count && list.Count + num < teamMax)
+ while (list.Count < newImpostors.Count && list.Count + num < teamMax)
{
list.Add(basicRole);
}
- AssignRolesFromList(players, teamMax, list, ref num);
+ AssignRolesFromList(newImpostors, teamMax, list, ref num);
}
return false;
}
From aee304fb96dde013ea5ef6fd6ea9e2114561270c Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 22 Nov 2025 08:19:58 -0600
Subject: [PATCH 21/36] Fix nullable roletype errors
---
.../Roles/LogicRoleSelectionNormalPatch.cs | 21 ++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs
index d32f0dd..0d00008 100644
--- a/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs
+++ b/MiraAPI/Patches/Roles/LogicRoleSelectionNormalPatch.cs
@@ -71,15 +71,22 @@ public static class LogicRoleSelectionNormalPatch
}
__instance.AssignRolesFromList(players, teamMax, list, ref num);
- if (defaultRole != null)
+ var defaultRole2 = team is RoleTeamTypes.Crewmate ? RoleTypes.Crewmate : RoleTypes.Impostor;
+ try
{
- while (list.Count < players.Count && list.Count + num < teamMax)
- {
- list.Add(defaultRole.Value);
- }
-
- __instance.AssignRolesFromList(players, teamMax, list, ref num);
+ defaultRole2 = defaultRole.Value;
}
+ catch
+ {
+ // Ignored
+ }
+
+ while (list.Count < players.Count && list.Count + num < teamMax)
+ {
+ list.Add(defaultRole2);
+ }
+
+ __instance.AssignRolesFromList(players, teamMax, list, ref num);
return false;
}
From c65511d03e0f52efa34181ab1fb870ef6fdab01a Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 22 Nov 2025 15:26:07 -0600
Subject: [PATCH 22/36] Add checks to hide roles from menus
---
MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 4 ++--
MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 2 +-
MiraAPI/Roles/ICustomRole.cs | 8 +++++++-
3 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
index 13f1f5a..32c0d2a 100644
--- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
+++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
@@ -382,7 +382,7 @@ public static class LobbyViewPanePatches
foreach (var grouping in sortedRoleGroups)
{
- if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings))
+ if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings()))
{
continue;
}
@@ -413,7 +413,7 @@ public static class LobbyViewPanePatches
foreach (var customRole in grouping)
{
- if (customRole.Configuration.HideSettings)
+ if (customRole.Configuration.HideSettings || !customRole.VisibleInSettings())
{
continue;
}
diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
index 17913d5..7aa7a61 100644
--- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
@@ -160,7 +160,7 @@ public static class RoleSettingMenuPatches
foreach (var grouping in sortedRoleGroups)
{
- if (!grouping.Any() || grouping.All(x=>x.Configuration.HideSettings))
+ if (!grouping.Any() || grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings()))
{
continue;
}
diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs
index 9ed860d..3570654 100644
--- a/MiraAPI/Roles/ICustomRole.cs
+++ b/MiraAPI/Roles/ICustomRole.cs
@@ -1,4 +1,5 @@
-using System.Text;
+using System;
+using System.Text;
using BepInEx.Configuration;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
@@ -245,4 +246,9 @@ public interface ICustomRole : IOptionable
///
/// True if the role is able to spawn, otherwise false.
public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek();
+
+ ///
+ /// Gets the function that determines whether the role should be toggled on or off in the game settings.
+ ///
+ public virtual Func VisibleInSettings => () => true;
}
From 089d2c1ea541afc92df120e0e6cfd7764be328f2 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 23 Nov 2025 12:01:41 -0600
Subject: [PATCH 23/36] Add Hide and seek task event
---
.../Vanilla/Player/CompleteHnsTaskEvent.cs | 28 ++++++++++++
MiraAPI/Patches/Events/HnsFinishTaskPatch.cs | 43 +++++++++++++++++++
2 files changed, 71 insertions(+)
create mode 100644 MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
create mode 100644 MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
diff --git a/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs b/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
new file mode 100644
index 0000000..8c0321c
--- /dev/null
+++ b/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
@@ -0,0 +1,28 @@
+namespace MiraAPI.Events.Vanilla.Player;
+
+///
+/// The event that is invoked when a player completes a task within Hide and Seek. Task cannot be cancelled, but timer changes can.
+///
+public class CompleteHnsTaskEvent : MiraCancelableEvent
+{
+ ///
+ /// Gets the instance of the .
+ ///
+ public PlayerControl Player { get; }
+
+ ///
+ /// Gets the instance of the that the player completed.
+ ///
+ public PlayerTask Task { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player who completed the task.
+ /// The task that the player completed.
+ public CompleteHnsTaskEvent(PlayerControl player, PlayerTask task)
+ {
+ Player = player;
+ Task = task;
+ }
+}
diff --git a/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs b/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
new file mode 100644
index 0000000..dc9af34
--- /dev/null
+++ b/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
@@ -0,0 +1,43 @@
+
+using HarmonyLib;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Player;
+
+namespace MiraAPI.Patches.Events;
+
+[HarmonyPatch(typeof(HideAndSeekManager))]
+public static class HnsFinishTaskPatch
+{
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(HideAndSeekManager.FinishTask))]
+ public static bool FinishTask(HideAndSeekManager __instance, PlayerTask task)
+ {
+ NormalPlayerTask normalPlayerTask = task as NormalPlayerTask;
+ if (normalPlayerTask != null)
+ {
+ var completeTaskEvent = new CompleteHnsTaskEvent(task.Owner, task);
+ MiraEventManager.InvokeEvent(completeTaskEvent);
+ if (completeTaskEvent.IsCancelled)
+ {
+ return false;
+ }
+ switch (normalPlayerTask.Length)
+ {
+ case NormalPlayerTask.TaskLength.None:
+ case NormalPlayerTask.TaskLength.Common:
+ __instance.LogicFlowHnS.OnTaskComplete(__instance.LogicOptionsHnS.GetCommonTaskTimeValue());
+ break;
+ case NormalPlayerTask.TaskLength.Short:
+ __instance.LogicFlowHnS.OnTaskComplete(__instance.LogicOptionsHnS.GetShortTaskTimeValue());
+ break;
+ case NormalPlayerTask.TaskLength.Long:
+ __instance.LogicFlowHnS.OnTaskComplete(__instance.LogicOptionsHnS.GetLongTaskTimeValue());
+ break;
+ }
+
+ SoundManager.Instance.PlaySoundImmediate(__instance.TaskFinishedSound, false, 1f, 1f, null);
+ }
+
+ return false;
+ }
+}
From 62c01b2f729546a8e5487510e555f61b70723636 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 23 Nov 2025 12:05:31 -0600
Subject: [PATCH 24/36] Add Hns Manager into the task event
---
MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs | 9 ++++++++-
MiraAPI/Patches/Events/HnsFinishTaskPatch.cs | 2 +-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs b/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
index 8c0321c..a719bdc 100644
--- a/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
+++ b/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
@@ -15,13 +15,20 @@ public class CompleteHnsTaskEvent : MiraCancelableEvent
///
public PlayerTask Task { get; }
+ ///
+ /// Gets the instance of the attached to the game.
+ ///
+ public HideAndSeekManager HnsManager { get; }
+
///
/// Initializes a new instance of the class.
///
+ /// The Hide and Seek Manager currently active.
/// The player who completed the task.
/// The task that the player completed.
- public CompleteHnsTaskEvent(PlayerControl player, PlayerTask task)
+ public CompleteHnsTaskEvent(HideAndSeekManager hnsManager, PlayerControl player, PlayerTask task)
{
+ HnsManager = hnsManager;
Player = player;
Task = task;
}
diff --git a/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs b/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
index dc9af34..300fd40 100644
--- a/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
+++ b/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
@@ -15,7 +15,7 @@ public static class HnsFinishTaskPatch
NormalPlayerTask normalPlayerTask = task as NormalPlayerTask;
if (normalPlayerTask != null)
{
- var completeTaskEvent = new CompleteHnsTaskEvent(task.Owner, task);
+ var completeTaskEvent = new CompleteHnsTaskEvent(__instance, task.Owner, task);
MiraEventManager.InvokeEvent(completeTaskEvent);
if (completeTaskEvent.IsCancelled)
{
From 3afe4371013cc3194a3e650f04421e624700c41f Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 23 Nov 2025 12:50:52 -0600
Subject: [PATCH 25/36] Fix Hns Task Event not registering properly
---
.../Vanilla/Player/CompleteHnsTaskEvent.cs | 4 +--
MiraAPI/Patches/Events/HnsFinishTaskPatch.cs | 36 +++----------------
2 files changed, 7 insertions(+), 33 deletions(-)
diff --git a/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs b/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
index a719bdc..7d8f71d 100644
--- a/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
+++ b/MiraAPI/Events/Vanilla/Player/CompleteHnsTaskEvent.cs
@@ -1,9 +1,9 @@
namespace MiraAPI.Events.Vanilla.Player;
///
-/// The event that is invoked when a player completes a task within Hide and Seek. Task cannot be cancelled, but timer changes can.
+/// The event that is invoked when a player completes a task within Hide and Seek. Non-cancelable.
///
-public class CompleteHnsTaskEvent : MiraCancelableEvent
+public class CompleteHnsTaskEvent : MiraEvent
{
///
/// Gets the instance of the .
diff --git a/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs b/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
index 300fd40..41b193f 100644
--- a/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
+++ b/MiraAPI/Patches/Events/HnsFinishTaskPatch.cs
@@ -1,5 +1,4 @@
-
-using HarmonyLib;
+using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Player;
@@ -8,36 +7,11 @@ namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(HideAndSeekManager))]
public static class HnsFinishTaskPatch
{
- [HarmonyPrefix]
+ [HarmonyPostfix]
[HarmonyPatch(nameof(HideAndSeekManager.FinishTask))]
- public static bool FinishTask(HideAndSeekManager __instance, PlayerTask task)
+ public static void FinishTaskPostfix(HideAndSeekManager __instance, PlayerTask task)
{
- NormalPlayerTask normalPlayerTask = task as NormalPlayerTask;
- if (normalPlayerTask != null)
- {
- var completeTaskEvent = new CompleteHnsTaskEvent(__instance, task.Owner, task);
- MiraEventManager.InvokeEvent(completeTaskEvent);
- if (completeTaskEvent.IsCancelled)
- {
- return false;
- }
- switch (normalPlayerTask.Length)
- {
- case NormalPlayerTask.TaskLength.None:
- case NormalPlayerTask.TaskLength.Common:
- __instance.LogicFlowHnS.OnTaskComplete(__instance.LogicOptionsHnS.GetCommonTaskTimeValue());
- break;
- case NormalPlayerTask.TaskLength.Short:
- __instance.LogicFlowHnS.OnTaskComplete(__instance.LogicOptionsHnS.GetShortTaskTimeValue());
- break;
- case NormalPlayerTask.TaskLength.Long:
- __instance.LogicFlowHnS.OnTaskComplete(__instance.LogicOptionsHnS.GetLongTaskTimeValue());
- break;
- }
-
- SoundManager.Instance.PlaySoundImmediate(__instance.TaskFinishedSound, false, 1f, 1f, null);
- }
-
- return false;
+ var completeTaskEvent = new CompleteHnsTaskEvent(__instance, task.Owner, task);
+ MiraEventManager.InvokeEvent(completeTaskEvent);
}
}
From e93ef6fd1d084f38eda0daa67218f5c96233eb15 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Thu, 27 Nov 2025 21:34:20 -0600
Subject: [PATCH 26/36] Add special modded number type
---
.../OptionTypes/ModdedNumberOption.cs | 72 ++++++++++++++++++-
1 file changed, 70 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
index 04cfd09..ec9e24f 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
@@ -32,16 +32,83 @@ public class ModdedNumberOption : ModdedOption
///
public MiraNumberSuffixes SuffixType { get; }
+ ///
+ /// Gets a value indicating what zero appears as.
+ ///
+ public string ZeroWordValue { get; }
+
+ ///
+ /// Gets a value indicating what negative one appears as.
+ ///
+ public string NegativeWordValue { get; }
+
///
/// Gets a value indicating whether zero is infinity.
///
- public bool ZeroInfinity { get; }
+ public bool ZeroInfinity => ZeroWordValue == "∞";
///
/// Gets a value indicating whether holding shift will divide the increment.
///
public bool ShiftIncrement { get; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The title of the option.
+ /// The default value as a float.
+ /// The minimum value.
+ /// The maximum value.
+ /// The increment.
+ /// The suffix type.
+ /// Optional format string for the option screen.
+ /// Determines what is shown for zero. Options: ∞, #, or a word value (such as Off).
+ /// Determines what is shown for negative 1. Options: ∞, #, or a word value (such as Off).
+ /// Whether increments can be split in half.
+ /// Whether to include this option in the preset or not.
+ public ModdedNumberOption(
+ string title,
+ float defaultValue,
+ float min,
+ float max,
+ float increment,
+ string zeroBehavior,
+ string negativeBehavior,
+ MiraNumberSuffixes suffixType,
+ string? formatString = null,
+ bool halfIncrements = false,
+ bool includeInPreset = true) : base(title, defaultValue, includeInPreset)
+ {
+ Min = min;
+ Max = max;
+ Increment = increment;
+ SuffixType = suffixType;
+ ZeroWordValue = zeroBehavior;
+ NegativeWordValue = negativeBehavior;
+ ShiftIncrement = halfIncrements;
+
+ Value = Mathf.Clamp(defaultValue, min, max);
+
+ Data = ScriptableObject.CreateInstance();
+
+ var data = (FloatGameSetting)Data;
+ data.Type = global::OptionTypes.Float;
+ data.Title = StringName;
+ data.Value = Value;
+ data.Increment = Increment;
+ data.ValidRange = new FloatRange(Min, Max);
+ data.FormatString = formatString ??
+ (defaultValue.IsInteger() &&
+ Increment.IsInteger() &&
+ Value.IsInteger() &&
+ Min.IsInteger() &&
+ Max.IsInteger() ? "0" : "0.0");
+
+ data.ZeroIsInfinity = ZeroInfinity;
+ data.SuffixType = (NumberSuffixes)SuffixType;
+ data.OptionName = FloatOptionNames.Invalid;
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -69,7 +136,8 @@ public class ModdedNumberOption : ModdedOption
Max = max;
Increment = increment;
SuffixType = suffixType;
- ZeroInfinity = zeroInfinity;
+ ZeroWordValue = zeroInfinity ? "∞" : "#";
+ NegativeWordValue = "#";
ShiftIncrement = increment != 1f;
Value = Mathf.Clamp(defaultValue, min, max);
From d97df1a8915fb92f9b8fc531a94266b8af5a5bb4 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Thu, 27 Nov 2025 22:30:04 -0600
Subject: [PATCH 27/36] Add options for negatives as infinity
---
.../GameOptions/MiraNumberOptionComponent.cs | 12 ++++++-
.../OptionTypes/ModdedNumberOption.cs | 4 ++-
MiraAPI/Patches/Options/SettingPatches.cs | 31 ++++++++++++++++++-
3 files changed, 44 insertions(+), 3 deletions(-)
diff --git a/MiraAPI/GameOptions/MiraNumberOptionComponent.cs b/MiraAPI/GameOptions/MiraNumberOptionComponent.cs
index a00274f..2494f93 100644
--- a/MiraAPI/GameOptions/MiraNumberOptionComponent.cs
+++ b/MiraAPI/GameOptions/MiraNumberOptionComponent.cs
@@ -22,7 +22,17 @@ public class MiraNumberOptionComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
public float DefaultIncrement { get; set; }
///
- /// Gets or sets the value of whether or not holding shift will split the increment in half.
+ /// Gets or sets a value indicating whether holding shift will split the increment in half.
///
public bool ShiftIncrementToggle { get; set; }
+
+ ///
+ /// Gets or sets a value indicating what Zero displays.
+ ///
+ public string ZeroValue { get; set; } = "#";
+
+ ///
+ /// Gets or sets a value indicating what Negative One displays.
+ ///
+ public string NegativeValue { get; set; } = "#";
}
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
index ec9e24f..b9a1f59 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
@@ -38,7 +38,7 @@ public class ModdedNumberOption : ModdedOption
public string ZeroWordValue { get; }
///
- /// Gets a value indicating what negative one appears as.
+ /// Gets a value indicating what a negative one appears as.
///
public string NegativeWordValue { get; }
@@ -177,6 +177,8 @@ public class ModdedNumberOption : ModdedOption
optionComponent.NumberOption = this;
optionComponent.DefaultIncrement = Increment;
optionComponent.ShiftIncrementToggle = ShiftIncrement;
+ optionComponent.NegativeValue = NegativeWordValue;
+ optionComponent.ZeroValue = ZeroWordValue;
numberOption.SetUpFromData(Data, 20);
numberOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged;
diff --git a/MiraAPI/Patches/Options/SettingPatches.cs b/MiraAPI/Patches/Options/SettingPatches.cs
index 72a0efe..e33633a 100644
--- a/MiraAPI/Patches/Options/SettingPatches.cs
+++ b/MiraAPI/Patches/Options/SettingPatches.cs
@@ -1,5 +1,8 @@
using System.Globalization;
+using System.Linq;
using HarmonyLib;
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Utilities;
using UnityEngine;
@@ -28,7 +31,33 @@ public static class SettingPatches
string result;
var suffix = (MiraNumberSuffixes)__instance.SuffixType;
- if (__instance.ZeroIsInfinity && Mathf.Abs(value) < 0.0001f)
+ var custom =
+ ModdedOptionsManager.ModdedOptions.Values.FirstOrDefault(opt =>
+ opt.OptionBehaviour != null && opt.OptionBehaviour.Data == __instance);
+ if (custom is ModdedNumberOption moddedNumberOption)
+ {
+ if (moddedNumberOption.NegativeWordValue != "#" && (int)value == -1)
+ {
+ result = $"{moddedNumberOption.NegativeWordValue}";
+ }
+ else if (moddedNumberOption.ZeroWordValue != "#" && Mathf.Abs(value) < 0.0001f)
+ {
+ result = $"{moddedNumberOption.ZeroWordValue}";
+ }
+ else
+ {
+ result = suffix switch
+ {
+ MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
+ MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
+ MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
+ _ => TranslationController.Instance.GetString(
+ StringNames.GameSecondsAbbrev,
+ (Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
+ };
+ }
+ }
+ else if (__instance.ZeroIsInfinity && Mathf.Abs(value) < 0.0001f)
{
result = "∞";
}
From c9b1c57774e6deff41043a22b237149b264587b5 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Thu, 27 Nov 2025 22:54:03 -0600
Subject: [PATCH 28/36] Add toggle for buttons to change uses check
---
MiraAPI/Hud/CustomActionButton.cs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 2fd5c63..80cfae8 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -91,7 +91,12 @@ public abstract class CustomActionButton
///
/// Gets a value indicating whether the button has limited uses.
///
- public bool LimitedUses => MaxUses > 0;
+ public bool LimitedUses => ZeroIsInfinite ? MaxUses > 0 : MaxUses >= 0;
+
+ ///
+ /// Gets or sets a value indicating whether limited uses are determined via zero or a negative number of uses.
+ ///
+ public bool ZeroIsInfinite { get; set; } = true;
///
/// Gets or sets a value indicating whether the effect is currently active, if there is one.
From 52526201089345bd0e62f6f9a03207f13e2e76da Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Thu, 27 Nov 2025 23:00:24 -0600
Subject: [PATCH 29/36] Update CustomActionButton.cs
---
MiraAPI/Hud/CustomActionButton.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 80cfae8..9f4444b 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -96,7 +96,7 @@ public abstract class CustomActionButton
///
/// Gets or sets a value indicating whether limited uses are determined via zero or a negative number of uses.
///
- public bool ZeroIsInfinite { get; set; } = true;
+ public virtual bool ZeroIsInfinite { get; set; } = true;
///
/// Gets or sets a value indicating whether the effect is currently active, if there is one.
From 136bbde516f34a5fd2a005490cc42e8bcb3ffc34 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 29 Nov 2025 13:40:21 -0600
Subject: [PATCH 30/36] Fix MaxUses check
---
MiraAPI/Hud/CustomActionButton.cs | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 9f4444b..1d256e8 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -64,9 +64,14 @@ public abstract class CustomActionButton
public virtual float EffectDuration => 0;
///
- /// Gets the maximum amount of uses the button has. If the button has infinite uses, set to 0.
+ /// Gets a value indicating whether the button has limited uses.
///
- public virtual int MaxUses => 0;
+ public bool LimitedUses => ZeroIsInfinite ? MaxUses > 0 : MaxUses >= 0;
+
+ ///
+ /// Gets the maximum number of uses the button has. If the button has infinite uses, set to 0 or -1 based on what value that ZeroIsInfinite is set to.
+ ///
+ public virtual int MaxUses => ZeroIsInfinite ? 0 : -1;
///
/// Gets the value indicating uses mode.
@@ -88,11 +93,6 @@ public abstract class CustomActionButton
///
public virtual ButtonLocation Location { get; set; } = ButtonLocation.BottomLeft;
- ///
- /// Gets a value indicating whether the button has limited uses.
- ///
- public bool LimitedUses => ZeroIsInfinite ? MaxUses > 0 : MaxUses >= 0;
-
///
/// Gets or sets a value indicating whether limited uses are determined via zero or a negative number of uses.
///
From 798ce451d54017eae22d3e3f727b0770ca8ebbb0 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 29 Nov 2025 14:21:25 -0600
Subject: [PATCH 31/36] Add text for setting descriptions
---
MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 3 +++
MiraAPI/PluginLoading/IMiraPlugin.cs | 15 +++++++++++++++
2 files changed, 18 insertions(+)
diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
index 273cef2..89e6f57 100644
--- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
@@ -110,12 +110,15 @@ internal static class GameSettingMenuPatches
{
case 3:
_modifiersButton?.SelectButton(true);
+ __instance.MenuDescriptionText.text = SelectedMod != null ? SelectedMod.MiraPlugin.ModifierMenuDescription : "Configure modifiers and their settings here!";
break;
case 4:
_customOneButton?.SelectButton(true);
+ __instance.MenuDescriptionText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuOneDescription : "Apply game settings for this mod!";
break;
case 5:
_customTwoButton?.SelectButton(true);
+ __instance.MenuDescriptionText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuTwoDescription : "Apply game settings for this mod!";
break;
}
}
diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs
index 8e2f2b6..e6b2dbb 100644
--- a/MiraAPI/PluginLoading/IMiraPlugin.cs
+++ b/MiraAPI/PluginLoading/IMiraPlugin.cs
@@ -22,6 +22,21 @@ public interface IMiraPlugin
///
public virtual string CustomOptionMenuNameTwo => "Custom Category 2";
+ ///
+ /// Gets the description for the second custom category in the game options menu, if any.
+ ///
+ public virtual string ModifierMenuDescription => "Configure modifiers and their settings here!";
+
+ ///
+ /// Gets the description for the first custom category in the game options menu, if any.
+ ///
+ public virtual string CustomOptionMenuOneDescription => "Apply game settings for this mod!";
+
+ ///
+ /// Gets the description for the second custom category in the game options menu, if any.
+ ///
+ public virtual string CustomOptionMenuTwoDescription => "Apply game settings for this mod!";
+
///
/// Gets the BepInEx configuration file for the plugin.
///
From 8d5bc710e75441ea01c07ae3fd7e015fbe13380a Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 30 Nov 2025 15:22:37 -0600
Subject: [PATCH 32/36] Move all binds to the top right of buttons
---
MiraAPI/Hud/CustomActionButton.cs | 2 +-
MiraAPI/Patches/HudManagerPatches.cs | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 1d256e8..c95b347 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -219,7 +219,7 @@ public abstract class CustomActionButton
Helpers.CreateKeybindIcon(
Button.gameObject,
Keybind.CurrentKey,
- new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f)
+ new Vector3(0.4f, 0.45f, -9f)
);
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent();
HudManagerPatches.ModdedKeybindIcons.Add(KeybindText);
diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs
index 8a40b85..b75d0d0 100644
--- a/MiraAPI/Patches/HudManagerPatches.cs
+++ b/MiraAPI/Patches/HudManagerPatches.cs
@@ -123,15 +123,17 @@ public static class HudManagerPatches
aspectPosition.AdjustPosition();
vanillaKeybindIcons = [];
- var keybindIconPos = new Vector3(-0.4f, 0.4f, -9.5f);
+ var keybindIconPos = new Vector3(0.4f, 0.4f, -9.5f);
var vanillaButtons = new Dictionary
{
{ __instance.KillButton.gameObject, 8 },
{ __instance.UseButton.gameObject, 6 },
+ { __instance.PetButton.gameObject, 6 },
{ __instance.ReportButton.gameObject, 7 },
{ __instance.ImpostorVentButton.gameObject, 50 },
{ __instance.SabotageButton.gameObject, 4 },
{ __instance.AbilityButton.gameObject, 49 },
+ { __instance.SecondaryAbilityButton.gameObject, 8 },
};
foreach (var kvp in vanillaButtons)
From 76b02835a79445c9c3bf4219ea73b60ac126075e Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 30 Nov 2025 15:30:17 -0600
Subject: [PATCH 33/36] Update HudManagerPatches.cs
---
MiraAPI/Patches/HudManagerPatches.cs | 2 --
1 file changed, 2 deletions(-)
diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs
index b75d0d0..e95b72c 100644
--- a/MiraAPI/Patches/HudManagerPatches.cs
+++ b/MiraAPI/Patches/HudManagerPatches.cs
@@ -128,12 +128,10 @@ public static class HudManagerPatches
{
{ __instance.KillButton.gameObject, 8 },
{ __instance.UseButton.gameObject, 6 },
- { __instance.PetButton.gameObject, 6 },
{ __instance.ReportButton.gameObject, 7 },
{ __instance.ImpostorVentButton.gameObject, 50 },
{ __instance.SabotageButton.gameObject, 4 },
{ __instance.AbilityButton.gameObject, 49 },
- { __instance.SecondaryAbilityButton.gameObject, 8 },
};
foreach (var kvp in vanillaButtons)
From 8ed435550467012afff47fc52ec43f77b0ffec43 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Mon, 8 Dec 2025 12:28:02 -0600
Subject: [PATCH 34/36] Clear up I2lCpp warning
---
MiraAPI/GameOptions/MiraNumberOptionComponent.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/MiraAPI/GameOptions/MiraNumberOptionComponent.cs b/MiraAPI/GameOptions/MiraNumberOptionComponent.cs
index 2494f93..dce033d 100644
--- a/MiraAPI/GameOptions/MiraNumberOptionComponent.cs
+++ b/MiraAPI/GameOptions/MiraNumberOptionComponent.cs
@@ -1,4 +1,5 @@
using System;
+using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions.OptionTypes;
using Reactor.Utilities.Attributes;
using UnityEngine;
@@ -14,6 +15,7 @@ public class MiraNumberOptionComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
///
/// Gets or sets the modded option associated with the object.
///
+ [HideFromIl2Cpp]
public ModdedNumberOption NumberOption { get; set; }
///
From 303aedafc7fb416e57e77b36350804d085f477f0 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sat, 13 Dec 2025 15:29:12 -0600
Subject: [PATCH 35/36] Fix keybinds bypassing button click events
---
MiraAPI/Hud/CustomActionButton.cs | 34 ++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index c95b347..e54c10c 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -211,7 +211,39 @@ public abstract class CustomActionButton
{
if (Enabled(PlayerControl.LocalPlayer.Data.Role))
{
- ClickHandler();
+ // Invoke the generic button click event.
+ var genericEvent = new MiraButtonClickEvent(this);
+ MiraEventManager.InvokeEvent(genericEvent);
+ if (genericEvent.IsCancelled)
+ {
+ MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this));
+ }
+
+ // Invoke the button click event for specific button.
+ var eventType = CustomButtonManager.ButtonEventTypes[GetType()];
+ var @event = (MiraCancelableEvent)Activator.CreateInstance(eventType, this, genericEvent)!;
+ var specificInvoked = MiraEventManager.InvokeEvent(@event, eventType);
+ if (@event.IsCancelled)
+ {
+ var cancelEventType = CustomButtonManager.ButtonCancelledEventTypes[GetType()];
+ var cancelEvent = (MiraEvent)Activator.CreateInstance(cancelEventType, this)!;
+ MiraEventManager.InvokeEvent(cancelEvent, cancelEventType);
+ }
+
+ if (specificInvoked)
+ {
+ if (!@event.IsCancelled)
+ {
+ ClickHandler();
+ }
+ }
+ else
+ {
+ if (!genericEvent.IsCancelled)
+ {
+ ClickHandler();
+ }
+ }
}
});
From 93eea6ea2f8b7c5ef8e9d20fc70b7f091c187aee Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Sun, 14 Dec 2025 07:50:55 -0600
Subject: [PATCH 36/36] Bump version
---
Directory.Build.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Build.props b/Directory.Build.props
index 56e0d52..4f6bc10 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,7 +8,7 @@
true
- 0.3.3
+ 0.3.4
dev
All Of Us, XtraCube, Angxl