From dbacd044ebf90e654064f45316b88c4578a9c7f2 Mon Sep 17 00:00:00 2001 From: AlchlcDvl <56699741+AlchlcDvl@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:46:26 -0500 Subject: [PATCH 001/102] game mode stuff, missing implementation of 4 methods coming soon:tm: --- AmongUs.props | 2 +- ...{CustomGameMode.cs => AbstractGameMode.cs} | 12 ++- MiraAPI/GameModes/CustomGameModeManager.cs | 100 +++++++++++------- MiraAPI/GameModes/DefaultMode.cs | 10 +- MiraAPI/GameModes/GameModeOption.cs | 18 ++++ .../GameModes/RegisterGameModeAttribute.cs | 27 +---- MiraAPI/MiraApiPlugin.cs | 12 ++- MiraAPI/Patches/GameModes/ConsolePatches.cs | 18 ++-- MiraAPI/Patches/GameModes/DeadBodyPatch.cs | 11 +- MiraAPI/Patches/GameModes/GameLogicPatches.cs | 6 +- MiraAPI/Patches/GameModes/HudPatches.cs | 12 +++ .../Patches/GameModes/ImpostorTargetPatch.cs | 11 +- .../Patches/GameModes/MapBehaviourPatch.cs | 9 +- MiraAPI/Patches/GameModes/OnDeathPatch.cs | 9 +- MiraAPI/Patches/GameModes/VentPatches.cs | 47 -------- MiraAPI/Patches/HudManagerPatches.cs | 11 -- MiraAPI/PluginLoading/MiraPluginInfo.cs | 15 ++- MiraAPI/PluginLoading/MiraPluginManager.cs | 51 ++++++++- 18 files changed, 216 insertions(+), 165 deletions(-) rename MiraAPI/GameModes/{CustomGameMode.cs => AbstractGameMode.cs} (95%) create mode 100644 MiraAPI/GameModes/GameModeOption.cs create mode 100644 MiraAPI/Patches/GameModes/HudPatches.cs delete mode 100644 MiraAPI/Patches/GameModes/VentPatches.cs diff --git a/AmongUs.props b/AmongUs.props index 33c571f..b83c764 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -2,7 +2,7 @@ - + diff --git a/MiraAPI/GameModes/CustomGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs similarity index 95% rename from MiraAPI/GameModes/CustomGameMode.cs rename to MiraAPI/GameModes/AbstractGameMode.cs index c7cf639..466c282 100644 --- a/MiraAPI/GameModes/CustomGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using UnityEngine; namespace MiraAPI.GameModes; /// /// Base class for custom gamemodes. /// -public abstract class CustomGameMode +public abstract class AbstractGameMode { /// /// Gets the game mode name. @@ -18,9 +19,14 @@ public abstract class CustomGameMode public abstract string Description { get; } /// - /// Gets the game mode ID. + /// Gets the game mode description. /// - public abstract int Id { get; } + public virtual Color Color { get; } = Color.white; + + /// + /// Gets or sets the game mode id. + /// + public uint ID { get; set; } /// /// Called when Intro Cutscene is destroyed. diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 388eb5d..62d27e2 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,7 +1,9 @@ -using Reactor.Utilities; -using System; +using System; using System.Collections.Generic; -using System.Linq; +using Il2CppSystem.Threading.Tasks; +using MiraAPI.GameOptions; +using MiraAPI.PluginLoading; +using Reactor.Utilities; namespace MiraAPI.GameModes; @@ -10,62 +12,84 @@ namespace MiraAPI.GameModes; /// public static class CustomGameModeManager { - /// - /// List of registered gamemodes. - /// - internal static readonly Dictionary GameModes = []; + private static readonly Dictionary IdToModeMap = []; - public static bool IsDefault() + private static uint _nextId; + + private static uint GetNextId() { - return ActiveMode?.Id == 0; + _nextId++; + return _nextId; } - /// - /// Current gamemode. - /// - public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode(); - - /// - /// Set current gamemode. - /// - /// gamemode ID. - public static void SetGameMode(int id) - { - if (GameModes.TryGetValue(id, out var gameMode)) - { - ActiveMode = gameMode; - return; - } - - Logger.Error($"No gamemode with id {id} found!"); - } + internal static uint LastId => _nextId; /// /// Register gamemode from type. /// - /// Type of gamemode class, should inherit from . - internal static void RegisterGameMode(Type gameModeType) + /// Type of gamemode class, should inherit from . + /// The custom plugin info of the mod. + internal static void RegisterGameMode(Type gameModeType, MiraPluginInfo pluginInfo) { - if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType)) + if (!typeof(AbstractGameMode).IsAssignableFrom(gameModeType)) { - Logger.Warning($"{gameModeType.Name} does not inherit CustomGameMode!"); return; } - var modeObj = Activator.CreateInstance(gameModeType); + var instance = Activator.CreateInstance(gameModeType); - if (modeObj is not CustomGameMode gameMode) + if (instance is not AbstractGameMode mode) { - Logger.Error($"Failed to create instance of {gameModeType.Name}"); return; } - if (GameModes.Any(x => x.Key == gameMode.Id)) + IdToModeMap.Add(GetNextId(), mode); + pluginInfo.GameModes.Add(_nextId, mode); + + mode.ID = _nextId; + } + + /// + /// Checks to see if the default game mode is on. + /// + /// True if the default mode is one. + public static bool IsDefault() => (uint)OptionGroupSingleton.Instance.CurrentMode.Value == 0; + + /// + /// Gets the current gamemode. + /// + public static AbstractGameMode? ActiveMode { get; internal set; } + + internal static void RegisterDefaultMode() + { + var defaultMode = new DefaultMode(); + IdToModeMap.Add(0, defaultMode); + defaultMode.ID = 0; + } + + internal static void SetGameMode(uint id) + { + if (IdToModeMap.TryGetValue(id, out var mode)) { - Logger.Error($"ID for gamemode {gameMode.Name} already exists!"); + ActiveMode = mode; return; } - GameModes.Add(gameMode.Id, gameMode); + ActiveMode = IdToModeMap[0]; + Logger.Warning($"Unable to find game mode of id {id}!"); + } + + internal static void GetAndSetGameMode() + { + var id = (uint)OptionGroupSingleton.Instance.CurrentMode.Value; + + if (IdToModeMap.TryGetValue(id, out var mode)) + { + ActiveMode = mode; + return; + } + + ActiveMode = IdToModeMap[0]; + Logger.Warning($"Unable to find game mode of id {id}!"); } } diff --git a/MiraAPI/GameModes/DefaultMode.cs b/MiraAPI/GameModes/DefaultMode.cs index 73c4ca8..99f64df 100644 --- a/MiraAPI/GameModes/DefaultMode.cs +++ b/MiraAPI/GameModes/DefaultMode.cs @@ -1,9 +1,13 @@ namespace MiraAPI.GameModes; -[RegisterGameMode] -public class DefaultMode : CustomGameMode +/// +/// The default game mode. +/// +public class DefaultMode : AbstractGameMode { + /// public override string Name => "Default"; + + /// public override string Description => "Default Among Us GameMode"; - public override int Id => 0; } diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs new file mode 100644 index 0000000..729cdc3 --- /dev/null +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -0,0 +1,18 @@ +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; + +namespace MiraAPI.GameModes; + +/// +/// The game mode option. +/// +public class GameModeOption : AbstractOptionGroup +{ + /// + public override string GroupName => "Game Mode"; + + public ModdedNumberOption CurrentMode { get; set; } = new("Current Game Mode", 0, 0, CustomGameModeManager.LastId, 1, Utilities.MiraNumberSuffixes.None) + { + ChangedEvent = x => CustomGameModeManager.SetGameMode((uint)x), + }; +} diff --git a/MiraAPI/GameModes/RegisterGameModeAttribute.cs b/MiraAPI/GameModes/RegisterGameModeAttribute.cs index 4c66c35..68ab348 100644 --- a/MiraAPI/GameModes/RegisterGameModeAttribute.cs +++ b/MiraAPI/GameModes/RegisterGameModeAttribute.cs @@ -1,28 +1,9 @@ using System; -using System.Collections.Generic; -using System.Reflection; namespace MiraAPI.GameModes; +/// +/// Registers a game mode to MiraAPI. +/// [AttributeUsage(AttributeTargets.Class)] -public class RegisterGameModeAttribute : Attribute -{ - private static readonly HashSet RegisteredAssemblies = []; - - internal static void Register(Assembly assembly) - { - if (!RegisteredAssemblies.Add(assembly)) - { - return; - } - - foreach (var type in assembly.GetTypes()) - { - var attribute = type.GetCustomAttribute(); - if (attribute != null) - { - CustomGameModeManager.RegisterGameMode(type); - } - } - } -} +public class RegisterGameModeAttribute : Attribute; diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index cfe314d..6efca17 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,4 +1,5 @@ using BepInEx; +using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; @@ -14,15 +15,22 @@ namespace MiraAPI; [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] [ReactorModFlags(ModFlags.RequireOnAllClients)] -public partial class MiraApiPlugin : BasePlugin +public partial class MiraApiPlugin : BasePlugin, IMiraPlugin { private static MiraPluginManager? PluginManager { get; set; } public Harmony Harmony { get; } = new(Id); + public static Color MiraColor { get; } = new Color32(238, 154, 112, 255); + public static MiraApiPlugin Instance { get; private set; } + + public string OptionsTitleText => "Mira API"; + public override void Load() { + Instance = this; + Harmony.PatchAll(); ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow); @@ -30,4 +38,6 @@ public partial class MiraApiPlugin : BasePlugin PluginManager = new MiraPluginManager(); PluginManager.Initialize(); } + + public ConfigFile GetConfigFile() => Config; } diff --git a/MiraAPI/Patches/GameModes/ConsolePatches.cs b/MiraAPI/Patches/GameModes/ConsolePatches.cs index e2f24b2..09fbe17 100644 --- a/MiraAPI/Patches/GameModes/ConsolePatches.cs +++ b/MiraAPI/Patches/GameModes/ConsolePatches.cs @@ -1,14 +1,16 @@ -/* +using HarmonyLib; +using InnerNet; +using MiraAPI.GameModes; + [HarmonyPatch] public static class ConsolePatches { - [HarmonyPrefix] - [HarmonyPatch(typeof(Console), nameof(Console.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(Console), nameof(Console.CanUse))] public static bool CanUsePatch(Console __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseConsole(__instance)) + if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseTasks(__instance)) { var task = __instance.FindTask(pc.Object); @@ -31,8 +33,7 @@ public static class ConsolePatches return true; } - [HarmonyPrefix] - [HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.CanUse))] public static bool SystemCanUsePatch(SystemConsole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) @@ -50,8 +51,7 @@ public static class ConsolePatches return true; } - [HarmonyPrefix] - [HarmonyPatch(typeof(MapConsole), nameof(MapConsole.CanUse))] + [HarmonyPrefix, HarmonyPatch(typeof(MapConsole), nameof(MapConsole.CanUse))] public static bool MapCanUsePatch(MapConsole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) @@ -68,4 +68,4 @@ public static class ConsolePatches canUse = couldUse = true; return true; } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs index 07a2899..1070336 100644 --- a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs +++ b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs @@ -1,11 +1,12 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + [HarmonyPatch(typeof(DeadBody))] public static class DeadBodyPatch { - [HarmonyPrefix] - [HarmonyPatch(nameof(DeadBody.OnClick))] + [HarmonyPrefix, HarmonyPatch(nameof(DeadBody.OnClick))] public static bool OnClickPatch(DeadBody __instance) { - return CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanReport(__instance); + return CustomGameModeManager.ActiveMode?.CanReport(__instance) == true; } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/GameLogicPatches.cs b/MiraAPI/Patches/GameModes/GameLogicPatches.cs index a62be83..75d4ed9 100644 --- a/MiraAPI/Patches/GameModes/GameLogicPatches.cs +++ b/MiraAPI/Patches/GameModes/GameLogicPatches.cs @@ -1,4 +1,6 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + [HarmonyPatch] public static class GameLogicPatches { @@ -17,4 +19,4 @@ public static class GameLogicPatches CustomGameModeManager.ActiveMode?.AssignRoles(out runOriginal, __instance); return runOriginal; } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs new file mode 100644 index 0000000..50871b9 --- /dev/null +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -0,0 +1,12 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +[HarmonyPatch] +public static class HudPatches +{ + [HarmonyPostfix, HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] + public static void HudStartPatch(HudManager __instance) => CustomGameModeManager.ActiveMode?.HudStart(__instance); + + [HarmonyPostfix, HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] + public static void HudUpdatePatch(HudManager __instance) => CustomGameModeManager.ActiveMode?.HudUpdate(__instance); +} diff --git a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs index ad0d9e0..08e39f9 100644 --- a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs +++ b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs @@ -1,4 +1,6 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + /// /// Allow Impostors to kill each other if can kill is enabled in gamemode or friendly fire is toggled on /// @@ -16,9 +18,8 @@ public static class ImpostorTargetPatch } __result = target is { Disconnected: false, IsDead: false } && - target.PlayerId != __instance.Player.PlayerId && !(target.Role == null) && - !(target.Object == null) && !target.Object.inVent && !target.Object.inMovingPlat; + target.PlayerId != __instance.Player.PlayerId && target.Role != null && + target.Object != null && !target.Object.inVent && !target.Object.inMovingPlat; return false; - } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs index 9a4bd8b..b6c48fc 100644 --- a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs +++ b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs @@ -1,9 +1,10 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + [HarmonyPatch(typeof(MapBehaviour))] public static class MapBehaviourPatch { - [HarmonyPrefix] - [HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))] + [HarmonyPrefix, HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))] public static bool ShowSabotagePatch(MapBehaviour __instance) { var shouldShow = CustomGameModeManager.ActiveMode?.ShouldShowSabotageMap(__instance); @@ -15,4 +16,4 @@ public static class MapBehaviourPatch return true; } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/OnDeathPatch.cs b/MiraAPI/Patches/GameModes/OnDeathPatch.cs index 99f9211..dcedbab 100644 --- a/MiraAPI/Patches/GameModes/OnDeathPatch.cs +++ b/MiraAPI/Patches/GameModes/OnDeathPatch.cs @@ -1,11 +1,12 @@ -/* +using HarmonyLib; +using MiraAPI.GameModes; + [HarmonyPatch(typeof(KillAnimation))] public static class OnDeathPatch { - [HarmonyPostfix] - [HarmonyPatch(nameof(KillAnimation.CoPerformKill))] + [HarmonyPostfix, HarmonyPatch(nameof(KillAnimation.CoPerformKill))] public static void OnDeathPostfix([HarmonyArgument(0)] PlayerControl source, [HarmonyArgument(1)] PlayerControl target) { CustomGameModeManager.ActiveMode?.OnDeath(target); } -}*/ +} diff --git a/MiraAPI/Patches/GameModes/VentPatches.cs b/MiraAPI/Patches/GameModes/VentPatches.cs deleted file mode 100644 index 597c40a..0000000 --- a/MiraAPI/Patches/GameModes/VentPatches.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* -[HarmonyPatch(typeof(Vent))] -public static class VentPatches -{ - [HarmonyPrefix] - [HarmonyPatch(nameof(Vent.CanUse))] - public static bool CanUseVentPatch(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse) - { - if (CustomGameModeManager.ActiveMode?.CanVent(__instance, pc) == false) - { - return couldUse = canUse = false; - } - - if (pc.Role is not ICustomRole role) - { - return couldUse = canUse = true; - } - - var num = float.MaxValue; - var @object = pc.Object; - var customRoleUsable = role.CanUseVent; - - canUse = couldUse = customRoleUsable && !pc.IsDead && (@object.CanMove || @object.inVent); - if (canUse) - { - var center = @object.Collider.bounds.center; - var position = __instance.transform.position; - num = Vector2.Distance(center, position); - canUse &= num <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(@object.Collider, center, position, Constants.ShipOnlyMask, false); - } - __result = num; - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(Vent.SetOutline))] - public static bool SetOutlinePatch(Vent __instance, [HarmonyArgument(0)] bool on, [HarmonyArgument(1)] bool mainTarget) - { - var color = PlayerControl.LocalPlayer.Data.Role is ICustomRole role - ? role.RoleColor : PlayerControl.LocalPlayer.Data.Role.IsImpostor ? Palette.ImpostorRed : Palette.CrewmateBlue; - __instance.myRend.material.SetFloat(ShaderID.Outline, on ? 1 : 0); - __instance.myRend.material.SetColor(ShaderID.OutlineColor, color); - __instance.myRend.material.SetColor(ShaderID.AddColor, mainTarget ? color : Color.clear); - - return false; - } -}*/ diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index af7057d..3dbb7c8 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -16,17 +16,6 @@ public static class HudManagerPatches // Custom buttons parent. private static GameObject? _bottomLeft; - /* - /// - /// Trigger hudstart on current custom gamemode - /// - [HarmonyPostfix] - [HarmonyPatch(nameof(HudManager.OnGameStart))] - public static void GameStartPatch(HudManager __instance) - { - CustomGameModeManager.ActiveMode?.HudStart(__instance); - }*/ - /// /// Create custom buttons and arrange them on the hud. /// diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index ecdfa5e..905342c 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -3,12 +3,8 @@ using BepInEx.Configuration; using MiraAPI.GameModes; using MiraAPI.GameOptions; using System.Collections.Generic; -using System.Collections.Immutable; using System.Collections.ObjectModel; -using System.Linq; -using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Hud; -using MiraAPI.Roles; namespace MiraAPI.PluginLoading; @@ -52,6 +48,15 @@ public class MiraPluginInfo return new ReadOnlyDictionary(CustomRoles); } + /// + /// Gets a read only dictionary of Game Mode IDs and the AbstractGameMode object they are associated with. + /// + /// Read only dictionary of IDs and Game Modes. + public ReadOnlyDictionary GetModes() + { + return new ReadOnlyDictionary(GameModes); + } + /// /// Gets a read only collection of this plugin's custom buttons. /// @@ -67,7 +72,7 @@ public class MiraPluginInfo internal Dictionary CustomRoles { get; } = []; - internal Dictionary GameModes { get; } = []; + internal Dictionary GameModes { get; } = []; internal List Buttons { get; } = []; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index feeba48..1615049 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using BepInEx.Unity.IL2CPP; using MiraAPI.Colors; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; @@ -29,6 +30,7 @@ public sealed class MiraPluginManager internal void Initialize() { Instance = this; + IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => { if (plugin is not IMiraPlugin miraPlugin) @@ -46,21 +48,49 @@ public sealed class MiraPluginManager RegisterColorClasses(assembly); + RegisterGameModeAttribute(assembly, info); + _registeredPlugins.Add(assembly, info); Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); }; - IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; + IL2CPPChainloader.Instance.Finished += FinishInitialisation; + } + + private void FinishInitialisation() + { + var plugin = IL2CPPChainloader.Instance.Plugins["mira.api"]; + var info = new MiraPluginInfo(MiraApiPlugin.Instance, plugin); + var assembly = typeof(MiraPluginManager).Assembly; + + RegisterModifierAttribute(assembly); + RegisterAllOptions(assembly, info); + + RegisterRoleAttribute(assembly, info); + RegisterButtonAttribute(assembly, info); + + RegisterColorClasses(assembly); + + RegisterGameModeAttribute(assembly, info); + + _registeredPlugins.Add(assembly, info); + + PaletteManager.RegisterAllColors(); + CustomGameModeManager.RegisterDefaultMode(); + + CustomGameModeManager.GetAndSetGameMode(); + + Logger.Info("Initialised mira stuff"); } /// /// Get a mira plugin by its GUID. /// - /// The plugin GUID. + /// The plugin GUID. /// A MiraPluginInfo. - public static MiraPluginInfo GetPluginByGuid(string guid) + public static MiraPluginInfo GetPluginByGuid(string pluginGUID) { - return Instance._registeredPlugins.Values.First(plugin => plugin.PluginId == guid); + return Instance._registeredPlugins.Values.First(plugin => plugin.PluginId == pluginGUID); } private static void RegisterAllOptions(Assembly assembly, MiraPluginInfo pluginInfo) @@ -180,4 +210,17 @@ public sealed class MiraPluginManager } } } + + private static void RegisterGameModeAttribute(Assembly assembly, MiraPluginInfo pluginInfo) + { + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttribute(); + + if (attribute != null) + { + CustomGameModeManager.RegisterGameMode(type, pluginInfo); + } + } + } } From f9f7898277cfdd6c337783feb8f5d5abfa664a86 Mon Sep 17 00:00:00 2001 From: AlchlcDvl <56699741+AlchlcDvl@users.noreply.github.com> Date: Sat, 7 Dec 2024 11:18:58 -0500 Subject: [PATCH 002/102] set option title --- MiraAPI/GameModes/AbstractGameMode.cs | 3 +++ MiraAPI/GameModes/CustomGameModeManager.cs | 14 +++++++++++--- MiraAPI/GameModes/GameModeOption.cs | 5 ++++- MiraAPI/Patches/GameModes/ConsolePatches.cs | 4 +++- MiraAPI/Patches/GameModes/DeadBodyPatch.cs | 4 +++- MiraAPI/Patches/GameModes/GameLogicPatches.cs | 4 +++- MiraAPI/Patches/GameModes/HudPatches.cs | 10 ++++++---- MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs | 6 ++++-- MiraAPI/Patches/GameModes/MapBehaviourPatch.cs | 4 +++- MiraAPI/Patches/GameModes/OnDeathPatch.cs | 4 +++- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 11 +++++++++++ MiraAPI/Patches/Options/OptionsPatches.cs | 15 +++++++++++++-- 12 files changed, 67 insertions(+), 17 deletions(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 466c282..0f820c8 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -157,4 +157,7 @@ public abstract class AbstractGameMode /// Player attempting to vent. /// True if venting is enabled in this mode. public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true; + + /// + public override string ToString() => Name; } diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 62d27e2..99bff39 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -4,6 +4,7 @@ using Il2CppSystem.Threading.Tasks; using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using Reactor.Utilities; +using Reactor.Utilities.Extensions; namespace MiraAPI.GameModes; @@ -72,11 +73,18 @@ public static class CustomGameModeManager if (IdToModeMap.TryGetValue(id, out var mode)) { ActiveMode = mode; - return; + } + else if (id != 0) + { + ActiveMode = IdToModeMap[0]; + OptionGroupSingleton.Instance.CurrentMode.SetValue(0); + Logger.Warning($"Unable to find game mode of id {id}!"); } - ActiveMode = IdToModeMap[0]; - Logger.Warning($"Unable to find game mode of id {id}!"); + if (OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour != null && ActiveMode != null) + { + ((NumberOption)OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour).TitleText.SetText($"Game Mode: <#{ActiveMode.Color.ToHtmlStringRGBA()}>{ActiveMode}"); + } } internal static void GetAndSetGameMode() diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 729cdc3..9c02dc6 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -11,7 +11,10 @@ public class GameModeOption : AbstractOptionGroup /// public override string GroupName => "Game Mode"; - public ModdedNumberOption CurrentMode { get; set; } = new("Current Game Mode", 0, 0, CustomGameModeManager.LastId, 1, Utilities.MiraNumberSuffixes.None) + /// + /// Gets or sets the game mode option. + /// + public ModdedNumberOption CurrentMode { get; set; } = new("Game Mode: {0}", 0, 0, CustomGameModeManager.LastId, 1, Utilities.MiraNumberSuffixes.None) { ChangedEvent = x => CustomGameModeManager.SetGameMode((uint)x), }; diff --git a/MiraAPI/Patches/GameModes/ConsolePatches.cs b/MiraAPI/Patches/GameModes/ConsolePatches.cs index 09fbe17..d8a6dd8 100644 --- a/MiraAPI/Patches/GameModes/ConsolePatches.cs +++ b/MiraAPI/Patches/GameModes/ConsolePatches.cs @@ -2,8 +2,10 @@ using InnerNet; using MiraAPI.GameModes; +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch] -public static class ConsolePatches +internal static class ConsolePatches { [HarmonyPrefix, HarmonyPatch(typeof(Console), nameof(Console.CanUse))] public static bool CanUsePatch(Console __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) diff --git a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs index 1070336..fff7e40 100644 --- a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs +++ b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs @@ -1,8 +1,10 @@ using HarmonyLib; using MiraAPI.GameModes; +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch(typeof(DeadBody))] -public static class DeadBodyPatch +internal static class DeadBodyPatch { [HarmonyPrefix, HarmonyPatch(nameof(DeadBody.OnClick))] public static bool OnClickPatch(DeadBody __instance) diff --git a/MiraAPI/Patches/GameModes/GameLogicPatches.cs b/MiraAPI/Patches/GameModes/GameLogicPatches.cs index 75d4ed9..46dea2a 100644 --- a/MiraAPI/Patches/GameModes/GameLogicPatches.cs +++ b/MiraAPI/Patches/GameModes/GameLogicPatches.cs @@ -1,8 +1,10 @@ using HarmonyLib; using MiraAPI.GameModes; +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch] -public static class GameLogicPatches +internal static class GameLogicPatches { [HarmonyPrefix, HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] public static bool EndCritPatch(LogicGameFlowNormal __instance) diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index 50871b9..e232920 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -1,12 +1,14 @@ using HarmonyLib; using MiraAPI.GameModes; -[HarmonyPatch] -public static class HudPatches +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(HudManager))] +internal static class HudPatches { - [HarmonyPostfix, HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] + [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Start))] public static void HudStartPatch(HudManager __instance) => CustomGameModeManager.ActiveMode?.HudStart(__instance); - [HarmonyPostfix, HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] + [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] public static void HudUpdatePatch(HudManager __instance) => CustomGameModeManager.ActiveMode?.HudUpdate(__instance); } diff --git a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs index 08e39f9..de9bbf1 100644 --- a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs +++ b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs @@ -1,11 +1,13 @@ using HarmonyLib; using MiraAPI.GameModes; +namespace MiraAPI.Patches.GameModes; + /// -/// Allow Impostors to kill each other if can kill is enabled in gamemode or friendly fire is toggled on +/// Allow Impostors to kill each other if can kill is enabled in gamemode or friendly fire is toggled on. /// [HarmonyPatch(typeof(ImpostorRole), "IsValidTarget")] -public static class ImpostorTargetPatch +internal static class ImpostorTargetPatch { public static bool Prefix(ImpostorRole __instance, [HarmonyArgument(0)] NetworkedPlayerInfo target, ref bool __result) { diff --git a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs index b6c48fc..06ea282 100644 --- a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs +++ b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs @@ -1,8 +1,10 @@ using HarmonyLib; using MiraAPI.GameModes; +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch(typeof(MapBehaviour))] -public static class MapBehaviourPatch +internal static class MapBehaviourPatch { [HarmonyPrefix, HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))] public static bool ShowSabotagePatch(MapBehaviour __instance) diff --git a/MiraAPI/Patches/GameModes/OnDeathPatch.cs b/MiraAPI/Patches/GameModes/OnDeathPatch.cs index dcedbab..ba96c6c 100644 --- a/MiraAPI/Patches/GameModes/OnDeathPatch.cs +++ b/MiraAPI/Patches/GameModes/OnDeathPatch.cs @@ -1,8 +1,10 @@ using HarmonyLib; using MiraAPI.GameModes; +namespace MiraAPI.Patches.GameModes; + [HarmonyPatch(typeof(KillAnimation))] -public static class OnDeathPatch +internal static class OnDeathPatch { [HarmonyPostfix, HarmonyPatch(nameof(KillAnimation.CoPerformKill))] public static void OnDeathPostfix([HarmonyArgument(0)] PlayerControl source, [HarmonyArgument(1)] PlayerControl target) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index ee26822..66db435 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -1,8 +1,11 @@ using HarmonyLib; +using MiraAPI.GameModes; +using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -181,6 +184,14 @@ public static class LobbyViewPanePatches else { viewSettingsInfoPanel.SetInfo(data.Title, data.GetValueString(option.GetFloatData()), 61); + + if (viewSettingsInfoPanel.titleText.text.Contains("Game Mode")) + { + viewSettingsInfoPanel.titleText.text = TranslationController.Instance.GetString( + OptionGroupSingleton.Instance.CurrentMode.StringName, + $"<#{CustomGameModeManager.ActiveMode?.Color.ToHtmlStringRGBA()}>{CustomGameModeManager.ActiveMode}" + ); + } } instance.settingsInfo.Add(viewSettingsInfoPanel.gameObject); diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index dae6905..bb520c0 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -1,6 +1,9 @@ using HarmonyLib; using Il2CppSystem; +using MiraAPI.GameModes; +using MiraAPI.GameOptions; using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; namespace MiraAPI.Patches.Options; @@ -38,8 +41,16 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = - TranslationController.Instance.GetString(__instance.Title, Array.Empty()); + if (__instance == OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour) + { + __instance.TitleText.text = + TranslationController.Instance.GetString(__instance.Title, $"<#{CustomGameModeManager.ActiveMode?.Color.ToHtmlStringRGBA()}>{CustomGameModeManager.ActiveMode}"); + } + else + { + __instance.TitleText.text = + TranslationController.Instance.GetString(__instance.Title); + } return false; } From b6ba8f59bd13d17fa9059a2c2dcdadc93f337bd0 Mon Sep 17 00:00:00 2001 From: AlchlcDvl <56699741+AlchlcDvl@users.noreply.github.com> Date: Sat, 7 Dec 2024 11:21:07 -0500 Subject: [PATCH 003/102] better approach --- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 66db435..0649873 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -188,8 +188,7 @@ public static class LobbyViewPanePatches if (viewSettingsInfoPanel.titleText.text.Contains("Game Mode")) { viewSettingsInfoPanel.titleText.text = TranslationController.Instance.GetString( - OptionGroupSingleton.Instance.CurrentMode.StringName, - $"<#{CustomGameModeManager.ActiveMode?.Color.ToHtmlStringRGBA()}>{CustomGameModeManager.ActiveMode}" + data.Title, $"<#{CustomGameModeManager.ActiveMode?.Color.ToHtmlStringRGBA()}>{CustomGameModeManager.ActiveMode}" ); } } From edca42247d96cce24e6d56680889edc3a353a1a1 Mon Sep 17 00:00:00 2001 From: AlchlcDvl <56699741+AlchlcDvl@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:52:18 -0500 Subject: [PATCH 004/102] finish full implementation of game modes i hope --- MiraAPI/GameModes/AbstractGameMode.cs | 30 +++++++++---------- MiraAPI/GameOptions/AbstractOptionGroup.cs | 5 ++++ .../Attributes/ModdedEnumOptionAttribute.cs | 11 +++++-- .../Attributes/ModdedNumberOptionAttribute.cs | 15 ++++++++-- .../Attributes/ModdedOptionAttribute.cs | 8 ++++- .../Attributes/ModdedToggleOptionAttribute.cs | 5 ++-- MiraAPI/GameOptions/IModdedOption.cs | 1 + MiraAPI/GameOptions/ModdedOptionsManager.cs | 1 + .../OptionTypes/ModdedEnumOption.cs | 3 +- .../OptionTypes/ModdedNumberOption.cs | 4 ++- .../GameOptions/OptionTypes/ModdedOption.cs | 14 ++++++++- .../OptionTypes/ModdedToggleOption.cs | 3 +- .../Patches/GameModes/IntroCutscenePatch.cs | 20 +++++++++++++ MiraAPI/Patches/GameModes/WinnersPatch.cs | 27 +++++++++++++++++ .../Patches/Options/GameOptionsMenuPatch.cs | 6 ++-- .../Patches/Options/LobbyViewPanePatches.cs | 2 +- .../Patches/Options/RoleSettingMenuPatches.cs | 15 ++++++---- MiraAPI/Roles/ICustomRole.cs | 9 +++++- 18 files changed, 141 insertions(+), 38 deletions(-) create mode 100644 MiraAPI/Patches/GameModes/IntroCutscenePatch.cs create mode 100644 MiraAPI/Patches/GameModes/WinnersPatch.cs diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 0f820c8..b7acea3 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using UnityEngine; namespace MiraAPI.GameModes; @@ -28,6 +29,11 @@ public abstract class AbstractGameMode /// public uint ID { get; set; } + /// + /// Gets a value indicating whether a custom intro sequence is implemented by the game mode. + /// + public virtual bool ShowGameModeIntroCutscene => false; + /// /// Called when Intro Cutscene is destroyed. /// @@ -91,18 +97,6 @@ public abstract class AbstractGameMode runOriginal = true; } - /// - /// Should Roles Settings be available when this gamemode is selected. - /// - /// True if Role Settings are enabled in this game mode. - public virtual bool AreRoleSettingsEnabled() => true; - - /// - /// Should Game Settings be available when this gamemode is selected. - /// - /// True if Game Settings are enabled in this mode. - public virtual bool AreGameSettingsEnabled() => true; - /// /// Custom winner selection. /// @@ -110,10 +104,14 @@ public abstract class AbstractGameMode public virtual List? CalculateWinners() => null; /// - /// Show gamemode in Intro Cutscene. + /// The IEnumerator that plays the intro cutscene for this gamemode. /// - /// True if the game mode should be shown in the intro cutscene. - public virtual bool ShowGameModeIntroCutscene() => false; + /// An instance of IntroCutscene. + /// An IEnumerator to run the intro cutscene instead of the base game one. + public virtual IEnumerator IntroCutscene(IntroCutscene __instance) + { + yield return new WaitForEndOfFrame(); + } /// /// Can Admin be used in this gamemode. diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 418f25b..9cbdc95 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -37,6 +37,11 @@ public abstract class AbstractOptionGroup /// public virtual Type? AdvancedRole => null; + /// + /// Gets the role the group is associated with. This is used for the advanced role options menu. + /// + public virtual Type? AdvancedMode => null; + internal bool AllOptionsHidden { get; set; } internal CategoryHeaderMasked? Header { get; set; } diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index 2944520..5bb8433 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -7,13 +7,18 @@ namespace MiraAPI.GameOptions.Attributes; /// /// Attribute for creating an enum option. /// +/// The title of the option. +/// The Enum type. +/// An option list of string values to use in place of the enum name. +/// An optional role type. +/// An optional game mode type. [AttributeUsage(AttributeTargets.Property)] -public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null, Type? roleType = null) - : ModdedOptionAttribute(title, roleType) +public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null, Type? roleType = null, Type? modeType = null) + : ModdedOptionAttribute(title, roleType, modeType) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values, RoleType); + var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values, RoleType, ModeType); return opt; } diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index 2bd4438..50db882 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -8,6 +8,14 @@ namespace MiraAPI.GameOptions.Attributes; /// /// A number option attribute for the modded options system. /// +/// The title of the option. +/// The minimum value. +/// The maximum value. +/// The increment. +/// The suffix type. +/// Whether zero is infinity or not. +/// An optional role type. +/// An optional game mode type. [AttributeUsage(AttributeTargets.Property)] public class ModdedNumberOptionAttribute( string title, @@ -16,12 +24,13 @@ public class ModdedNumberOptionAttribute( float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, bool zeroInfinity = false, - Type? roleType = null) - : ModdedOptionAttribute(title, roleType) + Type? roleType = null, + Type? modeType = null) + : ModdedOptionAttribute(title, roleType, modeType) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, zeroInfinity, RoleType); + return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, zeroInfinity, RoleType, ModeType); } /// diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index 702d75e..bb9dab5 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -8,8 +8,9 @@ namespace MiraAPI.GameOptions.Attributes; /// /// The option title. /// Optional parameter to specify a role Type. +/// Optional parameter to specify a game mode Type. [AttributeUsage(AttributeTargets.Property)] -public abstract class ModdedOptionAttribute(string title, Type? roleType = null) : Attribute +public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : Attribute { internal IModdedOption? HolderOption { get; set; } @@ -23,6 +24,11 @@ public abstract class ModdedOptionAttribute(string title, Type? roleType = null) /// protected Type? RoleType { get; private set; } = roleType; + /// + /// Gets the game mode type of the option. + /// + protected Type? ModeType { get; private set; } = modeType; + /// /// Sets the value of the option. /// diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs index c389292..5adfe77 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs @@ -9,12 +9,13 @@ namespace MiraAPI.GameOptions.Attributes; /// /// The option title. /// An optional role type. +/// An optional game mode type. [AttributeUsage(AttributeTargets.Property)] -public class ModdedToggleOptionAttribute(string title, Type? roleType = null) : ModdedOptionAttribute(title, roleType) +public class ModdedToggleOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : ModdedOptionAttribute(title, roleType, modeType) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), RoleType); + var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), RoleType, ModeType); return toggleOpt; } diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 1aee3c4..b2c542e 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -14,6 +14,7 @@ public interface IModdedOption IMiraPlugin? ParentMod { get; set; } BaseGameSetting Data { get; } Type? AdvancedRole { get; set; } + Type? AdvancedMode { get; set; } OptionBehaviour? OptionBehaviour { get; } Func Visible { get; set; } ConfigDefinition? ConfigDefinition { get; set; } diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 3f30019..1913d07 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -121,6 +121,7 @@ public static class ModdedOptionsManager option.ParentMod = pluginInfo.MiraPlugin; option.AdvancedRole = group.AdvancedRole; + option.AdvancedMode = group.AdvancedMode; pluginInfo.Options.Add(option); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 0d236a0..74c1860 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -26,7 +26,8 @@ public class ModdedEnumOption : ModdedOption /// The Enum type. /// An option list of string values to use in place of the enum name. /// An optional role type to specify for a specific role. - public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType) + /// An optional game mode type to specify for a specific mode. + public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, Type? roleType = null, Type? modeType = null) : base(title, defaultValue, roleType, modeType) { Values = values ?? Enum.GetNames(enumType); Data = ScriptableObject.CreateInstance(); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index e86d88d..3256654 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -48,6 +48,7 @@ public class ModdedNumberOption : ModdedOption /// The suffix type. /// Whether zero is infinity or not. /// An optional role type. + /// An optional game mode type. public ModdedNumberOption( string title, float defaultValue, @@ -56,7 +57,8 @@ public class ModdedNumberOption : ModdedOption float increment, MiraNumberSuffixes suffixType, bool zeroInfinity = false, - Type? roleType = null) : base(title, defaultValue, roleType) + Type? roleType = null, + Type? modeType = null) : base(title, defaultValue, roleType, modeType) { Min = min; Max = max; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index d811f1d..3719353 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -1,4 +1,5 @@ using BepInEx.Configuration; +using MiraAPI.GameModes; using MiraAPI.Networking; using MiraAPI.PluginLoading; using MiraAPI.Roles; @@ -77,6 +78,11 @@ public abstract class ModdedOption : IModdedOption /// public Type? AdvancedRole { get; set; } + /// + /// Gets or sets the advanced mode of the option. + /// + public Type? AdvancedMode { get; set; } + /// /// Gets or sets the option behaviour of the option. /// @@ -103,7 +109,8 @@ public abstract class ModdedOption : IModdedOption /// The option title. /// The default value. /// The Role Type or null if it doesn't belong to a role. - protected ModdedOption(string title, T defaultValue, Type? roleType) + /// The Game Mode Type or null if it doesn't belong to a mode. + protected ModdedOption(string title, T defaultValue, Type? roleType, Type? modeType) { Id = ModdedOptionsManager.NextId; Title = title; @@ -116,6 +123,11 @@ public abstract class ModdedOption : IModdedOption { AdvancedRole = roleType; } + + if (modeType is not null && modeType.IsAssignableTo(typeof(AbstractGameMode))) + { + AdvancedMode = modeType; + } } internal void ValueChanged(OptionBehaviour optionBehaviour) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 12f8f5f..79e8281 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -16,7 +16,8 @@ public class ModdedToggleOption : ModdedOption /// The option title. /// The default value. /// An optional role type. - public ModdedToggleOption(string title, bool defaultValue, Type? roleType = null) : base(title, defaultValue, roleType) + /// An optional game mode type. + public ModdedToggleOption(string title, bool defaultValue, Type? roleType = null, Type? modeType = null) : base(title, defaultValue, roleType, modeType) { Data = ScriptableObject.CreateInstance(); diff --git a/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs b/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs new file mode 100644 index 0000000..5701014 --- /dev/null +++ b/MiraAPI/Patches/GameModes/IntroCutscenePatch.cs @@ -0,0 +1,20 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.CoBegin))] +internal static class IntroCutscenePatch +{ + public static bool Prefix(IntroCutscene __instance, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene == true) + { + __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/WinnersPatch.cs b/MiraAPI/Patches/GameModes/WinnersPatch.cs new file mode 100644 index 0000000..01f9859 --- /dev/null +++ b/MiraAPI/Patches/GameModes/WinnersPatch.cs @@ -0,0 +1,27 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))] +internal static class WinnersPatch +{ + [HarmonyPostfix] + public static void ShowWinningScreen() + { + if (CustomGameModeManager.ActiveMode is null or DefaultMode) + return; + + var winners = CustomGameModeManager.ActiveMode.CalculateWinners(); + + if (winners is null) + return; + + EndGameResult.CachedWinners.Clear(); + + foreach (var data in winners) + { + EndGameResult.CachedWinners.Add(new(data)); + } + } +} diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 92cd925..7e75ede 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; @@ -32,7 +33,7 @@ public static class GameOptionsMenuPatch var num = 2.1f; var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups.Where( - x => x.AdvancedRole is null) ?? []; + x => x.AdvancedRole is null && (x.AdvancedMode is null || x.AdvancedMode == CustomGameModeManager.ActiveMode?.GetType())) ?? []; foreach (var group in filteredGroups) { @@ -99,7 +100,8 @@ public static class GameOptionsMenuPatch __instance.MapPicker.gameObject.SetActive(false); - var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(x => x.AdvancedRole is null) ?? []; + var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(x => x.AdvancedRole is null && (x.AdvancedMode is null || x.AdvancedMode == + CustomGameModeManager.ActiveMode?.GetType())) ?? []; foreach (var group in filteredGroups) { diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 0649873..e00d1cd 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -280,7 +280,7 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f); var advancedRoleOptions = SelectedMod.Options - .Where(x => x.AdvancedRole == customRole.GetType()) + .Where(x => x.AdvancedRole == customRole.GetType() && (x.AdvancedMode is null || x.AdvancedMode == customRole.AssociatedGameMode)) .ToList(); if (!flag && advancedRoleOptions.Count > 0) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 4461423..17bce70 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -1,5 +1,6 @@ using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.GameModes; using MiraAPI.Networking; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -18,7 +19,7 @@ using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(RolesSettingsMenu))] -public static class RoleSettingMenuPatches +internal static class RoleSettingMenuPatches { [HarmonyPrefix] [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] @@ -56,7 +57,8 @@ public static class RoleSettingMenuPatches var crewRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values .OfType() - .Where(role => role is { Team: ModdedRoleTeams.Crewmate, Configuration.HideSettings: false }) + .Where(role => role is { Team: ModdedRoleTeams.Crewmate, Configuration.HideSettings: false } && (role.AssociatedGameMode is null || role.AssociatedGameMode == + CustomGameModeManager.ActiveMode?.GetType())) .ToList() ?? []; if (crewRoles is { Count: > 0 }) @@ -82,7 +84,8 @@ public static class RoleSettingMenuPatches var impRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values .OfType() - .Where(role => role is { Team: ModdedRoleTeams.Impostor, Configuration.HideSettings: false }) + .Where(role => role is { Team: ModdedRoleTeams.Impostor, Configuration.HideSettings: false } && (role.AssociatedGameMode is null || role.AssociatedGameMode == + CustomGameModeManager.ActiveMode?.GetType())) .ToList() ?? []; if (impRoles is { Count: > 0 }) @@ -109,7 +112,8 @@ public static class RoleSettingMenuPatches var neutRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values .OfType() - .Where(role => role is { Team: ModdedRoleTeams.Neutral, Configuration.HideSettings: false }) + .Where(role => role is { Team: ModdedRoleTeams.Neutral, Configuration.HideSettings: false } && (role.AssociatedGameMode is null || role.AssociatedGameMode == + CustomGameModeManager.ActiveMode?.GetType())) .ToList() ?? []; if (neutRoles is { Count: > 0 }) @@ -208,7 +212,8 @@ public static class RoleSettingMenuPatches var num = -0.872f; - var filteredOptions = GameSettingMenuPatches.SelectedMod?.Options.Where(x => x.AdvancedRole == role.GetType()) ?? []; + var filteredOptions = GameSettingMenuPatches.SelectedMod?.Options?.Where(x => x.AdvancedRole == role.GetType() && (x.AdvancedMode is null || x.AdvancedMode == + CustomGameModeManager.ActiveMode?.GetType())) ?? []; foreach (var option in filteredOptions) { diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index ba318b2..eae4d30 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,9 +1,11 @@ using BepInEx.Configuration; +using MiraAPI.GameModes; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; -using System.Text; using Reactor.Utilities; +using System; +using System.Text; using UnityEngine; namespace MiraAPI.Roles; @@ -48,6 +50,11 @@ public interface ICustomRole /// MiraPluginInfo ParentMod => CustomRoleManager.FindParentMod(this); + /// + /// Gets the game mode this role is a part of. + /// + Type AssociatedGameMode => typeof(DefaultMode); + /// /// This method runs on the PlayerControl.FixedUpdate method for ALL players with this role. /// From 409503a2615e22bcf388f9de673a24b781ac4db2 Mon Sep 17 00:00:00 2001 From: AlchlcDvl <56699741+AlchlcDvl@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:57:53 -0500 Subject: [PATCH 005/102] read me change to reflect now mode filtering --- README.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 08a6d67..3605491 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,13 @@ MyMiraMod/ ## Roles Roles are very simple in Mira API. There are 3 things you need to do to create a custom role: -1. Create a class that inherits from a base game role (like `CrewmateRole`, `ImpostorRole`, etc) +1. Create a class that inherits from a base game role (like `CrewmateRole`, `ImpostorRole`, etc) 2. Implement the `ICustomRole` interface from Mira API. 3. Add the `[RegisterCustomRole]` attribute to the class. **Disclaimer: Make sure your plugin class has the following attribute `[ReactorModFlags(ModFlags.RequireOnAllClients)]` or else your roles will not register correctly.** -Note: For step 1, if you are making neutral roles, choose either `CrewmateRole` or `ImpostorRole` as the base depending on if it can kill or not! +Note: For step 1, if you are making neutral roles, choose either `CrewmateRole` or `ImpostorRole` as the base depending on if it can kill or not! Mira API handles everything else, from adding the proper options to the settings menu, to managing the role assignment at the start of the game. There are no extra steps on the developer's part. @@ -92,7 +92,7 @@ Here is an example of a group class: public class MyOptionsGroup : AbstractOptionGroup { public override string GroupName => "My Options"; // this is required - + [ModdedNumberOption("My Number Option", min: 0, max: 10)] public float MyNumberOption { get; set; } = 5f; } @@ -106,7 +106,7 @@ Logger.Info(myGroup.MyNumberOption); // prints the value of the option ``` Once you have an options group, there are two ways to make the actual options: -- Use an Option Attribute with a property. +- Use an Option Attribute with a property. - Create a ModdedOption property. ### Option Attributes @@ -121,7 +121,7 @@ public float SussyLevel { get; set; } = 4f; // You can set a default value here. Here are the available Option Attributes and their signatures: ```csharp ModdedEnumOption(string name, Type enumType, string[]? values = null, Type? roleType = null) - + ModdedNumberOption( string name, float min, @@ -141,7 +141,7 @@ And this is an example of a ModdedOption property: public ModdedToggleOption YeezusAbility { get; } = new ModdedToggleOption("Yeezus Ability", false); ``` -Here is a full list of ModdedOption classes you can use: +Here is a full list of ModdedOption classes you can use: - `ModdedEnumOption` - `ModdedNumberOption` - `ModdedToggleOption` @@ -150,29 +150,30 @@ To see a full example of an options class, see [this file](https://github.com/Al ### Role Options -You can also specify a role type for an option or option group. +You can also specify a role or game mode type for an option or option group. -To set the role type for an entire group, set the `AdvancedRole` property on that group like this: +For an entire group, you can do this: ```csharp public class MyOptionsGroup : AbstractOptionGroup { public override string GroupName => "My Options"; public override Type AdvancedRole => typeof(MyRole); // this is the role that will have these options - + public override Type AdvancedMode => typeof(MyMode); // this is the mode that these options are attached to + [ModdedNumberOption("Ability Uses", min: 0, max: 10)] public float AbilityUses { get; set; } = 5f; } ``` -To set the role type for individual options, specify the `roleType` parameter in the option like this: +To set the role type for individual options, do this: ```csharp // this group doesnt specify a role, so it will show up in the global settings public class MyOptionsGroup : AbstractOptionGroup { public override string GroupName => "My Options"; - - // this option will only show up in the settings for MyRole - [ModdedNumberOption("Ability Uses", min: 0, max: 10, roleType: typeof(MyRole))] + + // this option will only show up in the settings for MyRole when the current mode is set to MyMode + [ModdedNumberOption("Ability Uses", min: 0, max: 10, roleType: typeof(MyRole), modeType: typeof(MyMode))] public float AbilityUses { get; set; } = 5f; } ``` @@ -182,7 +183,7 @@ An example can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/ma ## Custom Murders Mira API provides it's own implementation for murders. Our implementation allows for more customization on kills, and helps bypass server checks. You can use `PlayerControl.RpcCustomMurder` to perform a networked custom murder, or `PlayerControl.CustomMurder` to normally perform a custom murder. -For example: +For example: ```cs PlayerControl.LocalPlayer.RpcCustomMurder(Target, createDeadBody: false, teleportMurderer: false, playKillSound: false, resetKillTimer: false, showKillAnim: false); ``` @@ -220,17 +221,17 @@ Here is an example of a custom color class: [RegisterCustomColors] public static class MyCustomColors { - public static CustomColor Cerulean { get; } = new CustomColor("Cerulean", new Color(0.0f, 0.48f, 0.65f)); + public static CustomColor Cerulean { get; } = new CustomColor("Cerulean", new Color(0.0f, 0.48f, 0.65f)); public static CustomColor Rose { get; } = new CustomColor("Rose", new Color(0.98f, 0.26f, 0.62f)); - + public static CustomColor Gold { get; } = new CustomColor("Gold", new Color(1.0f, 0.84f, 0.0f)); } ``` ## Assets -Mira API provides a simple, but expandable asset system. The core of the system is the `LoadableAsset` class. This is a generic abstract class that provides a pattern for loading assets. +Mira API provides a simple, but expandable asset system. The core of the system is the `LoadableAsset` class. This is a generic abstract class that provides a pattern for loading assets. Mira API comes with two asset loaders: 1. `LoadableBundleAsset`: This is used for loading assets from AssetBundles. From 9c15ca4d0616c266416f82f083a18b9a0eab6e18 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:43:25 -0500 Subject: [PATCH 006/102] update to optimization --- MiraAPI/GameModes/AbstractGameMode.cs | 2 + MiraAPI/GameModes/CustomGameModeManager.cs | 14 ++-- MiraAPI/GameModes/DefaultMode.cs | 5 +- MiraAPI/GameModes/GameModeOption.cs | 2 +- MiraAPI/GameOptions/AbstractOptionGroup.cs | 2 + .../OptionTypes/ModdedEnumOption.cs | 4 +- MiraAPI/Hud/CustomActionButton.cs | 3 + MiraAPI/MiraApiPlugin.cs | 1 + MiraAPI/Modifiers/BaseModifier.cs | 5 +- MiraAPI/Modifiers/Types/GameModifier.cs | 5 +- MiraAPI/Modifiers/Types/TimedModifier.cs | 2 + .../Patches/Options/LobbyViewPanePatches.cs | 2 +- MiraAPI/Patches/Options/OptionsPatches.cs | 2 +- MiraAPI/PluginLoading/MiraPluginManager.cs | 79 ++++++++++--------- 14 files changed, 76 insertions(+), 52 deletions(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index b7acea3..67304a6 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using MiraAPI.PluginLoading; using UnityEngine; namespace MiraAPI.GameModes; @@ -7,6 +8,7 @@ namespace MiraAPI.GameModes; /// /// Base class for custom gamemodes. /// +[MiraDisable] public abstract class AbstractGameMode { /// diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 99bff39..c51c409 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -15,15 +15,13 @@ public static class CustomGameModeManager { private static readonly Dictionary IdToModeMap = []; - private static uint _nextId; - private static uint GetNextId() { - _nextId++; - return _nextId; + LastId++; + return LastId; } - internal static uint LastId => _nextId; + internal static uint LastId { get; private set; } /// /// Register gamemode from type. @@ -45,9 +43,9 @@ public static class CustomGameModeManager } IdToModeMap.Add(GetNextId(), mode); - pluginInfo.GameModes.Add(_nextId, mode); + pluginInfo.GameModes.Add(LastId, mode); - mode.ID = _nextId; + mode.ID = LastId; } /// @@ -83,7 +81,7 @@ public static class CustomGameModeManager if (OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour != null && ActiveMode != null) { - ((NumberOption)OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour).TitleText.SetText($"Game Mode: <#{ActiveMode.Color.ToHtmlStringRGBA()}>{ActiveMode}"); + ((NumberOption)OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour).TitleText.SetText($"Game Mode: {ActiveMode}"); } } diff --git a/MiraAPI/GameModes/DefaultMode.cs b/MiraAPI/GameModes/DefaultMode.cs index 99f64df..7f4cc70 100644 --- a/MiraAPI/GameModes/DefaultMode.cs +++ b/MiraAPI/GameModes/DefaultMode.cs @@ -1,8 +1,11 @@ -namespace MiraAPI.GameModes; +using MiraAPI.PluginLoading; + +namespace MiraAPI.GameModes; /// /// The default game mode. /// +[MiraDisable] public class DefaultMode : AbstractGameMode { /// diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 9c02dc6..3d9ec03 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -14,7 +14,7 @@ public class GameModeOption : AbstractOptionGroup /// /// Gets or sets the game mode option. /// - public ModdedNumberOption CurrentMode { get; set; } = new("Game Mode: {0}", 0, 0, CustomGameModeManager.LastId, 1, Utilities.MiraNumberSuffixes.None) + public ModdedNumberOption CurrentMode { get; set; } = new("Game Mode: {0}", 0, 0, CustomGameModeManager.LastId-1, 1, Utilities.MiraNumberSuffixes.None) { ChangedEvent = x => CustomGameModeManager.SetGameMode((uint)x), }; diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 9cbdc95..ac4fb0c 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using MiraAPI.PluginLoading; using UnityEngine; namespace MiraAPI.GameOptions; @@ -7,6 +8,7 @@ namespace MiraAPI.GameOptions; /// /// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. /// +[MiraDisable] public abstract class AbstractOptionGroup { internal List Options { get; } = []; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index b3c5283..861df98 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -119,10 +119,10 @@ public class ModdedEnumOption : ModdedOption where T : Enum /// /// The title of the option. /// The default value as an int. - /// The Enum type. /// An option list of string values to use in place of the enum name. /// An optional role type to specify for a specific role. - public ModdedEnumOption(string title, T defaultValue, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType) + /// An optional mode type to specify for a specific gamemode. + public ModdedEnumOption(string title, T defaultValue, string[]? values = null, Type? roleType = null, Type? modeType = null) : base(title, defaultValue, roleType, modeType) { Values = values ?? Enum.GetNames(typeof(T)); Data = ScriptableObject.CreateInstance(); diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 3e47d59..01cedb8 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -1,6 +1,7 @@ using System; using MiraAPI.Events; using MiraAPI.Events.Mira; +using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using UnityEngine; using UnityEngine.Events; @@ -12,6 +13,7 @@ namespace MiraAPI.Hud; /// /// Class for making custom action buttons. More customizable than the default Action/Ability buttons in the base game. /// +[MiraDisable] public abstract class CustomActionButton { /// @@ -353,6 +355,7 @@ public abstract class CustomActionButton /// Custom action button that has a target object. /// /// The type of the target object. +[MiraDisable] public abstract class CustomActionButton : CustomActionButton where T : MonoBehaviour { /// diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 6efca17..8955531 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -2,6 +2,7 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.PluginLoading; using Reactor; using Reactor.Networking; diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 47cd491..11701cc 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,8 +1,11 @@ -namespace MiraAPI.Modifiers; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Modifiers; /// /// Base class for all modifiers. /// +[MiraDisable] public abstract class BaseModifier { /// diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index a7b1fe1..fdf8422 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -1,8 +1,11 @@ -namespace MiraAPI.Modifiers.Types; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Modifiers.Types; /// /// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role. /// +[MiraDisable] public abstract class GameModifier : BaseModifier { /// diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 286b80a..4711d4a 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -1,4 +1,5 @@ using System; +using MiraAPI.PluginLoading; using Reactor.Utilities; using UnityEngine; @@ -7,6 +8,7 @@ namespace MiraAPI.Modifiers.Types; /// /// The base class for all timed modifiers. Timed modifiers have a duration and can be started and stopped. /// +[MiraDisable] public abstract class TimedModifier : BaseModifier { /// diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index e00d1cd..a125709 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -188,7 +188,7 @@ public static class LobbyViewPanePatches if (viewSettingsInfoPanel.titleText.text.Contains("Game Mode")) { viewSettingsInfoPanel.titleText.text = TranslationController.Instance.GetString( - data.Title, $"<#{CustomGameModeManager.ActiveMode?.Color.ToHtmlStringRGBA()}>{CustomGameModeManager.ActiveMode}" + data.Title, $"{CustomGameModeManager.ActiveMode}" ); } } diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index bb520c0..f5c7da3 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -44,7 +44,7 @@ public static class OptionsPatches if (__instance == OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour) { __instance.TitleText.text = - TranslationController.Instance.GetString(__instance.Title, $"<#{CustomGameModeManager.ActiveMode?.Color.ToHtmlStringRGBA()}>{CustomGameModeManager.ActiveMode}"); + TranslationController.Instance.GetString(__instance.Title, $"{CustomGameModeManager.ActiveMode}"); } else { diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 3842479..12e0875 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using BepInEx; using BepInEx.Unity.IL2CPP; using MiraAPI.Colors; using MiraAPI.Events; @@ -31,42 +32,11 @@ public sealed class MiraPluginManager internal void Initialize() { Instance = this; - IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => - { - if (plugin is not IMiraPlugin miraPlugin) - { - return; - } + RegisterPlugin(IL2CPPChainloader.Instance.Plugins[MiraApiPlugin.Id], typeof(MiraApiPlugin).Assembly, MiraApiPlugin.Instance); + CustomGameModeManager.RegisterDefaultMode(); + CustomGameModeManager.GetAndSetGameMode(); - var info = new MiraPluginInfo(miraPlugin, pluginInfo); - var roles = new List(); - - foreach (var type in assembly.GetTypes()) - { - if (type.GetCustomAttribute() != null) - { - continue; - } - - RegisterModifier(type); - RegisterOptions(type, info); - - if (RegisterRoleAttribute(type, info, out var role)) - { - roles.Add(role!); - } - - RegisterButtonAttribute(type, info); - RegisterColorClasses(type); - RegisterGameModeAttribute(type, info); - } - - info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); - CustomRoleManager.RegisterRoleTypes(roles, info); - - _registeredPlugins.Add(assembly, info); - Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); - }; + IL2CPPChainloader.Instance.PluginLoad += RegisterPlugin; IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; IL2CPPChainloader.Instance.Finished += MiraEventManager.SortAllHandlers; } @@ -81,6 +51,44 @@ public sealed class MiraPluginManager return Instance._registeredPlugins.Values.First(plugin => plugin.PluginId == guid); } + private void RegisterPlugin(PluginInfo pluginInfo, Assembly assembly, BasePlugin plugin) + { + if (plugin is not IMiraPlugin miraPlugin || _registeredPlugins.ContainsKey(assembly)) + { + return; + } + + var info = new MiraPluginInfo(miraPlugin, pluginInfo); + var roles = new List(); + + foreach (var type in assembly.GetTypes()) + { + if (type.GetCustomAttribute(false) != null) + { + Logger.Info($"Skipping {type.Name}."); + continue; + } + + RegisterModifier(type); + RegisterOptions(type, info); + + if (RegisterRoleAttribute(type, info, out var role)) + { + roles.Add(role!); + } + + RegisterButtonAttribute(type, info); + RegisterColorClasses(type); + RegisterGameModeAttribute(type, info); + } + + info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + CustomRoleManager.RegisterRoleTypes(roles, info); + + _registeredPlugins.Add(assembly, info); + Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); + } + private static void RegisterOptions(Type type, MiraPluginInfo pluginInfo) { if (!type.IsAssignableTo(typeof(AbstractOptionGroup))) @@ -117,7 +125,6 @@ public sealed class MiraPluginManager if (!(typeof(RoleBehaviour).IsAssignableFrom(type) && typeof(ICustomRole).IsAssignableFrom(type))) { - Logger.Error($"{type.Name} does not inherit from RoleBehaviour or ICustomRole."); return false; } From fa8ea9dbab1ec600791b26d5f56d1b81e23f5c22 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:44:35 -0500 Subject: [PATCH 007/102] final fix to logic --- MiraAPI/GameModes/GameModeOption.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 3d9ec03..9c02dc6 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -14,7 +14,7 @@ public class GameModeOption : AbstractOptionGroup /// /// Gets or sets the game mode option. /// - public ModdedNumberOption CurrentMode { get; set; } = new("Game Mode: {0}", 0, 0, CustomGameModeManager.LastId-1, 1, Utilities.MiraNumberSuffixes.None) + public ModdedNumberOption CurrentMode { get; set; } = new("Game Mode: {0}", 0, 0, CustomGameModeManager.LastId, 1, Utilities.MiraNumberSuffixes.None) { ChangedEvent = x => CustomGameModeManager.SetGameMode((uint)x), }; From 47dfa63423674107e660682475317267b98d982c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:13:32 -0500 Subject: [PATCH 008/102] improve CustomRoleConfiguration.cs --- MiraAPI/Roles/CustomRoleConfiguration.cs | 98 ++++++++++++------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index a6b1b21..41c3e6b 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,4 +1,6 @@ -using AmongUs.GameOptions; +using System; +using AmongUs.GameOptions; +using MiraAPI.GameModes; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -17,11 +19,6 @@ public struct CustomRoleConfiguration { var roleBehaviour = role as RoleBehaviour; - MaxRoleCount = 15; - DefaultRoleCount = 0; - DefaultChance = 0; - - OptionsScreenshot = Icon = MiraAssets.Empty; AffectedByLightOnAirship = role.Team == ModdedRoleTeams.Crewmate; KillButtonOutlineColor = role.Team switch { @@ -29,7 +26,6 @@ public struct CustomRoleConfiguration ModdedRoleTeams.Crewmate => Palette.CrewmateBlue, _ => role.RoleColor, }; - RoleHintType = RoleHintType.RoleTab; GhostRole = role.Team is ModdedRoleTeams.Impostor ? RoleTypes.ImpostorGhost : RoleTypes.CrewmateGhost; CanGetKilled = roleBehaviour?.IsDead == false && role.Team is not ModdedRoleTeams.Impostor; UseVanillaKillButton = role.Team is ModdedRoleTeams.Impostor; @@ -38,7 +34,6 @@ public struct CustomRoleConfiguration TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate; HideSettings = roleBehaviour?.IsDead == true; ShowInFreeplay = roleBehaviour?.IsDead == false; - CanModifyChance = true; IntroTeamColor = role.Team switch { ModdedRoleTeams.Custom => Color.gray, @@ -59,108 +54,113 @@ public struct CustomRoleConfiguration ModdedRoleTeams.Crewmate => RoleGroup.Crewmate, ModdedRoleTeams.Impostor => RoleGroup.Impostor, ModdedRoleTeams.Custom => RoleGroup.Neutral, + _ => new RoleGroup(role.RoleName, role.RoleColor), }; } - public RoleGroup RoleGroup; - /// - /// Gets the hard limit of players that can have this role. This property is used to set a limit in the Role Options menu. If set to 0, the role will not be assigned at start. + /// Gets or sets the role group for the options menu. /// - public int MaxRoleCount; + public RoleGroup RoleGroup { get; set; } /// - /// Gets the default role count. + /// Gets or sets the hard limit of players that can have this role. This property is used to set a limit in the Role Options menu. If set to 0, the role will not be assigned at start. /// - public int DefaultRoleCount; + public int MaxRoleCount { get; set; } = 15; /// - /// Gets the default role chance. + /// Gets or sets the default role count. /// - public int DefaultChance; + public int DefaultRoleCount { get; set; } = 0; /// - /// Whether the chance option can be changed or not. + /// Gets or sets the default role chance. /// - public bool CanModifyChance; + public int DefaultChance { get; set; } = 0; /// - /// Gets the Sprite used for the Role Options menu screenshot. + /// Gets or sets a value indicating whether the chance option can be changed or not. /// - public LoadableAsset OptionsScreenshot; + public bool CanModifyChance { get; set; } = true; /// - /// Gets the Sprite used for the Role Icon. + /// Gets or sets the Sprite used for the Role Options menu screenshot. + /// + public LoadableAsset OptionsScreenshot { get; set; } = MiraAssets.Empty; + + /// + /// Gets or sets the Sprite used for the Role Icon. /// public LoadableAsset Icon; + public LoadableAsset Icon { get; set; } = MiraAssets.Empty; /// - /// Gets a value indicating whether the role is affected by light affectors on Airship. + /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. /// - public bool AffectedByLightOnAirship; + public bool AffectedByLightOnAirship { get; set; } /// - /// Gets a value indicating whether the role can be killed by vanilla murder system. + /// Gets or sets a value indicating whether the role can be killed by vanilla murder system. /// - public bool CanGetKilled; + public bool CanGetKilled { get; set; } /// - /// Gets a value indicating whether the role should use the vanilla kill button. + /// Gets or sets a value indicating whether the role should use the vanilla kill button. /// - public bool UseVanillaKillButton; + public bool UseVanillaKillButton { get; set; } /// - /// Gets a value indicating whether the role can use vents. + /// Gets or sets a value indicating whether the role can use vents. /// - public bool CanUseVent; + public bool CanUseVent { get; set; } /// - /// Gets a value indicating whether the role can use the sabotage button. + /// Gets or sets a value indicating whether the role can use the sabotage button. /// - public bool CanUseSabotage; + public bool CanUseSabotage { get; set; } /// - /// Gets a value indicating whether the role's tasks count towards task progress. + /// Gets or sets a value indicating whether the role's tasks count towards task progress. /// - public bool TasksCountForProgress; + public bool TasksCountForProgress { get; set; } /// - /// Gets a value indicating whether the role should show up in the Role Options menu. + /// Gets or sets a value indicating whether the role should show up in the Role Options menu. /// - public bool HideSettings; + public bool HideSettings { get; set; } /// - /// Gets a value indicating whether the role should show up in the Freeplay Role Selection menu. + /// Gets or sets a value indicating whether the role should show up in the Freeplay Role Selection menu. /// - public bool ShowInFreeplay; + public bool ShowInFreeplay { get; set; } /// - /// Gets the color to show during the "Team" part of the intro cutscene. + /// Gets or sets the color to show during the "Team" part of the intro cutscene. /// - public Color? IntroTeamColor; + public Color? IntroTeamColor { get; set; } /// - /// Gets the title text to show during the "Team" part of the intro cutscene. + /// Gets or sets the title text to show during the "Team" part of the intro cutscene. /// - public string? IntroTeamTitle; + public string? IntroTeamTitle { get; set; } /// - /// Gets the description text to show during the "Team" part of the intro cutscene. + /// Gets or sets the description text to show during the "Team" part of the intro cutscene. /// - public string? IntroTeamDescription; + public string? IntroTeamDescription { get; set; } /// - /// Gets the outline color for the KillButton if is true. + /// Gets or sets the outline color for the KillButton if is true. /// - public Color KillButtonOutlineColor; + public Color KillButtonOutlineColor { get; set; } /// - /// Gets the role hint style. See enum for all options. + /// Gets or sets the role hint style. See enum for all options. /// - public RoleHintType RoleHintType; + public RoleHintType RoleHintType { get; set; } = RoleHintType.RoleTab; /// - /// Gets the Ghost role that is applied when the player is killed. + /// Gets or sets the Ghost role that is applied when the player is killed. /// - public RoleTypes GhostRole; + public RoleTypes GhostRole { get; set; } } From 284a0f58b107a557a0f39e8c6c8d5a1a6f92f232 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:14:26 -0500 Subject: [PATCH 009/102] oops --- MiraAPI/Roles/CustomRoleConfiguration.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 41c3e6b..e406305 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -91,9 +91,13 @@ public struct CustomRoleConfiguration /// /// Gets or sets the Sprite used for the Role Icon. /// - public LoadableAsset Icon; public LoadableAsset Icon { get; set; } = MiraAssets.Empty; + /// + /// Gets or sets the game mode this role is a part of. + /// + public Type AssociatedGameMode { get; set; } = typeof(DefaultMode); + /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. /// From 9177d5b456da747b6ec3a5e46044bc9040c01ac8 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:14:38 -0500 Subject: [PATCH 010/102] fix merge stuff --- MiraAPI.Example/Roles/ChameleonRole.cs | 2 +- MiraAPI/GameModes/AbstractGameMode.cs | 2 +- MiraAPI/GameModes/DefaultMode.cs | 2 +- MiraAPI/GameOptions/AbstractOptionGroup.cs | 2 +- MiraAPI/Hud/CustomActionButton.cs | 5 +++-- MiraAPI/Modifiers/BaseModifier.cs | 2 +- MiraAPI/Modifiers/Types/GameModifier.cs | 2 +- MiraAPI/Modifiers/Types/TimedModifier.cs | 2 +- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 2 +- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 7 +++++-- MiraAPI/Roles/ICustomRole.cs | 5 ----- 11 files changed, 16 insertions(+), 17 deletions(-) diff --git a/MiraAPI.Example/Roles/ChameleonRole.cs b/MiraAPI.Example/Roles/ChameleonRole.cs index 1f2f61f..b193885 100644 --- a/MiraAPI.Example/Roles/ChameleonRole.cs +++ b/MiraAPI.Example/Roles/ChameleonRole.cs @@ -12,7 +12,7 @@ public class ChameloenRole : CrewmateRole, ICustomRole public Color RoleColor => Palette.AcceptedGreen; public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; - public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this) + public CustomRoleConfiguration Configuration => new(this) { OptionsScreenshot = ExampleAssets.Banner, }; diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 67304a6..3bb80ff 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -8,7 +8,7 @@ namespace MiraAPI.GameModes; /// /// Base class for custom gamemodes. /// -[MiraDisable] +[MiraIgnore] public abstract class AbstractGameMode { /// diff --git a/MiraAPI/GameModes/DefaultMode.cs b/MiraAPI/GameModes/DefaultMode.cs index 7f4cc70..f50dcd4 100644 --- a/MiraAPI/GameModes/DefaultMode.cs +++ b/MiraAPI/GameModes/DefaultMode.cs @@ -5,7 +5,7 @@ namespace MiraAPI.GameModes; /// /// The default game mode. /// -[MiraDisable] +[MiraIgnore] public class DefaultMode : AbstractGameMode { /// diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index ac4fb0c..6eac8bd 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -8,7 +8,7 @@ namespace MiraAPI.GameOptions; /// /// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. /// -[MiraDisable] +[MiraIgnore] public abstract class AbstractOptionGroup { internal List Options { get; } = []; diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 92d7a11..73603ea 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -3,6 +3,7 @@ using MiraAPI.Events.Mira; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using System; +using MiraAPI.Patches; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -13,7 +14,7 @@ namespace MiraAPI.Hud; /// /// Class for making custom action buttons. More customizable than the default Action/Ability buttons in the base game. /// -[MiraDisable] +[MiraIgnore] public abstract class CustomActionButton { /// @@ -438,7 +439,7 @@ public abstract class CustomActionButton /// Custom action button that has a target object. /// /// The type of the target object. -[MiraDisable] +[MiraIgnore] public abstract class CustomActionButton : CustomActionButton where T : MonoBehaviour { /// diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 11701cc..98aace9 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -5,7 +5,7 @@ namespace MiraAPI.Modifiers; /// /// Base class for all modifiers. /// -[MiraDisable] +[MiraIgnore] public abstract class BaseModifier { /// diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index fdf8422..6274917 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -5,7 +5,7 @@ namespace MiraAPI.Modifiers.Types; /// /// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role. /// -[MiraDisable] +[MiraIgnore] public abstract class GameModifier : BaseModifier { /// diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 4711d4a..ef60056 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -8,7 +8,7 @@ namespace MiraAPI.Modifiers.Types; /// /// The base class for all timed modifiers. Timed modifiers have a duration and can be started and stopped. /// -[MiraDisable] +[MiraIgnore] public abstract class TimedModifier : BaseModifier { /// diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index a125709..9456200 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -280,7 +280,7 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f); var advancedRoleOptions = SelectedMod.Options - .Where(x => x.AdvancedRole == customRole.GetType() && (x.AdvancedMode is null || x.AdvancedMode == customRole.AssociatedGameMode)) + .Where(x => x.AdvancedRole == customRole.GetType() && (x.AdvancedMode is null || x.AdvancedMode == customRole.Configuration.AssociatedGameMode)) .ToList(); if (!flag && advancedRoleOptions.Count > 0) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 254f74a..f3efd12 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -62,6 +62,7 @@ internal static class RoleSettingMenuPatches var num3 = 0; var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType() + .Where(x => !x.Configuration.HideSettings) .ToLookup(x => x.Configuration.RoleGroup); if (roleGroups is null) @@ -76,8 +77,7 @@ internal static class RoleSettingMenuPatches foreach (var grouping in sortedRoleGroups) { - if (!grouping.Any(role => role.AssociatedGameMode is null || role.AssociatedGameMode == - CustomGameModeManager.ActiveMode?.GetType())) + if (grouping.All(role => role.Configuration.AssociatedGameMode != CustomGameModeManager.ActiveMode?.GetType())) { continue; } @@ -147,11 +147,13 @@ internal static class RoleSettingMenuPatches { header.gameObject.Destroy(); } + CategoryHeaderEditRoles.Clear(); foreach (var option in RoleOptionSettings) { option.gameObject.Destroy(); } + RoleOptionSettings.Clear(); __instance.SetQuotaTab(); })); @@ -162,6 +164,7 @@ internal static class RoleSettingMenuPatches num -= 0.4f; } } + __instance.scrollBar.CalculateAndSetYBounds(__instance.roleChances.Count + 5, 1f, 6f, 0.43f); return false; } diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 8c901fc..7042bd6 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -50,11 +50,6 @@ public interface ICustomRole /// MiraPluginInfo ParentMod => CustomRoleManager.FindParentMod(this); - /// - /// Gets the game mode this role is a part of. - /// - Type AssociatedGameMode => typeof(DefaultMode); - /// /// This method runs on the PlayerControl.FixedUpdate method for ALL players with this role. /// From c85cf7ef2f5e7339aa58514177740e3f165bb29e Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:15:39 -0500 Subject: [PATCH 011/102] finish merge --- MiraAPI/Roles/CustomRoleConfiguration.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 4500331..59e208a 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; @@ -64,6 +65,11 @@ public record struct CustomRoleConfiguration /// public LoadableAsset Icon { get; set; } = MiraAssets.Empty; + /// + /// Gets or sets the associated game mode for this role. This is used to determine if the role should be available in a specific game mode. + /// + public Type? AssociatedGameMode { get; set; } = null; + /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. /// From a62c75a13bf04e167d4ac3382db879258c4bf39b Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:19:22 -0500 Subject: [PATCH 012/102] that was stupid --- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 1 - MiraAPI/Roles/CustomRoleConfiguration.cs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 8cbf31b..2247572 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -62,7 +62,6 @@ internal static class RoleSettingMenuPatches var num3 = 0; var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType() - .Where(x => !x.Configuration.HideSettings) .ToLookup(x => x.RoleOptionsGroup); if (roleGroups is null) diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 59e208a..38cc4dd 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,5 +1,6 @@ using System; using AmongUs.GameOptions; +using MiraAPI.GameModes; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -68,7 +69,7 @@ public record struct CustomRoleConfiguration /// /// Gets or sets the associated game mode for this role. This is used to determine if the role should be available in a specific game mode. /// - public Type? AssociatedGameMode { get; set; } = null; + public Type AssociatedGameMode { get; set; } = typeof(DefaultMode); /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. From 2d59b29c15378d98c4670a7bf95a30a184ef79ea Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:19:55 -0500 Subject: [PATCH 013/102] cleanup --- MiraAPI.Example/Modifiers/CaptainModifier.cs | 3 +-- MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs | 1 - MiraAPI.Example/Modifiers/HighPriorityModifier.cs | 3 +-- MiraAPI/GameModes/CustomGameModeManager.cs | 1 - MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs | 1 - MiraAPI/MiraApiPlugin.cs | 1 - MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 1 - MiraAPI/PluginLoading/MiraPluginManager.cs | 4 ---- MiraAPI/Roles/ICustomRole.cs | 1 - 9 files changed, 2 insertions(+), 14 deletions(-) diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 60691a8..63b7c41 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -1,5 +1,4 @@ -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index 642e941..12a9587 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -1,6 +1,5 @@ using Il2CppSystem; using MiraAPI.Example.Roles; -using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using UnityEngine; diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index b94c4d0..c640ed8 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -1,5 +1,4 @@ -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index c51c409..8e531bb 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Il2CppSystem.Threading.Tasks; using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using Reactor.Utilities; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 861df98..0f7eb80 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Linq; using System.Text; using Il2CppInterop.Runtime.InteropTypes.Arrays; diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 8955531..6efca17 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -2,7 +2,6 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; -using MiraAPI.GameModes; using MiraAPI.PluginLoading; using Reactor; using Reactor.Networking; diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 9456200..6d5bbc5 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -1,6 +1,5 @@ using HarmonyLib; using MiraAPI.GameModes; -using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 301300e..7e7cf00 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -15,11 +15,7 @@ using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; namespace MiraAPI.PluginLoading; diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 8064b2d..a452d1e 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,5 +1,4 @@ using BepInEx.Configuration; -using MiraAPI.GameModes; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; From 65cd177551b100ce274b95f53543eb15ca8edd86 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:15:00 -0500 Subject: [PATCH 014/102] fix more scrolling issues --- MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 2 -- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index a44b35c..0cdca29 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -157,8 +157,6 @@ public static class GameSettingMenuPatches roles.AdvancedRolesSettings.gameObject.SetActive(false); roles.RoleChancesSettings.gameObject.SetActive(true); roles.SetQuotaTab(); - - roles.scrollBar.CalculateAndSetYBounds(roles.roleChances.Count + 5, 1f, 6f, 0.43f); roles.scrollBar.ScrollToTop(); } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index c3427c2..97dd6f4 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -33,6 +33,10 @@ internal static class RoleSettingMenuPatches [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] public static bool PatchStart(RolesSettingsMenu __instance) { + CategoryHeaderEditRoles.ForEach(Object.Destroy); + CategoryHeaderEditRoles.Clear(); + RoleOptionSettings.ForEach(Object.Destroy); + RoleOptionSettings.Clear(); __instance.roleChances = new Il2CppSystem.Collections.Generic.List(); __instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List(); From 7890ec94569c3ca14a22162bf358297bfbf24fe1 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:21:57 -0500 Subject: [PATCH 015/102] WIP update gamemodes --- MiraAPI.Example/GameModes/ExampleMode.cs | 9 ++ MiraAPI/GameModes/AbstractGameMode.cs | 3 +- MiraAPI/GameModes/CustomGameModeManager.cs | 2 +- MiraAPI/GameModes/GameModeOption.cs | 1 + MiraAPI/GameOptions/ModdedOptionsManager.cs | 24 ++++ .../Patches/GameModes/CreateOptionsPatch.cs | 18 +++ MiraAPI/PluginLoading/MiraPluginManager.cs | 108 +++++++++++------- 7 files changed, 119 insertions(+), 46 deletions(-) create mode 100644 MiraAPI.Example/GameModes/ExampleMode.cs create mode 100644 MiraAPI/Patches/GameModes/CreateOptionsPatch.cs diff --git a/MiraAPI.Example/GameModes/ExampleMode.cs b/MiraAPI.Example/GameModes/ExampleMode.cs new file mode 100644 index 0000000..ff8292d --- /dev/null +++ b/MiraAPI.Example/GameModes/ExampleMode.cs @@ -0,0 +1,9 @@ +using MiraAPI.GameModes; + +namespace MiraAPI.Example.GameModes; + +public class ExampleMode : AbstractGameMode +{ + public override string Name => "Example Mode"; + public override string Description => "An example gamemode."; +} diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 3bb80ff..2da08aa 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using UnityEngine; @@ -9,7 +10,7 @@ namespace MiraAPI.GameModes; /// Base class for custom gamemodes. /// [MiraIgnore] -public abstract class AbstractGameMode +public abstract class AbstractGameMode : IOptionable { /// /// Gets the game mode name. diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 8e531bb..f179944 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -12,7 +12,7 @@ namespace MiraAPI.GameModes; /// public static class CustomGameModeManager { - private static readonly Dictionary IdToModeMap = []; + public static readonly Dictionary IdToModeMap = []; private static uint GetNextId() { diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 9c02dc6..83de6ff 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -6,6 +6,7 @@ namespace MiraAPI.GameModes; /// /// The game mode option. /// +// TODO: Replace this with a proper gamemode selection. public class GameModeOption : AbstractOptionGroup { /// diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 7aaa154..8a89500 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using BepInEx.Configuration; using HarmonyLib; +using Il2CppInterop.Runtime.Injection; using MiraAPI.GameOptions.Attributes; using MiraAPI.Networking; using MiraAPI.PluginLoading; @@ -27,6 +28,29 @@ public static class ModdedOptionsManager internal static uint NextId => _nextId++; private static uint _nextId = 1; + internal static bool RegisterGroup(Type type) + { + if (Activator.CreateInstance(type) is not AbstractOptionGroup group) + { + return false; + } + + if (TypeToGroup.ContainsKey(type)) + { + Logger.Error($"Group {type.Name} already exists."); + return false; + } + + Groups.Add(group); + TypeToGroup.Add(type, group); + + typeof(OptionGroupSingleton<>).MakeGenericType(type) + .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! + .SetValue(null, group); + + return true; + } + internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo) { if (Activator.CreateInstance(type) is not AbstractOptionGroup group) diff --git a/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs b/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs new file mode 100644 index 0000000..7e8144e --- /dev/null +++ b/MiraAPI/Patches/GameModes/CreateOptionsPatch.cs @@ -0,0 +1,18 @@ +using HarmonyLib; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.SetGameMode))] +internal static class CreateOptionsPatch +{ + public static bool Prefix(CreateOptionsPicker __instance, AmongUs.GameOptions.GameModes mode) + { + if (mode <= AmongUs.GameOptions.GameModes.SeekFools) + { + return true; + } + + // TODO: finish this + return false; + } +} \ No newline at end of file diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index c2e2e61..f636d61 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -6,8 +6,10 @@ using System.Linq; using System.Reflection; using BepInEx; using BepInEx.Unity.IL2CPP; +using Il2CppInterop.Runtime.Injection; using MiraAPI.Colors; using MiraAPI.Events; +using MiraAPI.GameEnd; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; @@ -15,6 +17,7 @@ using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Roles; using MiraAPI.Utilities; +using Reactor.Localization.Utilities; using Reactor.Networking; using Reactor.Utilities; @@ -34,6 +37,7 @@ public sealed class MiraPluginManager internal void Initialize() { Instance = this; + ModdedOptionsManager.RegisterGroup(typeof(GameModeOption)); // TODO: dont do this CustomGameModeManager.RegisterDefaultMode(); CustomGameModeManager.GetAndSetGameMode(); IL2CPPChainloader.Instance.PluginLoad += RegisterPlugin; @@ -44,6 +48,20 @@ public sealed class MiraPluginManager { CustomButtonManager.Buttons = new ReadOnlyCollection(CustomButtonManager.CustomButtons); }; + IL2CPPChainloader.Instance.Finished += () => + { + var dict = new Dictionary(); + foreach (var (key, value) in CustomGameModeManager.IdToModeMap) + { + if (value is DefaultMode) + { + continue; + } + dict.Add(value.Name, key); + GameModesHelpers.ModeToName.Add((AmongUs.GameOptions.GameModes)key, CustomStringName.CreateAndRegister(value.Name)); + } + EnumInjector.InjectEnumValues(dict); + }; } private void RegisterPlugin(PluginInfo pluginInfo, Assembly assembly, BasePlugin plugin) @@ -63,57 +81,57 @@ public sealed class MiraPluginManager continue; } - foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)) + foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)) + { + var eventAttribute = method.GetCustomAttribute(); + if (eventAttribute != null) { - var eventAttribute = method.GetCustomAttribute(); - if (eventAttribute != null) + var parameters = method.GetParameters(); + if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) { - var parameters = method.GetParameters(); - if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) - { - Logger.Error($"Invalid event registration method {method.Name} in {type.Name}"); - continue; - } - - var paramType = parameters[0].ParameterType; - MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + Logger.Error($"Invalid event registration method {method.Name} in {type.Name}"); + continue; } - } - if (RegisterModifier(type, info)) - { - continue; + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); } - - if (RegisterOptions(type, info)) - { - continue; - } - - if (RegisterRoleAttribute(type, info, out var role)) - { - roles.Add(role); - continue; - } - - if (RegisterButtonAttribute(type, info)) - { - continue; - } - - if (RegisterGameOver(type)) - { - continue; - } - - if (RegisterGameModeAttribute(type, info)) - { - continue; - } - - RegisterColorClasses(type); } + if (RegisterModifier(type, info)) + { + continue; + } + + if (RegisterOptions(type, info)) + { + continue; + } + + if (RegisterRoleAttribute(type, info, out var role)) + { + roles.Add(role); + continue; + } + + if (RegisterButtonAttribute(type, info)) + { + continue; + } + + if (RegisterGameOver(type)) + { + continue; + } + + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + + RegisterColorClasses(type); + } + info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); CustomRoleManager.RegisterRoleTypes(roles, info); @@ -181,6 +199,7 @@ public sealed class MiraPluginManager { Logger.Error($"Failed to register options for {type.Name}: {e}"); } + return false; } @@ -207,6 +226,7 @@ public sealed class MiraPluginManager { Logger.Error($"Failed to register role for {type.Name}: {e}"); } + return false; } From 02f9c208040488691ea1284e3d994dfd2fd88f95 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:23:26 -0600 Subject: [PATCH 016/102] Add wip string option --- Directory.Build.props | 4 +- MiraAPI/GameOptions/ModdedOptionsManager.cs | 1 + .../OptionTypes/ModdedStringOption.cs | 183 ++++++++++++++++++ 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs diff --git a/Directory.Build.props b/Directory.Build.props index 4b87d91..8fad4d6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,8 +8,8 @@ true - 0.3.8 - dev + 0.3.9 + wip All Of Us, XtraCube, Angxl All Of Us diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 3b85a73..8045e3f 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -24,6 +24,7 @@ public static class ModdedOptionsManager private static readonly Dictionary TypeToGroup = []; internal static readonly Dictionary CreatedPlayerOptions = []; + internal static readonly Dictionary CreatedStringOptions = []; internal static readonly Dictionary ModdedOptions = []; internal static readonly List Groups = []; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs new file mode 100644 index 0000000..70206db --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.Networking; +using Reactor.Localization.Utilities; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.GameOptions.OptionTypes; + +/// +/// An option for selecting an enum value. +/// +public class ModdedStringOption : ModdedOption +{ + /// + /// Gets or sets the string values of the option. + /// + public string[] Values { get; set; } + + /// + /// Gets or sets the String Name values of the option. + /// + public StringNames[] StringNameValues { get; set; } + + /// + /// Gets the string's index + /// + /// The option's string value. + /// The option's value in the index. + public int GetIndex(string value, int location) + { + if (Values.Contains(value)) + { + return Values.ToList().IndexOf(value); + } + Info($"Cannot find a valid index for {value} in {location}. Returning 0 instead. Values: {string.Join(", ", Values)}"); + return 0; + } + + /// + /// Adds a value to the option. + /// + /// The option's string value. + /// The additional value's index on the list. + public void RegisterExtraValue(string value, int index = -1) + { + if (!Values.Contains(value)) + { + if (index <= -1) + { + Values.AddItem(value); + StringNameValues.AddItem(CustomStringName.CreateAndRegister(value)); + } + else + { + var newVals = Values.ToList(); + newVals.Insert(index, value); + Values = newVals.ToArray(); + var newStrVals = StringNameValues.ToList(); + newStrVals.Insert(index, CustomStringName.CreateAndRegister(value)); + StringNameValues = newStrVals.ToArray(); + } + } + + if (Data != null) + { + var data = (StringGameSetting)Data; + data.Values = StringNameValues; + + data.Index = GetIndex(Value, 1); + if (OptionBehaviour != null) + { + var strOpt = OptionBehaviour.TryCast(); + if (strOpt != null) + { + strOpt.Values = StringNameValues; + strOpt.Value = GetIndex(Value, 2); + Warning("StringOption is valid"); + } + else + { + Error("StringOption is null."); + } + Warning("OptionBehaviour is valid"); + } + else + { + Error("OptionBehaviour is null."); + } + Warning("Data is valid"); + } + else + { + Error("Data is null."); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// The default value as a string. + /// An option list of string values to use. + /// Whether to include this option in the preset or not. + public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset=true) : base(title, defaultValue, includeInPreset) + { + Values = values; + Data = ScriptableObject.CreateInstance(); + var data = (StringGameSetting)Data; + + data.Title = StringName; + data.Type = global::OptionTypes.String; + StringNameValues = values.Select(CustomStringName.CreateAndRegister).ToArray(); + data.Values = StringNameValues; + + data.Index = GetIndex(Value, 3); + } + + /// + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container) + { + var stringOption = Object.Instantiate(stringOpt, container); + stringOption.name = + $"{ParentMod!.OptionsTitleText}.StringOption.{TranslationController.Instance.GetString(StringName)}"; + + stringOption.SetUpFromData(Data, 20); + stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + + // SetUpFromData method doesnt work correctly so we must set the values manually + stringOption.Title = StringName; + stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray(0); + stringOption.Value = GetIndex(Value, 4); + + OptionBehaviour = stringOption; + ModdedOptionsManager.CreatedStringOptions.TryAdd(stringOption, this); + + return stringOption; + } + + /// + public override float GetFloatData() + { + return Convert.ToSingle(Value, NumberFormatInfo.InvariantInfo); + } + + /// + public override NetData GetNetData() + { + return new NetData(Id, Encoding.Unicode.GetBytes(Convert.ToString(Value, NumberFormatInfo.InvariantInfo)!)); + } + + /// + public override void HandleNetData(byte[] data) + { + SetValue(Encoding.Unicode.GetString(data)); + } + + /// + public override string GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) + { + return Values[optionBehaviour.GetInt()]; + } + + /// + protected override void OnValueChanged(string newValue) + { + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(GetIndex(newValue, 5)), false); + if (!OptionBehaviour) + { + return; + } + + if (OptionBehaviour is StringOption opt) + { + opt.Value = GetIndex(newValue, 5); + } + } +} From 51c7e248f9a156a61b22e2e170da9b9d92cb04ce Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:24:28 -0600 Subject: [PATCH 017/102] Update Directory.Build.props --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8fad4d6..4b87d91 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,8 +8,8 @@ true - 0.3.9 - wip + 0.3.8 + dev All Of Us, XtraCube, Angxl All Of Us From 8774c9da70e465241582b6a88d8a81c05d56a7a3 Mon Sep 17 00:00:00 2001 From: amber Date: Sat, 7 Mar 2026 21:48:28 -0500 Subject: [PATCH 018/102] Fix tony's -1 index --- .../OptionTypes/ModdedStringOption.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs index 70206db..0629313 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; -using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Networking; using Reactor.Localization.Utilities; @@ -52,19 +51,14 @@ public class ModdedStringOption : ModdedOption if (!Values.Contains(value)) { if (index <= -1) - { - Values.AddItem(value); - StringNameValues.AddItem(CustomStringName.CreateAndRegister(value)); - } - else - { - var newVals = Values.ToList(); - newVals.Insert(index, value); - Values = newVals.ToArray(); - var newStrVals = StringNameValues.ToList(); - newStrVals.Insert(index, CustomStringName.CreateAndRegister(value)); - StringNameValues = newStrVals.ToArray(); - } + index = Values.Length; + + var newVals = Values.ToList(); + newVals.Insert(index, value); + Values = newVals.ToArray(); + var newStrVals = StringNameValues.ToList(); + newStrVals.Insert(index, CustomStringName.CreateAndRegister(value)); + StringNameValues = newStrVals.ToArray(); } if (Data != null) From fdfb876b4431468401e1348391aa8fa63bf7b858 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:36:39 -0600 Subject: [PATCH 019/102] Fix plugin loading --- MiraAPI/PluginLoading/MiraPluginManager.cs | 69 +++++++++++++++------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index da1266f..336fef9 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -50,12 +50,15 @@ public sealed class MiraPluginManager IL2CPPChainloader.Instance.PluginLoad += RegisterPlugin; IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; - IL2CPPChainloader.Instance.Finished += MiraEventManager.SortAllHandlers; IL2CPPChainloader.Instance.Finished += () => { // Save all buttons into a read-only collection for easy access CustomButtonManager.Buttons = new ReadOnlyCollection(CustomButtonManager.CustomButtons); + RegisteredPlugins = [.. _registeredPlugins.Values]; + + ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); + var dict = new Dictionary(); foreach (var (key, value) in CustomGameModeManager.IdToModeMap) { @@ -68,6 +71,9 @@ public sealed class MiraPluginManager } EnumInjector.InjectEnumValues(dict); }; + + RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); + RegisterLocalTabs(typeof(MiraApiSettings), PluginSingleton.Instance); } private void RegisterPlugin(PluginInfo pluginInfo, Assembly assembly, BasePlugin plugin) @@ -80,6 +86,9 @@ public sealed class MiraPluginManager var info = new MiraPluginInfo(miraPlugin, pluginInfo); var roles = new List(); + var oldConfigSetting = info.PluginConfig.SaveOnConfigSet; + info.PluginConfig.SaveOnConfigSet = false; + foreach (var type in assembly.GetTypes()) { if (type.GetCustomAttribute() != null) @@ -87,21 +96,29 @@ public sealed class MiraPluginManager continue; } - foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)) + foreach (var method in type.GetMethods()) { var eventAttribute = method.GetCustomAttribute(); - if (eventAttribute != null) + if (eventAttribute == null) { - var parameters = method.GetParameters(); - if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) - { - Logger.Error($"Invalid event registration method {method.Name} in {type.Name}"); - continue; - } - - var paramType = parameters[0].ParameterType; - MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + continue; } + + if (!method.IsStatic) + { + Error($"Event method {method.Name} in {type.Name} must be static."); + continue; + } + + var parameters = method.GetParameters(); + if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) + { + Error($"Invalid event registration method {method.Name} in {type.Name}"); + continue; + } + + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); } if (RegisterModifier(type, info)) @@ -114,13 +131,18 @@ public sealed class MiraPluginManager continue; } - if (RegisterRoleAttribute(type, info, out var role)) + if (RegisterLocalTabs(type, plugin)) + { + continue; + } + + if (RegisterRole(type, info, out var role)) { roles.Add(role); continue; } - if (RegisterButtonAttribute(type, info)) + if (RegisterButton(type, info)) { continue; } @@ -136,16 +158,21 @@ public sealed class MiraPluginManager } RegisterColorClasses(type); + RegisterKeybinds(type, plugin); } - info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); - CustomRoleManager.RegisterRoleTypes(roles, info); + info.PluginConfig.Save(); + info.PluginConfig.SaveOnConfigSet = oldConfigSetting; + + info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + QueuedRoleRegistrations.Add(info, roles); _registeredPlugins.Add(assembly, info); - Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); - RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); - RegisterLocalTabs(typeof(MiraApiSettings), PluginSingleton.Instance); + info.SavePublicCollections(); + PresetManager.CreateDefaultPreset(info); + PresetManager.LoadPresets(info); + Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); } /// @@ -332,8 +359,10 @@ public sealed class MiraPluginManager } catch (Exception e) { - Logger.Error($"Failed to register gamemode {type.Name}: {e}"); + Error($"Failed to register gamemode {type.Name}: {e}"); } + + return false; } private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo) From c29e882d7ed37f3852238ea72177374d9bf185ac Mon Sep 17 00:00:00 2001 From: amber Date: Sun, 8 Mar 2026 13:24:24 -0400 Subject: [PATCH 020/102] Fix error from float data in ModdedStringOption.cs --- MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs index 0629313..3de3945 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -139,7 +139,7 @@ public class ModdedStringOption : ModdedOption /// public override float GetFloatData() { - return Convert.ToSingle(Value, NumberFormatInfo.InvariantInfo); + return Values.IndexOf(Value); } /// From 44415019cc35401f84d354750486be429af1bd8b Mon Sep 17 00:00:00 2001 From: amber Date: Tue, 10 Mar 2026 19:49:44 -0400 Subject: [PATCH 021/102] Full gamemode option in vanilla `Game Options` menu. --- MiraAPI.Example/GameModes/ExampleMode.cs | 2 + MiraAPI/GameModes/CustomGameModeManager.cs | 16 ++-- MiraAPI/GameModes/GameModeOption.cs | 89 ++++++++++++++++--- .../Patches/Options/GameOptionsMenuPatch.cs | 2 - MiraAPI/Patches/Options/OptionsPatches.cs | 21 +---- MiraAPI/PluginLoading/MiraPluginManager.cs | 4 +- MiraAPI/Utilities/Extensions.cs | 5 +- 7 files changed, 93 insertions(+), 46 deletions(-) diff --git a/MiraAPI.Example/GameModes/ExampleMode.cs b/MiraAPI.Example/GameModes/ExampleMode.cs index ff8292d..4868516 100644 --- a/MiraAPI.Example/GameModes/ExampleMode.cs +++ b/MiraAPI.Example/GameModes/ExampleMode.cs @@ -1,4 +1,5 @@ using MiraAPI.GameModes; +using UnityEngine; namespace MiraAPI.Example.GameModes; @@ -6,4 +7,5 @@ public class ExampleMode : AbstractGameMode { public override string Name => "Example Mode"; public override string Description => "An example gamemode."; + public override Color Color => Color.red; } diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 7aef0fc..3cf4562 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -45,7 +44,7 @@ public static class CustomGameModeManager IdToModeMap.Add(GetNextId(), mode); pluginInfo.GameModes.Add(LastId, mode); - + GameModeOption.AddOption($"{mode.Name}"); mode.ID = LastId; } @@ -53,7 +52,7 @@ public static class CustomGameModeManager /// Checks to see if the default game mode is on. /// /// True if the default mode is one. - public static bool IsDefault() => (uint)OptionGroupSingleton.Instance.CurrentMode.Value == 0; + public static bool IsDefault() => (uint) GameModeOption.Value == 0; /// /// Gets the current gamemode. @@ -64,6 +63,8 @@ public static class CustomGameModeManager { var defaultMode = new DefaultMode(); IdToModeMap.Add(0, defaultMode); + // no need to add to game mode option as it already contains it + // because we cannot have the option be created with no values defaultMode.ID = 0; } @@ -76,19 +77,14 @@ public static class CustomGameModeManager else if (id != 0) { ActiveMode = IdToModeMap[0]; - OptionGroupSingleton.Instance.CurrentMode.SetValue(0); + GameModeOption.Value = 0; Logger.Warning($"Unable to find game mode of id {id}!"); } - - if (OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour != null && ActiveMode != null) - { - ((NumberOption)OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour).TitleText.SetText($"Game Mode: {ActiveMode}"); - } } internal static void GetAndSetGameMode() { - var id = (uint)OptionGroupSingleton.Instance.CurrentMode.Value; + var id = (uint)GameModeOption.Value; if (IdToModeMap.TryGetValue(id, out var mode)) { diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 83de6ff..cc8ee49 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -1,22 +1,85 @@ -using MiraAPI.GameOptions; -using MiraAPI.GameOptions.OptionTypes; +using System.Collections.Generic; +using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppSystem; +using Reactor.Localization.Utilities; +using UnityEngine; +using UnityEngine.ProBuilder; +using Object = UnityEngine.Object; namespace MiraAPI.GameModes; /// /// The game mode option. /// -// TODO: Replace this with a proper gamemode selection. -public class GameModeOption : AbstractOptionGroup +[HarmonyPatch] +public static class GameModeOption { - /// - public override string GroupName => "Game Mode"; - - /// - /// Gets or sets the game mode option. - /// - public ModdedNumberOption CurrentMode { get; set; } = new("Game Mode: {0}", 0, 0, CustomGameModeManager.LastId, 1, Utilities.MiraNumberSuffixes.None) + public static int Value { - ChangedEvent = x => CustomGameModeManager.SetGameMode((uint)x), - }; + get => + OptionBehaviour != null + ? OptionBehaviour.GetInt() + : 0; + set + { + if (OptionBehaviour == null) + return; + OptionBehaviour.Value = value; + OptionBehaviour.UpdateValue(); + } + } + + internal static StringOption OptionBehaviour { get; private set; } = null!; + + private static readonly List Queue = []; + // loading takes place before option creation + internal static void AddOption(string opt) + { + if (OptionBehaviour == null) + { + Queue.Add(opt); + return; + } + OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(CustomStringName.CreateAndRegister(opt)); + } + [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] + [HarmonyPostfix] + public static void CreateSettingsPatch(GameOptionsMenu __instance) + { + float num = -9.5f; + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); + categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister("Custom"), 20); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); + OptionBehaviour = Object.Instantiate( + __instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + __instance.settingsContainer); + OptionBehaviour.transform.localPosition = new Vector3(0.952f, num - 0.63f, -2f); + OptionBehaviour.SetClickMask(__instance.ButtonClickMask); + StringGameSetting setting = ScriptableObject.CreateInstance(); + setting.Type = OptionTypes.MultipleChoice; + setting.Title = CustomStringName.CreateAndRegister("Gamemode"); + setting.Index = 0; + setting.Values = new Il2CppStructArray([CustomStringName.CreateAndRegister("Default")]); + OptionBehaviour.SetUpFromData(setting, 20); + OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => + { + CustomGameModeManager.SetGameMode((uint)opt.GetInt()); + }); + __instance.Children.Add(OptionBehaviour); + __instance.scrollBar.SetYBoundsMax(9f); + foreach (var str in Queue) + AddOption(str); + Queue.Clear(); + } + + [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] + [HarmonyPrefix] + public static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option) + { + return !OptionBehaviour.Equals(option); + } } diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index e0afd34..c8ee3ce 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,8 +1,6 @@ using System; using System.Linq; -using AmongUs.GameOptions; using HarmonyLib; -using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.Presets; diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index 7398b7c..fbcbe38 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -1,15 +1,10 @@ -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; +using HarmonyLib; using Il2CppSystem; using MiraAPI.GameModes; using MiraAPI.GameOptions; -using MiraAPI.Utilities; -using Reactor.Utilities.Extensions; -using MiraAPI.GameOptions; -using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Roles; using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; using UnityEngine; using Object = Il2CppSystem.Object; @@ -206,17 +201,7 @@ public static class OptionsPatches { return true; } - - if (__instance == OptionGroupSingleton.Instance.CurrentMode.OptionBehaviour) - { - __instance.TitleText.text = - TranslationController.Instance.GetString(__instance.Title, $"{CustomGameModeManager.ActiveMode}"); - } - else - { - __instance.TitleText.text = - TranslationController.Instance.GetString(__instance.Title); - } + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title); return false; } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 336fef9..2a52201 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -44,7 +44,6 @@ public sealed class MiraPluginManager internal void Initialize() { Instance = this; - ModdedOptionsManager.RegisterGroup(typeof(GameModeOption)); // TODO: dont do this CustomGameModeManager.RegisterDefaultMode(); CustomGameModeManager.GetAndSetGameMode(); IL2CPPChainloader.Instance.PluginLoad += RegisterPlugin; @@ -67,6 +66,9 @@ public sealed class MiraPluginManager continue; } dict.Add(value.Name, key); + if (((AmongUs.GameOptions.GameModes) key) == AmongUs.GameOptions.GameModes.Normal) + return; + // 'Default' was still being registered twice and idk why GameModesHelpers.ModeToName.Add((AmongUs.GameOptions.GameModes)key, CustomStringName.CreateAndRegister(value.Name)); } EnumInjector.InjectEnumValues(dict); diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 70f8c15..d895664 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -5,11 +5,11 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; using MiraAPI.Voting; -using Reactor.Utilities; using Rewired; using Rewired.Data; using TMPro; @@ -429,7 +429,8 @@ public static class Extensions public static bool IsCustom(this OptionBehaviour optionBehaviour) { return ModdedOptionsManager.ModdedOptions.Values.Any( - opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour); + opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour) + || optionBehaviour.Equals(GameModeOption.OptionBehaviour); } /// From f59e0af7e8108d985625dabc6fc872ac232d66db Mon Sep 17 00:00:00 2001 From: amber Date: Tue, 10 Mar 2026 19:54:10 -0400 Subject: [PATCH 022/102] make patch methods private --- MiraAPI/GameModes/GameModeOption.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index cc8ee49..b590ae7 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -45,7 +45,7 @@ public static class GameModeOption } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] - public static void CreateSettingsPatch(GameOptionsMenu __instance) + private static void CreateSettingsPatch(GameOptionsMenu __instance) { float num = -9.5f; CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); @@ -78,7 +78,7 @@ public static class GameModeOption [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] [HarmonyPrefix] - public static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option) + private static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option) { return !OptionBehaviour.Equals(option); } From bb5c212e971df1e3964af9a576733bfb28174d35 Mon Sep 17 00:00:00 2001 From: amber Date: Tue, 10 Mar 2026 19:57:11 -0400 Subject: [PATCH 023/102] Summary tag for the only public part of the option --- MiraAPI/GameModes/GameModeOption.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index b590ae7..91ca509 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -15,6 +15,10 @@ namespace MiraAPI.GameModes; [HarmonyPatch] public static class GameModeOption { + /// + /// Gets or Sets the current index of the Game Mode Option + /// For the value as an AbstractGameMode, see CustomGameModeManager.ActiveMode + /// public static int Value { get => From ba2deb66f104c2d6b8ffaca85e0d5a8c3fa86906 Mon Sep 17 00:00:00 2001 From: amber Date: Tue, 10 Mar 2026 20:24:39 -0400 Subject: [PATCH 024/102] Remove from HNS, fix spacing. --- MiraAPI/GameModes/GameModeOption.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 91ca509..b3cc95d 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -36,7 +36,7 @@ public static class GameModeOption internal static StringOption OptionBehaviour { get; private set; } = null!; - private static readonly List Queue = []; + private static readonly List Queue = []; // loading takes place before option creation internal static void AddOption(string opt) { @@ -51,7 +51,15 @@ public static class GameModeOption [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) { - float num = -9.5f; + if (GameManager.Instance.IsHideAndSeek()) + return; + float num = 0.713f; + foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) + { + num -= 0.63f; + foreach (BaseGameSetting a in rulesCategory.AllGameSettings) + num -= 0.45f; + } CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister("Custom"), 20); categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; @@ -61,7 +69,8 @@ public static class GameModeOption Vector3.zero, Quaternion.identity, __instance.settingsContainer); - OptionBehaviour.transform.localPosition = new Vector3(0.952f, num - 0.63f, -2f); + num -= 0.63f; + OptionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); OptionBehaviour.SetClickMask(__instance.ButtonClickMask); StringGameSetting setting = ScriptableObject.CreateInstance(); setting.Type = OptionTypes.MultipleChoice; @@ -73,8 +82,9 @@ public static class GameModeOption { CustomGameModeManager.SetGameMode((uint)opt.GetInt()); }); + num -= 0.37f; // scrollbar offset __instance.Children.Add(OptionBehaviour); - __instance.scrollBar.SetYBoundsMax(9f); + __instance.scrollBar.SetYBoundsMax(-num - 1.65f); foreach (var str in Queue) AddOption(str); Queue.Clear(); From 00eaa2601c0c10ee4a1e34590fb8e1dcfc6a7660 Mon Sep 17 00:00:00 2001 From: amber Date: Wed, 11 Mar 2026 21:55:40 -0400 Subject: [PATCH 025/102] Make Gamemode text bigger --- MiraAPI/GameModes/GameModeOption.cs | 1 + MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index b3cc95d..0c189ea 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -78,6 +78,7 @@ public static class GameModeOption setting.Index = 0; setting.Values = new Il2CppStructArray([CustomStringName.CreateAndRegister("Default")]); OptionBehaviour.SetUpFromData(setting, 20); + OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => { CustomGameModeManager.SetGameMode((uint)opt.GetInt()); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs index 3de3945..d0c5f16 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; @@ -27,7 +26,7 @@ public class ModdedStringOption : ModdedOption public StringNames[] StringNameValues { get; set; } /// - /// Gets the string's index + /// Gets the string's index. /// /// The option's string value. /// The option's value in the index. @@ -145,7 +144,7 @@ public class ModdedStringOption : ModdedOption /// public override NetData GetNetData() { - return new NetData(Id, Encoding.Unicode.GetBytes(Convert.ToString(Value, NumberFormatInfo.InvariantInfo)!)); + return new NetData(Id, Encoding.Unicode.GetBytes(Convert.ToString(Value, NumberFormatInfo.InvariantInfo))); } /// From 77c13bebdc5fd78cfa8598498319daac29d47487 Mon Sep 17 00:00:00 2001 From: Amber <169551692+Nix-main@users.noreply.github.com> Date: Sun, 22 Mar 2026 03:30:33 -0400 Subject: [PATCH 026/102] fix goofy 'works on my machine' --- MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs index d0c5f16..62d07cf 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -138,7 +138,7 @@ public class ModdedStringOption : ModdedOption /// public override float GetFloatData() { - return Values.IndexOf(Value); + return Array.IndexOf(Values, Value); } /// From af363000c7e602045dabfbbc0f0d1dc4b82cabbe Mon Sep 17 00:00:00 2001 From: Nix-main Date: Sun, 26 Apr 2026 23:06:38 -0400 Subject: [PATCH 027/102] Fix bugs and add HuD --- MiraAPI/GameModes/GameModeOption.cs | 31 ++++++++++++++----------- MiraAPI/Patches/GameModes/HudPatches.cs | 31 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 0c189ea..e65f3db 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; +using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem; +using MiraAPI.Patches.GameModes; using Reactor.Localization.Utilities; using UnityEngine; using UnityEngine.ProBuilder; @@ -31,21 +33,23 @@ public static class GameModeOption return; OptionBehaviour.Value = value; OptionBehaviour.UpdateValue(); + _lastValue = value; } } + private static int _lastValue; internal static StringOption OptionBehaviour { get; private set; } = null!; - private static readonly List Queue = []; + private static readonly Dictionary Values = new() + { + ["Default"] = CustomStringName.CreateAndRegister("Default"), + }; + // loading takes place before option creation internal static void AddOption(string opt) { - if (OptionBehaviour == null) - { - Queue.Add(opt); - return; - } - OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(CustomStringName.CreateAndRegister(opt)); + if (!Values.ContainsKey(opt)) + Values.Add(opt, CustomStringName.CreateAndRegister(opt)); } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] @@ -75,20 +79,21 @@ public static class GameModeOption StringGameSetting setting = ScriptableObject.CreateInstance(); setting.Type = OptionTypes.MultipleChoice; setting.Title = CustomStringName.CreateAndRegister("Gamemode"); - setting.Index = 0; - setting.Values = new Il2CppStructArray([CustomStringName.CreateAndRegister("Default")]); + setting.Index = _lastValue; + setting.Values = new Il2CppStructArray([Values["Default"]]); OptionBehaviour.SetUpFromData(setting, 20); OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => { - CustomGameModeManager.SetGameMode((uint)opt.GetInt()); + _lastValue = opt.GetInt(); + CustomGameModeManager.SetGameMode((uint)_lastValue); + HudPatches.SetGameModeText(Values.ElementAt(_lastValue).Key); }); num -= 0.37f; // scrollbar offset __instance.Children.Add(OptionBehaviour); __instance.scrollBar.SetYBoundsMax(-num - 1.65f); - foreach (var str in Queue) - AddOption(str); - Queue.Clear(); + for (var i = 1; i < Values.Count; i++) + OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index e232920..4cb5076 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -1,5 +1,7 @@ using HarmonyLib; using MiraAPI.GameModes; +using TMPro; +using UnityEngine; namespace MiraAPI.Patches.GameModes; @@ -11,4 +13,33 @@ internal static class HudPatches [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] public static void HudUpdatePatch(HudManager __instance) => CustomGameModeManager.ActiveMode?.HudUpdate(__instance); + + [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] + [HarmonyPostfix] + public static void PostHudStart(HudManager __instance) + { + if (GameManager.Instance.IsHideAndSeek()) + return; + var infoPane = __instance.gameObject.transform.FindChild("LobbyInfoPane"); + var aspect = infoPane.gameObject.transform.Find("AspectSize"); + var modeLabel = aspect!.Find("ModeLabel"); + var modeValue = aspect.Find("ModeValue"); + var modelText = modeLabel!.Find("Text_TMP").gameObject; + var modelTextClone = Object.Instantiate(modelText, modeLabel); + Object.Destroy(modelTextClone.GetComponent()); + modelTextClone.GetComponent().text = "Gamemode"; + var gmText = modeValue!.Find("GameModeText").gameObject; + var gmTextClone = Object.Instantiate(gmText, modeValue); + gmText.SetActive(false); + modelText.SetActive(false); + _text = gmTextClone.GetComponent(); + _text.text = "Default"; + } + + private static TextMeshPro? _text; + internal static void SetGameModeText(string text) + { + if (_text != null) + _text.text = text; + } } From 3066720e6ec4268966433176b6c7eee8f64b952c Mon Sep 17 00:00:00 2001 From: Nix-main Date: Sun, 26 Apr 2026 23:09:00 -0400 Subject: [PATCH 028/102] Rename Default -> Classic --- MiraAPI/GameModes/GameModeOption.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index e65f3db..ccf512d 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -42,7 +42,7 @@ public static class GameModeOption private static readonly Dictionary Values = new() { - ["Default"] = CustomStringName.CreateAndRegister("Default"), + ["Classic"] = CustomStringName.CreateAndRegister("Classic"), }; // loading takes place before option creation @@ -80,7 +80,7 @@ public static class GameModeOption setting.Type = OptionTypes.MultipleChoice; setting.Title = CustomStringName.CreateAndRegister("Gamemode"); setting.Index = _lastValue; - setting.Values = new Il2CppStructArray([Values["Default"]]); + setting.Values = new Il2CppStructArray([Values["Classic"]]); OptionBehaviour.SetUpFromData(setting, 20); OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => From 08c6883a0d1adc4b20c02d82a21fd5458176810f Mon Sep 17 00:00:00 2001 From: amber Date: Sun, 26 Apr 2026 23:19:01 -0400 Subject: [PATCH 029/102] Remove more StringNames duplication --- MiraAPI/GameModes/GameModeOption.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index ccf512d..fb2ba52 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -36,10 +36,10 @@ public static class GameModeOption _lastValue = value; } } - - private static int _lastValue; internal static StringOption OptionBehaviour { get; private set; } = null!; - + private static int _lastValue; + private static readonly StringNames GamemodeName = CustomStringName.CreateAndRegister("Gamemode"); + private static readonly StringNames CustomName = CustomStringName.CreateAndRegister("Custom"); private static readonly Dictionary Values = new() { ["Classic"] = CustomStringName.CreateAndRegister("Classic"), @@ -58,14 +58,14 @@ public static class GameModeOption if (GameManager.Instance.IsHideAndSeek()) return; float num = 0.713f; - foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) + foreach (var rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) { num -= 0.63f; - foreach (BaseGameSetting a in rulesCategory.AllGameSettings) + foreach (var a in rulesCategory.AllGameSettings) num -= 0.45f; } CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); - categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister("Custom"), 20); + categoryHeaderMasked.SetHeader(CustomName, 20); categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); OptionBehaviour = Object.Instantiate( @@ -78,7 +78,7 @@ public static class GameModeOption OptionBehaviour.SetClickMask(__instance.ButtonClickMask); StringGameSetting setting = ScriptableObject.CreateInstance(); setting.Type = OptionTypes.MultipleChoice; - setting.Title = CustomStringName.CreateAndRegister("Gamemode"); + setting.Title = GamemodeName; setting.Index = _lastValue; setting.Values = new Il2CppStructArray([Values["Classic"]]); OptionBehaviour.SetUpFromData(setting, 20); From bf7c800613cd1746ab9c7e27f6bc34932e0ee9dd Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:58:02 -0500 Subject: [PATCH 030/102] Add basic Hide n Seek gamemode --- MiraAPI/GameModes/CustomGameModeManager.cs | 3 ++ MiraAPI/GameModes/DefaultMode.cs | 4 +- MiraAPI/GameModes/GameModeOption.cs | 5 ++- MiraAPI/GameModes/HideAndSeekMode.cs | 16 ++++++++ MiraAPI/Patches/GameModes/HudPatches.cs | 2 +- MiraAPI/PluginLoading/MiraPluginManager.cs | 44 ++++++++++++++++++++++ 6 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 MiraAPI/GameModes/HideAndSeekMode.cs diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 3cf4562..8c97d28 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -66,6 +66,9 @@ public static class CustomGameModeManager // no need to add to game mode option as it already contains it // because we cannot have the option be created with no values defaultMode.ID = 0; + var hnsMode = new HideAndSeekMode(); + IdToModeMap.Add(1, hnsMode); + hnsMode.ID = 1; } internal static void SetGameMode(uint id) diff --git a/MiraAPI/GameModes/DefaultMode.cs b/MiraAPI/GameModes/DefaultMode.cs index f50dcd4..d7541fd 100644 --- a/MiraAPI/GameModes/DefaultMode.cs +++ b/MiraAPI/GameModes/DefaultMode.cs @@ -9,8 +9,8 @@ namespace MiraAPI.GameModes; public class DefaultMode : AbstractGameMode { /// - public override string Name => "Default"; + public override string Name => "Classic"; /// - public override string Description => "Default Among Us GameMode"; + public override string Description => "The classic Among Us experience"; } diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index fb2ba52..3b87612 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -39,10 +39,11 @@ public static class GameModeOption internal static StringOption OptionBehaviour { get; private set; } = null!; private static int _lastValue; private static readonly StringNames GamemodeName = CustomStringName.CreateAndRegister("Gamemode"); - private static readonly StringNames CustomName = CustomStringName.CreateAndRegister("Custom"); + private static readonly StringNames CustomName = CustomStringName.CreateAndRegister("Custom"); private static readonly Dictionary Values = new() { ["Classic"] = CustomStringName.CreateAndRegister("Classic"), + ["Hide n Seek"] = CustomStringName.CreateAndRegister("Hide n Seek"), }; // loading takes place before option creation @@ -80,7 +81,7 @@ public static class GameModeOption setting.Type = OptionTypes.MultipleChoice; setting.Title = GamemodeName; setting.Index = _lastValue; - setting.Values = new Il2CppStructArray([Values["Classic"]]); + setting.Values = new Il2CppStructArray([Values["Classic"], Values["Hide n Seek"]]); OptionBehaviour.SetUpFromData(setting, 20); OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs new file mode 100644 index 0000000..e2eb989 --- /dev/null +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -0,0 +1,16 @@ +using MiraAPI.PluginLoading; + +namespace MiraAPI.GameModes; + +/// +/// The vanilla Hide and Seek game mode, ported to Mira. +/// +[MiraIgnore] +public class HideAndSeekMode : AbstractGameMode +{ + /// + public override string Name => "Hide n Seek"; + + /// + public override string Description => "You can run, but you can't hide!"; +} diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index 4cb5076..8ca96a3 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -33,7 +33,7 @@ internal static class HudPatches gmText.SetActive(false); modelText.SetActive(false); _text = gmTextClone.GetComponent(); - _text.text = "Default"; + _text.text = CustomGameModeManager.ActiveMode?.Name ?? "Classic"; } private static TextMeshPro? _text; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 26a881f..a99eae7 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -7,9 +7,11 @@ using System.Reflection; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using Il2CppInterop.Runtime.Injection; using MiraAPI.Colors; using MiraAPI.Events; using MiraAPI.GameEnd; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; @@ -20,6 +22,7 @@ using MiraAPI.Modifiers; using MiraAPI.Presets; using MiraAPI.Roles; using MiraAPI.Utilities; +using Reactor.Localization.Utilities; using Reactor.Networking; using Reactor.Utilities; @@ -40,6 +43,8 @@ public sealed class MiraPluginManager internal void Initialize() { Instance = this; + CustomGameModeManager.RegisterDefaultMode(); + CustomGameModeManager.GetAndSetGameMode(); IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => { if (plugin is not IMiraPlugin miraPlugin) @@ -116,6 +121,11 @@ public sealed class MiraPluginManager continue; } + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + RegisterColorClasses(type); RegisterKeybinds(type, plugin); } @@ -143,6 +153,21 @@ public sealed class MiraPluginManager RegisteredPlugins = [.. _registeredPlugins.Values]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); + + var dict = new Dictionary(); + foreach (var (key, value) in CustomGameModeManager.IdToModeMap) + { + if (value is DefaultMode) + { + continue; + } + dict.Add(value.Name, key); + if (((AmongUs.GameOptions.GameModes) key) == AmongUs.GameOptions.GameModes.Normal) + return; + // 'Default' was still being registered twice and idk why + GameModesHelpers.ModeToName.Add((AmongUs.GameOptions.GameModes)key, CustomStringName.CreateAndRegister(value.Name)); + } + EnumInjector.InjectEnumValues(dict); }; RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); @@ -318,6 +343,25 @@ public sealed class MiraPluginManager return false; } + private static bool RegisterGameModeAttribute(Type type, MiraPluginInfo pluginInfo) + { + try + { + if (type.IsAssignableTo(typeof(AbstractGameMode))) + { + // TODO: bool + CustomGameModeManager.RegisterGameMode(type, pluginInfo); + return true; + } + } + catch (Exception e) + { + Error($"Failed to register gamemode {type.Name}: {e}"); + } + + return false; + } + private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo) { try From b0ceb129a4cacd7605de75def789b44480921e05 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:37:54 -0500 Subject: [PATCH 031/102] Re-add Hns options as modded options The options don't appear yet though, this needs to be fixed! --- .../Options/HnsCrewmateOptions.cs | 89 +++++++++++++++++++ .../Options/HnsFinalHideOptions.cs | 52 +++++++++++ .../Options/HnsImpostorOptions.cs | 40 +++++++++ .../Options/HnsTaskOptions.cs | 43 +++++++++ 4 files changed, 224 insertions(+) create mode 100644 MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs create mode 100644 MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs create mode 100644 MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs create mode 100644 MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs diff --git a/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs new file mode 100644 index 0000000..cf1a7c6 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsCrewmateOptions.cs @@ -0,0 +1,89 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsCrewmateOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Crewmates"; + + public ModdedNumberOption PlayerSpeed { get; set; } = new( + "Player Speed", + 1f, + 0.5f, + 3f, + 0.25f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption HidingTime { get; set; } = new( + "Hiding Time", + 200, + 160, + 300, + 20, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedNumberOption CrewmateVision { get; set; } = new( + "Crewmate Vision", + 0.6f, + 0.25f, + 1, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption MaxVentUses { get; set; } = new( + "Max Vent Uses", + 1, + 0, + 5, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption MaxTimeInVent { get; set; } = new( + "Max Time in Vent", + 3, + 1, + 10, + 1, + "#", + "#", + MiraNumberSuffixes.Seconds) + { + Visible = () => (int)OptionGroupSingleton.Instance.MaxVentUses.Value > 0, + }; + + public ModdedToggleOption FlashlightMode { get; set; } = new( + "Flashlight Mode", + true); + + public ModdedNumberOption CrewmateFlashlightSize { get; set; } = new( + "Crewmate Flashlight Size", + 0.35f, + 0.1f, + 0.5f, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier) + { + Visible = () => OptionGroupSingleton.Instance.FlashlightMode.Value, + }; + + public ModdedToggleOption ShowNames { get; set; } = new( + "Show Names", + false); +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs new file mode 100644 index 0000000..009b248 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsFinalHideOptions.cs @@ -0,0 +1,52 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsFinalHideOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Final Hide"; + + public ModdedNumberOption FinalHideTime { get; set; } = new( + "Hiding Time", + 50, + 30, + 120, + 5, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedToggleOption FinalHidePings { get; set; } = new( + "Final Hide Pings", + true); + + public ModdedNumberOption FinalHideImpostorSpeed { get; set; } = new( + "Final Hide Impostor Speed", + 1.2f, + 1f, + 3, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption PingInterval { get; set; } = new( + "Ping Interval", + 6, + 3, + 10, + 5, + "#", + "#", + MiraNumberSuffixes.Seconds); + + public ModdedToggleOption FinalHideSeekMap { get; set; } = new( + "Final Hide Seek Map", + true); +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs new file mode 100644 index 0000000..c0c61b5 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsImpostorOptions.cs @@ -0,0 +1,40 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsImpostorOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Impostors"; + + public ModdedPlayerOption SelectedSeeker { get; set; } = new( + "Forced Impostor"); + + public ModdedNumberOption ImpostorVision { get; set; } = new( + "Impostor Vision", + 0.6f, + 0.25f, + 1, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier, + "0.00"); + + public ModdedNumberOption ImpostorFlashlightSize { get; set; } = new( + "Impostor Flashlight Size", + 0.35f, + 0.1f, + 0.5f, + 0.05f, + "#", + "#", + MiraNumberSuffixes.Multiplier) + { + Visible = () => OptionGroupSingleton.Instance.FlashlightMode.Value, + }; +} diff --git a/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs new file mode 100644 index 0000000..112f674 --- /dev/null +++ b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs @@ -0,0 +1,43 @@ +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.HnsReimplemented.Options; + +/// +public class HnsTaskOptions : AbstractOptionGroup +{ + /// + public override string GroupName => "Tasks"; + + public ModdedNumberOption CommonTasks { get; set; } = new( + "# Common Tasks", + 1, + 0, + 2, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption LongTasks { get; set; } = new( + "# Long Tasks", + 1, + 0, + 3, + 1, + "#", + "#", + MiraNumberSuffixes.None); + + public ModdedNumberOption ShortTasks { get; set; } = new( + "# Short Tasks", + 2, + 0, + 5, + 1, + "#", + "#", + MiraNumberSuffixes.None); +} From b627006ba1ac5be33f4c7f6c82c2b06c6bfc5f3c Mon Sep 17 00:00:00 2001 From: amber Date: Fri, 1 May 2026 12:44:54 -0400 Subject: [PATCH 032/102] Move gamemode option to the top, fix registration. --- MiraAPI/GameModes/CustomGameModeManager.cs | 17 ++++++++------- MiraAPI/GameModes/GameModeOption.cs | 24 +++++++++++++--------- MiraAPI/GameModes/HideAndSeekMode.cs | 3 +++ 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 8c97d28..919f32b 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -13,11 +13,7 @@ public static class CustomGameModeManager { public static readonly Dictionary IdToModeMap = []; - private static uint GetNextId() - { - LastId++; - return LastId; - } + private static uint GetNextId() => ++LastId; internal static uint LastId { get; private set; } @@ -44,15 +40,16 @@ public static class CustomGameModeManager IdToModeMap.Add(GetNextId(), mode); pluginInfo.GameModes.Add(LastId, mode); - GameModeOption.AddOption($"{mode.Name}"); + GameModeOption.AddOption(mode); mode.ID = LastId; } /// /// Checks to see if the default game mode is on. /// - /// True if the default mode is one. - public static bool IsDefault() => (uint) GameModeOption.Value == 0; + /// True if the default mode is the current one. + public static bool IsDefault() => ActiveMode is DefaultMode; + public static bool IsHideNSeek() => ActiveMode is HideAndSeekMode; /// /// Gets the current gamemode. @@ -69,6 +66,8 @@ public static class CustomGameModeManager var hnsMode = new HideAndSeekMode(); IdToModeMap.Add(1, hnsMode); hnsMode.ID = 1; + GameModeOption.AddOption(hnsMode); + LastId++; } internal static void SetGameMode(uint id) @@ -98,4 +97,4 @@ public static class CustomGameModeManager ActiveMode = IdToModeMap[0]; Logger.Warning($"Unable to find game mode of id {id}!"); } -} +} \ No newline at end of file diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 3b87612..a9a9fab 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -5,6 +5,7 @@ using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem; using MiraAPI.Patches.GameModes; using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.ProBuilder; using Object = UnityEngine.Object; @@ -37,18 +38,18 @@ public static class GameModeOption } } internal static StringOption OptionBehaviour { get; private set; } = null!; + private static int _lastValue; private static readonly StringNames GamemodeName = CustomStringName.CreateAndRegister("Gamemode"); private static readonly StringNames CustomName = CustomStringName.CreateAndRegister("Custom"); private static readonly Dictionary Values = new() { ["Classic"] = CustomStringName.CreateAndRegister("Classic"), - ["Hide n Seek"] = CustomStringName.CreateAndRegister("Hide n Seek"), }; - // loading takes place before option creation - internal static void AddOption(string opt) + internal static void AddOption(AbstractGameMode mode) { + var opt = $"{mode.Name}"; if (!Values.ContainsKey(opt)) Values.Add(opt, CustomStringName.CreateAndRegister(opt)); } @@ -59,12 +60,12 @@ public static class GameModeOption if (GameManager.Instance.IsHideAndSeek()) return; float num = 0.713f; - foreach (var rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) + foreach (var category in __instance.settingsContainer.GetComponentsInChildren()) { - num -= 0.63f; - foreach (var a in rulesCategory.AllGameSettings) - num -= 0.45f; + if (category) + category.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); } + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); categoryHeaderMasked.SetHeader(CustomName, 20); categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; @@ -81,7 +82,7 @@ public static class GameModeOption setting.Type = OptionTypes.MultipleChoice; setting.Title = GamemodeName; setting.Index = _lastValue; - setting.Values = new Il2CppStructArray([Values["Classic"], Values["Hide n Seek"]]); + setting.Values = new Il2CppStructArray([Values["Classic"]]); OptionBehaviour.SetUpFromData(setting, 20); OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => @@ -90,9 +91,12 @@ public static class GameModeOption CustomGameModeManager.SetGameMode((uint)_lastValue); HudPatches.SetGameModeText(Values.ElementAt(_lastValue).Key); }); - num -= 0.37f; // scrollbar offset + foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) + { + optionBehaviour.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); + } __instance.Children.Add(OptionBehaviour); - __instance.scrollBar.SetYBoundsMax(-num - 1.65f); + __instance.scrollBar.SetYBoundsMax(__instance.scrollBar.GetYBounds().max + 1); for (var i = 1; i < Values.Count; i++) OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); } diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index e2eb989..3d86c13 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -1,4 +1,5 @@ using MiraAPI.PluginLoading; +using UnityEngine; namespace MiraAPI.GameModes; @@ -13,4 +14,6 @@ public class HideAndSeekMode : AbstractGameMode /// public override string Description => "You can run, but you can't hide!"; + + public override Color Color { get; } = new Color32(255, 88, 90, 255); } From 4f4b880ad4ba263f385a866424d6b14d81982093 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 1 May 2026 20:34:45 -0500 Subject: [PATCH 033/102] Reimplement Hide n Seek intro (no music) --- MiraAPI/GameModes/HideAndSeekMode.cs | 166 ++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 3d86c13..b9a25dc 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -1,4 +1,12 @@ -using MiraAPI.PluginLoading; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using MiraAPI.PluginLoading; +using MiraAPI.Utilities; +using PowerTools; using UnityEngine; namespace MiraAPI.GameModes; @@ -16,4 +24,160 @@ public class HideAndSeekMode : AbstractGameMode public override string Description => "You can run, but you can't hide!"; public override Color Color { get; } = new Color32(255, 88, 90, 255); + + public override bool CanReport(DeadBody body) => false; + public override bool ShouldShowSabotageMap(MapBehaviour map) => false; + public override bool ShowGameModeIntroCutscene => true; + + public override IEnumerator IntroCutscene(IntroCutscene __instance) + { + SoundManager.Instance.PlaySound(__instance.IntroStinger, false, 1f, null); + ShipStatus.Instance.BreakEmergencyButton(); + Logger.GlobalInstance.Info("IntroCutscene :: CoBegin() :: Game Mode: Hide and Seek (MiraAPI)", null); + __instance.LogPlayerRoleData(); + __instance.HideAndSeekPanels.SetActive(true); + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + __instance.CrewmateRules.SetActive(false); + __instance.ImpostorRules.SetActive(true); + } + else + { + __instance.CrewmateRules.SetActive(true); + __instance.ImpostorRules.SetActive(false); + } + + __instance.ImpostorName.gameObject.SetActive(true); + __instance.ImpostorTitle.gameObject.SetActive(true); + __instance.BackgroundBar.enabled = false; + __instance.TeamTitle.gameObject.SetActive(false); + var impostor = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(x => x.Data.Role.IsImpostor); + if (impostor == null) + { + Logger.GlobalInstance.Error("IntroCutscene :: CoBegin() :: impostor is NULL", null); + } + + GameManager.Instance.SetSpecialCosmetics(impostor); + if (impostor != null) + { + __instance.ImpostorName.text = impostor.Data.PlayerName; + } + else + { + __instance.ImpostorName.text = "???"; + } + + yield return new WaitForSecondsRealtime(0.1f); + if (impostor != null) + { + __instance.ImpostorTitle.text = impostor.Data.Role.GetRoleName(); + } + PoolablePlayer playerSlot = null; + if (impostor != null) + { + playerSlot = __instance.CreatePlayer(1, 1, impostor.Data, false); + playerSlot.SetBodyType(PlayerBodyTypes.Normal); + playerSlot.SetFlipX(false); + playerSlot.transform.localPosition = __instance.impostorPos; + playerSlot.transform.localScale = Vector3.one * __instance.impostorScale; + } + + yield return ShipStatus.Instance.CosmeticsCache.PopulateFromPlayers(); + yield return new WaitForSecondsRealtime(6f); + if (playerSlot != null) + { + playerSlot.gameObject.SetActive(false); + } + + __instance.HideAndSeekPanels.SetActive(false); + __instance.CrewmateRules.SetActive(false); + __instance.ImpostorRules.SetActive(false); + /*LogicOptionsHnS logicOptionsHnS = GameManager.Instance.LogicOptions as LogicOptionsHnS; + LogicHnSMusic logicHnSMusic = GameManager.Instance.GetLogicComponent() as LogicHnSMusic; + if (logicHnSMusic != null) + { + logicHnSMusic.StartMusicWithIntro(); + }*/ + var hideTimer = 10f; + + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + __instance.HideAndSeekTimerText.gameObject.SetActive(true); + PoolablePlayer poolablePlayer; + AnimationClip anim; + if (AprilFoolsMode.ShouldHorseAround()) + { + poolablePlayer = __instance.HorseWrangleVisualSuit; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); + anim = __instance.HnSSeekerSpawnHorseAnim; + __instance.HorseWrangleVisualPlayer.SetBodyType(PlayerBodyTypes.Normal); + __instance.HorseWrangleVisualPlayer.UpdateFromPlayerData( + PlayerControl.LocalPlayer.Data, + PlayerControl.LocalPlayer.CurrentOutfitType, + PlayerMaterial.MaskType.None, + false, + null, + false); + } + else if (AprilFoolsMode.ShouldLongAround()) + { + poolablePlayer = __instance.HideAndSeekPlayerVisual; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.LongSeeker); + anim = __instance.HnSSeekerSpawnLongAnim; + } + else + { + poolablePlayer = __instance.HideAndSeekPlayerVisual; + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.SetBodyType(PlayerBodyTypes.Seeker); + anim = __instance.HnSSeekerSpawnAnim; + } + + poolablePlayer.SetBodyCosmeticsVisible(false); + poolablePlayer.UpdateFromPlayerData( + PlayerControl.LocalPlayer.Data, + PlayerControl.LocalPlayer.CurrentOutfitType, + PlayerMaterial.MaskType.None, + false, + null, + false); + SpriteAnim component = poolablePlayer.GetComponent(); + poolablePlayer.gameObject.SetActive(true); + poolablePlayer.ToggleName(false); + component.Play(anim, 1f); + while (hideTimer > 0f) + { + __instance.HideAndSeekTimerText.text = Mathf.RoundToInt(hideTimer).ToString(); + hideTimer -= Time.deltaTime; + yield return null; + } + } + else + { + ShipStatus.Instance.HideCountdown = hideTimer; + if (AprilFoolsMode.ShouldHorseAround()) + { + if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnHorseInGameAnim); + } + } + else if (AprilFoolsMode.ShouldLongAround()) + { + if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnLongInGameAnim); + } + } + else if (impostor != null) + { + impostor.AnimateCustom(__instance.HnSSeekerSpawnAnim); + impostor.cosmetics.SetBodyCosmeticsVisible(false); + } + } + ShipStatus.Instance.StartSFX(); + UnityEngine.Object.Destroy(__instance.gameObject); + } } From 50cb8ffc3d28196c9fdf555b8bbc2bbfa3378b10 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 1 May 2026 20:52:54 -0500 Subject: [PATCH 034/102] Add body type patch for Hide n Seek --- MiraAPI/GameModes/AbstractGameMode.cs | 23 +++++++++++ MiraAPI/GameModes/HideAndSeekMode.cs | 41 +++++++++++++++++++ MiraAPI/Patches/GameModes/BodyTypePatch.cs | 20 +++++++++ MiraAPI/Patches/GameModes/GameLogicPatches.cs | 21 ++++------ 4 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 MiraAPI/Patches/GameModes/BodyTypePatch.cs diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 2da08aa..245e044 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -32,6 +32,29 @@ public abstract class AbstractGameMode : IOptionable /// public uint ID { get; set; } + /// + /// Gets a value indicating whether a specific body type is used by the game mode. + /// + public virtual bool GameModeBodyTypeOverride => false; + + /// + /// Called on GameManager.GetBodyType(). + /// + /// Player to get body type from. + /// Resulting body type. + public virtual PlayerBodyTypes GetBodyType(PlayerControl player) + { + if (AprilFoolsMode.ShouldHorseAround()) + { + return PlayerBodyTypes.Horse; + } + if (AprilFoolsMode.ShouldLongAround()) + { + return PlayerBodyTypes.Long; + } + return PlayerBodyTypes.Normal; + } + /// /// Gets a value indicating whether a custom intro sequence is implemented by the game mode. /// diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index b9a25dc..3cf4921 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -28,6 +28,7 @@ public class HideAndSeekMode : AbstractGameMode public override bool CanReport(DeadBody body) => false; public override bool ShouldShowSabotageMap(MapBehaviour map) => false; public override bool ShowGameModeIntroCutscene => true; + public override bool GameModeBodyTypeOverride => true; public override IEnumerator IntroCutscene(IntroCutscene __instance) { @@ -180,4 +181,44 @@ public class HideAndSeekMode : AbstractGameMode ShipStatus.Instance.StartSFX(); UnityEngine.Object.Destroy(__instance.gameObject); } + + public override PlayerBodyTypes GetBodyType(PlayerControl player) + { + if (player == null || player.Data == null || player.Data.Role == null) + { + if (AprilFoolsMode.ShouldHorseAround()) + { + return PlayerBodyTypes.Horse; + } + if (AprilFoolsMode.ShouldLongAround()) + { + return PlayerBodyTypes.Long; + } + return PlayerBodyTypes.Normal; + } + else if (AprilFoolsMode.ShouldHorseAround()) + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.Normal; + } + return PlayerBodyTypes.Horse; + } + else if (AprilFoolsMode.ShouldLongAround()) + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.LongSeeker; + } + return PlayerBodyTypes.Long; + } + else + { + if (player.Data.Role.IsImpostor) + { + return PlayerBodyTypes.Seeker; + } + return PlayerBodyTypes.Normal; + } + } } diff --git a/MiraAPI/Patches/GameModes/BodyTypePatch.cs b/MiraAPI/Patches/GameModes/BodyTypePatch.cs new file mode 100644 index 0000000..2aede92 --- /dev/null +++ b/MiraAPI/Patches/GameModes/BodyTypePatch.cs @@ -0,0 +1,20 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.CoBegin))] +internal static class BodyTypePatch +{ + public static bool Prefix(IntroCutscene __instance, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (CustomGameModeManager.ActiveMode?.ShowGameModeIntroCutscene == true) + { + __result = CustomGameModeManager.ActiveMode.IntroCutscene(__instance).WrapToIl2Cpp(); + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/GameLogicPatches.cs b/MiraAPI/Patches/GameModes/GameLogicPatches.cs index 46dea2a..6f734fa 100644 --- a/MiraAPI/Patches/GameModes/GameLogicPatches.cs +++ b/MiraAPI/Patches/GameModes/GameLogicPatches.cs @@ -6,19 +6,14 @@ namespace MiraAPI.Patches.GameModes; [HarmonyPatch] internal static class GameLogicPatches { - [HarmonyPrefix, HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] - public static bool EndCritPatch(LogicGameFlowNormal __instance) + [HarmonyPrefix, HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetBodyType))] + public static bool BodyTypePatch(NormalGameManager __instance, PlayerControl player, ref PlayerBodyTypes __result) { - var runOriginal = true; - CustomGameModeManager.ActiveMode?.CheckGameEnd(out runOriginal, __instance); - return runOriginal; - } - - [HarmonyPrefix, HarmonyPatch(typeof(LogicRoleSelectionNormal), nameof(LogicRoleSelectionNormal.AssignRolesFromList))] - public static bool AssignRolesPatch(LogicRoleSelectionNormal __instance) - { - var runOriginal = true; - CustomGameModeManager.ActiveMode?.AssignRoles(out runOriginal, __instance); - return runOriginal; + if (CustomGameModeManager.ActiveMode == null || !CustomGameModeManager.ActiveMode.GameModeBodyTypeOverride) + { + return true; + } + __result = CustomGameModeManager.ActiveMode.GetBodyType(player); + return false; } } From ac87003d0aabf8135bd7647238ab21f652954c72 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Tue, 12 May 2026 21:46:01 -0500 Subject: [PATCH 035/102] Fix color not applying on game mode name --- MiraAPI/Patches/GameModes/HudPatches.cs | 3 ++- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index 8ca96a3..5ffa4fb 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -1,5 +1,6 @@ using HarmonyLib; using MiraAPI.GameModes; +using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -33,7 +34,7 @@ internal static class HudPatches gmText.SetActive(false); modelText.SetActive(false); _text = gmTextClone.GetComponent(); - _text.text = CustomGameModeManager.ActiveMode?.Name ?? "Classic"; + _text.text = CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic"; } private static TextMeshPro? _text; diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 0ad86f0..94f510a 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -354,7 +354,7 @@ public static class LobbyViewPanePatches if (viewSettingsInfoPanel.titleText.text.Contains("Game Mode")) { viewSettingsInfoPanel.titleText.text = TranslationController.Instance.GetString( - data.Title, $"{CustomGameModeManager.ActiveMode}" + data.Title, CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic" ); } } From b33a66cbd9e1d569c43666f0a6a8a817d5701af6 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Tue, 12 May 2026 21:48:14 -0500 Subject: [PATCH 036/102] Fix OnPlayerDeath for HnS --- MiraAPI/GameModes/AbstractGameMode.cs | 4 +++- MiraAPI/GameModes/HideAndSeekMode.cs | 10 ++++++++++ MiraAPI/Patches/GameModes/OnDeathPatch.cs | 14 ++++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 245e044..93d97bc 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -87,8 +87,10 @@ public abstract class AbstractGameMode : IOptionable /// Called when a player is killed. /// /// PlayerControl that was killed. - public virtual void OnDeath(PlayerControl player) + /// Determines whether to assign a ghost role on the player. + public virtual void OnPlayerDeath(PlayerControl player, bool assignGhostRole) { + GameManager.Instance.LogicRoleSelection.OnPlayerDeath(player, assignGhostRole); } /// diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 3cf4921..e10526b 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -32,6 +32,7 @@ public class HideAndSeekMode : AbstractGameMode public override IEnumerator IntroCutscene(IntroCutscene __instance) { + deadPlayerCount = 0; SoundManager.Instance.PlaySound(__instance.IntroStinger, false, 1f, null); ShipStatus.Instance.BreakEmergencyButton(); Logger.GlobalInstance.Info("IntroCutscene :: CoBegin() :: Game Mode: Hide and Seek (MiraAPI)", null); @@ -221,4 +222,13 @@ public class HideAndSeekMode : AbstractGameMode return PlayerBodyTypes.Normal; } } + private int deadPlayerCount; + public override void OnPlayerDeath(PlayerControl player, bool assignGhostRole) + { + base.OnPlayerDeath(player, assignGhostRole); + var popup = GameManagerCreator.Instance.HideAndSeekManagerPrefab.DeathPopupPrefab; + deadPlayerCount++; + var item = UnityEngine.Object.Instantiate(popup, HudManager.Instance.transform.parent); + item.Show(player, deadPlayerCount); + } } diff --git a/MiraAPI/Patches/GameModes/OnDeathPatch.cs b/MiraAPI/Patches/GameModes/OnDeathPatch.cs index ba96c6c..82a16f6 100644 --- a/MiraAPI/Patches/GameModes/OnDeathPatch.cs +++ b/MiraAPI/Patches/GameModes/OnDeathPatch.cs @@ -3,12 +3,18 @@ using MiraAPI.GameModes; namespace MiraAPI.Patches.GameModes; -[HarmonyPatch(typeof(KillAnimation))] +[HarmonyPatch(typeof(GameManager))] internal static class OnDeathPatch { - [HarmonyPostfix, HarmonyPatch(nameof(KillAnimation.CoPerformKill))] - public static void OnDeathPostfix([HarmonyArgument(0)] PlayerControl source, [HarmonyArgument(1)] PlayerControl target) + [HarmonyPrefix, HarmonyPatch(nameof(GameManager.OnPlayerDeath))] + public static bool OnDeathPostfix([HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] bool assignGhostRole) { - CustomGameModeManager.ActiveMode?.OnDeath(target); + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.OnPlayerDeath(player, assignGhostRole); + return false; + } + + return true; } } From ae4b4d872b8c32be8a73aaaed7dfff5ff4be7391 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Tue, 12 May 2026 22:09:38 -0500 Subject: [PATCH 037/102] Add crewmate kill tracker --- MiraAPI/GameModes/AbstractGameMode.cs | 5 +++ MiraAPI/GameModes/HideAndSeekMode.cs | 2 ++ .../Patches/GameModes/GameManagerPatches.cs | 31 +++++++++++++++++++ .../GameModes/NormalGameManagerPatches.cs | 20 ++++++++++++ MiraAPI/Patches/GameModes/OnDeathPatch.cs | 20 ------------ 5 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 MiraAPI/Patches/GameModes/GameManagerPatches.cs create mode 100644 MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs delete mode 100644 MiraAPI/Patches/GameModes/OnDeathPatch.cs diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 93d97bc..cd35deb 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -37,6 +37,11 @@ public abstract class AbstractGameMode : IOptionable /// public virtual bool GameModeBodyTypeOverride => false; + /// + /// Gets a value indicating whether the gamemode is based on Hide n Seek. + /// + public virtual bool IsHideAndSeek => false; + /// /// Called on GameManager.GetBodyType(). /// diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index e10526b..5645141 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -29,6 +29,7 @@ public class HideAndSeekMode : AbstractGameMode public override bool ShouldShowSabotageMap(MapBehaviour map) => false; public override bool ShowGameModeIntroCutscene => true; public override bool GameModeBodyTypeOverride => true; + public override bool IsHideAndSeek => true; public override IEnumerator IntroCutscene(IntroCutscene __instance) { @@ -226,6 +227,7 @@ public class HideAndSeekMode : AbstractGameMode public override void OnPlayerDeath(PlayerControl player, bool assignGhostRole) { base.OnPlayerDeath(player, assignGhostRole); + HudManager.Instance.NotifyOfDeath(); var popup = GameManagerCreator.Instance.HideAndSeekManagerPrefab.DeathPopupPrefab; deadPlayerCount++; var item = UnityEngine.Object.Instantiate(popup, HudManager.Instance.transform.parent); diff --git a/MiraAPI/Patches/GameModes/GameManagerPatches.cs b/MiraAPI/Patches/GameModes/GameManagerPatches.cs new file mode 100644 index 0000000..7467d9e --- /dev/null +++ b/MiraAPI/Patches/GameModes/GameManagerPatches.cs @@ -0,0 +1,31 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(GameManager))] +internal static class GameManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(GameManager.OnPlayerDeath))] + public static bool OnDeathPrefix([HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] bool assignGhostRole) + { + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.OnPlayerDeath(player, assignGhostRole); + return false; + } + + return true; + } + [HarmonyPrefix, HarmonyPatch(nameof(GameManager.ShowCrewmatesKilled))] + public static bool ShowCrewmatesKilledPrefix(ref bool __result) + { + if (CustomGameModeManager.ActiveMode != null) + { + __result = CustomGameModeManager.ActiveMode.IsHideAndSeek; + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs new file mode 100644 index 0000000..bed43b9 --- /dev/null +++ b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs @@ -0,0 +1,20 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; + +[HarmonyPatch(typeof(NormalGameManager))] +internal static class NormalGameManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(NormalGameManager.IsNormal))] + public static bool IsNormal(ref bool __result) + { + if (CustomGameModeManager.ActiveMode != null) + { + __result = !CustomGameModeManager.ActiveMode.IsHideAndSeek; + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/OnDeathPatch.cs b/MiraAPI/Patches/GameModes/OnDeathPatch.cs deleted file mode 100644 index 82a16f6..0000000 --- a/MiraAPI/Patches/GameModes/OnDeathPatch.cs +++ /dev/null @@ -1,20 +0,0 @@ -using HarmonyLib; -using MiraAPI.GameModes; - -namespace MiraAPI.Patches.GameModes; - -[HarmonyPatch(typeof(GameManager))] -internal static class OnDeathPatch -{ - [HarmonyPrefix, HarmonyPatch(nameof(GameManager.OnPlayerDeath))] - public static bool OnDeathPostfix([HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] bool assignGhostRole) - { - if (CustomGameModeManager.ActiveMode != null) - { - CustomGameModeManager.ActiveMode.OnPlayerDeath(player, assignGhostRole); - return false; - } - - return true; - } -} From 0852f769ae04feb2de3b3a08abd6c077c85e445b Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Tue, 12 May 2026 22:18:48 -0500 Subject: [PATCH 038/102] Fix game mode option appearing in mod tabs --- MiraAPI/GameModes/GameModeOption.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index a9a9fab..5622b5f 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -4,6 +4,7 @@ using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem; using MiraAPI.Patches.GameModes; +using MiraAPI.Patches.Options; using Reactor.Localization.Utilities; using Reactor.Utilities.Extensions; using UnityEngine; @@ -57,6 +58,10 @@ public static class GameModeOption [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) { + if (GameSettingMenuPatches.SelectedModIdx != 0) + { + return; + } if (GameManager.Instance.IsHideAndSeek()) return; float num = 0.713f; From d313089521074a4da15941c5004246d682321617 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 13 May 2026 08:32:11 -0500 Subject: [PATCH 039/102] Register MiraAPI within itself --- MiraAPI/Events/MiraCancelableEvent.cs | 5 +- MiraAPI/Events/MiraEvent.cs | 5 +- MiraAPI/GameOptions/ModifierOptionGroup.cs | 2 + .../SettingTypes/LocalSettingBase.cs | 2 + MiraAPI/MiraApiPlugin.cs | 14 ++- MiraAPI/PluginLoading/MiraPluginManager.cs | 101 +++++++++++++++++- 6 files changed, 124 insertions(+), 5 deletions(-) diff --git a/MiraAPI/Events/MiraCancelableEvent.cs b/MiraAPI/Events/MiraCancelableEvent.cs index b4d3f05..fe772a9 100644 --- a/MiraAPI/Events/MiraCancelableEvent.cs +++ b/MiraAPI/Events/MiraCancelableEvent.cs @@ -1,8 +1,11 @@ -namespace MiraAPI.Events; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Events; /// /// Abstract class for Mira Events that can be cancelled. /// +[MiraIgnore] public abstract class MiraCancelableEvent : MiraEvent { /// diff --git a/MiraAPI/Events/MiraEvent.cs b/MiraAPI/Events/MiraEvent.cs index f0f49bd..7fdca49 100644 --- a/MiraAPI/Events/MiraEvent.cs +++ b/MiraAPI/Events/MiraEvent.cs @@ -1,6 +1,9 @@ -namespace MiraAPI.Events; +using MiraAPI.PluginLoading; + +namespace MiraAPI.Events; /// /// Abstract class for Mira Events. /// +[MiraIgnore] public abstract class MiraEvent; diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs index 3db6566..6309649 100644 --- a/MiraAPI/GameOptions/ModifierOptionGroup.cs +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -1,8 +1,10 @@ using System.Linq; +using MiraAPI.PluginLoading; namespace MiraAPI.GameOptions; #pragma warning disable CA1852 +[MiraIgnore] internal class ModifierOptionGroup : AbstractOptionGroup #pragma warning restore CA1852 { diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index c25f04e..48fd70d 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -1,11 +1,13 @@ using System; using System.Linq; using BepInEx.Configuration; +using MiraAPI.PluginLoading; using UnityEngine; namespace MiraAPI.LocalSettings.SettingTypes; /// +[MiraIgnore] public abstract class LocalSettingBase : ILocalSetting { /// diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 594d927..f6364d9 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,6 +1,7 @@ global using static Reactor.Utilities.Logger; using System; using BepInEx; +using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; @@ -20,8 +21,17 @@ namespace MiraAPI; [BepInDependency(ReactorPlugin.Id)] [BepInDependency(ModCompatibility.SubmergedId, BepInDependency.DependencyFlags.SoftDependency)] [ReactorModFlags(ModFlags.RequireOnAllClients)] -public partial class MiraApiPlugin : BasePlugin +public partial class MiraApiPlugin : BasePlugin, IMiraPlugin { + /// + public ConfigFile GetConfigFile() + { + return Config; + } + + /// + public string OptionsTitleText => "MiraAPI"; + /// /// Gets a value indicating whether the current device is running Starlight (on mobile). /// @@ -53,6 +63,6 @@ public partial class MiraApiPlugin : BasePlugin ReactorCredits.Register("Mira API", Version, IsDevBuild, ReactorCredits.AlwaysShow); PluginManager = new MiraPluginManager(); - PluginManager.Initialize(); + PluginManager.Initialize(this, this); } } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index a99eae7..86247a1 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -39,12 +39,111 @@ public sealed class MiraPluginManager internal Dictionary> QueuedRoleRegistrations { get; } = []; internal static MiraPluginManager Instance { get; private set; } = new(); + public static ConfigFile MiraApiConfig { get; private set; } + public static MiraPluginInfo MiraApiPluginInfo { get; private set; } - internal void Initialize() + internal void Initialize(BasePlugin miraApiPlugin, IMiraPlugin apiPlugin) { Instance = this; CustomGameModeManager.RegisterDefaultMode(); CustomGameModeManager.GetAndSetGameMode(); + if (IL2CPPChainloader.Instance.Plugins.TryGetValue(MiraApiPlugin.Id, out var newPlugin)) + { + var assembly = Assembly.GetCallingAssembly(); + var info = new MiraPluginInfo(apiPlugin, newPlugin); + var roles = new List(); + + var oldConfigSetting = info.PluginConfig.SaveOnConfigSet; + info.PluginConfig.SaveOnConfigSet = false; + + foreach (var type in assembly.GetTypes()) + { + if (type.IsAbstract || type.GetCustomAttribute() != null) + { + continue; + } + + foreach (var method in AccessTools.GetDeclaredMethods(type)) + { + var eventAttribute = method.GetCustomAttribute(); + if (eventAttribute == null) + { + continue; + } + + if (!method.IsStatic) + { + Error($"Event method {method.Name} in {type.Name} must be static."); + continue; + } + + var parameters = method.GetParameters(); + if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) + { + Error($"Invalid event registration method {method.Name} in {type.Name}"); + continue; + } + + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + } + + if (RegisterModifier(type, info)) + { + continue; + } + + if (RegisterOptions(type, info)) + { + continue; + } + + if (RegisterLocalTabs(type, miraApiPlugin)) + { + continue; + } + + if (RegisterRole(type, info, out var role)) + { + roles.Add(role); + continue; + } + + if (RegisterButton(type, info)) + { + continue; + } + + if (RegisterGameOver(type)) + { + continue; + } + + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + + RegisterColorClasses(type); + RegisterKeybinds(type, miraApiPlugin); + } + + info.PluginConfig.Save(); + info.PluginConfig.SaveOnConfigSet = oldConfigSetting; + MiraApiConfig = info.PluginConfig; + + info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + QueuedRoleRegistrations.Add(info, roles); + + _registeredPlugins.Add(assembly, info); + MiraApiPluginInfo = info; + + info.SavePublicCollections(); + PresetManager.CreateDefaultPreset(info); + PresetManager.LoadPresets(info); + Info($"Registering Mira API as mod."); + } + IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => { if (plugin is not IMiraPlugin miraPlugin) From 59b8e92c4876c9ada4b28860bdc1bbb220ddad88 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 13 May 2026 08:32:26 -0500 Subject: [PATCH 040/102] WIP option toggling --- MiraAPI/GameModes/AbstractGameMode.cs | 5 ++ MiraAPI/GameModes/GameModeOption.cs | 76 ++++++++++++++++++- MiraAPI/GameModes/HideAndSeekMode.cs | 1 + MiraAPI/GameOptions/ModdedOptionsManager.cs | 6 ++ .../Patches/Options/GameOptionsMenuPatch.cs | 2 +- 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index cd35deb..33d0af9 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -42,6 +42,11 @@ public abstract class AbstractGameMode : IOptionable /// public virtual bool IsHideAndSeek => false; + /// + /// Gets a value indicating whether the gamemode removes the normal game settings. + /// + public virtual bool ShowNormalGameSettings => true; + /// /// Called on GameManager.GetBodyType(). /// diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 5622b5f..4f5a1fc 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -3,6 +3,7 @@ using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem; +using MiraAPI.GameOptions; using MiraAPI.Patches.GameModes; using MiraAPI.Patches.Options; using Reactor.Localization.Utilities; @@ -54,16 +55,23 @@ public static class GameModeOption if (!Values.ContainsKey(opt)) Values.Add(opt, CustomStringName.CreateAndRegister(opt)); } + + private static List VanillaCategories = new(); + private static List VanillaOptions = new(); + private static List BaseCategories = new(); + private static List BaseOptions = new(); [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) { - if (GameSettingMenuPatches.SelectedModIdx != 0) + if (GameSettingMenuPatches.SelectedModIdx != 0 || GameManager.Instance.IsHideAndSeek() || CustomGameModeManager.ActiveMode == null) { return; } - if (GameManager.Instance.IsHideAndSeek()) - return; + VanillaCategories.Clear(); + VanillaOptions.Clear(); + BaseCategories.Clear(); + BaseOptions.Clear(); float num = 0.713f; foreach (var category in __instance.settingsContainer.GetComponentsInChildren()) { @@ -71,6 +79,21 @@ public static class GameModeOption category.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); } + var showOpts = CustomGameModeManager.ActiveMode.ShowNormalGameSettings; + + var newList = __instance.Children.ToArray(); + BaseOptions.Add(newList[0]); + foreach (var obj in newList.Skip(1)) + { + VanillaOptions.Add(obj); + obj.gameObject.SetActive(showOpts); + } + foreach (var category in __instance.settingsContainer.GetComponentsInChildren()) + { + VanillaCategories.Add(category); + category.gameObject.SetActive(showOpts); + } + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); categoryHeaderMasked.SetHeader(CustomName, 20); categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; @@ -95,15 +118,60 @@ public static class GameModeOption _lastValue = opt.GetInt(); CustomGameModeManager.SetGameMode((uint)_lastValue); HudPatches.SetGameModeText(Values.ElementAt(_lastValue).Key); + __instance.RefreshOptions(CustomGameModeManager.ActiveMode); }); foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) { optionBehaviour.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); } __instance.Children.Add(OptionBehaviour); - __instance.scrollBar.SetYBoundsMax(__instance.scrollBar.GetYBounds().max + 1); + BaseCategories.Add(categoryHeaderMasked); + BaseOptions.Add(OptionBehaviour); for (var i = 1; i < Values.Count; i++) OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); + __instance.RefreshOptions(CustomGameModeManager.ActiveMode); + } + + private static void RefreshOptions(this GameOptionsMenu instance, AbstractGameMode mode) + { + instance.Children.Clear(); + foreach (var group in BaseOptions) + { + instance.Children.Add(group); + } + + var showOpts = mode.ShowNormalGameSettings; + var num = 0.083f; + foreach (var category in VanillaCategories) + { + category.gameObject.SetActive(showOpts); + } + if (showOpts) + { + foreach (var obj in VanillaOptions) + { + obj.gameObject.SetActive(true); + instance.Children.Add(obj); + } + instance.scrollBar.SetYBoundsMax(instance.scrollBar.GetYBounds().max + 1); + } + else + { + foreach (var obj in VanillaOptions) + { + obj.gameObject.SetActive(false); + } + /*var filteredGroups = + ModdedOptionsManager.GameModeOptionGroups + .Where(x => x.Key == mode.GetType()) ?? []; + + foreach (var group in filteredGroups) + { + GameOptionsMenuPatch.UpdateGroup(group.Value, ref num); + }*/ + + instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 5645141..ae0e184 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -30,6 +30,7 @@ public class HideAndSeekMode : AbstractGameMode public override bool ShowGameModeIntroCutscene => true; public override bool GameModeBodyTypeOverride => true; public override bool IsHideAndSeek => true; + public override bool ShowNormalGameSettings => false; public override IEnumerator IntroCutscene(IntroCutscene __instance) { diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index e89a272..65dc7c6 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -6,6 +6,7 @@ using System.Reflection; using BepInEx.Configuration; using HarmonyLib; using Il2CppInterop.Runtime.Injection; +using MiraAPI.GameModes; using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Networking; @@ -26,6 +27,7 @@ public static class ModdedOptionsManager internal static readonly Dictionary CreatedPlayerOptions = []; internal static readonly Dictionary CreatedStringOptions = []; + internal static readonly Dictionary GameModeOptionGroups = []; internal static readonly Dictionary ModdedOptions = []; internal static readonly List Groups = []; @@ -70,6 +72,10 @@ public static class ModdedOptionsManager Groups.Add(group); TypeToGroup.Add(type, group); + if (group.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true) + { + GameModeOptionGroups.Add(group.OptionableType, group); + } pluginInfo.InternalOptionGroups.Add(group); typeof(OptionGroupSingleton<>).MakeGenericType(type) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index c8ee3ce..62dae21 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -72,7 +72,7 @@ internal static class GameOptionsMenuPatch __instance.scrollBar.SetYBoundsMax(-num - 1.65f); } - private static void UpdateGroup(AbstractOptionGroup group, ref float num) + internal static void UpdateGroup(AbstractOptionGroup group, ref float num) { if (group.Options.Count == 0 || group.Header is null) { From ad113a47abaf18f2819a76ea04ac660453a1ff11 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 13 May 2026 10:15:03 -0500 Subject: [PATCH 041/102] Hide and add gamemode options --- MiraAPI/GameModes/GameModeOption.cs | 226 +++++++++++++++++++- MiraAPI/GameOptions/ModdedOptionsManager.cs | 11 +- 2 files changed, 229 insertions(+), 8 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 4f5a1fc..d999cdd 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -6,10 +6,16 @@ using Il2CppSystem; using MiraAPI.GameOptions; using MiraAPI.Patches.GameModes; using MiraAPI.Patches.Options; +using MiraAPI.Presets; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; using Reactor.Utilities.Extensions; +using TMPro; using UnityEngine; +using UnityEngine.Events; using UnityEngine.ProBuilder; +using UnityEngine.UI; using Object = UnityEngine.Object; namespace MiraAPI.GameModes; @@ -60,6 +66,8 @@ public static class GameModeOption private static List VanillaOptions = new(); private static List BaseCategories = new(); private static List BaseOptions = new(); + private static List ModeCategories = new(); + private static List ModeOptions = new(); [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) @@ -68,6 +76,8 @@ public static class GameModeOption { return; } + ModeCategories.Clear(); + ModeOptions.Clear(); VanillaCategories.Clear(); VanillaOptions.Clear(); BaseCategories.Clear(); @@ -134,6 +144,16 @@ public static class GameModeOption private static void RefreshOptions(this GameOptionsMenu instance, AbstractGameMode mode) { + foreach (var option in ModeOptions) + { + option.gameObject.Destroy(); + } + foreach (var header in ModeCategories) + { + header.gameObject.Destroy(); + } + ModeOptions.Clear(); + ModeCategories.Clear(); instance.Children.Clear(); foreach (var group in BaseOptions) { @@ -146,6 +166,14 @@ public static class GameModeOption { category.gameObject.SetActive(showOpts); } + var filteredGroups = + ModdedOptionsManager.GameModeOptionGroups + .Where(x => x.Key == mode.GetType()).Select(y => y.Value).SelectMany(y => y) ?? []; + + foreach (var group in filteredGroups) + { + CreateGroup(instance, group); + } if (showOpts) { foreach (var obj in VanillaOptions) @@ -161,17 +189,21 @@ public static class GameModeOption { obj.gameObject.SetActive(false); } - /*var filteredGroups = - ModdedOptionsManager.GameModeOptionGroups - .Where(x => x.Key == mode.GetType()) ?? []; - foreach (var group in filteredGroups) { - GameOptionsMenuPatch.UpdateGroup(group.Value, ref num); - }*/ + GameOptionsMenuPatch.UpdateGroup(group, ref num); + } instance.scrollBar.SetYBoundsMax(-num - 1.65f); } + foreach (var option in ModeOptions) + { + option.gameObject.transform.localPosition += new Vector3(0, -0.67f, 0); + } + foreach (var header in ModeCategories) + { + header.gameObject.transform.localPosition += new Vector3(0, -0.67f, 0); + } } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] @@ -180,4 +212,186 @@ public static class GameModeOption { return !OptionBehaviour.Equals(option); } + + private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group) + { + var categoryHeaderMasked = Object.Instantiate( + menu.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + menu.settingsContainer); + + categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); + categoryHeaderMasked.Background.color = group.GroupColor; + categoryHeaderMasked.Divider.color = group.GroupColor; + categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + + categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); + categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear; + categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp; + + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0); + categoryHeaderMasked.Background.size = new Vector2( + categoryHeaderMasked.Background.size.x + 1.5f, + categoryHeaderMasked.Background.size.y); + + categoryHeaderMasked.gameObject.SetActive(false); + group.Header = categoryHeaderMasked; + categoryHeaderMasked.transform.localPosition += Vector3.down; + + var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); + newText.text = "(Click to close)"; + newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); + newText.gameObject.GetComponent().Destroy(); + + var options = group.Options.Select(opt => opt.CreateOption( + menu.checkboxOrigin, + menu.numberOptionOrigin, + menu.stringOptionOrigin, + menu.playerOptionOrigin, + menu.settingsContainer)); + ModeCategories.Add(categoryHeaderMasked); + + /*OptionPreset? defaultPreset = null; + if (GameSettingMenuPatches.SelectedMod != null && PresetManager.DefaultPresets.TryGetValue( + GameSettingMenuPatches.SelectedMod, + out var preset)) + { + defaultPreset = preset; + }*/ + + foreach (var newOpt in options) + { + newOpt.SetClickMask(menu.ButtonClickMask); + + SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); + foreach (var renderer in componentsInChildren) + { + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) + { + renderer.color = group.GroupColor.FindAlternateColor(); + if (renderer.transform.parent.TryGetComponent(out var btn)) + { + btn.interactableColor = group.GroupColor.FindAlternateColor(); + btn.interactableHoveredColor = Color.white; + } + } + + renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); + } + + foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) + { + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) + { + textMeshPro.color = group.GroupColor; + } + + textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); + } + + if (newOpt is ToggleOption toggle) + { + toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); + toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") + .GetComponent(); + rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); + rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + } + + menu.Children.Add(newOpt); + + /*var resetBtn = new GameObject("ResetOption"); + resetBtn.transform.parent = newOpt.transform; + resetBtn.transform.localScale = new(.5f, .5f, 1); + resetBtn.layer = LayerMask.NameToLayer("UI"); + resetBtn.transform.localPosition = new Vector3(-3.1f, 0f, -2f); + var resetRend = resetBtn.AddComponent(); + resetRend.sprite = MiraAssets.ResetButton.LoadAsset(); + resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + var resetBoxCol = resetBtn.gameObject.AddComponent(); + resetBoxCol.size = new Vector2(1f, 1f); + resetBoxCol.offset = new Vector2(0, 0); + var passiveButton = resetBtn.AddComponent(); + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.ClickSound = menu.BackButton.GetComponent().ClickSound; + passiveButton.OnMouseOver = new UnityEvent(); + passiveButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + })); + passiveButton.OnMouseOut = new UnityEvent(); + passiveButton.OnMouseOut.AddListener( + (UnityAction)(() => + { + resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + })); + if (newOpt is ToggleOption toggleOpt) + { + passiveButton.OnClick.AddListener( + (UnityAction)(() => + { + defaultPreset!.ResetOption(toggleOpt); + })); + } + else if (newOpt is NumberOption numOpt) + { + passiveButton.OnClick.AddListener( + (UnityAction)(() => + { + defaultPreset!.ResetOption(numOpt); + })); + } + else if (newOpt is StringOption strOpt) + { + passiveButton.OnClick.AddListener( + (UnityAction)(() => + { + defaultPreset!.ResetOption(strOpt); + })); + } + if (!defaultPreset!.IsOptionInPreset(newOpt)) + { + resetBtn.Destroy(); + }*/ + + newOpt.Initialize(); + newOpt.gameObject.SetActive(false); + ModeOptions.Add(newOpt); + } + + var boxCol = categoryHeaderMasked.gameObject.AddComponent(); + boxCol.size = new Vector2(7, 0.7f); + boxCol.offset = new Vector2(1.5f, -0.3f); + + var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); + headerBtn.ClickSound = menu.BackButton.GetComponent().ClickSound; + headerBtn.OnMouseOver = new UnityEvent(); + headerBtn.OnMouseOut = new UnityEvent(); + headerBtn.OnClick.AddListener( + (UnityAction)(() => + { + group.AllOptionsHidden = !group.AllOptionsHidden; + newText.text = group.AllOptionsHidden + ? "(Click to open)" + : "(Click to close)"; + })); + headerBtn.SetButtonEnableState(true); + } } diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 65dc7c6..4900309 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -27,7 +27,7 @@ public static class ModdedOptionsManager internal static readonly Dictionary CreatedPlayerOptions = []; internal static readonly Dictionary CreatedStringOptions = []; - internal static readonly Dictionary GameModeOptionGroups = []; + internal static readonly Dictionary> GameModeOptionGroups = []; internal static readonly Dictionary ModdedOptions = []; internal static readonly List Groups = []; @@ -74,7 +74,14 @@ public static class ModdedOptionsManager TypeToGroup.Add(type, group); if (group.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true) { - GameModeOptionGroups.Add(group.OptionableType, group); + if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList)) + { + oldList.Add(group); + } + else + { + GameModeOptionGroups.Add(group.OptionableType, new List() { group }); + } } pluginInfo.InternalOptionGroups.Add(group); From 8e5028c2367153a7efbae60b1037cabfa667482c Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 13 May 2026 12:07:15 -0500 Subject: [PATCH 042/102] Fix scroller issues --- MiraAPI/GameModes/CustomGameModeManager.cs | 2 +- MiraAPI/GameModes/GameModeOption.cs | 38 ++++++++++--------- MiraAPI/GameOptions/AbstractOptionGroup.cs | 2 +- MiraAPI/Keybinds/BaseKeybind.cs | 2 +- .../LocalSettings/OptionsMenuPatches.cs | 2 +- MiraAPI/Patches/Menu/NameplatesTabPatches.cs | 2 +- MiraAPI/Patches/Menu/SkinsTabPatches.cs | 2 +- MiraAPI/Patches/Menu/VisorsTabPatches.cs | 2 +- MiraAPI/PluginLoading/MiraPluginManager.cs | 4 +- MiraAPI/Roles/CustomRoleConfiguration.cs | 2 +- 10 files changed, 31 insertions(+), 27 deletions(-) diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 919f32b..bc41432 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -97,4 +97,4 @@ public static class CustomGameModeManager ActiveMode = IdToModeMap[0]; Logger.Warning($"Unable to find game mode of id {id}!"); } -} \ No newline at end of file +} diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index d999cdd..86de073 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -62,12 +62,12 @@ public static class GameModeOption Values.Add(opt, CustomStringName.CreateAndRegister(opt)); } - private static List VanillaCategories = new(); - private static List VanillaOptions = new(); - private static List BaseCategories = new(); - private static List BaseOptions = new(); - private static List ModeCategories = new(); - private static List ModeOptions = new(); + private static readonly List VanillaCategories = new(); + private static readonly List VanillaOptions = new(); + private static readonly List BaseCategories = new(); + private static readonly List BaseOptions = new(); + private static readonly List ModeCategories = new(); + private static readonly List ModeOptions = new(); [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) @@ -76,13 +76,15 @@ public static class GameModeOption { return; } + + var num = 0.713f; + ModeCategories.Clear(); ModeOptions.Clear(); VanillaCategories.Clear(); VanillaOptions.Clear(); BaseCategories.Clear(); BaseOptions.Clear(); - float num = 0.713f; foreach (var category in __instance.settingsContainer.GetComponentsInChildren()) { if (category) @@ -161,7 +163,7 @@ public static class GameModeOption } var showOpts = mode.ShowNormalGameSettings; - var num = 0.083f; + var num = 0.713f - (0.63f * BaseCategories.Count) - (0.45f * BaseOptions.Count); foreach (var category in VanillaCategories) { category.gameObject.SetActive(showOpts); @@ -181,7 +183,8 @@ public static class GameModeOption obj.gameObject.SetActive(true); instance.Children.Add(obj); } - instance.scrollBar.SetYBoundsMax(instance.scrollBar.GetYBounds().max + 1); + num -= 0.63f * VanillaCategories.Count; + num -= 0.45f * VanillaOptions.Count; } else { @@ -189,21 +192,22 @@ public static class GameModeOption { obj.gameObject.SetActive(false); } + } + + if (filteredGroups.Any()) + { + num += 0.225f; foreach (var group in filteredGroups) { GameOptionsMenuPatch.UpdateGroup(group, ref num); } - - instance.scrollBar.SetYBoundsMax(-num - 1.65f); } - foreach (var option in ModeOptions) + instance.ControllerSelectable.Clear(); + foreach (var obj in instance.scrollBar.GetComponentsInChildren()) { - option.gameObject.transform.localPosition += new Vector3(0, -0.67f, 0); - } - foreach (var header in ModeCategories) - { - header.gameObject.transform.localPosition += new Vector3(0, -0.67f, 0); + instance.ControllerSelectable.Add(obj); } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 51e18bd..dfd8f49 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -96,5 +96,5 @@ public enum MenuCategory Roles, Modifiers, CustomOne, - CustomTwo + CustomTwo, } diff --git a/MiraAPI/Keybinds/BaseKeybind.cs b/MiraAPI/Keybinds/BaseKeybind.cs index 525ffef..e9407ed 100644 --- a/MiraAPI/Keybinds/BaseKeybind.cs +++ b/MiraAPI/Keybinds/BaseKeybind.cs @@ -14,7 +14,7 @@ public class BaseKeybind public InputAction? RewiredInputAction { get; internal set; } /// - /// Gets the unique identifier for this keybind. Used in Rewired. + /// Gets or sets the unique identifier for this keybind. Used in Rewired. /// public string Id { get; protected set; } diff --git a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs index 7434aae..dbbe851 100644 --- a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -15,7 +15,7 @@ namespace MiraAPI.Patches.LocalSettings; public static class OptionsMenuPatches { internal static OptionsMenuBehaviour? Instance { get; private set; } - internal static BoxCollider2D MaskCollider; + internal static BoxCollider2D MaskCollider = null!; private static SpriteRenderer? background; private static int currentPage = 1; diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs index 1526cce..bc6f01f 100644 --- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs +++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs @@ -2,11 +2,11 @@ using HarmonyLib; using Innersloth.Assets; using MiraAPI.Utilities; -using Reactor.Utilities.Extensions; using System; using System.Collections.Generic; using System.Linq; using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using Object = UnityEngine.Object; diff --git a/MiraAPI/Patches/Menu/SkinsTabPatches.cs b/MiraAPI/Patches/Menu/SkinsTabPatches.cs index 8e99bf2..2001154 100644 --- a/MiraAPI/Patches/Menu/SkinsTabPatches.cs +++ b/MiraAPI/Patches/Menu/SkinsTabPatches.cs @@ -1,12 +1,12 @@ using AmongUs.Data; using HarmonyLib; using MiraAPI.Utilities; -using Reactor.Utilities.Extensions; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using Object = UnityEngine.Object; diff --git a/MiraAPI/Patches/Menu/VisorsTabPatches.cs b/MiraAPI/Patches/Menu/VisorsTabPatches.cs index c543a94..070c7ea 100644 --- a/MiraAPI/Patches/Menu/VisorsTabPatches.cs +++ b/MiraAPI/Patches/Menu/VisorsTabPatches.cs @@ -1,11 +1,11 @@ using AmongUs.Data; using HarmonyLib; using MiraAPI.Utilities; -using Reactor.Utilities.Extensions; using System; using System.Collections.Generic; using System.Linq; using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using Object = UnityEngine.Object; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 86247a1..5d16945 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -39,8 +39,8 @@ public sealed class MiraPluginManager internal Dictionary> QueuedRoleRegistrations { get; } = []; internal static MiraPluginManager Instance { get; private set; } = new(); - public static ConfigFile MiraApiConfig { get; private set; } - public static MiraPluginInfo MiraApiPluginInfo { get; private set; } + public static ConfigFile MiraApiConfig { get; private set; } = null!; + public static MiraPluginInfo MiraApiPluginInfo { get; private set; } = null!; internal void Initialize(BasePlugin miraApiPlugin, IMiraPlugin apiPlugin) { diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 2d88141..e6f79a6 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,7 +1,7 @@ using System; using AmongUs.GameOptions; -using MiraAPI.GameModes; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameModes; using MiraAPI.Patches.Freeplay; using MiraAPI.Utilities.Assets; using UnityEngine; From 35b444854a3ae7aad897ef61d6c652cb6627b5a6 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 13 May 2026 12:23:51 -0500 Subject: [PATCH 043/102] Fix showing/hiding groups --- MiraAPI/GameModes/GameModeOption.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 86de073..b7971ea 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -392,9 +392,8 @@ public static class GameModeOption (UnityAction)(() => { group.AllOptionsHidden = !group.AllOptionsHidden; - newText.text = group.AllOptionsHidden - ? "(Click to open)" - : "(Click to close)"; + newText.text = "(Click to open/close)"; + menu.RefreshOptions(CustomGameModeManager.ActiveMode!); })); headerBtn.SetButtonEnableState(true); } From 390e4c11575a694621f155d09fda0a2a33f157bd Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 13 May 2026 12:26:24 -0500 Subject: [PATCH 044/102] Fix toggle title --- MiraAPI/GameModes/GameModeOption.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index b7971ea..b78b5c7 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -246,7 +246,9 @@ public static class GameModeOption categoryHeaderMasked.transform.localPosition += Vector3.down; var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); - newText.text = "(Click to close)"; + newText.text = group.AllOptionsHidden + ? "(Click to open)" + : "(Click to close)"; newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); newText.gameObject.GetComponent().Destroy(); @@ -392,7 +394,9 @@ public static class GameModeOption (UnityAction)(() => { group.AllOptionsHidden = !group.AllOptionsHidden; - newText.text = "(Click to open/close)"; + newText.text = group.AllOptionsHidden + ? "(Click to open)" + : "(Click to close)"; menu.RefreshOptions(CustomGameModeManager.ActiveMode!); })); headerBtn.SetButtonEnableState(true); From 154b50fd76a6aeef8990b79131fa68848f13ac4a Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 31 May 2026 08:54:27 -0500 Subject: [PATCH 045/102] Mods can now choose to appear in the settings menu --- MiraAPI/MiraApiPlugin.cs | 3 +++ MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 12 ++++++------ MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 4 ++-- MiraAPI/PluginLoading/IMiraPlugin.cs | 5 +++++ MiraAPI/PluginLoading/MiraPluginManager.cs | 7 +++++++ 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index f6364d9..88af5e7 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -32,6 +32,9 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin /// public string OptionsTitleText => "MiraAPI"; + /// + public bool ShowInSettings => false; + /// /// Gets a value indicating whether the current device is running Starlight (on mobile). /// diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 89e6f57..c9a6379 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -199,7 +199,7 @@ internal static class GameSettingMenuPatches { SaveScrollPositions(__instance); SelectedModIdx += 1; - if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length) + if (SelectedModIdx > MiraPluginManager.Instance.ConfigurablePlugins.Length) { SelectedModIdx = 0; } @@ -220,7 +220,7 @@ internal static class GameSettingMenuPatches SelectedModIdx -= 1; if (SelectedModIdx < 0) { - SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins.Length; + SelectedModIdx = MiraPluginManager.Instance.ConfigurablePlugins.Length; } UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); @@ -352,7 +352,7 @@ internal static class GameSettingMenuPatches _modifierBtnOgPos = _modifiersButton.transform.localPosition; - foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins) + foreach (var plugin in MiraPluginManager.Instance.ConfigurablePlugins) { PresetManager.LoadPresets(plugin); } @@ -399,17 +399,17 @@ internal static class GameSettingMenuPatches { if (_text is not null && SelectedModIdx == 0) { - _text.text = $"(Page 0/{MiraPluginManager.Instance.RegisteredPlugins.Length})\nDefault"; + _text.text = $"(Page 0/{MiraPluginManager.Instance.ConfigurablePlugins.Length})\nDefault"; _text.fontSizeMax = 3.2f; SelectedMod = null; } else if (_text is not null) { _text.fontSizeMax = 2.3f; - SelectedMod = MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1]; + SelectedMod = MiraPluginManager.Instance.ConfigurablePlugins[SelectedModIdx - 1]; var name = SelectedMod.MiraPlugin.OptionsTitleText; - _text.text = $"(Page {SelectedModIdx}/{MiraPluginManager.Instance.RegisteredPlugins.Length})\n" + name[..Math.Min(name.Length, 25)]; + _text.text = $"(Page {SelectedModIdx}/{MiraPluginManager.Instance.ConfigurablePlugins.Length})\n" + name[..Math.Min(name.Length, 25)]; } bool replaceWithModifiers = true; diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 94f510a..65d34d7 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -26,7 +26,7 @@ public static class LobbyViewPanePatches private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0 ? null - : MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1]; + : MiraPluginManager.Instance.ConfigurablePlugins[SelectedModIdx - 1]; private static PassiveButton? ModifiersTabButton { get; set; } @@ -76,7 +76,7 @@ public static class LobbyViewPanePatches (UnityAction)(() => { SelectedModIdx += 1; - if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length) + if (SelectedModIdx > MiraPluginManager.Instance.ConfigurablePlugins.Length) { SelectedModIdx = 0; } diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs index e6b2dbb..e1a7ef3 100644 --- a/MiraAPI/PluginLoading/IMiraPlugin.cs +++ b/MiraAPI/PluginLoading/IMiraPlugin.cs @@ -12,6 +12,11 @@ public interface IMiraPlugin /// string OptionsTitleText { get; } + /// + /// Gets the value indicating if the mod appears in the settings menu. + /// + public virtual bool ShowInSettings => true; + /// /// Gets the name for the first custom category in the game options menu, if any. /// diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 5d16945..91a660c 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -34,8 +34,10 @@ namespace MiraAPI.PluginLoading; public sealed class MiraPluginManager { private readonly Dictionary _registeredPlugins = []; + private readonly Dictionary _configurablePlugins = []; internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!; + internal MiraPluginInfo[] ConfigurablePlugins { get; private set; } = null!; internal Dictionary> QueuedRoleRegistrations { get; } = []; internal static MiraPluginManager Instance { get; private set; } = new(); @@ -236,6 +238,10 @@ public sealed class MiraPluginManager QueuedRoleRegistrations.Add(info, roles); _registeredPlugins.Add(assembly, info); + if (miraPlugin.ShowInSettings) + { + _configurablePlugins.Add(assembly, info); + } info.SavePublicCollections(); PresetManager.CreateDefaultPreset(info); @@ -250,6 +256,7 @@ public sealed class MiraPluginManager // Cache all the registered plugins into an array for easy access RegisteredPlugins = [.. _registeredPlugins.Values]; + ConfigurablePlugins = [.. _configurablePlugins.Values]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); From 2eac3d04f12ef937f10ba0ae9b167968f4dd55eb Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 31 May 2026 09:16:48 -0500 Subject: [PATCH 046/102] Add more configuration for hns tasks --- MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs index 112f674..7be2864 100644 --- a/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs +++ b/MiraAPI/HnsReimplemented/Options/HnsTaskOptions.cs @@ -15,7 +15,7 @@ public class HnsTaskOptions : AbstractOptionGroup "# Common Tasks", 1, 0, - 2, + 4, 1, "#", "#", @@ -25,7 +25,7 @@ public class HnsTaskOptions : AbstractOptionGroup "# Long Tasks", 1, 0, - 3, + 4, 1, "#", "#", @@ -35,7 +35,7 @@ public class HnsTaskOptions : AbstractOptionGroup "# Short Tasks", 2, 0, - 5, + 8, 1, "#", "#", From e9730f018dad8c20070cd28b2a711f0c72b905a4 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 31 May 2026 10:08:41 -0500 Subject: [PATCH 047/102] Check valid gamemode roles to spawn --- MiraAPI/GameModes/HideAndSeekMode.cs | 197 +++++++++++++++++++++++++++ MiraAPI/Roles/CustomRoleUtils.cs | 8 +- 2 files changed, 202 insertions(+), 3 deletions(-) diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index ae0e184..7f4c0c6 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -2,9 +2,12 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using AmongUs.GameOptions; +using InnerNet; using MiraAPI.GameOptions; using MiraAPI.HnsReimplemented.Options; using MiraAPI.PluginLoading; +using MiraAPI.Roles; using MiraAPI.Utilities; using PowerTools; using UnityEngine; @@ -32,6 +35,200 @@ public class HideAndSeekMode : AbstractGameMode public override bool IsHideAndSeek => true; public override bool ShowNormalGameSettings => false; + /*public override void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) + { + runOriginal = false; + List list = new List(); + AmongUsClient.Instance.GetAllClients(list); + List list2 = (from c in list + where c.Character != null + where c.Character.Data != null + where !c.Character.Data.Disconnected && !c.Character.Data.IsDead + orderby c.Id + select c.Character.Data).ToList(); + foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) + { + if (networkedPlayerInfo.Object != null && networkedPlayerInfo.Object.isDummy) + { + list2.Add(networkedPlayerInfo); + } + } + IGameOptions currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; + int adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count); + this.DebugRoleAssignments(list2, ref adjustedNumImpostors); + GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, adjustedNumImpostors, new RoleTypes?(RoleTypes.Impostor)); + GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, new RoleTypes?(RoleTypes.Crewmate)); + } + 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))) + { + while (true) + { + 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) + { + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); + 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); + Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); + } + // 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++; + 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))) + { + while (true) + { + RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData; + int count = roleAssignmentData2.Count; + roleAssignmentData2.Count = count - 1; + if (count <= 0) + { + break; + } + + list.Add(roleAssignmentData.Role.Role); + } + } + AssignRolesFromList(newImpostors, 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(newImpostors, teamMax, list, ref num); + var basicRole = RoleTypes.Impostor; + while (list.Count < newImpostors.Count && list.Count + num < teamMax) + { + list.Add(basicRole); + } + + AssignRolesFromList(newImpostors, 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++; + } + }*/ + public override IEnumerator IntroCutscene(IntroCutscene __instance) { deadPlayerCount = 0; diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index fba91bf..5a049a5 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Linq; using System.Text; using AmongUs.GameOptions; +using MiraAPI.GameModes; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -23,14 +25,14 @@ public static class CustomRoleUtils { if (role is ICustomRole custom) { - return custom.CanSpawnOnCurrentMode(); + return custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode == CustomGameModeManager.ActiveMode?.GetType(); } - if (GameManager.Instance.IsHideAndSeek()) + if (CustomGameModeManager.ActiveMode is HideAndSeekMode) { return role.Role is RoleTypes.Engineer || role.Role is RoleTypes.Impostor; } - return true; + return !Helpers.IsRoleBlacklisted(role); } public static List<(ushort RoleType, int Chance)> GetPossibleRoles( From af2cc9a3c1296ca03b8d4da5ba1e3cfb59530ec7 Mon Sep 17 00:00:00 2001 From: amber Date: Sun, 31 May 2026 11:27:46 -0400 Subject: [PATCH 048/102] Renames DefaultMode to ClassicMode among other util changes. --- MiraAPI/GameModes/AbstractGameMode.cs | 11 ++++-- .../{DefaultMode.cs => ClassicMode.cs} | 4 +-- MiraAPI/GameModes/CustomGameModeManager.cs | 34 ++++++++++++++----- MiraAPI/GameModes/GameModeOption.cs | 14 ++++---- MiraAPI/Patches/GameModes/WinnersPatch.cs | 2 +- MiraAPI/PluginLoading/MiraPluginManager.cs | 2 +- MiraAPI/Roles/CustomRoleConfiguration.cs | 2 +- 7 files changed, 47 insertions(+), 22 deletions(-) rename MiraAPI/GameModes/{DefaultMode.cs => ClassicMode.cs} (79%) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 33d0af9..d716e78 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using MiraAPI.GameOptions; using MiraAPI.PluginLoading; +using Reactor.Utilities.Extensions; using UnityEngine; namespace MiraAPI.GameModes; @@ -28,9 +29,9 @@ public abstract class AbstractGameMode : IOptionable public virtual Color Color { get; } = Color.white; /// - /// Gets or sets the game mode id. + /// Gets the game mode id. /// - public uint ID { get; set; } + public uint ID { get; internal set; } /// /// Gets a value indicating whether a specific body type is used by the game mode. @@ -65,6 +66,12 @@ public abstract class AbstractGameMode : IOptionable return PlayerBodyTypes.Normal; } + /// + /// Used to construct the Gamemode option. + /// + /// The colored version of the Gamemode name using its color. + public string GetColoredName() => $"{Name}"; + /// /// Gets a value indicating whether a custom intro sequence is implemented by the game mode. /// diff --git a/MiraAPI/GameModes/DefaultMode.cs b/MiraAPI/GameModes/ClassicMode.cs similarity index 79% rename from MiraAPI/GameModes/DefaultMode.cs rename to MiraAPI/GameModes/ClassicMode.cs index d7541fd..952f918 100644 --- a/MiraAPI/GameModes/DefaultMode.cs +++ b/MiraAPI/GameModes/ClassicMode.cs @@ -3,10 +3,10 @@ namespace MiraAPI.GameModes; /// -/// The default game mode. +/// The classic game mode. /// [MiraIgnore] -public class DefaultMode : AbstractGameMode +public class ClassicMode : AbstractGameMode { /// public override string Name => "Classic"; diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index bc41432..7af2faf 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using MiraAPI.PluginLoading; using Reactor.Utilities; -using Reactor.Utilities.Extensions; namespace MiraAPI.GameModes; @@ -11,11 +10,11 @@ namespace MiraAPI.GameModes; /// public static class CustomGameModeManager { - public static readonly Dictionary IdToModeMap = []; + internal static readonly Dictionary IdToModeMap = []; private static uint GetNextId() => ++LastId; - internal static uint LastId { get; private set; } + private static uint LastId { get; set; } /// /// Register gamemode from type. @@ -45,20 +44,39 @@ public static class CustomGameModeManager } /// - /// Checks to see if the default game mode is on. + /// Checks to see if the classic game mode is on. /// - /// True if the default mode is the current one. - public static bool IsDefault() => ActiveMode is DefaultMode; - public static bool IsHideNSeek() => ActiveMode is HideAndSeekMode; + /// True if the classic mode is the current one. + public static bool IsClassic() => IsActiveGameMode(); + + /// + /// Checks to see if the HNS game mode is on. + /// + /// True if the Hide & Seek mode is the current one. + public static bool IsHideNSeek() => IsActiveGameMode(); + + /// + /// Checks if a provided GameMode is the current active one. + /// + /// The AbstractGameMode subclass being checked. + /// Whether the provided mode is the current active one. + public static bool IsActiveGameMode() where T : AbstractGameMode => ActiveMode is T; /// /// Gets the current gamemode. /// public static AbstractGameMode? ActiveMode { get; internal set; } + /// + /// Gets a gamemode from an ID. + /// + /// The ID of the gamemode to fetch. + /// The gamemode matching that ID. + public static AbstractGameMode GetMode(uint id) => IdToModeMap[id]; + internal static void RegisterDefaultMode() { - var defaultMode = new DefaultMode(); + var defaultMode = new ClassicMode(); IdToModeMap.Add(0, defaultMode); // no need to add to game mode option as it already contains it // because we cannot have the option be created with no values diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index b78b5c7..70592ea 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -50,16 +50,15 @@ public static class GameModeOption private static int _lastValue; private static readonly StringNames GamemodeName = CustomStringName.CreateAndRegister("Gamemode"); private static readonly StringNames CustomName = CustomStringName.CreateAndRegister("Custom"); - private static readonly Dictionary Values = new() + private static readonly Dictionary Values = new() { - ["Classic"] = CustomStringName.CreateAndRegister("Classic"), + [0] = CustomStringName.CreateAndRegister("Classic"), }; internal static void AddOption(AbstractGameMode mode) { - var opt = $"{mode.Name}"; - if (!Values.ContainsKey(opt)) - Values.Add(opt, CustomStringName.CreateAndRegister(opt)); + if (!Values.ContainsKey(mode.ID)) + Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.GetColoredName())); } private static readonly List VanillaCategories = new(); @@ -122,14 +121,15 @@ public static class GameModeOption setting.Type = OptionTypes.MultipleChoice; setting.Title = GamemodeName; setting.Index = _lastValue; - setting.Values = new Il2CppStructArray([Values["Classic"]]); + setting.Values = new Il2CppStructArray([Values[0]]); OptionBehaviour.SetUpFromData(setting, 20); OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => { _lastValue = opt.GetInt(); CustomGameModeManager.SetGameMode((uint)_lastValue); - HudPatches.SetGameModeText(Values.ElementAt(_lastValue).Key); + HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).Name); + // could make Values a dict of AbstractGameMode to __instance.RefreshOptions(CustomGameModeManager.ActiveMode); }); foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) diff --git a/MiraAPI/Patches/GameModes/WinnersPatch.cs b/MiraAPI/Patches/GameModes/WinnersPatch.cs index 01f9859..c7c9385 100644 --- a/MiraAPI/Patches/GameModes/WinnersPatch.cs +++ b/MiraAPI/Patches/GameModes/WinnersPatch.cs @@ -9,7 +9,7 @@ internal static class WinnersPatch [HarmonyPostfix] public static void ShowWinningScreen() { - if (CustomGameModeManager.ActiveMode is null or DefaultMode) + if (CustomGameModeManager.ActiveMode is null or ClassicMode) return; var winners = CustomGameModeManager.ActiveMode.CalculateWinners(); diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 91a660c..c61929f 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -263,7 +263,7 @@ public sealed class MiraPluginManager var dict = new Dictionary(); foreach (var (key, value) in CustomGameModeManager.IdToModeMap) { - if (value is DefaultMode) + if (value is ClassicMode) { continue; } diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index e6f79a6..6253430 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -97,7 +97,7 @@ public record struct CustomRoleConfiguration /// /// Gets or sets the associated game mode for this role. This is used to determine if the role should be available in a specific game mode. /// - public Type AssociatedGameMode { get; set; } = typeof(DefaultMode); + public Type AssociatedGameMode { get; set; } = typeof(ClassicMode); /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. From 25a55b1b60011e523a895425f85141270caf00ed Mon Sep 17 00:00:00 2001 From: amber Date: Sun, 31 May 2026 11:33:14 -0400 Subject: [PATCH 049/102] ColoredName property rather than a method --- MiraAPI/GameModes/AbstractGameMode.cs | 11 +++++------ MiraAPI/GameModes/GameModeOption.cs | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index d716e78..8fd612a 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -28,6 +28,11 @@ public abstract class AbstractGameMode : IOptionable /// public virtual Color Color { get; } = Color.white; + /// + /// Gets the colored game mode name, using the color and name properties. + /// + public string ColoredName => $"{Name}"; + /// /// Gets the game mode id. /// @@ -66,12 +71,6 @@ public abstract class AbstractGameMode : IOptionable return PlayerBodyTypes.Normal; } - /// - /// Used to construct the Gamemode option. - /// - /// The colored version of the Gamemode name using its color. - public string GetColoredName() => $"{Name}"; - /// /// Gets a value indicating whether a custom intro sequence is implemented by the game mode. /// diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 70592ea..5f59d76 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -58,7 +58,7 @@ public static class GameModeOption internal static void AddOption(AbstractGameMode mode) { if (!Values.ContainsKey(mode.ID)) - Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.GetColoredName())); + Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.ColoredName)); } private static readonly List VanillaCategories = new(); From 9148a1e8da0707fdaa5c1c2a711535af9661c16f Mon Sep 17 00:00:00 2001 From: Nix-main Date: Wed, 29 Jul 2026 18:13:14 -0400 Subject: [PATCH 050/102] Sync gamemode option surely --- MiraAPI/GameModes/GameModeOption.cs | 58 ++++++++++++++++++++++++----- MiraAPI/Networking/MiraRpc.cs | 5 +++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 5f59d76..9c671c2 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -1,21 +1,23 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; +using Hazel; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem; using MiraAPI.GameOptions; +using MiraAPI.Networking; using MiraAPI.Patches.GameModes; using MiraAPI.Patches.Options; -using MiraAPI.Presets; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.ProBuilder; -using UnityEngine.UI; using Object = UnityEngine.Object; namespace MiraAPI.GameModes; @@ -29,15 +31,17 @@ public static class GameModeOption /// /// Gets or Sets the current index of the Game Mode Option /// For the value as an AbstractGameMode, see CustomGameModeManager.ActiveMode + /// For the value as an AbstractGameMode, see CustomGameModeManager.ActiveMode. /// public static int Value { get => OptionBehaviour != null ? OptionBehaviour.GetInt() - : 0; + : _lastValue; set { + _lastValue = value; if (OptionBehaviour == null) return; OptionBehaviour.Value = value; @@ -121,16 +125,22 @@ public static class GameModeOption setting.Type = OptionTypes.MultipleChoice; setting.Title = GamemodeName; setting.Index = _lastValue; + Set(_lastValue); + __instance.RefreshOptions(CustomGameModeManager.ActiveMode); setting.Values = new Il2CppStructArray([Values[0]]); OptionBehaviour.SetUpFromData(setting, 20); OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => { _lastValue = opt.GetInt(); - CustomGameModeManager.SetGameMode((uint)_lastValue); - HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).Name); - // could make Values a dict of AbstractGameMode to + Set(opt.GetInt()); __instance.RefreshOptions(CustomGameModeManager.ActiveMode); + foreach (var player in PlayerControl.AllPlayerControls) + { + if (player == PlayerControl.LocalPlayer) + continue; + Rpc.Instance.SendTo(player.OwnerId, _lastValue); + } }); foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) { @@ -144,6 +154,14 @@ public static class GameModeOption __instance.RefreshOptions(CustomGameModeManager.ActiveMode); } + private static void Set(int val) + { + _lastValue = val; + CustomGameModeManager.SetGameMode((uint)_lastValue); + HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).Name); + // could make Values a dict of AbstractGameMode too + } + private static void RefreshOptions(this GameOptionsMenu instance, AbstractGameMode mode) { foreach (var option in ModeOptions) @@ -172,7 +190,9 @@ public static class GameModeOption ModdedOptionsManager.GameModeOptionGroups .Where(x => x.Key == mode.GetType()).Select(y => y.Value).SelectMany(y => y) ?? []; - foreach (var group in filteredGroups) + var optionGroups = filteredGroups.ToList(); + var abstractOptionGroups = filteredGroups as AbstractOptionGroup[] ?? optionGroups.ToArray(); + foreach (var group in abstractOptionGroups) { CreateGroup(instance, group); } @@ -194,10 +214,10 @@ public static class GameModeOption } } - if (filteredGroups.Any()) + if (optionGroups.Count != 0) { num += 0.225f; - foreach (var group in filteredGroups) + foreach (var group in optionGroups) { GameOptionsMenuPatch.UpdateGroup(group, ref num); } @@ -401,4 +421,24 @@ public static class GameModeOption })); headerBtn.SetButtonEnableState(true); } + + [RegisterCustomRpc((uint)MiraRpc.SyncGamemodeOption)] + private sealed class GameModeOptionUpdateRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) + { + public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before; + public override void Write(MessageWriter writer, int data) + { + writer.Write(data); + } + + public override int Read(MessageReader reader) + { + return reader.ReadInt32(); + } + + public override void Handle(PlayerControl innerNetObject, int data) + { + Value = data; + } + } } diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index edc3a94..8f0d7ce 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -54,4 +54,9 @@ public enum MiraRpc : uint /// Custom RPC to replace Among Us CastVote. /// CastVote, + + /// + /// Custom RPC to sync the Gamemode option. + /// + SyncGamemodeOption, } From 0012fe8cd6db6761f224e1795c121bce886adb31 Mon Sep 17 00:00:00 2001 From: Nix-main Date: Wed, 29 Jul 2026 18:14:27 -0400 Subject: [PATCH 051/102] Make ActiveMode setter private --- MiraAPI/GameModes/CustomGameModeManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 7af2faf..20be16b 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -65,7 +65,7 @@ public static class CustomGameModeManager /// /// Gets the current gamemode. /// - public static AbstractGameMode? ActiveMode { get; internal set; } + public static AbstractGameMode? ActiveMode { get; private set; } /// /// Gets a gamemode from an ID. From c99f468abf45b3b85413e054591ed871a3f0940b Mon Sep 17 00:00:00 2001 From: Nix-main Date: Thu, 30 Jul 2026 00:39:02 -0400 Subject: [PATCH 052/102] sync gamemode option --- MiraAPI/GameModes/GameModeOption.cs | 20 +++++++----------- MiraAPI/Patches/GameModes/HudPatches.cs | 21 +++++++++++++------ .../Patches/Options/GameSettingMenuPatches.cs | 2 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 9c671c2..f77f55f 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -30,8 +30,7 @@ public static class GameModeOption { /// /// Gets or Sets the current index of the Game Mode Option - /// For the value as an AbstractGameMode, see CustomGameModeManager.ActiveMode - /// For the value as an AbstractGameMode, see CustomGameModeManager.ActiveMode. + /// For the value as an , see . /// public static int Value { @@ -71,6 +70,7 @@ public static class GameModeOption private static readonly List BaseOptions = new(); private static readonly List ModeCategories = new(); private static readonly List ModeOptions = new(); + [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) @@ -126,20 +126,17 @@ public static class GameModeOption setting.Title = GamemodeName; setting.Index = _lastValue; Set(_lastValue); - __instance.RefreshOptions(CustomGameModeManager.ActiveMode); setting.Values = new Il2CppStructArray([Values[0]]); OptionBehaviour.SetUpFromData(setting, 20); OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => { - _lastValue = opt.GetInt(); Set(opt.GetInt()); - __instance.RefreshOptions(CustomGameModeManager.ActiveMode); foreach (var player in PlayerControl.AllPlayerControls) { - if (player == PlayerControl.LocalPlayer) + if (player.AmOwner) continue; - Rpc.Instance.SendTo(player.OwnerId, _lastValue); + Rpc.Instance.SendTo(PlayerControl.LocalPlayer, player.OwnerId, _lastValue); } }); foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) @@ -151,14 +148,14 @@ public static class GameModeOption BaseOptions.Add(OptionBehaviour); for (var i = 1; i < Values.Count; i++) OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); - __instance.RefreshOptions(CustomGameModeManager.ActiveMode); + __instance.RefreshOptions(CustomGameModeManager.ActiveMode!); } private static void Set(int val) { _lastValue = val; CustomGameModeManager.SetGameMode((uint)_lastValue); - HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).Name); + HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); // could make Values a dict of AbstractGameMode too } @@ -291,7 +288,6 @@ public static class GameModeOption foreach (var newOpt in options) { newOpt.SetClickMask(menu.ButtonClickMask); - SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); foreach (var renderer in componentsInChildren) { @@ -425,7 +421,7 @@ public static class GameModeOption [RegisterCustomRpc((uint)MiraRpc.SyncGamemodeOption)] private sealed class GameModeOptionUpdateRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) { - public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before; + public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; public override void Write(MessageWriter writer, int data) { writer.Write(data); @@ -438,7 +434,7 @@ public static class GameModeOption public override void Handle(PlayerControl innerNetObject, int data) { - Value = data; + Set(data); } } } diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index 5ffa4fb..e30fc79 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -1,27 +1,35 @@ +using System.Collections; using HarmonyLib; +using Il2CppSystem; using MiraAPI.GameModes; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; +using Object = UnityEngine.Object; namespace MiraAPI.Patches.GameModes; [HarmonyPatch(typeof(HudManager))] internal static class HudPatches { - [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Start))] - public static void HudStartPatch(HudManager __instance) => CustomGameModeManager.ActiveMode?.HudStart(__instance); - [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] - public static void HudUpdatePatch(HudManager __instance) => CustomGameModeManager.ActiveMode?.HudUpdate(__instance); + public static void HudUpdatePatch(HudManager __instance) => CustomGameModeManager.ActiveMode.HudUpdate(__instance); + [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] [HarmonyPostfix] public static void PostHudStart(HudManager __instance) { + Coroutines.Start(CoPostHudStart(__instance)); + } + + private static IEnumerator CoPostHudStart(HudManager instance) + { + yield return new WaitUntil((Func)(() => GameManager.Instance != null)); if (GameManager.Instance.IsHideAndSeek()) - return; - var infoPane = __instance.gameObject.transform.FindChild("LobbyInfoPane"); + yield return null; + var infoPane = instance.gameObject.transform.FindChild("LobbyInfoPane"); var aspect = infoPane.gameObject.transform.Find("AspectSize"); var modeLabel = aspect!.Find("ModeLabel"); var modeValue = aspect.Find("ModeValue"); @@ -35,6 +43,7 @@ internal static class HudPatches modelText.SetActive(false); _text = gmTextClone.GetComponent(); _text.text = CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic"; + CustomGameModeManager.ActiveMode?.HudStart(instance); } private static TextMeshPro? _text; diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 23eb4b3..a49de87 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -352,7 +352,7 @@ internal static class GameSettingMenuPatches _modifierBtnOgPos = _modifiersButton.transform.localPosition; - foreach (var plugin in MiraPluginManager.Instance.ConfigurablePlugins) + foreach (var plugin in MiraPluginManager.Instance.RegisteredOptionablePlugins) { PresetManager.LoadPresets(plugin); } From 119d4c51525644bda1808893708fdc10b41613ac Mon Sep 17 00:00:00 2001 From: amber Date: Thu, 30 Jul 2026 01:58:04 -0400 Subject: [PATCH 053/102] use methodrpc --- MiraAPI/GameModes/CustomGameModeManager.cs | 3 +- MiraAPI/GameModes/GameModeOption.cs | 36 +++++----------------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 20be16b..8076e17 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -96,8 +96,7 @@ public static class CustomGameModeManager } else if (id != 0) { - ActiveMode = IdToModeMap[0]; - GameModeOption.Value = 0; + GameModeOption.Set(0); Logger.Warning($"Unable to find game mode of id {id}!"); } } diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index f77f55f..0bc596a 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -29,7 +29,7 @@ namespace MiraAPI.GameModes; public static class GameModeOption { /// - /// Gets or Sets the current index of the Game Mode Option + /// Gets the current index of the Game Mode Option /// For the value as an , see . /// public static int Value @@ -38,7 +38,7 @@ public static class GameModeOption OptionBehaviour != null ? OptionBehaviour.GetInt() : _lastValue; - set + private set { _lastValue = value; if (OptionBehaviour == null) @@ -131,13 +131,7 @@ public static class GameModeOption OptionBehaviour.TitleText.fontSize = 3; OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => { - Set(opt.GetInt()); - foreach (var player in PlayerControl.AllPlayerControls) - { - if (player.AmOwner) - continue; - Rpc.Instance.SendTo(PlayerControl.LocalPlayer, player.OwnerId, _lastValue); - } + RpcSyncGamemode(PlayerControl.LocalPlayer, opt.GetInt()); }); foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) { @@ -151,9 +145,9 @@ public static class GameModeOption __instance.RefreshOptions(CustomGameModeManager.ActiveMode!); } - private static void Set(int val) + public static void Set(int val) { - _lastValue = val; + Value = val; CustomGameModeManager.SetGameMode((uint)_lastValue); HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); // could make Values a dict of AbstractGameMode too @@ -418,23 +412,9 @@ public static class GameModeOption headerBtn.SetButtonEnableState(true); } - [RegisterCustomRpc((uint)MiraRpc.SyncGamemodeOption)] - private sealed class GameModeOptionUpdateRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) + [MethodRpc((uint) MiraRpc.SyncGamemodeOption)] + private static void RpcSyncGamemode(PlayerControl host, int data) { - public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; - public override void Write(MessageWriter writer, int data) - { - writer.Write(data); - } - - public override int Read(MessageReader reader) - { - return reader.ReadInt32(); - } - - public override void Handle(PlayerControl innerNetObject, int data) - { - Set(data); - } + Set(data); } } From 83751a25a972f74a489eec57d4d9a41cf26192ca Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:58:25 -0500 Subject: [PATCH 054/102] add back some fixes --- MiraAPI/GameModes/GameModeOption.cs | 2 +- MiraAPI/MiraApiPlugin.cs | 2 +- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 0bc596a..a181e5b 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -75,7 +75,7 @@ public static class GameModeOption [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) { - if (GameSettingMenuPatches.SelectedModIdx != 0 || GameManager.Instance.IsHideAndSeek() || CustomGameModeManager.ActiveMode == null) + if (MenuState.Instance.CurrentModIdx != 0 || GameManager.Instance.IsHideAndSeek() || CustomGameModeManager.ActiveMode == null) { return; } diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 88af5e7..661a30a 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -33,7 +33,7 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin public string OptionsTitleText => "MiraAPI"; /// - public bool ShowInSettings => false; + public bool DisplayOnOptionsMenu => false; /// /// Gets a value indicating whether the current device is running Starlight (on mobile). diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 0dc8721..5e00ddc 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -5,6 +5,7 @@ using System.Linq; using AmongUs.GameOptions; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; @@ -175,8 +176,7 @@ public static class RoleSettingMenuPatches foreach (var grouping in sortedRoleGroups) { - if (!grouping.Any() || - grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings())) + if (!grouping.Any() || grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings() || x.Configuration.AssociatedGameMode != CustomGameModeManager.ActiveMode?.GetType())) { continue; } From 49d16b707c4278b94bc9cc3811137134fed0cf29 Mon Sep 17 00:00:00 2001 From: Nix-main Date: Thu, 6 Aug 2026 00:31:04 -0400 Subject: [PATCH 055/102] use ValueChanged patch for logic --- MiraAPI/GameModes/GameModeOption.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index a181e5b..d37d51c 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -1,9 +1,7 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; -using Hazel; using Il2CppInterop.Runtime.InteropTypes.Arrays; -using Il2CppSystem; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Patches.GameModes; @@ -12,7 +10,6 @@ using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; using Reactor.Networking.Attributes; -using Reactor.Networking.Rpc; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -129,10 +126,6 @@ public static class GameModeOption setting.Values = new Il2CppStructArray([Values[0]]); OptionBehaviour.SetUpFromData(setting, 20); OptionBehaviour.TitleText.fontSize = 3; - OptionBehaviour.OnValueChanged = (Action) ((OptionBehaviour opt) => - { - RpcSyncGamemode(PlayerControl.LocalPlayer, opt.GetInt()); - }); foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) { optionBehaviour.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); @@ -225,7 +218,13 @@ public static class GameModeOption [HarmonyPrefix] private static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option) { - return !OptionBehaviour.Equals(option); + if (OptionBehaviour.Equals(option)) + { + Info($"Game mode changed to {option.GetInt()}"); + RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt()); + return false; + } + return true; } private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group) From d5c39056844409227ce7f351e60271459c5e72bb Mon Sep 17 00:00:00 2001 From: Nix-main Date: Thu, 6 Aug 2026 00:34:30 -0400 Subject: [PATCH 056/102] fix reloading not occuring until next opening of the menu --- MiraAPI/GameModes/GameModeOption.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index d37d51c..d1c39dc 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -68,10 +68,13 @@ public static class GameModeOption private static readonly List ModeCategories = new(); private static readonly List ModeOptions = new(); + private static GameOptionsMenu? _instance; + [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) { + _instance = __instance; if (MenuState.Instance.CurrentModIdx != 0 || GameManager.Instance.IsHideAndSeek() || CustomGameModeManager.ActiveMode == null) { return; @@ -135,7 +138,7 @@ public static class GameModeOption BaseOptions.Add(OptionBehaviour); for (var i = 1; i < Values.Count; i++) OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); - __instance.RefreshOptions(CustomGameModeManager.ActiveMode!); + __instance.RefreshOptions(CustomGameModeManager.ActiveMode); } public static void Set(int val) @@ -144,6 +147,8 @@ public static class GameModeOption CustomGameModeManager.SetGameMode((uint)_lastValue); HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); // could make Values a dict of AbstractGameMode too + if (_instance != null) + RefreshOptions(_instance, CustomGameModeManager.ActiveMode!); } private static void RefreshOptions(this GameOptionsMenu instance, AbstractGameMode mode) From cbe44a40d07717b42b11172d3d9482ebd2a308cb Mon Sep 17 00:00:00 2001 From: Nix-main Date: Thu, 6 Aug 2026 01:10:21 -0400 Subject: [PATCH 057/102] Fix button issue and emove options reload logic for now --- MiraAPI/GameModes/CustomGameModeManager.cs | 13 --- MiraAPI/GameModes/GameModeOption.cs | 98 +------------------ MiraAPI/Patches/GameModes/HudPatches.cs | 6 +- .../GameModes/NormalGameManagerPatches.cs | 5 +- 4 files changed, 9 insertions(+), 113 deletions(-) diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 8076e17..d6c6a1c 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -88,19 +88,6 @@ public static class CustomGameModeManager LastId++; } - internal static void SetGameMode(uint id) - { - if (IdToModeMap.TryGetValue(id, out var mode)) - { - ActiveMode = mode; - } - else if (id != 0) - { - GameModeOption.Set(0); - Logger.Warning($"Unable to find game mode of id {id}!"); - } - } - internal static void GetAndSetGameMode() { var id = (uint)GameModeOption.Value; diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index d1c39dc..f851ad4 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -61,12 +61,6 @@ public static class GameModeOption Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.ColoredName)); } - private static readonly List VanillaCategories = new(); - private static readonly List VanillaOptions = new(); - private static readonly List BaseCategories = new(); - private static readonly List BaseOptions = new(); - private static readonly List ModeCategories = new(); - private static readonly List ModeOptions = new(); private static GameOptionsMenu? _instance; @@ -82,12 +76,6 @@ public static class GameModeOption var num = 0.713f; - ModeCategories.Clear(); - ModeOptions.Clear(); - VanillaCategories.Clear(); - VanillaOptions.Clear(); - BaseCategories.Clear(); - BaseOptions.Clear(); foreach (var category in __instance.settingsContainer.GetComponentsInChildren()) { if (category) @@ -96,19 +84,6 @@ public static class GameModeOption var showOpts = CustomGameModeManager.ActiveMode.ShowNormalGameSettings; - var newList = __instance.Children.ToArray(); - BaseOptions.Add(newList[0]); - foreach (var obj in newList.Skip(1)) - { - VanillaOptions.Add(obj); - obj.gameObject.SetActive(showOpts); - } - foreach (var category in __instance.settingsContainer.GetComponentsInChildren()) - { - VanillaCategories.Add(category); - category.gameObject.SetActive(showOpts); - } - CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); categoryHeaderMasked.SetHeader(CustomName, 20); categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; @@ -125,17 +100,15 @@ public static class GameModeOption setting.Type = OptionTypes.MultipleChoice; setting.Title = GamemodeName; setting.Index = _lastValue; - Set(_lastValue); setting.Values = new Il2CppStructArray([Values[0]]); OptionBehaviour.SetUpFromData(setting, 20); + Set(_lastValue); OptionBehaviour.TitleText.fontSize = 3; foreach (var optionBehaviour in __instance.Children.ToArray().Skip(1)) { optionBehaviour.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); } __instance.Children.Add(OptionBehaviour); - BaseCategories.Add(categoryHeaderMasked); - BaseOptions.Add(OptionBehaviour); for (var i = 1; i < Values.Count; i++) OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); __instance.RefreshOptions(CustomGameModeManager.ActiveMode); @@ -144,7 +117,7 @@ public static class GameModeOption public static void Set(int val) { Value = val; - CustomGameModeManager.SetGameMode((uint)_lastValue); + CustomGameModeManager.GetAndSetGameMode(); HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); // could make Values a dict of AbstractGameMode too if (_instance != null) @@ -153,70 +126,7 @@ public static class GameModeOption private static void RefreshOptions(this GameOptionsMenu instance, AbstractGameMode mode) { - foreach (var option in ModeOptions) - { - option.gameObject.Destroy(); - } - foreach (var header in ModeCategories) - { - header.gameObject.Destroy(); - } - ModeOptions.Clear(); - ModeCategories.Clear(); - instance.Children.Clear(); - foreach (var group in BaseOptions) - { - instance.Children.Add(group); - } - - var showOpts = mode.ShowNormalGameSettings; - var num = 0.713f - (0.63f * BaseCategories.Count) - (0.45f * BaseOptions.Count); - foreach (var category in VanillaCategories) - { - category.gameObject.SetActive(showOpts); - } - var filteredGroups = - ModdedOptionsManager.GameModeOptionGroups - .Where(x => x.Key == mode.GetType()).Select(y => y.Value).SelectMany(y => y) ?? []; - - var optionGroups = filteredGroups.ToList(); - var abstractOptionGroups = filteredGroups as AbstractOptionGroup[] ?? optionGroups.ToArray(); - foreach (var group in abstractOptionGroups) - { - CreateGroup(instance, group); - } - if (showOpts) - { - foreach (var obj in VanillaOptions) - { - obj.gameObject.SetActive(true); - instance.Children.Add(obj); - } - num -= 0.63f * VanillaCategories.Count; - num -= 0.45f * VanillaOptions.Count; - } - else - { - foreach (var obj in VanillaOptions) - { - obj.gameObject.SetActive(false); - } - } - - if (optionGroups.Count != 0) - { - num += 0.225f; - foreach (var group in optionGroups) - { - GameOptionsMenuPatch.UpdateGroup(group, ref num); - } - } - instance.ControllerSelectable.Clear(); - foreach (var obj in instance.scrollBar.GetComponentsInChildren()) - { - instance.ControllerSelectable.Add(obj); - } - instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] @@ -273,7 +183,6 @@ public static class GameModeOption menu.stringOptionOrigin, menu.playerOptionOrigin, menu.settingsContainer)); - ModeCategories.Add(categoryHeaderMasked); /*OptionPreset? defaultPreset = null; if (GameSettingMenuPatches.SelectedMod != null && PresetManager.DefaultPresets.TryGetValue( @@ -393,7 +302,6 @@ public static class GameModeOption newOpt.Initialize(); newOpt.gameObject.SetActive(false); - ModeOptions.Add(newOpt); } var boxCol = categoryHeaderMasked.gameObject.AddComponent(); diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index e30fc79..77c0f48 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -14,7 +14,11 @@ namespace MiraAPI.Patches.GameModes; internal static class HudPatches { [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] - public static void HudUpdatePatch(HudManager __instance) => CustomGameModeManager.ActiveMode.HudUpdate(__instance); + public static void HudUpdatePatch(HudManager __instance) + { + if (GameManager.Instance != null && GameManager.Instance.GameHasStarted) + CustomGameModeManager.ActiveMode?.HudUpdate(__instance); + } [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] diff --git a/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs index bed43b9..f4395d4 100644 --- a/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs +++ b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs @@ -7,14 +7,11 @@ namespace MiraAPI.Patches.GameModes; internal static class NormalGameManagerPatches { [HarmonyPrefix, HarmonyPatch(nameof(NormalGameManager.IsNormal))] - public static bool IsNormal(ref bool __result) + public static void IsNormal(ref bool __result) { if (CustomGameModeManager.ActiveMode != null) { __result = !CustomGameModeManager.ActiveMode.IsHideAndSeek; - return false; } - - return true; } } From 317e0b6ea99734873ddcfdf76cb5983bc84aeac4 Mon Sep 17 00:00:00 2001 From: Nix-main Date: Wed, 5 Aug 2026 21:26:09 -0400 Subject: [PATCH 058/102] Fix gamemodes not being properly added to the option --- MiraAPI/GameModes/CustomGameModeManager.cs | 10 ++++++---- MiraAPI/PluginLoading/MiraPluginManager.cs | 9 ++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index d6c6a1c..092911d 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -21,12 +21,13 @@ public static class CustomGameModeManager /// /// Type of gamemode class, should inherit from . /// The custom plugin info of the mod. - internal static void RegisterGameMode(Type gameModeType, MiraPluginInfo pluginInfo) + /// Whether the gamemode was successfully registered. + internal static bool RegisterGameMode(Type gameModeType, MiraPluginInfo pluginInfo) { if (!typeof(AbstractGameMode).IsAssignableFrom(gameModeType)) { Warning($"{gameModeType.Name} does not inherit CustomGameMode!"); - return; + return false; } var instance = Activator.CreateInstance(gameModeType); @@ -34,13 +35,14 @@ public static class CustomGameModeManager if (instance is not AbstractGameMode mode) { Error($"Failed to create instance of {gameModeType.Name}"); - return; + return false; } IdToModeMap.Add(GetNextId(), mode); pluginInfo.GameModes.Add(LastId, mode); - GameModeOption.AddOption(mode); mode.ID = LastId; + GameModeOption.AddOption(mode); + return true; } /// diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index ae1d93e..af78e8b 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -449,18 +449,13 @@ public sealed class MiraPluginManager { try { - if (type.IsAssignableTo(typeof(AbstractGameMode))) - { - // TODO: bool - CustomGameModeManager.RegisterGameMode(type, pluginInfo); - return true; - } + if (typeof(AbstractGameMode).IsAssignableFrom(type)) + return CustomGameModeManager.RegisterGameMode(type, pluginInfo); } catch (Exception e) { Error($"Failed to register gamemode {type.Name}: {e}"); } - return false; } From 04b7a9905d7308f4f53d00aad0d5955efe64c0ff Mon Sep 17 00:00:00 2001 From: Nix-main Date: Wed, 5 Aug 2026 22:00:09 -0400 Subject: [PATCH 059/102] make gamemode use assignable from / instance --- MiraAPI/GameModes/GameModeOption.cs | 3 --- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index f851ad4..94f24c4 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -81,9 +81,6 @@ public static class GameModeOption if (category) category.gameObject.transform.localPosition -= new Vector3(0, 1.3f, 0); } - - var showOpts = CustomGameModeManager.ActiveMode.ShowNormalGameSettings; - CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); categoryHeaderMasked.SetHeader(CustomName, 20); categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 871fe50..9de9229 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -177,7 +177,7 @@ public static class RoleSettingMenuPatches foreach (var grouping in sortedRoleGroups) { if (!grouping.Any() || - grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings() || x.Configuration.AssociatedGameMode != CustomGameModeManager.ActiveMode?.GetType())) + grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings() || !x.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode))) { continue; } From 0230d986424098aff4fb9bc39e0e26e5e51bea4b Mon Sep 17 00:00:00 2001 From: Nix-main Date: Wed, 5 Aug 2026 22:53:17 -0400 Subject: [PATCH 060/102] make IsClassic and IsHideNSeek use assignable --- MiraAPI/GameModes/CustomGameModeManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 092911d..90a96a1 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -46,16 +46,16 @@ public static class CustomGameModeManager } /// - /// Checks to see if the classic game mode is on. + /// Checks to see if the current game mode is an instance of the classic mode. /// /// True if the classic mode is the current one. - public static bool IsClassic() => IsActiveGameMode(); + public static bool IsClassic() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(ClassicMode)); /// - /// Checks to see if the HNS game mode is on. + /// Checks to see if the current game mode is an instance of the HNS mode. /// /// True if the Hide & Seek mode is the current one. - public static bool IsHideNSeek() => IsActiveGameMode(); + public static bool IsHideNSeek() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(HideAndSeekMode)); /// /// Checks if a provided GameMode is the current active one. From 1ab0c3f4a7a5d25aeea749063b5a81b9fc312736 Mon Sep 17 00:00:00 2001 From: Nix-main Date: Wed, 5 Aug 2026 22:55:01 -0400 Subject: [PATCH 061/102] Fix IsInstance for associated mode --- MiraAPI/Roles/CustomRoleUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index aa420e3..659076a 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -25,7 +25,7 @@ public static class CustomRoleUtils { if (role is ICustomRole custom) { - return custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode == CustomGameModeManager.ActiveMode?.GetType(); + return custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode); } if (CustomGameModeManager.ActiveMode is HideAndSeekMode) From 2f5c382eced3202c4cefa36f9c1bb66034f8a30b Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:00:20 -0500 Subject: [PATCH 062/102] Remove hns IsNormal patch --- MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs index f4395d4..e7c4f8b 100644 --- a/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs +++ b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +/* +using HarmonyLib; using MiraAPI.GameModes; namespace MiraAPI.Patches.GameModes; @@ -15,3 +16,4 @@ internal static class NormalGameManagerPatches } } } +*/ From 8f403f5408bec0b548393d46b955aeeb1db80e9a Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:05:27 -0500 Subject: [PATCH 063/102] Add modifier classic check --- MiraAPI/Modifiers/Types/GameModifier.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index be2448b..051ae07 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -1,4 +1,5 @@ -using MiraAPI.PluginLoading; +using MiraAPI.GameModes; +using MiraAPI.PluginLoading; namespace MiraAPI.Modifiers.Types; @@ -54,5 +55,5 @@ public abstract class GameModifier : BaseModifier /// Determines whether the modifier can spawn in general, accounting for gamemodes and everything else. /// /// if the modifier is able to spawn, otherwise . - public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek(); + public virtual bool CanSpawnOnCurrentMode() => CustomGameModeManager.IsClassic() && !GameManager.Instance.IsHideAndSeek(); } From 0cf4954eaf897ec17c685d3ccb6cff8cef610bac Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:36:34 -0500 Subject: [PATCH 064/102] Add in Hide and Seek role logic --- MiraAPI/GameModes/AbstractGameMode.cs | 8 ++++ MiraAPI/GameModes/HideAndSeekMode.cs | 53 +++++++++++++-------- MiraAPI/Patches/Roles/RoleManagerPatches.cs | 30 ++++++++++-- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 8fd612a..8e9cec1 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -129,6 +129,14 @@ public abstract class AbstractGameMode : IOptionable runOriginal = true; } + /// + /// Custom role assignment to run after all roles are handed out. + /// + /// Instance of LogicRoleSelection. + public virtual void PostAssignRoles(LogicRoleSelectionNormal instance) + { + } + /// /// Custom murder logic. /// diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 7f4c0c6..60680d5 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; +using HarmonyLib; using InnerNet; using MiraAPI.GameOptions; using MiraAPI.HnsReimplemented.Options; @@ -34,18 +35,32 @@ public class HideAndSeekMode : AbstractGameMode public override bool GameModeBodyTypeOverride => true; public override bool IsHideAndSeek => true; public override bool ShowNormalGameSettings => false; + + public static int ImpostorPlayerID() + { + return OptionGroupSingleton.Instance.SelectedSeeker.Value; + } - /*public override void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) + public static bool HasImpostorPlayerID() + { + return ImpostorPlayerID() > -1; + } + + public static bool ValidateImpostorPlayerID(List players) + { + return HasImpostorPlayerID() && players.Find((NetworkedPlayerInfo p) => (int)p.PlayerId == ImpostorPlayerID()) != null; + } + + public override void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) { runOriginal = false; - List list = new List(); + Il2CppSystem.Collections.Generic.List list = new(); AmongUsClient.Instance.GetAllClients(list); - List list2 = (from c in list - where c.Character != null - where c.Character.Data != null - where !c.Character.Data.Disconnected && !c.Character.Data.IsDead - orderby c.Id - select c.Character.Data).ToList(); + List list2 = list.ToArray() + .Where(c => c.Character != null && c.Character.Data != null && !c.Character.Data.Disconnected && + !c.Character.Data.IsDead).OrderBy(c => c.Id).Select(c => c.Character.Data) + .ToList(); + foreach (NetworkedPlayerInfo networkedPlayerInfo in GameData.Instance.AllPlayers) { if (networkedPlayerInfo.Object != null && networkedPlayerInfo.Object.isDummy) @@ -55,17 +70,16 @@ public class HideAndSeekMode : AbstractGameMode } IGameOptions currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; int adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count); - this.DebugRoleAssignments(list2, ref adjustedNumImpostors); - GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, adjustedNumImpostors, new RoleTypes?(RoleTypes.Impostor)); - GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, new RoleTypes?(RoleTypes.Crewmate)); + AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, adjustedNumImpostors, RoleTypes.Impostor); + AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Engineer); } - public static bool AssignRolesForTeam( - LogicRoleSelectionHnS __instance, + + public static void AssignRolesForTeam( List players, IGameOptions opts, RoleTeamTypes team, int teamMax, - Il2CppSystem.Nullable defaultRole) + RoleTypes defaultRole) { Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); int num = 0; @@ -137,12 +151,12 @@ public class HideAndSeekMode : AbstractGameMode Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); var newImpostors = new List(); // Specified Seeker - if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() && - __instance.hnsManager.LogicOptionsHnS.ValidateImpostorPlayerID(players) && + if (HasImpostorPlayerID() && + ValidateImpostorPlayerID(players) && !AmongUsClient.Instance.IsGamePublic) { NetworkedPlayerInfo networkedPlayerInfo = players.ToArray() - .First(p => p.PlayerId == __instance.hnsManager.LogicOptionsHnS.ImpostorPlayerID()); + .First(p => p.PlayerId == ImpostorPlayerID()); players.Remove(networkedPlayerInfo); newImpostors.Add(networkedPlayerInfo); Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}"); @@ -154,7 +168,7 @@ public class HideAndSeekMode : AbstractGameMode while (num2 < teamMax && players.Count > 0) { PseudoRandomList pseudoRandomList = new PseudoRandomList(AmongUsClient.Instance.GameId); - players._items.Do(x => pseudoRandomList.Add(x)); + players.Do(x => pseudoRandomList.Add(x)); for (int i = 0; i < GameData.RoundsPlayedInSession; i++) { pseudoRandomList.PickRandom(); @@ -212,7 +226,6 @@ public class HideAndSeekMode : AbstractGameMode AssignRolesFromList(newImpostors, teamMax, list, ref num); } - return false; } public static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned) @@ -227,7 +240,7 @@ public class HideAndSeekMode : AbstractGameMode players.RemoveAt(index2); rolesAssigned++; } - }*/ + } public override IEnumerator IntroCutscene(IntroCutscene __instance) { diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index 8d13595..03830dc 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -3,6 +3,7 @@ using AmongUs.GameOptions; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameModes; using MiraAPI.Modifiers; using MiraAPI.Roles; using UnityEngine; @@ -61,18 +62,39 @@ public static class RoleManagerPatches return false; } + [HarmonyPrefix] + [HarmonyPatch(nameof(RoleManager.SelectRoles))] + public static bool SelectRolePrefix(RoleManager __instance) + { + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (!AmongUsClient.Instance.AmHost || CustomGameModeManager.ActiveMode == null || roleSelection == null) + { + return true; + } + + CustomGameModeManager.ActiveMode.AssignRoles(out var runOriginal, roleSelection); + + return runOriginal; + } + [HarmonyPostfix] [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static void ModifierSelectionPatches(RoleManager __instance) { - if (!AmongUsClient.Instance.AmHost || !ModifierManager.MiraAssignsModifiers) + if (AmongUsClient.Instance.AmHost && ModifierManager.MiraAssignsModifiers) + { + ModifierManager.AssignModifiers( + PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) + .ToList()); + } + + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (CustomGameModeManager.ActiveMode == null || roleSelection == null) { return; } - ModifierManager.AssignModifiers( - PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) - .ToList()); + CustomGameModeManager.ActiveMode.PostAssignRoles(roleSelection); } [HarmonyPrefix] From 121baeb178fd16d41022007aba29b6e3d8e36718 Mon Sep 17 00:00:00 2001 From: amber Date: Thu, 6 Aug 2026 17:37:55 -0400 Subject: [PATCH 065/102] Remove IsHideAndSeek --- MiraAPI/GameModes/AbstractGameMode.cs | 7 +------ MiraAPI/GameModes/HideAndSeekMode.cs | 1 - MiraAPI/Patches/GameModes/GameManagerPatches.cs | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 8e9cec1..688aa1b 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -31,7 +31,7 @@ public abstract class AbstractGameMode : IOptionable /// /// Gets the colored game mode name, using the color and name properties. /// - public string ColoredName => $"{Name}"; + public string ColoredName => $"{this.Name}"; /// /// Gets the game mode id. @@ -43,11 +43,6 @@ public abstract class AbstractGameMode : IOptionable /// public virtual bool GameModeBodyTypeOverride => false; - /// - /// Gets a value indicating whether the gamemode is based on Hide n Seek. - /// - public virtual bool IsHideAndSeek => false; - /// /// Gets a value indicating whether the gamemode removes the normal game settings. /// diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 60680d5..d58424f 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -33,7 +33,6 @@ public class HideAndSeekMode : AbstractGameMode public override bool ShouldShowSabotageMap(MapBehaviour map) => false; public override bool ShowGameModeIntroCutscene => true; public override bool GameModeBodyTypeOverride => true; - public override bool IsHideAndSeek => true; public override bool ShowNormalGameSettings => false; public static int ImpostorPlayerID() diff --git a/MiraAPI/Patches/GameModes/GameManagerPatches.cs b/MiraAPI/Patches/GameModes/GameManagerPatches.cs index 7467d9e..fca4f75 100644 --- a/MiraAPI/Patches/GameModes/GameManagerPatches.cs +++ b/MiraAPI/Patches/GameModes/GameManagerPatches.cs @@ -22,7 +22,7 @@ internal static class GameManagerPatches { if (CustomGameModeManager.ActiveMode != null) { - __result = CustomGameModeManager.ActiveMode.IsHideAndSeek; + __result = CustomGameModeManager.ActiveMode is HideAndSeekMode; return false; } From ae45c506cbc06c9d1bc4e8051d263ca4b6fbb4ff Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:32:55 -0500 Subject: [PATCH 066/102] Temporary fix for scroller being too small --- MiraAPI/GameModes/GameModeOption.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 94f24c4..b502ae2 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -123,7 +123,7 @@ public static class GameModeOption private static void RefreshOptions(this GameOptionsMenu instance, AbstractGameMode mode) { - + instance.scrollBar.SetYBoundsMax(instance.scrollBar.GetYBounds().max + 1); } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] From 9606871cbecd7d2002b9156a375199fd41f26443 Mon Sep 17 00:00:00 2001 From: Nix-main Date: Fri, 7 Aug 2026 23:22:49 -0400 Subject: [PATCH 067/102] sync gamemode on join --- MiraAPI/GameModes/GameModeOption.cs | 4 ++-- MiraAPI/Patches/AmongUsClientSyncPatch.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index b502ae2..014bad4 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -111,7 +111,7 @@ public static class GameModeOption __instance.RefreshOptions(CustomGameModeManager.ActiveMode); } - public static void Set(int val) + private static void Set(int val) { Value = val; CustomGameModeManager.GetAndSetGameMode(); @@ -322,7 +322,7 @@ public static class GameModeOption } [MethodRpc((uint) MiraRpc.SyncGamemodeOption)] - private static void RpcSyncGamemode(PlayerControl host, int data) + internal static void RpcSyncGamemode(PlayerControl host, int data) { Set(data); } diff --git a/MiraAPI/Patches/AmongUsClientSyncPatch.cs b/MiraAPI/Patches/AmongUsClientSyncPatch.cs index 6af91c1..a8a3dfe 100644 --- a/MiraAPI/Patches/AmongUsClientSyncPatch.cs +++ b/MiraAPI/Patches/AmongUsClientSyncPatch.cs @@ -2,6 +2,7 @@ using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Player; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Roles; @@ -32,6 +33,7 @@ internal static class AmongUsClientSyncPatch ModdedOptionsManager.SyncAllOptions(clientData.Id); CustomRoleManager.SyncAllRoleSettings(clientData.Id); + GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value); } [HarmonyPostfix] From 72fa908cb0c61c2990c28c819177c4e436e97e70 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:34:10 -0500 Subject: [PATCH 068/102] Move patches to account for gamemodes --- MiraAPI/Patches/Roles/RoleManagerPatches.cs | 15 --------------- MiraAPI/Patches/Roles/SelectRolesPatch.cs | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index 03830dc..99359a6 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -62,21 +62,6 @@ public static class RoleManagerPatches return false; } - [HarmonyPrefix] - [HarmonyPatch(nameof(RoleManager.SelectRoles))] - public static bool SelectRolePrefix(RoleManager __instance) - { - var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); - if (!AmongUsClient.Instance.AmHost || CustomGameModeManager.ActiveMode == null || roleSelection == null) - { - return true; - } - - CustomGameModeManager.ActiveMode.AssignRoles(out var runOriginal, roleSelection); - - return runOriginal; - } - [HarmonyPostfix] [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static void ModifierSelectionPatches(RoleManager __instance) diff --git a/MiraAPI/Patches/Roles/SelectRolesPatch.cs b/MiraAPI/Patches/Roles/SelectRolesPatch.cs index dc51f03..1d17765 100644 --- a/MiraAPI/Patches/Roles/SelectRolesPatch.cs +++ b/MiraAPI/Patches/Roles/SelectRolesPatch.cs @@ -3,6 +3,7 @@ using AmongUs.GameOptions; using HarmonyLib; using System.Linq; using InnerNet; +using MiraAPI.GameModes; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -15,6 +16,21 @@ public static class SelectRolesPatch [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static bool SelectRoles() { + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (!AmongUsClient.Instance.AmHost || roleSelection == null) + { + return true; + } + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.AssignRoles(out var runOriginal, roleSelection); + + if (!runOriginal) + { + return false; + } + } if (!ApiHandlesRoleSelect) { return true; From ca7da0c343a7f0b0084f57066c7a1566ab88ed17 Mon Sep 17 00:00:00 2001 From: Amber <169551692+Nix-main@users.noreply.github.com> Date: Sun, 9 Aug 2026 08:43:56 -0400 Subject: [PATCH 069/102] update gamelibs --- AmongUs.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AmongUs.props b/AmongUs.props index afc343f..3c3ea71 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -2,7 +2,7 @@ - + @@ -11,4 +11,4 @@ - \ No newline at end of file + From 015d175aa00f7cb8a0f9fbaf02d2d788dc33a5a6 Mon Sep 17 00:00:00 2001 From: amber Date: Sun, 9 Aug 2026 09:11:05 -0400 Subject: [PATCH 070/102] check if header is not null --- MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 1cacb30..a11b2b4 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -132,7 +132,8 @@ internal static class GameOptionsMenuPatch if (!group.GroupVisible.Invoke()) { - group.Header.gameObject.SetActive(false); + if (group.Header.gameObject != null) + group.Header.gameObject.SetActive(false); foreach (var option in group.Options) { option.OptionBehaviour?.gameObject.SetActive(false); @@ -141,7 +142,8 @@ internal static class GameOptionsMenuPatch return; } - group.Header.gameObject.SetActive(true); + if (group.Header.gameObject != null) + group.Header.gameObject.SetActive(true); group.Header.transform.localScale = Vector3.one * 0.63f; group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); From 4e37f8b3d4984dcc41c38b4c47f5c6c2b4408b5b Mon Sep 17 00:00:00 2001 From: amber Date: Sun, 9 Aug 2026 09:31:59 -0400 Subject: [PATCH 071/102] fix scrollbar --- MiraAPI/GameModes/GameModeOption.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 014bad4..001e2ec 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -61,14 +61,11 @@ public static class GameModeOption Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.ColoredName)); } - - private static GameOptionsMenu? _instance; - + [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) { - _instance = __instance; if (MenuState.Instance.CurrentModIdx != 0 || GameManager.Instance.IsHideAndSeek() || CustomGameModeManager.ActiveMode == null) { return; @@ -108,7 +105,7 @@ public static class GameModeOption __instance.Children.Add(OptionBehaviour); for (var i = 1; i < Values.Count; i++) OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); - __instance.RefreshOptions(CustomGameModeManager.ActiveMode); + __instance.scrollBar.SetYBoundsMax(__instance.scrollBar.GetYBounds().max + 1); } private static void Set(int val) @@ -117,13 +114,6 @@ public static class GameModeOption CustomGameModeManager.GetAndSetGameMode(); HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); // could make Values a dict of AbstractGameMode too - if (_instance != null) - RefreshOptions(_instance, CustomGameModeManager.ActiveMode!); - } - - private static void RefreshOptions(this GameOptionsMenu instance, AbstractGameMode mode) - { - instance.scrollBar.SetYBoundsMax(instance.scrollBar.GetYBounds().max + 1); } [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))] @@ -316,7 +306,6 @@ public static class GameModeOption newText.text = group.AllOptionsHidden ? "(Click to open)" : "(Click to close)"; - menu.RefreshOptions(CustomGameModeManager.ActiveMode!); })); headerBtn.SetButtonEnableState(true); } From 4d187f3f79227367695fe83d3a4fe8c6a11b27f3 Mon Sep 17 00:00:00 2001 From: amber Date: Sun, 9 Aug 2026 09:49:25 -0400 Subject: [PATCH 072/102] more null checks, yay! --- MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index a11b2b4..74d782c 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -132,20 +132,24 @@ internal static class GameOptionsMenuPatch if (!group.GroupVisible.Invoke()) { - if (group.Header.gameObject != null) + if (group.Header != null && group.Header.gameObject != null) group.Header.gameObject.SetActive(false); foreach (var option in group.Options) { - option.OptionBehaviour?.gameObject.SetActive(false); + if (option.OptionBehaviour != null && option.OptionBehaviour.gameObject != null) + option.OptionBehaviour?.gameObject.SetActive(false); } return; } - if (group.Header.gameObject != null) - group.Header.gameObject.SetActive(true); - group.Header.transform.localScale = Vector3.one * 0.63f; - group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + if (group.Header != null) + { + if (group.Header.gameObject != null) + group.Header.gameObject.SetActive(true); + group.Header.transform.localScale = Vector3.one * 0.63f; + group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + } num -= 0.58f; From 4c3818944449c82810a92377a50eb3d57a0c0bf1 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:25:56 -0500 Subject: [PATCH 073/102] Fix initialize patch for gamemodes --- MiraAPI/GameModes/HideAndSeekMode.cs | 7 +++++-- MiraAPI/Patches/IntroCutscenePatches.cs | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index d58424f..fe5cf41 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -243,9 +243,7 @@ public class HideAndSeekMode : AbstractGameMode public override IEnumerator IntroCutscene(IntroCutscene __instance) { - deadPlayerCount = 0; SoundManager.Instance.PlaySound(__instance.IntroStinger, false, 1f, null); - ShipStatus.Instance.BreakEmergencyButton(); Logger.GlobalInstance.Info("IntroCutscene :: CoBegin() :: Game Mode: Hide and Seek (MiraAPI)", null); __instance.LogPlayerRoleData(); __instance.HideAndSeekPanels.SetActive(true); @@ -393,6 +391,11 @@ public class HideAndSeekMode : AbstractGameMode ShipStatus.Instance.StartSFX(); UnityEngine.Object.Destroy(__instance.gameObject); } + public override void Initialize() + { + deadPlayerCount = 0; + ShipStatus.Instance.BreakEmergencyButton(); + } public override PlayerBodyTypes GetBodyType(PlayerControl player) { diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index 4ee990c..e0e76d8 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -3,6 +3,7 @@ using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameModes; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -122,6 +123,10 @@ public static class IntroCutscenePatches } Info("IntroCutscene ended"); + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.Initialize(); + } MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene)); From 54bfd3002b0d513608a7400d9aad4fa1f9b8eefc Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:53:30 -0500 Subject: [PATCH 074/102] Add role settings toggle and imp cooldown for mode --- MiraAPI/GameModes/AbstractGameMode.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 688aa1b..7539277 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using AmongUs.GameOptions; using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using Reactor.Utilities.Extensions; @@ -48,6 +49,16 @@ public abstract class AbstractGameMode : IOptionable /// public virtual bool ShowNormalGameSettings => true; + /// + /// Gets a value indicating whether the gamemode removes the normal role settings. + /// + public virtual bool ShowNormalRoleSettings => true; + + /// + /// Gets a value indicating the default kill cooldown for impostors in this mode. + /// + public virtual float DefaultImpostorKillCooldown => GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown); + /// /// Called on GameManager.GetBodyType(). /// From a57f40fa790deb86627182b9071ae579b73df3a9 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:17:29 -0500 Subject: [PATCH 075/102] Apply role menu toggle --- MiraAPI/GameModes/GameModeOption.cs | 5 ++++- MiraAPI/Patches/Options/MenuState.cs | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 001e2ec..56e8aac 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -61,7 +61,6 @@ public static class GameModeOption Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.ColoredName)); } - [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) @@ -124,6 +123,10 @@ public static class GameModeOption { Info($"Game mode changed to {option.GetInt()}"); RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt()); + if (GameSettingMenu.Instance && CustomGameModeManager.ActiveMode != null) + { + GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); + } return false; } return true; diff --git a/MiraAPI/Patches/Options/MenuState.cs b/MiraAPI/Patches/Options/MenuState.cs index d459202..65b29bf 100644 --- a/MiraAPI/Patches/Options/MenuState.cs +++ b/MiraAPI/Patches/Options/MenuState.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; @@ -404,7 +405,8 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) if (CurrentModIdx == 0) { Gsm.GameSettingsButton.gameObject.SetActive(true); - Gsm.RoleSettingsButton.gameObject.SetActive(true); + Gsm.RoleSettingsButton.gameObject.SetActive( + CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); } else { From 6fe2750545d49406b0d83c199cf0f19506af5967 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:18:00 -0500 Subject: [PATCH 076/102] Make kill timer use whatever the gamemode sets --- MiraAPI/Networking/CustomMurderRpc.cs | 10 ++++++++-- MiraAPI/Patches/GameModes/HudPatches.cs | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 7009819..716a8ec 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -7,6 +7,7 @@ using Assets.CoreScripts; using BepInEx.Unity.IL2CPP.Utils; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameModes; using MiraAPI.Utilities; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; @@ -209,7 +210,10 @@ public static class CustomMurderRpc if (resetKillTimer) { source.SetKillTimer( - GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown) / 2f); + CustomGameModeManager.ActiveMode != null + ? CustomGameModeManager.ActiveMode.DefaultImpostorKillCooldown / 2f + : GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown) / + 2f); } } else @@ -246,7 +250,9 @@ public static class CustomMurderRpc if (resetKillTimer) { - source.SetKillTimer(GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown)); + source.SetKillTimer(CustomGameModeManager.ActiveMode != null + ? CustomGameModeManager.ActiveMode.DefaultImpostorKillCooldown + : GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown)); } } diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index 77c0f48..735d198 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -16,8 +16,11 @@ internal static class HudPatches [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] public static void HudUpdatePatch(HudManager __instance) { - if (GameManager.Instance != null && GameManager.Instance.GameHasStarted) - CustomGameModeManager.ActiveMode?.HudUpdate(__instance); + if (GameManager.Instance != null && GameManager.Instance.GameHasStarted && + CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.HudUpdate(__instance); + } } From 1c097b47dfa4444b705dbfc909b588bb7cb751e9 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:18:17 -0500 Subject: [PATCH 077/102] Add end game patches for all gamemodes --- MiraAPI/Patches/GameModes/GameLogicPatches.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/MiraAPI/Patches/GameModes/GameLogicPatches.cs b/MiraAPI/Patches/GameModes/GameLogicPatches.cs index 6f734fa..742a56c 100644 --- a/MiraAPI/Patches/GameModes/GameLogicPatches.cs +++ b/MiraAPI/Patches/GameModes/GameLogicPatches.cs @@ -6,6 +6,23 @@ namespace MiraAPI.Patches.GameModes; [HarmonyPatch] internal static class GameLogicPatches { + [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidHumansWin))] + public static bool DidHumansWin(GameOverReason reason, ref bool __result) + { + __result = reason == GameOverReason.CrewmatesByTask || reason == GameOverReason.CrewmatesByVote || + reason == GameOverReason.ImpostorDisconnect || reason == GameOverReason.HideAndSeek_CrewmatesByTimer; + return false; + } + + [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidImpostorsWin))] + public static bool DidImpostorsWin(GameOverReason reason, ref bool __result) + { + __result = reason == GameOverReason.ImpostorsByKill || reason == GameOverReason.ImpostorsBySabotage || + reason == GameOverReason.ImpostorsByVote || reason == GameOverReason.HideAndSeek_ImpostorsByKills || + reason == GameOverReason.CrewmateDisconnect; + return false; + } + [HarmonyPrefix, HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetBodyType))] public static bool BodyTypePatch(NormalGameManager __instance, PlayerControl player, ref PlayerBodyTypes __result) { @@ -16,4 +33,46 @@ internal static class GameLogicPatches __result = CustomGameModeManager.ActiveMode.GetBodyType(player); return false; } + + [HarmonyPrefix] + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] + public static bool EndGamePatch(LogicGameFlowNormal __instance) + { + if (TutorialManager.InstanceExists) + { + return true; + } + + if (!AmongUsClient.Instance.AmHost) + { + return false; + } + + if (!GameData.Instance) + { + return false; + } + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.CheckGameEnd(out var runOriginal, __instance); + + if (!runOriginal) + { + return false; + } + } + + return true; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.IsGameOverDueToDeath))] + public static void IsGameOverDueToDeathPatch(LogicGameFlowNormal __instance, ref bool __result) + { + if (CustomGameModeManager.IsActiveGameMode()) + { + __result = false; + } + } } From eef098317e92662e05e7e755dbc3183b16e3fe6b Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:20:19 -0500 Subject: [PATCH 078/102] Add hide and seek end logic and cooldown checks --- MiraAPI/GameModes/HideAndSeekMode.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index fe5cf41..952a268 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using AmongUs.Data; using AmongUs.GameOptions; using HarmonyLib; using InnerNet; @@ -34,7 +35,9 @@ public class HideAndSeekMode : AbstractGameMode public override bool ShowGameModeIntroCutscene => true; public override bool GameModeBodyTypeOverride => true; public override bool ShowNormalGameSettings => false; - + public override bool ShowNormalRoleSettings => false; + public override float DefaultImpostorKillCooldown => 1f; + public static int ImpostorPlayerID() { return OptionGroupSingleton.Instance.SelectedSeeker.Value; @@ -395,6 +398,25 @@ public class HideAndSeekMode : AbstractGameMode { deadPlayerCount = 0; ShipStatus.Instance.BreakEmergencyButton(); + PlayerControl.LocalPlayer.SetKillTimer(0.01f); + } + + public override void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) + { + runOriginal = false; + if (Helpers.GetAlivePlayers().Count(x => x.Data.Role.IsImpostor) == 0) + { + instance.Manager.RpcEndGame(GameOverReason.ImpostorDisconnect, !DataManager.Player.Ads.HasPurchasedAdRemoval); + } + if (Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor) != 0) + { + /*if (instance.AllTimersExpired()) + { + instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_CrewmatesByTimer, !DataManager.Player.Ads.HasPurchasedAdRemoval); + }*/ + return; + } + instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_ImpostorsByKills, !DataManager.Player.Ads.HasPurchasedAdRemoval); } public override PlayerBodyTypes GetBodyType(PlayerControl player) From d4a675f2a9843937e71fd7badc0e7a628542386b Mon Sep 17 00:00:00 2001 From: ang-xd Date: Thu, 13 Aug 2026 00:43:15 -0400 Subject: [PATCH 079/102] Hide roles tab if no roles in mod --- MiraAPI/Patches/Options/MenuState.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Options/MenuState.cs b/MiraAPI/Patches/Options/MenuState.cs index 65b29bf..6a7ba33 100644 --- a/MiraAPI/Patches/Options/MenuState.cs +++ b/MiraAPI/Patches/Options/MenuState.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Presets; +using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; @@ -413,7 +415,8 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) var hasOptions = CurrentMod.InternalOptionGroups.Exists(g => g.OptionableType == null && g.ParentMenu == MenuCategory.Game); - var hasRoles = CurrentMod.InternalRoles.Count > 0; + var customRoles = CurrentMod.InternalRoles.Values.OfType(); + var hasRoles = customRoles.Any(x => x.VisibleInSettings.Invoke()); var hasModifiers = CurrentMod.InternalOptionGroups.Exists(g => g.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true || g.ParentMenu == MenuCategory.Modifiers); From 847a4dd28760a95d9c9dbc10e4f9da65b1611ede Mon Sep 17 00:00:00 2001 From: ang-xd Date: Thu, 13 Aug 2026 14:37:52 -0400 Subject: [PATCH 080/102] Move initialize to begin --- MiraAPI/Patches/IntroCutscenePatches.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index e0e76d8..ae227bd 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -30,6 +30,11 @@ public static class IntroCutscenePatches { var @event = new IntroBeginEvent(__instance); MiraEventManager.InvokeEvent(@event); + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.Initialize(); + } } [HarmonyPatch] @@ -122,12 +127,6 @@ public static class IntroCutscenePatches introCutscene = __instance.Cast(); } - Info("IntroCutscene ended"); - if (CustomGameModeManager.ActiveMode != null) - { - CustomGameModeManager.ActiveMode.Initialize(); - } - MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene)); var @event = new BeforeRoundStartEvent(true); From f44ec1580429b90ff641a5264169be9bf995d389 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Thu, 13 Aug 2026 16:19:23 -0400 Subject: [PATCH 081/102] Patch console use --- MiraAPI/Patches/GameModes/ConsolePatches.cs | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/MiraAPI/Patches/GameModes/ConsolePatches.cs b/MiraAPI/Patches/GameModes/ConsolePatches.cs index d8a6dd8..cb0d5be 100644 --- a/MiraAPI/Patches/GameModes/ConsolePatches.cs +++ b/MiraAPI/Patches/GameModes/ConsolePatches.cs @@ -70,4 +70,40 @@ internal static class ConsolePatches canUse = couldUse = true; return true; } + + [HarmonyPrefix, HarmonyPatch(typeof(MapConsole), nameof(MapConsole.Use))] + public static bool MapUsePatch(MapConsole __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode!.CanUseMapConsole(__instance); + } + + return true; + } + + [HarmonyPrefix, HarmonyPatch(typeof(SystemConsole), nameof(SystemConsole.Use))] + public static bool SystemUsePatch(SystemConsole __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode!.CanUseSystemConsole(__instance); + } + + return true; + } + + [HarmonyPrefix, HarmonyPatch(typeof(Console), nameof(Console.Use))] + public static bool ConsoleUsePatch(Console __instance) + { + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) + { + return CustomGameModeManager.ActiveMode == null || + CustomGameModeManager.ActiveMode!.CanUseTasks(__instance); + } + + return true; + } } From b713599c13cb4bfec4977be4da8dab77e551c9e4 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:51:29 -0500 Subject: [PATCH 082/102] Fix option toggling for vanilla options --- MiraAPI/GameModes/GameModeOption.cs | 18 +- .../Patches/Options/GameOptionsMenuPatch.cs | 202 +++++++++++++++++- 2 files changed, 203 insertions(+), 17 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 56e8aac..784d9b9 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -45,12 +45,12 @@ public static class GameModeOption _lastValue = value; } } - internal static StringOption OptionBehaviour { get; private set; } = null!; + internal static StringOption OptionBehaviour { get; set; } = null!; - private static int _lastValue; - private static readonly StringNames GamemodeName = CustomStringName.CreateAndRegister("Gamemode"); - private static readonly StringNames CustomName = CustomStringName.CreateAndRegister("Custom"); - private static readonly Dictionary Values = new() + internal static int _lastValue; + internal static readonly StringNames GamemodeName = CustomStringName.CreateAndRegister("Gamemode"); + internal static readonly StringNames CustomName = CustomStringName.CreateAndRegister("Custom"); + internal static readonly Dictionary Values = new() { [0] = CustomStringName.CreateAndRegister("Classic"), }; @@ -60,8 +60,7 @@ public static class GameModeOption if (!Values.ContainsKey(mode.ID)) Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.ColoredName)); } - - [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] + /*[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] [HarmonyPostfix] private static void CreateSettingsPatch(GameOptionsMenu __instance) { @@ -105,9 +104,9 @@ public static class GameModeOption for (var i = 1; i < Values.Count; i++) OptionBehaviour.Values = (Il2CppStructArray)OptionBehaviour.Values.Add(Values.ElementAt(i).Value); __instance.scrollBar.SetYBoundsMax(__instance.scrollBar.GetYBounds().max + 1); - } + }*/ - private static void Set(int val) + internal static void Set(int val) { Value = val; CustomGameModeManager.GetAndSetGameMode(); @@ -125,6 +124,7 @@ public static class GameModeOption RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt()); if (GameSettingMenu.Instance && CustomGameModeManager.ActiveMode != null) { + GameOptionsMenuPatch.ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance); GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); } return false; diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 74d782c..a54dfca 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -3,6 +3,9 @@ using System.Collections; using System.Linq; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppSystem.Collections.Generic; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.Presets; @@ -13,6 +16,7 @@ using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using UnityEngine.ProBuilder; using UnityEngine.UI; using Object = UnityEngine.Object; @@ -21,6 +25,11 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(GameOptionsMenu))] internal static class GameOptionsMenuPatch { + private static List _vanillaHeaders = new(); + private static List _vanillaOptions = new(); + private static List _mainOptions = new(); + private static Dictionary _gameModeOptions = new(); + private static Dictionary _gameModeHeaders = new(); [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] // ReSharper disable once InconsistentNaming @@ -41,15 +50,10 @@ internal static class GameOptionsMenuPatch var mapNameSetting = GameManager.Instance.GameSettingsList.MapNameSetting; __instance.MapPicker.SetUpFromData(mapNameSetting, 20); __instance.Children.Add(__instance.MapPicker); - __instance.CreateSettings(); - foreach (var optionBehaviour in __instance.Children) + CreateSettings(__instance, container); + if (CustomGameModeManager.ActiveMode != null) { - if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost) - { - optionBehaviour.SetAsPlayer(); - } - - optionBehaviour.OnValueChanged = new Action(__instance.ValueChanged); + ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance); } } else @@ -69,6 +73,188 @@ internal static class GameOptionsMenuPatch return false; } + internal static void ToggleGamemodeOptions(AbstractGameMode gameMode, GameOptionsMenu instance) + { + float num = -1.217f; + instance.Children.Clear(); + foreach (var opt in _mainOptions) + { + instance.Children.Add(opt); + } + if (gameMode.ShowNormalGameSettings) + { + foreach (var opt in _vanillaHeaders) + { + num -= 0.63f; + opt.gameObject.SetActive(true); + } + foreach (var opt in _vanillaOptions) + { + opt.gameObject.SetActive(true); + instance.Children.Add(opt); + num -= 0.45f; + } + } + else + { + foreach (var opt in _vanillaHeaders) + { + opt.gameObject.SetActive(false); + } + foreach (var opt in _vanillaOptions) + { + opt.gameObject.SetActive(false); + } + } + instance.ControllerSelectable.Clear(); + foreach (var obj in instance.scrollBar.GetComponentsInChildren()) + { + if (obj.gameObject.activeSelf) + { + instance.ControllerSelectable.Add(obj); + } + } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } + private static void CreateSettings(GameOptionsMenu instance, Transform container) + { + float num = 0.713f; + _vanillaHeaders.Clear(); + _vanillaOptions.Clear(); + _mainOptions.Clear(); + _gameModeHeaders.Clear(); + _gameModeOptions.Clear(); + + CategoryHeaderMasked gmCategory = Object.Instantiate(instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, container); + gmCategory.SetHeader(GameModeOption.CustomName, 20); + gmCategory.transform.localScale = Vector3.one * 0.63f; + gmCategory.transform.localPosition = new Vector3(-0.903f, num, -2f); + GameModeOption.OptionBehaviour = Object.Instantiate( + instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + num -= 0.63f; + GameModeOption.OptionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + GameModeOption.OptionBehaviour.SetClickMask(instance.ButtonClickMask); + StringGameSetting setting = ScriptableObject.CreateInstance(); + setting.Type = OptionTypes.MultipleChoice; + setting.Title = GameModeOption.GamemodeName; + setting.Index = GameModeOption._lastValue; + setting.Values = new Il2CppStructArray([GameModeOption.Values[0]]); + GameModeOption.OptionBehaviour.SetUpFromData(setting, 20); + GameModeOption.Set(GameModeOption._lastValue); + GameModeOption.OptionBehaviour.TitleText.fontSize = 3; + instance.Children.Add(GameModeOption.OptionBehaviour); + foreach (var optionBehaviour in instance.Children) + { + _mainOptions.Add(optionBehaviour); + } + for (var i = 1; i < GameModeOption.Values.Count; i++) + { + GameModeOption.OptionBehaviour.Values = + (Il2CppStructArray)GameModeOption.OptionBehaviour.Values.Add( + GameModeOption.Values.ElementAt(i).Value); + } + + num -= 1.3f; + + foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) + { + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate( + instance.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + container); + categoryHeaderMasked.SetHeader(rulesCategory.CategoryName, 20); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); + _vanillaHeaders.Add(categoryHeaderMasked); + num -= 0.63f; + foreach (BaseGameSetting baseGameSetting in rulesCategory.AllGameSettings) + { + switch (baseGameSetting.Type) + { + case OptionTypes.Checkbox: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.checkboxOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.String: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.stringOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.Float: + case OptionTypes.Int: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.numberOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + case OptionTypes.Player: + { + OptionBehaviour optionBehaviour = Object.Instantiate( + instance.playerOptionOrigin, + Vector3.zero, + Quaternion.identity, + container); + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + _vanillaOptions.Add(optionBehaviour); + break; + } + } + + num -= 0.45f; + } + } + foreach (var optionBehaviour in _vanillaOptions) + { + if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost) + { + optionBehaviour.SetAsPlayer(); + } + instance.Children.Add(optionBehaviour); + } + + foreach (var optionBehaviour in instance.Children) + { + optionBehaviour.OnValueChanged = new Action(instance.ValueChanged); + } + + instance.ControllerSelectable.Clear(); + foreach (var obj in instance.scrollBar.GetComponentsInChildren()) + { + instance.ControllerSelectable.Add(obj); + } + instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } + [HarmonyPostfix] [HarmonyPatch(nameof(GameOptionsMenu.OnEnable))] public static void OpenPatch(GameOptionsMenu __instance) From 3cd8db1f040300a2d1f0ecf77222fdae193333d6 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:00:09 -0500 Subject: [PATCH 083/102] Show gamemode settings properly again --- MiraAPI/GameModes/GameModeOption.cs | 181 ------------------ .../Patches/Options/GameOptionsMenuPatch.cs | 157 ++++++++++++++- 2 files changed, 153 insertions(+), 185 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 784d9b9..9eb78ef 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -132,187 +132,6 @@ public static class GameModeOption return true; } - private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group) - { - var categoryHeaderMasked = Object.Instantiate( - menu.categoryHeaderOrigin, - Vector3.zero, - Quaternion.identity, - menu.settingsContainer); - - categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); - categoryHeaderMasked.Background.color = group.GroupColor; - categoryHeaderMasked.Divider.color = group.GroupColor; - categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) - ? Color.white - : group.GroupColor.FindAlternateColor(); - - categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); - categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear; - categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp; - - categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0); - categoryHeaderMasked.Background.size = new Vector2( - categoryHeaderMasked.Background.size.x + 1.5f, - categoryHeaderMasked.Background.size.y); - - categoryHeaderMasked.gameObject.SetActive(false); - group.Header = categoryHeaderMasked; - categoryHeaderMasked.transform.localPosition += Vector3.down; - - var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); - newText.text = group.AllOptionsHidden - ? "(Click to open)" - : "(Click to close)"; - newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); - newText.gameObject.GetComponent().Destroy(); - - var options = group.Options.Select(opt => opt.CreateOption( - menu.checkboxOrigin, - menu.numberOptionOrigin, - menu.stringOptionOrigin, - menu.playerOptionOrigin, - menu.settingsContainer)); - - /*OptionPreset? defaultPreset = null; - if (GameSettingMenuPatches.SelectedMod != null && PresetManager.DefaultPresets.TryGetValue( - GameSettingMenuPatches.SelectedMod, - out var preset)) - { - defaultPreset = preset; - }*/ - - foreach (var newOpt in options) - { - newOpt.SetClickMask(menu.ButtonClickMask); - SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); - foreach (var renderer in componentsInChildren) - { - if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) - { - renderer.color = group.GroupColor.FindAlternateColor(); - if (renderer.transform.parent.TryGetComponent(out var btn)) - { - btn.interactableColor = group.GroupColor.FindAlternateColor(); - btn.interactableHoveredColor = Color.white; - } - } - - renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); - } - - foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) - { - if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) - { - textMeshPro.color = group.GroupColor; - } - - textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); - textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); - } - - if (newOpt is ToggleOption toggle) - { - toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); - toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor - ? group.GroupColor - : MiraAssets.AcceptedTeal; - var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") - .GetComponent(); - rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); - rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor - ? group.GroupColor - : MiraAssets.AcceptedTeal; - } - - menu.Children.Add(newOpt); - - /*var resetBtn = new GameObject("ResetOption"); - resetBtn.transform.parent = newOpt.transform; - resetBtn.transform.localScale = new(.5f, .5f, 1); - resetBtn.layer = LayerMask.NameToLayer("UI"); - resetBtn.transform.localPosition = new Vector3(-3.1f, 0f, -2f); - var resetRend = resetBtn.AddComponent(); - resetRend.sprite = MiraAssets.ResetButton.LoadAsset(); - resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) - ? Color.white - : group.GroupColor.FindAlternateColor(); - var resetBoxCol = resetBtn.gameObject.AddComponent(); - resetBoxCol.size = new Vector2(1f, 1f); - resetBoxCol.offset = new Vector2(0, 0); - var passiveButton = resetBtn.AddComponent(); - passiveButton.OnClick = new Button.ButtonClickedEvent(); - passiveButton.ClickSound = menu.BackButton.GetComponent().ClickSound; - passiveButton.OnMouseOver = new UnityEvent(); - passiveButton.OnMouseOver.AddListener( - (UnityAction)(() => - { - resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor - ? group.GroupColor - : MiraAssets.AcceptedTeal; - })); - passiveButton.OnMouseOut = new UnityEvent(); - passiveButton.OnMouseOut.AddListener( - (UnityAction)(() => - { - resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) - ? Color.white - : group.GroupColor.FindAlternateColor(); - })); - if (newOpt is ToggleOption toggleOpt) - { - passiveButton.OnClick.AddListener( - (UnityAction)(() => - { - defaultPreset!.ResetOption(toggleOpt); - })); - } - else if (newOpt is NumberOption numOpt) - { - passiveButton.OnClick.AddListener( - (UnityAction)(() => - { - defaultPreset!.ResetOption(numOpt); - })); - } - else if (newOpt is StringOption strOpt) - { - passiveButton.OnClick.AddListener( - (UnityAction)(() => - { - defaultPreset!.ResetOption(strOpt); - })); - } - if (!defaultPreset!.IsOptionInPreset(newOpt)) - { - resetBtn.Destroy(); - }*/ - - newOpt.Initialize(); - newOpt.gameObject.SetActive(false); - } - - var boxCol = categoryHeaderMasked.gameObject.AddComponent(); - boxCol.size = new Vector2(7, 0.7f); - boxCol.offset = new Vector2(1.5f, -0.3f); - - var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); - headerBtn.ClickSound = menu.BackButton.GetComponent().ClickSound; - headerBtn.OnMouseOver = new UnityEvent(); - headerBtn.OnMouseOut = new UnityEvent(); - headerBtn.OnClick.AddListener( - (UnityAction)(() => - { - group.AllOptionsHidden = !group.AllOptionsHidden; - newText.text = group.AllOptionsHidden - ? "(Click to open)" - : "(Click to close)"; - })); - headerBtn.SetButtonEnableState(true); - } - [MethodRpc((uint) MiraRpc.SyncGamemodeOption)] internal static void RpcSyncGamemode(PlayerControl host, int data) { diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index a54dfca..1da222a 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -28,8 +28,9 @@ internal static class GameOptionsMenuPatch private static List _vanillaHeaders = new(); private static List _vanillaOptions = new(); private static List _mainOptions = new(); - private static Dictionary _gameModeOptions = new(); - private static Dictionary _gameModeHeaders = new(); + private static System.Collections.Generic.Dictionary> _gameModeGroups = new(); + private static System.Collections.Generic.Dictionary> _gameModeOptions = new(); + private static System.Collections.Generic.Dictionary> _gameModeHeaders = new(); [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] // ReSharper disable once InconsistentNaming @@ -73,6 +74,7 @@ internal static class GameOptionsMenuPatch return false; } + public static float AdditionalVanillaScrollNum; internal static void ToggleGamemodeOptions(AbstractGameMode gameMode, GameOptionsMenu instance) { float num = -1.217f; @@ -106,6 +108,41 @@ internal static class GameOptionsMenuPatch opt.gameObject.SetActive(false); } } + + foreach (var pair in _gameModeGroups) + { + if (pair.Value.Count != 0) + { + if (gameMode == pair.Key) + { + foreach (var opt in _gameModeHeaders[gameMode]) + { + num -= 0.63f; + opt.gameObject.SetActive(true); + } + + foreach (var opt in _gameModeOptions[gameMode]) + { + opt.gameObject.SetActive(true); + instance.Children.Add(opt); + num -= 0.45f; + } + } + else + { + foreach (var opt in _gameModeHeaders[pair.Key]) + { + opt.gameObject.SetActive(false); + } + + foreach (var opt in _gameModeOptions[pair.Key]) + { + opt.gameObject.SetActive(false); + } + } + } + } + instance.ControllerSelectable.Clear(); foreach (var obj in instance.scrollBar.GetComponentsInChildren()) { @@ -124,6 +161,7 @@ internal static class GameOptionsMenuPatch _mainOptions.Clear(); _gameModeHeaders.Clear(); _gameModeOptions.Clear(); + _gameModeGroups.Clear(); CategoryHeaderMasked gmCategory = Object.Instantiate(instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, container); gmCategory.SetHeader(GameModeOption.CustomName, 20); @@ -158,7 +196,7 @@ internal static class GameOptionsMenuPatch } num -= 1.3f; - + AdditionalVanillaScrollNum = 0f; foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) { CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate( @@ -171,6 +209,7 @@ internal static class GameOptionsMenuPatch categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); _vanillaHeaders.Add(categoryHeaderMasked); num -= 0.63f; + AdditionalVanillaScrollNum -= 0.63f; foreach (BaseGameSetting baseGameSetting in rulesCategory.AllGameSettings) { switch (baseGameSetting.Type) @@ -231,6 +270,7 @@ internal static class GameOptionsMenuPatch } num -= 0.45f; + AdditionalVanillaScrollNum -= 0.45f; } } foreach (var optionBehaviour in _vanillaOptions) @@ -247,6 +287,24 @@ internal static class GameOptionsMenuPatch optionBehaviour.OnValueChanged = new Action(instance.ValueChanged); } + foreach (var mode in CustomGameModeManager.IdToModeMap.Values) + { + var filteredGroups = + ModdedOptionsManager.GameModeOptionGroups + .Where(x => x.Key == mode.GetType()).Select(y => y.Value).SelectMany(y => y) ?? []; + _gameModeGroups.Add(mode, filteredGroups.ToList()); + + var optionBehaviours = new System.Collections.Generic.List(); + var categoryHeaders = new System.Collections.Generic.List(); + var newNum = mode.ShowNormalGameSettings ? num : num - AdditionalVanillaScrollNum; + foreach (var group in filteredGroups) + { + CreateGroup(instance, group, container, ref newNum, ref optionBehaviours, ref categoryHeaders); + } + _gameModeHeaders.Add(mode, categoryHeaders); + _gameModeOptions.Add(mode, optionBehaviours); + } + instance.ControllerSelectable.Clear(); foreach (var obj in instance.scrollBar.GetComponentsInChildren()) { @@ -278,7 +336,6 @@ internal static class GameOptionsMenuPatch { return; } - var num = 2.1f; switch (MenuState.Instance.CurrentMenu) @@ -308,6 +365,98 @@ internal static class GameOptionsMenuPatch __instance.scrollBar.SetYBoundsMax(-num - 1.65f); } + private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref System.Collections.Generic.List optionBehaviours, ref System.Collections.Generic.List categoryHeaders) + { + var categoryHeaderMasked = Object.Instantiate( + menu.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + container); + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num + 1, -2f); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaders.Add(categoryHeaderMasked); + + num -= 0.58f; + + categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); + categoryHeaderMasked.Background.color = group.GroupColor; + categoryHeaderMasked.Divider.color = group.GroupColor; + categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + + categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); + categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear; + categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp; + + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0); + categoryHeaderMasked.Background.size = new Vector2( + categoryHeaderMasked.Background.size.x + 1.5f, + categoryHeaderMasked.Background.size.y); + + categoryHeaderMasked.gameObject.SetActive(true); + group.Header = categoryHeaderMasked; + categoryHeaderMasked.transform.localPosition += Vector3.down; + + var options = group.Options.Select(opt => opt.CreateOption( + menu.checkboxOrigin, + menu.numberOptionOrigin, + menu.stringOptionOrigin, + menu.playerOptionOrigin, + container)); + + foreach (var newOpt in options) + { + newOpt.SetClickMask(menu.ButtonClickMask); + SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); + foreach (var renderer in componentsInChildren) + { + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) + { + renderer.color = group.GroupColor.FindAlternateColor(); + if (renderer.transform.parent.TryGetComponent(out var btn)) + { + btn.interactableColor = group.GroupColor.FindAlternateColor(); + btn.interactableHoveredColor = Color.white; + } + } + + renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); + } + + foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) + { + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) + { + textMeshPro.color = group.GroupColor; + } + + textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); + } + + if (newOpt is ToggleOption toggle) + { + toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); + toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") + .GetComponent(); + rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); + rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + } + + optionBehaviours.Add(newOpt); + newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); + num -= 0.45f; + + newOpt.Initialize(); + newOpt.gameObject.SetActive(true); + } + } internal static void UpdateGroup(AbstractOptionGroup? group, ref float num) { From cd97fab3a285bc9a7112e5ddb0169865b92bc55e Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:59:51 -0500 Subject: [PATCH 084/102] Show gamemode description in options --- MiraAPI/GameModes/ClassicMode.cs | 2 +- MiraAPI/GameModes/HideAndSeekMode.cs | 2 +- MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameModes/ClassicMode.cs b/MiraAPI/GameModes/ClassicMode.cs index 952f918..7eff012 100644 --- a/MiraAPI/GameModes/ClassicMode.cs +++ b/MiraAPI/GameModes/ClassicMode.cs @@ -12,5 +12,5 @@ public class ClassicMode : AbstractGameMode public override string Name => "Classic"; /// - public override string Description => "The classic Among Us experience"; + public override string Description => "The classic Among Us experience!\nFind the Impostors or deceive the Crew."; } diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 952a268..6ec83ae 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -26,7 +26,7 @@ public class HideAndSeekMode : AbstractGameMode public override string Name => "Hide n Seek"; /// - public override string Description => "You can run, but you can't hide!"; + public override string Description => "You can run, but you can't hide!\nOutlive the timer or eliminate the crew."; public override Color Color { get; } = new Color32(255, 88, 90, 255); diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 1da222a..5e5b27e 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -31,6 +31,7 @@ internal static class GameOptionsMenuPatch private static System.Collections.Generic.Dictionary> _gameModeGroups = new(); private static System.Collections.Generic.Dictionary> _gameModeOptions = new(); private static System.Collections.Generic.Dictionary> _gameModeHeaders = new(); + private static TextMeshPro _gamemodeDescription = null!; [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] // ReSharper disable once InconsistentNaming @@ -83,6 +84,7 @@ internal static class GameOptionsMenuPatch { instance.Children.Add(opt); } + _gamemodeDescription.text = gameMode.Description; if (gameMode.ShowNormalGameSettings) { foreach (var opt in _vanillaHeaders) @@ -195,6 +197,16 @@ internal static class GameOptionsMenuPatch GameModeOption.Values.ElementAt(i).Value); } + var gamemodeTextObj = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(1).gameObject, Vector3.zero, Quaternion.identity, container); + gamemodeTextObj.transform.localPosition = new Vector3(1, -0.675f, -2f); + gamemodeTextObj.transform.localScale = new Vector3(2.6f, 2.6f, 1); + _gamemodeDescription = gamemodeTextObj.GetComponentInChildren(); + _gamemodeDescription.fontSizeMin = 0.5f; + _gamemodeDescription.fontSizeMax = 1; + + var gamemodeBg = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(0).gameObject, Vector3.zero, Quaternion.identity, gamemodeTextObj.transform); + gamemodeBg.transform.localPosition = new Vector3(0, 0, 0.01f); + gamemodeBg.transform.localScale = new Vector3(0.61f, 0.375f, 1); num -= 1.3f; AdditionalVanillaScrollNum = 0f; foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) From 0141dfdea449ff280dc2280859884db321f2f131 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Fri, 14 Aug 2026 03:12:04 -0400 Subject: [PATCH 085/102] Update gamemodes info panel to show mod icon --- MiraAPI.Example/ExamplePlugin.cs | 3 + MiraAPI/GameModes/CustomGameModeManager.cs | 11 +++ .../Patches/Options/GameOptionsMenuPatch.cs | 71 ++++++++++++++++--- MiraAPI/PluginLoading/IMiraPlugin.cs | 7 ++ 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index b20ec41..ff4efb1 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -3,9 +3,11 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; +using UnityEngine; namespace MiraAPI.Example; @@ -18,6 +20,7 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin { public Harmony Harmony { get; } = new(Id); public string OptionsTitleText => "Mira API\nExample Mod"; + public LoadableAsset? ModIcon => ExampleAssets.ExampleButton; public ConfigFile GetConfigFile() => Config; public override void Load() { diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 90a96a1..d14a841 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using MiraAPI.PluginLoading; using Reactor.Utilities; @@ -76,6 +77,16 @@ public static class CustomGameModeManager /// The gamemode matching that ID. public static AbstractGameMode GetMode(uint id) => IdToModeMap[id]; + /// + /// Finds the parent mod of a custom gamemode. + /// + /// The object. + /// A object representing the parent mod of the gamemode. + public static MiraPluginInfo? FindParentMod(AbstractGameMode gameMode) + { + return MiraPluginManager.Instance.RegisteredPlugins.FirstOrDefault(plugin => plugin.GameModes.ContainsValue(gameMode)); + } + internal static void RegisterDefaultMode() { var defaultMode = new ClassicMode(); diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 5e5b27e..aeed200 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -32,6 +32,8 @@ internal static class GameOptionsMenuPatch private static System.Collections.Generic.Dictionary> _gameModeOptions = new(); private static System.Collections.Generic.Dictionary> _gameModeHeaders = new(); private static TextMeshPro _gamemodeDescription = null!; + private static SpriteRenderer _modIcon = null!; + [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] // ReSharper disable once InconsistentNaming @@ -84,6 +86,30 @@ internal static class GameOptionsMenuPatch { instance.Children.Add(opt); } + + var mod = CustomGameModeManager.FindParentMod(gameMode); + if (mod is { MiraPlugin.ModIcon: not null }) + { + _modIcon.gameObject.SetActive(true); + _gamemodeDescription.transform.localPosition = new Vector3(0.3f, 0, -0.01f); + _gamemodeDescription.horizontalAlignment = HorizontalAlignmentOptions.Left; + var texture = mod.MiraPlugin.ModIcon.LoadAsset().texture; + var newSprite = Sprite.Create( + texture, + new Rect(0, 0, 128, 128), + Vector2.one / 2, + 100 + ); + _modIcon.sprite = newSprite; + _modIcon.drawMode = SpriteDrawMode.Sliced; + } + else + { + _gamemodeDescription.transform.localPosition = new Vector3(0f, 0, -0.01f); + _gamemodeDescription.horizontalAlignment = HorizontalAlignmentOptions.Center; + _modIcon.gameObject.SetActive(false); + } + _gamemodeDescription.text = gameMode.Description; if (gameMode.ShowNormalGameSettings) { @@ -155,6 +181,8 @@ internal static class GameOptionsMenuPatch } instance.scrollBar.SetYBoundsMax(-num - 1.65f); } + + private static void CreateSettings(GameOptionsMenu instance, Transform container) { float num = 0.713f; @@ -197,16 +225,43 @@ internal static class GameOptionsMenuPatch GameModeOption.Values.ElementAt(i).Value); } - var gamemodeTextObj = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(1).gameObject, Vector3.zero, Quaternion.identity, container); - gamemodeTextObj.transform.localPosition = new Vector3(1, -0.675f, -2f); - gamemodeTextObj.transform.localScale = new Vector3(2.6f, 2.6f, 1); + var modeInfoHolder = new GameObject("GamemodeInfo") + { + transform = + { + parent = container, + localPosition = new Vector3(1.2f, -0.675f, 1), + localScale = Vector3.one, + }, + layer = container.gameObject.layer, + }.transform; + + var gamemodeBg = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(0).gameObject, Vector3.zero, Quaternion.identity, modeInfoHolder); + gamemodeBg.transform.localPosition = Vector3.zero; + gamemodeBg.transform.localScale = new Vector3(1.35f, 0.85f, 1); + gamemodeBg.name = "Background"; + + var modIconObj = new GameObject("ModIcon") + { + transform = + { + parent = modeInfoHolder, + localPosition = new Vector3(-1.75f, 0, -0.1f), + localScale = new Vector3(0.4f, 0.4f, 1), + }, + layer = gamemodeBg.layer, + }; + _modIcon = modIconObj.AddComponent(); + _modIcon.sprite = MiraAssets.BlankSprite.LoadAsset(); + + var gamemodeTextObj = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(1).gameObject, Vector3.zero, Quaternion.identity, modeInfoHolder); + gamemodeTextObj.transform.localPosition = new Vector3(0.3f, 0, -0.01f); + gamemodeTextObj.transform.localScale = new Vector3(2.12f, 2.12f, 1); _gamemodeDescription = gamemodeTextObj.GetComponentInChildren(); - _gamemodeDescription.fontSizeMin = 0.5f; - _gamemodeDescription.fontSizeMax = 1; + _gamemodeDescription.fontSizeMin = 0.65f; + _gamemodeDescription.fontSizeMax = 0.9f; + _gamemodeDescription.alignment = TextAlignmentOptions.Left; - var gamemodeBg = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(0).gameObject, Vector3.zero, Quaternion.identity, gamemodeTextObj.transform); - gamemodeBg.transform.localPosition = new Vector3(0, 0, 0.01f); - gamemodeBg.transform.localScale = new Vector3(0.61f, 0.375f, 1); num -= 1.3f; AdditionalVanillaScrollNum = 0f; foreach (RulesCategory rulesCategory in GameManager.Instance.GameSettingsList.AllCategories) diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs index ad5a992..bb3e29d 100644 --- a/MiraAPI/PluginLoading/IMiraPlugin.cs +++ b/MiraAPI/PluginLoading/IMiraPlugin.cs @@ -1,5 +1,7 @@ using System.Linq; using BepInEx.Configuration; +using MiraAPI.Utilities.Assets; +using UnityEngine; namespace MiraAPI.PluginLoading; @@ -52,6 +54,11 @@ public interface IMiraPlugin /// public virtual string CustomOptionMenuTwoDescription => "Apply game settings for this mod!"; + /// + /// Gets the icon of the mod. + /// + public virtual LoadableAsset? ModIcon => null; + /// /// Gets the for the plugin. /// From 9ab87955bbf58f7a3caff63721bd4700c1bc1f57 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Fri, 14 Aug 2026 03:19:06 -0400 Subject: [PATCH 086/102] i forgor to mask mod icon --- MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index aeed200..0468ef7 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -253,6 +253,7 @@ internal static class GameOptionsMenuPatch }; _modIcon = modIconObj.AddComponent(); _modIcon.sprite = MiraAssets.BlankSprite.LoadAsset(); + _modIcon.material = gamemodeBg.gameObject.GetComponent().material; var gamemodeTextObj = Object.Instantiate(GameModeOption.OptionBehaviour.transform.GetChild(1).gameObject, Vector3.zero, Quaternion.identity, modeInfoHolder); gamemodeTextObj.transform.localPosition = new Vector3(0.3f, 0, -0.01f); From 951af42358766cc3b68ddcdf45568f89ebf8089d Mon Sep 17 00:00:00 2001 From: ang-xd Date: Fri, 14 Aug 2026 16:39:16 -0400 Subject: [PATCH 087/102] Move icons to gamemode and add icons for vanilla modes --- MiraAPI.Example/ExamplePlugin.cs | 2 +- MiraAPI.Example/GameModes/ExampleMode.cs | 2 ++ MiraAPI/GameModes/AbstractGameMode.cs | 6 ++++++ MiraAPI/GameModes/ClassicMode.cs | 4 ++++ MiraAPI/GameModes/HideAndSeekMode.cs | 3 ++- .../Patches/Options/GameOptionsMenuPatch.cs | 17 ++++++++++------- MiraAPI/PluginLoading/IMiraPlugin.cs | 5 ----- MiraAPI/Resources/ClassicGamemodeIcon.png | Bin 0 -> 44061 bytes MiraAPI/Resources/HnSGamemodeIcon.png | Bin 0 -> 43233 bytes MiraAPI/Utilities/Assets/MiraAssets.cs | 10 ++++++++++ 10 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 MiraAPI/Resources/ClassicGamemodeIcon.png create mode 100644 MiraAPI/Resources/HnSGamemodeIcon.png diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index ff4efb1..dc0783b 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -20,7 +20,7 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin { public Harmony Harmony { get; } = new(Id); public string OptionsTitleText => "Mira API\nExample Mod"; - public LoadableAsset? ModIcon => ExampleAssets.ExampleButton; + public string CustomOptionMenuNameTwo => "angxl's Options"; public ConfigFile GetConfigFile() => Config; public override void Load() { diff --git a/MiraAPI.Example/GameModes/ExampleMode.cs b/MiraAPI.Example/GameModes/ExampleMode.cs index 4868516..c15e1d5 100644 --- a/MiraAPI.Example/GameModes/ExampleMode.cs +++ b/MiraAPI.Example/GameModes/ExampleMode.cs @@ -1,4 +1,5 @@ using MiraAPI.GameModes; +using MiraAPI.Utilities.Assets; using UnityEngine; namespace MiraAPI.Example.GameModes; @@ -7,5 +8,6 @@ public class ExampleMode : AbstractGameMode { public override string Name => "Example Mode"; public override string Description => "An example gamemode."; + public override LoadableAsset? Icon => ExampleAssets.ExampleButton; public override Color Color => Color.red; } diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 7539277..cf3b2d8 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using AmongUs.GameOptions; using MiraAPI.GameOptions; using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; using UnityEngine; @@ -29,6 +30,11 @@ public abstract class AbstractGameMode : IOptionable /// public virtual Color Color { get; } = Color.white; + /// + /// Gets the icon of the gamemode. Will be shown in options menu underneath the gamemode option. + /// + public virtual LoadableAsset? Icon => null; + /// /// Gets the colored game mode name, using the color and name properties. /// diff --git a/MiraAPI/GameModes/ClassicMode.cs b/MiraAPI/GameModes/ClassicMode.cs index 7eff012..44b43da 100644 --- a/MiraAPI/GameModes/ClassicMode.cs +++ b/MiraAPI/GameModes/ClassicMode.cs @@ -1,4 +1,6 @@ using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; +using UnityEngine; namespace MiraAPI.GameModes; @@ -13,4 +15,6 @@ public class ClassicMode : AbstractGameMode /// public override string Description => "The classic Among Us experience!\nFind the Impostors or deceive the Crew."; + + public override LoadableAsset? Icon => MiraAssets.ClassicGamemodeIcon; } diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 6ec83ae..f1b5953 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -11,6 +11,7 @@ using MiraAPI.HnsReimplemented.Options; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; using PowerTools; using UnityEngine; @@ -29,7 +30,7 @@ public class HideAndSeekMode : AbstractGameMode public override string Description => "You can run, but you can't hide!\nOutlive the timer or eliminate the crew."; public override Color Color { get; } = new Color32(255, 88, 90, 255); - + public override LoadableAsset? Icon => MiraAssets.HnSGamemodeIcon; public override bool CanReport(DeadBody body) => false; public override bool ShouldShowSabotageMap(MapBehaviour map) => false; public override bool ShowGameModeIntroCutscene => true; diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 0468ef7..9190568 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -87,21 +87,24 @@ internal static class GameOptionsMenuPatch instance.Children.Add(opt); } - var mod = CustomGameModeManager.FindParentMod(gameMode); - if (mod is { MiraPlugin.ModIcon: not null }) + var icon = gameMode.Icon; + if (icon != null) { _modIcon.gameObject.SetActive(true); _gamemodeDescription.transform.localPosition = new Vector3(0.3f, 0, -0.01f); _gamemodeDescription.horizontalAlignment = HorizontalAlignmentOptions.Left; - var texture = mod.MiraPlugin.ModIcon.LoadAsset().texture; + var texture = icon.LoadAsset().texture; + var scale = Mathf.Min(128f / texture.width, 128f / texture.height); + var newSprite = Sprite.Create( texture, - new Rect(0, 0, 128, 128), + new Rect(0, 0, texture.width, texture.height), Vector2.one / 2, - 100 + 100f / scale ); + _modIcon.sprite = newSprite; - _modIcon.drawMode = SpriteDrawMode.Sliced; + _modIcon.drawMode = SpriteDrawMode.Simple; } else { @@ -247,7 +250,7 @@ internal static class GameOptionsMenuPatch { parent = modeInfoHolder, localPosition = new Vector3(-1.75f, 0, -0.1f), - localScale = new Vector3(0.4f, 0.4f, 1), + localScale = new Vector3(0.45f, 0.45f, 1), }, layer = gamemodeBg.layer, }; diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs index bb3e29d..6fb13ed 100644 --- a/MiraAPI/PluginLoading/IMiraPlugin.cs +++ b/MiraAPI/PluginLoading/IMiraPlugin.cs @@ -54,11 +54,6 @@ public interface IMiraPlugin /// public virtual string CustomOptionMenuTwoDescription => "Apply game settings for this mod!"; - /// - /// Gets the icon of the mod. - /// - public virtual LoadableAsset? ModIcon => null; - /// /// Gets the for the plugin. /// diff --git a/MiraAPI/Resources/ClassicGamemodeIcon.png b/MiraAPI/Resources/ClassicGamemodeIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..163ac1a1ae993df0b49f5bbe988e2f67620dbfb8 GIT binary patch literal 44061 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 10bceda..799f324 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -130,6 +130,16 @@ public static class MiraAssets ///

public static LoadableResourceAsset ModifierFile { get; } = new("MiraAPI.Resources.ModifierFile.png"); + /// + /// Gets the sprite used for the Classic gamemode. + /// + public static LoadableResourceAsset ClassicGamemodeIcon { get; } = new("MiraAPI.Resources.ClassicGamemodeIcon.png"); + + /// + /// Gets the sprite used for the Hide n Seek gamemode. + /// + public static LoadableResourceAsset HnSGamemodeIcon { get; } = new("MiraAPI.Resources.HnSGamemodeIcon.png"); + /// /// Gets the sprite used for timed modifier file in TaskAdderGame. /// From b4ed14f7f2914d8ca7636b2059f5fa4d7e98d4d0 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:52:51 -0500 Subject: [PATCH 088/102] Remove Hide n Seek port on release builds for now --- MiraAPI/GameModes/CustomGameModeManager.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index d14a841..8a6c7dd 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -94,11 +94,15 @@ public static class CustomGameModeManager // no need to add to game mode option as it already contains it // because we cannot have the option be created with no values defaultMode.ID = 0; - var hnsMode = new HideAndSeekMode(); - IdToModeMap.Add(1, hnsMode); - hnsMode.ID = 1; - GameModeOption.AddOption(hnsMode); - LastId++; + // TODO: Once Hide n Seek port is ready for release, we will remove this case. + if (MiraApiPlugin.IsDevBuild) + { + var hnsMode = new HideAndSeekMode(); + IdToModeMap.Add(1, hnsMode); + hnsMode.ID = 1; + GameModeOption.AddOption(hnsMode); + LastId++; + } } internal static void GetAndSetGameMode() From d1d11e2bec766d7f30a3928659510f38eb1eb4b6 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:22:45 -0500 Subject: [PATCH 089/102] Show gamemode change notification --- MiraAPI/GameModes/AbstractGameMode.cs | 6 ++++++ MiraAPI/GameModes/CustomGameModeManager.cs | 1 + MiraAPI/GameModes/GameModeOption.cs | 8 ++++++++ MiraAPI/GameOptions/ModdedOptionsManager.cs | 2 +- MiraAPI/Patches/Roles/GameStartupPatch.cs | 9 +++++++++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index cf3b2d8..efb7384 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -5,6 +5,7 @@ using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; +using TMPro; using UnityEngine; namespace MiraAPI.GameModes; @@ -35,6 +36,11 @@ public abstract class AbstractGameMode : IOptionable ///
public virtual LoadableAsset? Icon => null; + /// + /// Gets the icon of the gamemode for use on the options pop-up. + /// + public TMP_SpriteAsset TmpIcon { get; set; } = null!; + /// /// Gets the colored game mode name, using the color and name properties. /// diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 8a6c7dd..f529ee7 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; using Reactor.Utilities; namespace MiraAPI.GameModes; diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 9eb78ef..2d6d318 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -111,6 +111,14 @@ public static class GameModeOption Value = val; CustomGameModeManager.GetAndSetGameMode(); HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); + var gm = CustomGameModeManager.ActiveMode!; + ModdedOptionsManager.AddSettingsChangeMessage( + HudManager.Instance.Notifier, + GamemodeName, + gm.ColoredName, + new Color(0.7333f, 0.7333f, 0.7333f, 1), + gm.TmpIcon, + true); // could make Values a dict of AbstractGameMode too } diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index a1ba1ee..d91d079 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -38,7 +38,7 @@ public static class ModdedOptionsManager public static void AddSettingsChangeMessage(NotificationPopper notif, StringNames key, string value, Color textColor, TMP_SpriteAsset? sprite, bool playSound = true) { - var item = string.Empty; + string item; var text = textColor.ToTextColor(); if (sprite != null) { diff --git a/MiraAPI/Patches/Roles/GameStartupPatch.cs b/MiraAPI/Patches/Roles/GameStartupPatch.cs index 9f88bdd..94a7ec1 100644 --- a/MiraAPI/Patches/Roles/GameStartupPatch.cs +++ b/MiraAPI/Patches/Roles/GameStartupPatch.cs @@ -1,6 +1,8 @@ using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Utilities.Assets; namespace MiraAPI.Patches.Roles; @@ -22,6 +24,13 @@ public static class GameStartupPatch } _runOnce = true; + foreach (var mode in CustomGameModeManager.IdToModeMap.Values) + { + if (mode.Icon != null) + { + mode.TmpIcon = TmpSpriteUtils.CreateSpriteAsset(mode.Icon.LoadAsset(), $"MiraApi.Gamemode.{mode.Name.Replace(" ", string.Empty)}.png", 1.35f); + } + } if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) { From 95819cf83ae72aa6e021e7be6bc76d0b0ccdfc12 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:29:40 -0500 Subject: [PATCH 090/102] Add (unsynced) in-game timebar + adjust ui pos --- MiraAPI/GameModes/AbstractGameMode.cs | 60 +++++ MiraAPI/GameModes/HideAndSeekMode.cs | 68 +++++- .../HnsReimplemented/HideAndSeekHudHelper.cs | 227 ++++++++++++++++++ .../Patches/GameModes/GameManagerPatches.cs | 25 ++ .../Patches/GameModes/ProgressTrackerPatch.cs | 17 ++ MiraAPI/Patches/GameModes/TaskPanelPatch.cs | 17 ++ 6 files changed, 410 insertions(+), 4 deletions(-) create mode 100644 MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs create mode 100644 MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs create mode 100644 MiraAPI/Patches/GameModes/TaskPanelPatch.cs diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index efb7384..6780efb 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -218,6 +218,20 @@ public abstract class AbstractGameMode : IOptionable /// True if the sabotage map should be shown. public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true; + /// + /// Gets the to display on the map. + /// + /// The for the gamemode. + public virtual MapOptions GetMapOptions() + { + return new MapOptions + { + Mode = PlayerControl.LocalPlayer.Data.Role.IsImpostor && !MeetingHud.Instance + ? MapOptions.Modes.Sabotage + : MapOptions.Modes.Normal, + }; + } + /// /// Can a player vent in this gamemode. /// @@ -226,6 +240,52 @@ public abstract class AbstractGameMode : IOptionable /// True if venting is enabled in this mode. public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true; + /// + /// Does the task bar appear in the gamemode. + /// + /// True if the task bar is enabled in this mode. + public virtual bool ShowTaskBar => true; + + public virtual void UpdateTaskPanel(TaskPanelBehaviour instance) + { + instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f) + ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f) + : Vector3.zero; + Vector3 vector = instance.background.sprite.bounds.extents; + vector.y = -vector.y; + vector = vector.Mul(instance.background.transform.localScale); + instance.background.transform.localPosition = vector; + Vector3 vector2 = instance.tab.sprite.bounds.extents; + vector2 = vector2.Mul(instance.tab.transform.localScale); + vector2.y = -vector2.y; + vector2.x += vector.x * 2f; + instance.tab.transform.localPosition = vector2; + if (GameManager.Instance == null) + { + return; + } + + var yPos = 0.6f; + var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x; + instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z); + instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z); + if (instance.open) + { + instance.timer = Mathf.Min(1f, instance.timer + Time.deltaTime / instance.animationTimeSeconds); + } + else + { + instance.timer = Mathf.Max(0f, instance.timer - Time.deltaTime / instance.animationTimeSeconds); + } + + Vector3 relativePos = new( + Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), + yPos, + instance.openPosition.z); + instance.transform.localPosition = + AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); + } + /// public override string ToString() => Name; } diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index f1b5953..2c0f38e 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -7,6 +7,7 @@ using AmongUs.GameOptions; using HarmonyLib; using InnerNet; using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented; using MiraAPI.HnsReimplemented.Options; using MiraAPI.PluginLoading; using MiraAPI.Roles; @@ -39,6 +40,7 @@ public class HideAndSeekMode : AbstractGameMode public override bool ShowNormalRoleSettings => false; public override float DefaultImpostorKillCooldown => 1f; + public override bool ShowTaskBar => false; public static int ImpostorPlayerID() { return OptionGroupSingleton.Instance.SelectedSeeker.Value; @@ -73,7 +75,7 @@ public class HideAndSeekMode : AbstractGameMode } IGameOptions currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions; int adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count); - AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, adjustedNumImpostors, RoleTypes.Impostor); + AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, Math.Max(adjustedNumImpostors, 1), RoleTypes.Impostor); AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Engineer); } @@ -84,7 +86,7 @@ public class HideAndSeekMode : AbstractGameMode int teamMax, RoleTypes defaultRole) { - Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); + Error($"Hide And Seek Mode - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}"); int num = 0; IRoleOptionsCollection roleOptions = opts.RoleOptions; var source = RoleManager.Instance.AllRoles.ToArray() @@ -400,6 +402,25 @@ public class HideAndSeekMode : AbstractGameMode deadPlayerCount = 0; ShipStatus.Instance.BreakEmergencyButton(); PlayerControl.LocalPlayer.SetKillTimer(0.01f); + if (HudManager.InstanceExists) + { + HudManager.Instance.gameObject.AddComponent(); + } + } + public override MapOptions GetMapOptions() + { + MapOptions mapOptions = new MapOptions + { + Mode = MapOptions.Modes.Normal, + }; + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && HideAndSeekHudHelper.Instance.SeekerAdminMapEnabled(PlayerControl.LocalPlayer)) + { + mapOptions.Mode = MapOptions.Modes.CountOverlay; + mapOptions.AllowMovementWhileMapOpen = true; + mapOptions.IncludeDeadBodies = false; + mapOptions.ShowLivePlayerPosition = false; + } + return mapOptions; } public override void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) @@ -411,10 +432,10 @@ public class HideAndSeekMode : AbstractGameMode } if (Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor) != 0) { - /*if (instance.AllTimersExpired()) + if (HideAndSeekHudHelper.Instance.AllTimersExpired()) { instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_CrewmatesByTimer, !DataManager.Player.Ads.HasPurchasedAdRemoval); - }*/ + } return; } instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_ImpostorsByKills, !DataManager.Player.Ads.HasPurchasedAdRemoval); @@ -459,6 +480,45 @@ public class HideAndSeekMode : AbstractGameMode return PlayerBodyTypes.Normal; } } + public override void UpdateTaskPanel(TaskPanelBehaviour instance) + { + instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f) + ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f) + : Vector3.zero; + Vector3 vector = instance.background.sprite.bounds.extents; + vector.y = -vector.y; + vector = vector.Mul(instance.background.transform.localScale); + instance.background.transform.localPosition = vector; + Vector3 vector2 = instance.tab.sprite.bounds.extents; + vector2 = vector2.Mul(instance.tab.transform.localScale); + vector2.y = -vector2.y; + vector2.x += vector.x * 2f; + instance.tab.transform.localPosition = vector2; + if (GameManager.Instance == null) + { + return; + } + + var yPos = 1.6f; + var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x; + instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z); + instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z); + if (instance.open) + { + instance.timer = Mathf.Min(1f, instance.timer + Time.deltaTime / instance.animationTimeSeconds); + } + else + { + instance.timer = Mathf.Max(0f, instance.timer - Time.deltaTime / instance.animationTimeSeconds); + } + + Vector3 relativePos = new( + Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer), + yPos, + instance.openPosition.z); + instance.transform.localPosition = + AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); + } private int deadPlayerCount; public override void OnPlayerDeath(PlayerControl player, bool assignGhostRole) { diff --git a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs new file mode 100644 index 0000000..329c8dc --- /dev/null +++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs @@ -0,0 +1,227 @@ +using System.Collections; +using System.Linq; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HideAndSeekHudHelper Instance { get; private set; } + + public AudioClip FinalHideAlertSfx; + + public AudioClip FinalHideCountdownSfx; + + private AudioClip TaskFinishedSound; + + private const int SECONDS_TO_BEEP = 10; + + private const float SECONDS_TO_SET_DIRTY = 1f; + + private HideAndSeekTimerBar timerBar; + + private float totalHideTime = float.MaxValue; + + private float currentHideTime = float.MaxValue; + + private float totalFinalHideTime = float.MaxValue; + + private float currentFinalHideTime = float.MaxValue; + + private float secondsSinceLastSetDirty; + private Coroutine beepCoroutine; + + private void Awake() + { + Instance = this; + } + + public bool IsFinalCountdown + { + get { return currentHideTime <= 0f; } + } + + public bool SeekerAdminMapEnabled(PlayerControl player) + { + int item = Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor); + return !player.inVent && player.Data != null && player.Data.Role != null && + ((!player.inVent && player.Data.Role.IsImpostor && IsFinalCountdown && + OptionGroupSingleton.Instance.FinalHideSeekMap.Value) || + (player.Data.Role.IsImpostor && + item <= + (GameData.Instance.PlayerCount - 1) / 3)); + } + + public void OnTaskComplete(float timeDeduction) + { + if (timerBar != null) + { + timerBar.TaskComplete(); + } + + AdjustEscapeTimer(timeDeduction, true); + } + + public float GetTotalRoundTime() + { + float escapeTime = OptionGroupSingleton.Instance.HidingTime.Value; + float finalCountdownTime = OptionGroupSingleton.Instance.FinalHideTime.Value; + return escapeTime + finalCountdownTime; + } + + public float GetTotalTimeRemaining() + { + return currentHideTime + currentFinalHideTime; + } + + public float GetRoundTimeElapsed() + { + return GetTotalRoundTime() - GetTotalTimeRemaining(); + } + + public void Start() + { + totalHideTime = OptionGroupSingleton.Instance.HidingTime.Value; + currentHideTime = totalHideTime; + totalFinalHideTime = OptionGroupSingleton.Instance.FinalHideTime.Value; + currentFinalHideTime = totalFinalHideTime; + if (timerBar != null) + { + Object.Destroy(timerBar); + } + + FinalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX; + FinalHideCountdownSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideCountdownSFX; + TaskFinishedSound = GameManagerCreator.Instance.HideAndSeekManagerPrefab.TaskFinishedSound; + timerBar = Object.Instantiate( + GameManagerCreator.Instance.HideAndSeekManagerPrefab.TimerBarPrefab, + HudManager.Instance.transform.parent); + } + + public void OnGameEnd() + { + if (timerBar != null) + { + Object.Destroy(timerBar.gameObject); + } + + if (beepCoroutine != null) + { + StopCoroutine(beepCoroutine); + } + + beepCoroutine = null!; + } + + public void FixedUpdate() + { + secondsSinceLastSetDirty += Time.fixedDeltaTime; + if (IsFinalCountdown) + { + AdjustFinalEscapeTimer(Time.fixedDeltaTime); + return; + } + + AdjustEscapeTimer(Time.fixedDeltaTime, false); + } + + public void OnDestroy() + { + if (timerBar != null) + { + Object.Destroy(timerBar); + } + } + + private void OnFinalCountdownTriggered() + { + foreach (PlayerControl playerControl in PlayerControl.AllPlayerControls) + { + if (!playerControl.Data.Role.IsImpostor && !playerControl.Data.IsDead) + { + playerControl.ClearTasks(); + PlayerTask.GetOrCreateTask(playerControl, 0).Text = + DestroyableSingleton.Instance.GetString(StringNames.HideActionButton); + } + } + + if (!PlayerControl.LocalPlayer.Data.IsDead && Minigame.Instance != null) + { + Minigame instance = Minigame.Instance; + if (instance != null) + { + instance.ForceClose(); + } + } + + timerBar.StartFinalHide(); + SoundManager.Instance.PlaySound(FinalHideAlertSfx, false, 1f, null); + DestroyableSingleton.Instance.SetAlertOverlay(true); + } + + private void AdjustEscapeTimer(float timeDeduction, bool forceDirty) + { + float num = currentHideTime; + currentHideTime -= timeDeduction; + currentHideTime = Mathf.Max(currentHideTime, 0f); + if (currentHideTime <= 10f && beepCoroutine == null) + { + beepCoroutine = StartCoroutine(BeepAlmostEverySecond().WrapToIl2Cpp()); + } + + if (num > 0f && currentHideTime <= 0f) + { + OnFinalCountdownTriggered(); + } + + timerBar.UpdateTimer(currentHideTime, totalHideTime); + if (forceDirty || secondsSinceLastSetDirty > 1f) + { + secondsSinceLastSetDirty = 0f; + } + } + + private IEnumerator BeepAlmostEverySecond() + { + while (!IsFinalCountdown) + { + float num = currentHideTime / 10f; + float pitch = 1.5f - num / 2f; + SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch, null); + yield return new WaitForSeconds(1f); + } + + yield return Effects.Wait(currentFinalHideTime - 10f); + while (currentFinalHideTime > 0f) + { + float num2 = currentFinalHideTime / 10f; + float pitch2 = 1.5f - num2 / 2f; + SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch2, null); + yield return new WaitForSeconds(1f); + } + + yield break; + } + + private void AdjustFinalEscapeTimer(float timeDeduction) + { + currentFinalHideTime -= timeDeduction; + currentFinalHideTime = Mathf.Max(currentFinalHideTime, 0f); + timerBar.UpdateTimer(currentFinalHideTime, totalFinalHideTime); + if (secondsSinceLastSetDirty > 1f) + { + secondsSinceLastSetDirty = 0f; + } + } + + public bool AllTimersExpired() + { + return currentHideTime <= 0f && currentFinalHideTime <= 0f; + } +} diff --git a/MiraAPI/Patches/GameModes/GameManagerPatches.cs b/MiraAPI/Patches/GameModes/GameManagerPatches.cs index fca4f75..3f56e82 100644 --- a/MiraAPI/Patches/GameModes/GameManagerPatches.cs +++ b/MiraAPI/Patches/GameModes/GameManagerPatches.cs @@ -28,4 +28,29 @@ internal static class GameManagerPatches return true; } + [HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetMapOptions))] + [HarmonyPrefix] + public static bool GetMapOptions(ref MapOptions __result) + { + if (CustomGameModeManager.ActiveMode == null) + { + return true; + } + + __result = CustomGameModeManager.ActiveMode.GetMapOptions(); + return false; + } } + +[HarmonyPatch(typeof(GameStartManager))] +internal static class GameStartManagerPatches +{ + [HarmonyPrefix, HarmonyPatch(nameof(GameStartManager.Start))] + public static void StartPatch(GameStartManager __instance) + { + if (MiraApiPlugin.IsDevBuild) + { + __instance.MinPlayers = 1; + } + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs new file mode 100644 index 0000000..53620e0 --- /dev/null +++ b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; +[HarmonyPatch(typeof(ProgressTracker), nameof(ProgressTracker.Start))] +internal static class ProgressTrackerPatch +{ + public static bool Prefix(ProgressTracker __instance) + { + if (CustomGameModeManager.ActiveMode != null && !CustomGameModeManager.ActiveMode.ShowTaskBar) + { + __instance.gameObject.SetActive(false); + return false; + } + return true; + } +} diff --git a/MiraAPI/Patches/GameModes/TaskPanelPatch.cs b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs new file mode 100644 index 0000000..f312a9c --- /dev/null +++ b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using MiraAPI.GameModes; + +namespace MiraAPI.Patches.GameModes; +[HarmonyPatch(typeof(TaskPanelBehaviour), nameof(TaskPanelBehaviour.Update))] +internal static class TaskPanelPatch +{ + public static bool Prefix(TaskPanelBehaviour __instance) + { + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.UpdateTaskPanel(__instance); + return false; + } + return true; + } +} From 06c746c6320f55ff3e9605a790ffdef543a1fed7 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:24:07 -0500 Subject: [PATCH 091/102] Add most music logic --- MiraAPI/GameModes/HideAndSeekMode.cs | 13 +- .../HnsReimplemented/HideAndSeekHudHelper.cs | 58 +++++ MiraAPI/HnsReimplemented/HnsDangerMeter.cs | 142 +++++++++++ MiraAPI/HnsReimplemented/HnsMusicHandler.cs | 237 ++++++++++++++++++ 4 files changed, 443 insertions(+), 7 deletions(-) create mode 100644 MiraAPI/HnsReimplemented/HnsDangerMeter.cs create mode 100644 MiraAPI/HnsReimplemented/HnsMusicHandler.cs diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 2c0f38e..dfbed5c 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -309,12 +309,7 @@ public class HideAndSeekMode : AbstractGameMode __instance.HideAndSeekPanels.SetActive(false); __instance.CrewmateRules.SetActive(false); __instance.ImpostorRules.SetActive(false); - /*LogicOptionsHnS logicOptionsHnS = GameManager.Instance.LogicOptions as LogicOptionsHnS; - LogicHnSMusic logicHnSMusic = GameManager.Instance.GetLogicComponent() as LogicHnSMusic; - if (logicHnSMusic != null) - { - logicHnSMusic.StartMusicWithIntro(); - }*/ + HnsMusicHandler.Instance.StartMusicWithIntro(); var hideTimer = 10f; if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) @@ -373,7 +368,7 @@ public class HideAndSeekMode : AbstractGameMode } else { - ShipStatus.Instance.HideCountdown = hideTimer; + HideAndSeekHudHelper.Instance.HideCountdown = hideTimer; if (AprilFoolsMode.ShouldHorseAround()) { if (impostor != null) @@ -395,6 +390,8 @@ public class HideAndSeekMode : AbstractGameMode } } ShipStatus.Instance.StartSFX(); + HnsMusicHandler.Instance.OnGameStart(); + HnsDangerMeter.Instance.OnGameStart(); UnityEngine.Object.Destroy(__instance.gameObject); } public override void Initialize() @@ -404,7 +401,9 @@ public class HideAndSeekMode : AbstractGameMode PlayerControl.LocalPlayer.SetKillTimer(0.01f); if (HudManager.InstanceExists) { + HudManager.Instance.gameObject.AddComponent(); HudManager.Instance.gameObject.AddComponent(); + HudManager.Instance.gameObject.AddComponent(); } } public override MapOptions GetMapOptions() diff --git a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs index 329c8dc..d41b445 100644 --- a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs +++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs @@ -118,7 +118,65 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr) beepCoroutine = null!; } + private float taskDirtyTimer; + public float HideCountdown { get; set; } + public void LateUpdate() + { + if (!HudManager.InstanceExists) + { + return; + } + if (HideCountdown > 0f && taskDirtyTimer > 0.25f) + { + float num = taskDirtyTimer; + taskDirtyTimer = 0f; + if (!PlayerControl.LocalPlayer) + { + HudManager.Instance.TaskPanel.SetTaskText(string.Empty); + return; + } + NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data; + if (data == null) + { + return; + } + bool flag = data.Role != null && data.Role.IsImpostor; + HudManager.Instance.tasksString.Clear(); + if (PlayerControl.LocalPlayer.myTasks == null || PlayerControl.LocalPlayer.myTasks.Count == 0) + { + HudManager.Instance.tasksString.Append("None"); + } + else + { + for (int i = 0; i < PlayerControl.LocalPlayer.myTasks.Count; i++) + { + PlayerTask playerTask = PlayerControl.LocalPlayer.myTasks[i]; + if (playerTask) + { + if (playerTask.TaskType == TaskTypes.FixComms && !flag) + { + HudManager.Instance.tasksString.Clear(); + playerTask.AppendTaskText(HudManager.Instance.tasksString); + break; + } + playerTask.AppendTaskText(HudManager.Instance.tasksString); + } + } + if (data.Role != null) + { + data.Role.AppendTaskHint(HudManager.Instance.tasksString); + } + if (HideCountdown > 0f) + { + HideCountdown -= num; + HudManager.Instance.tasksString.Append("\n\n" + ((int)HideCountdown)); + } + HudManager.Instance.tasksString.TrimEnd(); + } + HudManager.Instance.TaskPanel.SetTaskText(HudManager.Instance.tasksString.ToString()); + } + } public void FixedUpdate() { secondsSinceLastSetDirty += Time.fixedDeltaTime; diff --git a/MiraAPI/HnsReimplemented/HnsDangerMeter.cs b/MiraAPI/HnsReimplemented/HnsDangerMeter.cs new file mode 100644 index 0000000..d36c198 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HnsDangerMeter.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HnsDangerMeter(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HnsDangerMeter Instance; + + private void Awake() + { + Instance = this; + } + + private DangerMeter dangerMeter; + + private List impostors = []; + + private float scaryMusicDistance; + + private float veryScaryMusicDistance; + + private float dangerLevel1; + + private float dangerLevel2; + + private bool firstMusicActivation; + + private float firstCrossfadeCountdown; + + public void FixedUpdate() + { + PlayerControl localPlayer = PlayerControl.LocalPlayer; + if (impostors == null || localPlayer == null) + { + return; + } + + if (impostors.Count <= 0) + { + return; + } + + float num = float.MaxValue; + foreach (PlayerControl playerControl in impostors) + { + if (!(playerControl == null)) + { + float sqrMagnitude = (playerControl.transform.position - localPlayer.transform.position).sqrMagnitude; + if (sqrMagnitude < scaryMusicDistance && num > sqrMagnitude) + { + num = sqrMagnitude; + } + } + } + + if (HideAndSeekHudHelper.Instance.HideCountdown > 0f) + { + dangerLevel1 = 0f; + dangerLevel2 = 0f; + } + else + { + if (firstMusicActivation) + { + firstMusicActivation = false; + firstCrossfadeCountdown = 3f; + HnsMusicHandler.Instance.SetMusicCrossfadeSpeed(0.6f); + } + + if (firstCrossfadeCountdown > 0f) + { + firstCrossfadeCountdown -= Time.deltaTime; + if (firstCrossfadeCountdown <= 0f) + { + HnsMusicHandler.Instance.SetMusicCrossfadeSpeed(5f); + } + } + + dangerLevel1 = Mathf.Clamp01( + (scaryMusicDistance - num) / (scaryMusicDistance - veryScaryMusicDistance)); + dangerLevel2 = Mathf.Clamp01((veryScaryMusicDistance - num) / veryScaryMusicDistance); + } + + UpdateDangerMeter(); + UpdateDangerMusic(); + } + + private void UpdateDangerMusic() + { + PlayerControl localPlayer = PlayerControl.LocalPlayer; + if (localPlayer != null && localPlayer.Data != null && localPlayer.Data.IsDead) + { + HnsMusicHandler.Instance.SetTaskState(false); + HnsMusicHandler.Instance.ResetMusic(); + return; + } + + HnsMusicHandler.Instance.SetMusicValues(dangerLevel1, dangerLevel2); + } + + private void UpdateDangerMeter() + { + if (dangerMeter == null) + { + return; + } + + dangerMeter.SetDangerValue(dangerLevel1, dangerLevel2); + } + + public void OnGameStart() + { + firstMusicActivation = true; + if (!PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + dangerMeter = HudManager.Instance.DangerMeter; + dangerMeter.gameObject.SetActive(true); + } + + impostors = Helpers.GetAlivePlayers().Where(x => x.Data.Role.IsImpostor).ToList(); + + var baseSpeed = OptionGroupSingleton.Instance.PlayerSpeed.Value; + scaryMusicDistance = 55f * + baseSpeed; + veryScaryMusicDistance = 15f * + baseSpeed; + if (scaryMusicDistance < veryScaryMusicDistance) + { + float num = veryScaryMusicDistance; + float num2 = scaryMusicDistance; + scaryMusicDistance = num; + veryScaryMusicDistance = num2; + } + } +} diff --git a/MiraAPI/HnsReimplemented/HnsMusicHandler.cs b/MiraAPI/HnsReimplemented/HnsMusicHandler.cs new file mode 100644 index 0000000..e3b7d68 --- /dev/null +++ b/MiraAPI/HnsReimplemented/HnsMusicHandler.cs @@ -0,0 +1,237 @@ +using System.Collections.Generic; +using Amongus.GameModes.HideAndSeek; +using MiraAPI.GameOptions; +using MiraAPI.HnsReimplemented.Options; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.HnsReimplemented; + +[RegisterInIl2Cpp] +public sealed class HnsMusicHandler(nint cppPtr) : MonoBehaviour(cppPtr) +{ + public static HnsMusicHandler Instance; + + private void Awake() + { + Instance = this; + } + + private HideAndSeekMusicCollection musicCollection; + + private float lastMusicSyncTime; + + private bool isDoingTask; + + private float normalVolume; + + private float taskVolume; + + private float dangerLevel1Volume; + + private float dangerLevel2Volume; + + private AudioSource normalSource; + + private AudioSource taskSource; + + private AudioSource dangerLevel1Source; + + private AudioSource dangerLevel2Source; + + private float musicLerpSpeed = 5f; + + public void Start() + { + musicCollection = GameManagerCreator.Instance.HideAndSeekManagerPrefab.MusicCollection; + } + + public void OnGameStart() + { + InitMusic(); + ResetMusic(); + } + + public void OnDestroy() + { + ResetMusic(); + } + + private void InitMusic() + { + if (normalSource == null) + { + normalSource = + SoundManager.Instance.GetNamedSfxSource(musicNames[LogicHnSMusic.HideAndSeekMusicTrack.Normal]); + } + + normalSource.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + normalSource.clip = musicCollection.NormalMusic; + normalSource.loop = true; + if (taskSource == null) + { + taskSource = + SoundManager.Instance.GetNamedSfxSource(musicNames[LogicHnSMusic.HideAndSeekMusicTrack.Task]); + } + + taskSource.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + taskSource.volume = 0f; + taskSource.clip = musicCollection.TaskMusic; + taskSource.loop = true; + if (dangerLevel1Source == null) + { + dangerLevel1Source = + SoundManager.Instance.GetNamedSfxSource( + musicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1]); + } + + dangerLevel1Source.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + dangerLevel1Source.volume = 0f; + dangerLevel1Source.clip = musicCollection.DangerLevel1Music; + dangerLevel1Source.loop = true; + if (dangerLevel2Source == null) + { + dangerLevel2Source = + SoundManager.Instance.GetNamedSfxSource( + musicNames[LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2]); + } + + dangerLevel2Source.outputAudioMixerGroup = SoundManager.Instance.MusicChannel; + dangerLevel2Source.volume = 0f; + dangerLevel2Source.clip = musicCollection.DangerLevel2Music; + dangerLevel2Source.loop = true; + normalSource.Play(); + taskSource.Play(); + dangerLevel1Source.Play(); + dangerLevel2Source.Play(); + SyncMusic(); + } + + public void StartMusicWithIntro() + { + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + AudioClip clip = OptionGroupSingleton.Instance.HidingTime.Value <= 180f + ? musicCollection.ImpostorShortMusic + : musicCollection.ImpostorLongMusic; + if (AprilFoolsMode.ShouldHorseAround()) + { + clip = musicCollection.ImpostorRanchMusic; + } + + SoundManager.Instance.PlaySound(clip, true, 1f, SoundManager.Instance.MusicChannel); + } + } + + public void SetTaskState(bool isDoingTask) + { + isDoingTask = isDoingTask; + } + + public void FixedUpdate() + { + if (normalSource == null || taskSource == null || dangerLevel1Source == null || + dangerLevel2Source == null) + { + return; + } + + if (Time.unscaledTime > lastMusicSyncTime + 1f) + { + SyncMusic(); + } + + normalSource.volume = Mathf.Lerp( + normalSource.volume, + normalVolume, + Time.fixedDeltaTime * musicLerpSpeed); + taskSource.volume = Mathf.Lerp( + taskSource.volume, + taskVolume, + Time.fixedDeltaTime * musicLerpSpeed); + dangerLevel1Source.volume = Mathf.Lerp( + dangerLevel1Source.volume, + dangerLevel1Volume, + Time.fixedDeltaTime * musicLerpSpeed); + dangerLevel2Source.volume = Mathf.Lerp( + dangerLevel2Source.volume, + dangerLevel2Volume, + Time.fixedDeltaTime * musicLerpSpeed); + } + + private void SyncMusic() + { + taskSource.timeSamples = normalSource.timeSamples; + dangerLevel1Source.timeSamples = normalSource.timeSamples; + dangerLevel2Source.timeSamples = normalSource.timeSamples; + lastMusicSyncTime = Time.unscaledTime; + } + + public void ResetMusic() + { + SetMusicValues(0f, 0f); + } + + public void SetMusicCrossfadeSpeed(float lerpSpeed) + { + musicLerpSpeed = lerpSpeed; + } + + public void SetMusicValues(float dangerLevel1, float dangerLevel2) + { + if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.Data.Role.IsImpostor) + { + return; + } + + if (normalSource == null || taskSource == null || dangerLevel1Source == null || + dangerLevel2Source == null) + { + return; + } + + normalVolume = (isDoingTask ? 0f : 1f); + taskVolume = (isDoingTask ? 1f : 0f); + dangerLevel1Volume = 0f; + dangerLevel2Volume = 0f; + if (dangerLevel1 > 0f) + { + dangerLevel1Volume = dangerLevel1; + if (isDoingTask) + { + taskVolume = 1f - dangerLevel1; + } + else + { + normalVolume = 1f - dangerLevel1; + } + } + + if (dangerLevel2 > 0f) + { + dangerLevel2Volume = dangerLevel2; + dangerLevel1Volume = 1f - dangerLevel2; + } + } + + private readonly Dictionary musicNames = + new Dictionary + { + { + LogicHnSMusic.HideAndSeekMusicTrack.Normal, + "HnS_Music_Normal" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.Task, + "HnS_Music_Task" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel1, + "HnS_Music_DangerLevel1" + }, + { + LogicHnSMusic.HideAndSeekMusicTrack.DangerLevel2, + "HnS_Music_DangerLevel2" + }, + }; +} From 0e3b6be5be94c3a426e68c9c3b4909c66428a8da Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:46:19 -0400 Subject: [PATCH 092/102] show different plugin gamemodes in lobby view pane --- .../Patches/Options/LobbyViewPanePatches.cs | 31 +++++++++++++++---- MiraAPI/PluginLoading/MiraPluginManager.cs | 6 ++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index dcde18b..ec44875 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -24,9 +24,11 @@ public static class LobbyViewPanePatches { private static int SelectedModIdx { get; set; } + private static MiraPluginInfo[] Plugins => MiraPluginManager.Instance.PluginsWithOptionsOrGameModes; + private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0 ? null - : MiraPluginManager.Instance.RegisteredPluginsWithOptions[SelectedModIdx - 1]; + : Plugins[SelectedModIdx - 1]; private static PassiveButton? ModifiersTabButton { get; set; } @@ -76,7 +78,7 @@ public static class LobbyViewPanePatches (UnityAction)(() => { SelectedModIdx += 1; - if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length) + if (SelectedModIdx > Plugins.Length) { SelectedModIdx = 0; } @@ -100,7 +102,7 @@ public static class LobbyViewPanePatches SelectedModIdx -= 1; if (SelectedModIdx < 0) { - SelectedModIdx = MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length; + SelectedModIdx = Plugins.Length; } Refresh(__instance); @@ -109,7 +111,22 @@ public static class LobbyViewPanePatches private static void Refresh(LobbyViewSettingsPane menu) { - ModifiersTabButton?.gameObject.SetActive(SelectedModIdx != 0); + if (SelectedMod == null) + { + ModifiersTabButton?.gameObject.SetActive(false); + menu.rolesTabButton.gameObject.SetActive(true); + } + else + { + var showModifierButton = SelectedMod.InternalOptionGroups.Any(m => + m.ParentMenu is MenuCategory.Modifiers || m.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true); + var showRolesButton = SelectedMod.InternalOptionGroups.Any(r => + r.ParentMenu is MenuCategory.Roles || r.OptionableType?.IsAssignableTo(typeof(ICustomRole)) == true); + + ModifiersTabButton?.gameObject.SetActive(showModifierButton); + menu.rolesTabButton.gameObject.SetActive(showRolesButton); + } + menu.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Main"; menu.RefreshTab(); menu.scrollBar.ScrollToTop(); @@ -142,7 +159,7 @@ public static class LobbyViewPanePatches [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))] public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance) { - if (SelectedModIdx == 0 || SelectedMod == null) + if ((CustomGameModeManager.IsClassic() || CustomGameModeManager.IsHideNSeek()) && (SelectedModIdx == 0 || SelectedMod == null)) { return true; } @@ -153,7 +170,9 @@ public static class LobbyViewPanePatches } var filteredGroups = SelectedMod.InternalOptionGroups - .Where(x => x is { ParentMenu: not MenuCategory.Modifiers, OptionableType: null } && x.GroupVisible()); + .Where(x => (x is { ParentMenu: not MenuCategory.Modifiers, OptionableType: null } + || x.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true) + && x.GroupVisible()); DrawOptions(__instance, filteredGroups); return false; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index af78e8b..a98a8cd 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -36,8 +36,9 @@ public sealed class MiraPluginManager { private readonly Dictionary _registeredPlugins = []; - internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!; - internal MiraPluginInfo[] RegisteredPluginsWithOptions { get; private set; } = null!; + internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = []; + internal MiraPluginInfo[] RegisteredPluginsWithOptions { get; private set; } = []; + internal MiraPluginInfo[] PluginsWithOptionsOrGameModes { get; private set; } = []; internal Dictionary> QueuedRoleRegistrations { get; } = []; internal static MiraPluginManager Instance { get; private set; } = new(); @@ -253,6 +254,7 @@ public sealed class MiraPluginManager // Cache all the registered plugins into an array for easy access RegisteredPlugins = [.. _registeredPlugins.Values]; RegisteredPluginsWithOptions = [.. RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu)]; + PluginsWithOptionsOrGameModes = [..RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu || m.GameModes.Count > 0)]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); From 721bd20d2bb2a9229da77aa4c6b465a6f93bdaa1 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:08:08 -0400 Subject: [PATCH 093/102] improve lobby view pane gamemode handling --- .../Patches/Options/LobbyViewPanePatches.cs | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index ec44875..a048c33 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -24,7 +24,7 @@ public static class LobbyViewPanePatches { private static int SelectedModIdx { get; set; } - private static MiraPluginInfo[] Plugins => MiraPluginManager.Instance.PluginsWithOptionsOrGameModes; + private static MiraPluginInfo[] Plugins => MiraPluginManager.Instance.RegisteredPluginsWithOptions; private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0 ? null @@ -34,6 +34,41 @@ public static class LobbyViewPanePatches private static StringNames ModifiersTabName { get; } = CustomStringName.CreateAndRegister("ModifiersTab"); + private static AbstractGameMode? GetCustomGamemode() + { + if (!CustomGameModeManager.IsClassic() && !CustomGameModeManager.IsHideNSeek()) + { + return CustomGameModeManager.ActiveMode; + } + + return null; + } + + private static IEnumerable? GetCustomGamemodeOptions() + { + if (CustomGameModeManager.ActiveMode == null) + { + return null; + } + + if (!CustomGameModeManager.IsClassic() && !CustomGameModeManager.IsHideNSeek()) + { + var plugin = CustomGameModeManager.FindParentMod(CustomGameModeManager.ActiveMode); + + return plugin?.InternalOptionGroups + .Where(x => x.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true && x.GroupVisible()); + } + + return null; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.OnEnable))] + public static void OnEnable(LobbyViewSettingsPane __instance) + { + Refresh(__instance); + } + [HarmonyPostfix] [HarmonyPatch(nameof(LobbyViewSettingsPane.Awake))] public static void AwakePatch(LobbyViewSettingsPane __instance) @@ -113,8 +148,10 @@ public static class LobbyViewPanePatches { if (SelectedMod == null) { + var cgm = GetCustomGamemode(); ModifiersTabButton?.gameObject.SetActive(false); - menu.rolesTabButton.gameObject.SetActive(true); + menu.rolesTabButton.gameObject.SetActive(cgm is null or { ShowNormalRoleSettings: true }); + menu.gameModeText.text = cgm is null ? "Main" : cgm.ColoredName; } else { @@ -125,9 +162,9 @@ public static class LobbyViewPanePatches ModifiersTabButton?.gameObject.SetActive(showModifierButton); menu.rolesTabButton.gameObject.SetActive(showRolesButton); + menu.gameModeText.text = SelectedMod.PluginInfo.Metadata.Name; } - menu.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Main"; menu.RefreshTab(); menu.scrollBar.ScrollToTop(); } @@ -159,8 +196,15 @@ public static class LobbyViewPanePatches [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))] public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance) { - if ((CustomGameModeManager.IsClassic() || CustomGameModeManager.IsHideNSeek()) && (SelectedModIdx == 0 || SelectedMod == null)) + if (SelectedMod == null) { + var gamemodeOpts = GetCustomGamemodeOptions(); + if (gamemodeOpts != null) + { + DrawOptions(__instance, gamemodeOpts); + return false; + } + return true; } @@ -170,9 +214,7 @@ public static class LobbyViewPanePatches } var filteredGroups = SelectedMod.InternalOptionGroups - .Where(x => (x is { ParentMenu: not MenuCategory.Modifiers, OptionableType: null } - || x.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true) - && x.GroupVisible()); + .Where(x => x is { ParentMenu: not MenuCategory.Modifiers, OptionableType: null } && x.GroupVisible()); DrawOptions(__instance, filteredGroups); return false; From cf1a75f5eab02b702d372978d82c58a2bb18877c Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sun, 16 Aug 2026 11:16:37 -0400 Subject: [PATCH 094/102] Fix gamemode options visibility --- .../Patches/Options/GameOptionsMenuPatch.cs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 9190568..34566c0 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -371,6 +371,7 @@ internal static class GameOptionsMenuPatch foreach (var group in filteredGroups) { CreateGroup(instance, group, container, ref newNum, ref optionBehaviours, ref categoryHeaders); + group.Ready = true; } _gameModeHeaders.Add(mode, categoryHeaders); _gameModeOptions.Add(mode, optionBehaviours); @@ -403,14 +404,14 @@ internal static class GameOptionsMenuPatch // ReSharper disable once InconsistentNaming public static void UpdatePatch(GameOptionsMenu __instance) { - if (MenuState.Instance.CurrentModIdx == 0) - { - return; - } - var num = 2.1f; + if (MenuState.Instance.CurrentModIdx == 0 && _gameModeGroups.Count <= 0) return; + var num = MenuState.Instance.CurrentMenu == MenuCategory.Game ? -1.217f : 2.1f; switch (MenuState.Instance.CurrentMenu) { + case MenuCategory.Game: + GamemodeOptionsUpdate(ref num); + break; case MenuCategory.Modifiers: ModifiersUpdate(ref num); break; @@ -587,6 +588,26 @@ internal static class GameOptionsMenuPatch } } + private static void GamemodeOptionsUpdate(ref float num) + { + var mode = CustomGameModeManager.ActiveMode; + if (mode == null) return; + + if (mode.ShowNormalGameSettings) + { + num += AdditionalVanillaScrollNum; + } + + if (!_gameModeGroups.TryGetValue(mode, out var groups) || groups.Count == 0) + { + return; + } + + foreach (var group in groups) + { + UpdateGroup(group, ref num); + } + } private static void CustomMenuOneUpdate(ref float num) { var groups = MenuState.Instance.CurrentMod.InternalOptionGroups From 5d54a0c2d3f0f2a2869b60e43b791e59e8f72918 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:16:37 -0400 Subject: [PATCH 095/102] cleanup this file --- MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index c31b519..d4df499 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,17 +1,4 @@ -using System; -using System.Collections.Generic; -using HarmonyLib; -using MiraAPI.GameOptions; -using MiraAPI.Modifiers; -using MiraAPI.PluginLoading; -using MiraAPI.Presets; -using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; -using TMPro; -using UnityEngine; -using UnityEngine.Events; -using static UnityEngine.UI.Button; -using Object = UnityEngine.Object; +using HarmonyLib; namespace MiraAPI.Patches.Options; From fb164a54e38b99f32efa438a69c295a5a93735f9 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:05:30 -0500 Subject: [PATCH 096/102] Fix broken game options tab for mods --- .../Patches/Options/GameOptionsMenuPatch.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 34566c0..ba3d268 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Linq; using BepInEx.Unity.IL2CPP.Utils.Collections; @@ -404,14 +404,14 @@ internal static class GameOptionsMenuPatch // ReSharper disable once InconsistentNaming public static void UpdatePatch(GameOptionsMenu __instance) { - if (MenuState.Instance.CurrentModIdx == 0 && _gameModeGroups.Count <= 0) return; - var num = MenuState.Instance.CurrentMenu == MenuCategory.Game ? -1.217f : 2.1f; + if (MenuState.Instance.CurrentModIdx == 0 && _gameModeGroups.Count <= 0) + { + return; + } + var num = 2.1f; switch (MenuState.Instance.CurrentMenu) { - case MenuCategory.Game: - GamemodeOptionsUpdate(ref num); - break; case MenuCategory.Modifiers: ModifiersUpdate(ref num); break; @@ -422,9 +422,15 @@ internal static class GameOptionsMenuPatch CustomMenuTwoUpdate(ref num); break; default: + if (MenuState.Instance.CurrentModIdx == 0) + { + num = -1.217f; + GamemodeOptionsUpdate(ref num); + break; + } var filteredGroups = MenuState.Instance.CurrentMod.InternalOptionGroups - .Where(x => x.OptionableType == null && + .Where(x => (x.OptionableType == null || CustomGameModeManager.ActiveMode!.GetType().IsAssignableTo(x.OptionableType)) && x.ParentMenu == MenuCategory.Game) ?? []; foreach (var group in filteredGroups) From b83cb5cc299a5997922be368972bb41d78915a5d Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:56:48 -0500 Subject: [PATCH 097/102] Force role menu to be refreshed on gamemode change --- MiraAPI/GameModes/GameModeOption.cs | 26 ++++++++++++++----- MiraAPI/Patches/Options/MenuState.cs | 2 ++ .../Patches/Options/RoleSettingMenuPatches.cs | 21 ++++++++++++++- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 2d6d318..95acd9c 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -109,16 +109,28 @@ public static class GameModeOption internal static void Set(int val) { Value = val; + var previousMode = CustomGameModeManager.ActiveMode; CustomGameModeManager.GetAndSetGameMode(); HudPatches.SetGameModeText(CustomGameModeManager.GetMode(Values.ElementAt(_lastValue).Key).ColoredName); var gm = CustomGameModeManager.ActiveMode!; - ModdedOptionsManager.AddSettingsChangeMessage( - HudManager.Instance.Notifier, - GamemodeName, - gm.ColoredName, - new Color(0.7333f, 0.7333f, 0.7333f, 1), - gm.TmpIcon, - true); + if (gm != previousMode) + { + ModdedOptionsManager.AddSettingsChangeMessage( + HudManager.Instance.Notifier, + GamemodeName, + gm.ColoredName, + new Color(0.7333f, 0.7333f, 0.7333f, 1), + gm.TmpIcon, + true); + if (MenuState.Instance) + { + // should force reset roles for gamemodes properly + foreach (var roleMenu in MenuState.Instance.QueuedRoleMenuRefresh) + { + MenuState.Instance.QueuedRoleMenuRefresh[roleMenu.Key] = true; + } + } + } // could make Values a dict of AbstractGameMode too } diff --git a/MiraAPI/Patches/Options/MenuState.cs b/MiraAPI/Patches/Options/MenuState.cs index 6a7ba33..b52bd95 100644 --- a/MiraAPI/Patches/Options/MenuState.cs +++ b/MiraAPI/Patches/Options/MenuState.cs @@ -63,6 +63,8 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] internal Dictionary FinishedRoleMenus { get; } = []; + [HideFromIl2Cpp] + internal Dictionary QueuedRoleMenuRefresh { get; } = []; public void Awake() { diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 953796b..8faee3b 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -75,7 +75,24 @@ public static class RoleSettingMenuPatches dividerImage.gameObject.SetActive(false); } - if (MenuState.Instance.FinishedRoleMenus.TryGetValue(MenuState.Instance.CurrentModIdx, out var finished) && finished) + var queuedRefresh = false; + if (MenuState.Instance.QueuedRoleMenuRefresh.TryGetValue(MenuState.Instance.CurrentModIdx, out var queued) && queued) + { + queuedRefresh = true; + var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); + var headers = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); + foreach (var child in roleOptionSettings) + { + Object.Destroy(child.gameObject); + } + foreach (var child in headers) + { + Object.Destroy(child.gameObject); + } + yield return new WaitForEndOfFrame(); + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; + } + if (!queuedRefresh && MenuState.Instance.FinishedRoleMenus.TryGetValue(MenuState.Instance.CurrentModIdx, out var finished) && finished) { var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); foreach (var r in roleOptionSettings) @@ -308,6 +325,7 @@ public static class RoleSettingMenuPatches RoleGroupHidden[group] = !groupHidden; } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = false; foreach (var child in roleMenu.RoleChancesSettings.transform) @@ -327,6 +345,7 @@ public static class RoleSettingMenuPatches } } } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = true; } From e22f4daf3ee540ec27509c5393878d71ce34218a Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:00:24 -0500 Subject: [PATCH 098/102] Fix the first role option being duped in the menu --- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 8faee3b..cc15a16 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -171,6 +171,17 @@ public static class RoleSettingMenuPatches ScrollerNum = 0.522f; var num4 = 0; + var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(false); + var headers = roleMenu.scrollBar.Inner.GetComponentsInChildren(false); + foreach (var child in roleOptionSettings) + { + Object.Destroy(child.gameObject); + } + foreach (var child in headers) + { + Object.Destroy(child.gameObject); + } + yield return new WaitForEndOfFrame(); var roleGroups = MenuState.Instance.CurrentMod.InternalRoles.Values.OfType() .ToLookup(x => x.RoleOptionsGroup); From 01fef0f747d5eb83846831dc778e564e764eb251 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:36:57 -0500 Subject: [PATCH 099/102] Fix vanilla advanced role settings tab breaking --- .../Patches/Options/RoleSettingMenuPatches.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index cc15a16..c66ea52 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -547,6 +547,26 @@ public static class RoleSettingMenuPatches CurrentRoleOptions = filteredOptions; } + [HarmonyPrefix] + [HarmonyPatch(nameof(RolesSettingsMenu.ChangeTab))] + public static void ChangeTabPatch(RolesSettingsMenu __instance) + { + var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); + categoryHeaderMasked.Title.text = TranslationController.Instance.GetString(StringNames.RoleSettingsLabel); + var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground"); + var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); + imgBg.gameObject.SetActive(true); + __instance.roleScreenshot.gameObject.SetActive(true); + __instance.roleDescriptionText.transform.parent.localPosition = new Vector3(2.5176f, -0.2731f, -1f); + __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.0675f, 0.1494f, 0.5687f); + labelBg.transform.localPosition = new Vector3(1.082f, 0.1054f, -2.5f); + __instance.roleScreenshot.drawMode = SpriteDrawMode.Simple; + foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren()) + { + optBehaviour.gameObject.Destroy(); + } + } + private static void ChangeTab(RoleBehaviour role, RolesSettingsMenu __instance) { if (role is not ICustomRole customRole) From a491592669beac633fbe1e009671da53bcb65e38 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:01:58 -0500 Subject: [PATCH 100/102] Allow gamemodes to be hidden when registered --- MiraAPI/GameModes/AbstractGameMode.cs | 5 +++++ MiraAPI/GameModes/CustomGameModeManager.cs | 17 +++++++++-------- MiraAPI/GameModes/HideAndSeekMode.cs | 3 +++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs index 6780efb..6fa2d8c 100644 --- a/MiraAPI/GameModes/AbstractGameMode.cs +++ b/MiraAPI/GameModes/AbstractGameMode.cs @@ -21,6 +21,11 @@ public abstract class AbstractGameMode : IOptionable ///
public abstract string Name { get; } + /// + /// Gets a value indicating whether the mode is visible in the menu. Only checked at startup. + /// + public virtual bool HideMode => false; + /// /// Gets the game mode description. /// diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index f529ee7..30877c3 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using MiraAPI.PluginLoading; -using MiraAPI.Utilities.Assets; using Reactor.Utilities; namespace MiraAPI.GameModes; @@ -40,10 +39,13 @@ public static class CustomGameModeManager return false; } - IdToModeMap.Add(GetNextId(), mode); - pluginInfo.GameModes.Add(LastId, mode); - mode.ID = LastId; - GameModeOption.AddOption(mode); + if (!mode.HideMode) + { + IdToModeMap.Add(GetNextId(), mode); + pluginInfo.GameModes.Add(LastId, mode); + mode.ID = LastId; + GameModeOption.AddOption(mode); + } return true; } @@ -95,10 +97,9 @@ public static class CustomGameModeManager // no need to add to game mode option as it already contains it // because we cannot have the option be created with no values defaultMode.ID = 0; - // TODO: Once Hide n Seek port is ready for release, we will remove this case. - if (MiraApiPlugin.IsDevBuild) + var hnsMode = new HideAndSeekMode(); + if (!hnsMode.HideMode) { - var hnsMode = new HideAndSeekMode(); IdToModeMap.Add(1, hnsMode); hnsMode.ID = 1; GameModeOption.AddOption(hnsMode); diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index dfbed5c..d5c90e8 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -30,6 +30,9 @@ public class HideAndSeekMode : AbstractGameMode /// public override string Description => "You can run, but you can't hide!\nOutlive the timer or eliminate the crew."; + // TODO: Once the Hide n Seek port is ready for release, we will remove this. + public override bool HideMode => !MiraApiPlugin.IsDevBuild; + public override Color Color { get; } = new Color32(255, 88, 90, 255); public override LoadableAsset? Icon => MiraAssets.HnSGamemodeIcon; public override bool CanReport(DeadBody body) => false; From 73e4506016f611af48cc2ad32b62c421c94d7fc8 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:41:24 -0500 Subject: [PATCH 101/102] Fix role tab issue --- MiraAPI/Patches/GameModes/TaskPanelPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/GameModes/TaskPanelPatch.cs b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs index f312a9c..30a30cc 100644 --- a/MiraAPI/Patches/GameModes/TaskPanelPatch.cs +++ b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs @@ -7,7 +7,7 @@ internal static class TaskPanelPatch { public static bool Prefix(TaskPanelBehaviour __instance) { - if (CustomGameModeManager.ActiveMode != null) + if (__instance != Roles.HudManagerPatches.RoleTab && CustomGameModeManager.ActiveMode != null) { CustomGameModeManager.ActiveMode.UpdateTaskPanel(__instance); return false; From 75662a6c25c4bd7518251f4c7c31e3e49a0d8f48 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:47:33 -0500 Subject: [PATCH 102/102] Improve vent checks --- MiraAPI/Patches/GameModes/ConsolePatches.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Patches/GameModes/ConsolePatches.cs b/MiraAPI/Patches/GameModes/ConsolePatches.cs index cb0d5be..4427b16 100644 --- a/MiraAPI/Patches/GameModes/ConsolePatches.cs +++ b/MiraAPI/Patches/GameModes/ConsolePatches.cs @@ -12,7 +12,7 @@ internal static class ConsolePatches { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseTasks(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseTasks(__instance)) { var task = __instance.FindTask(pc.Object); @@ -40,9 +40,10 @@ internal static class ConsolePatches { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance)) { - return canUse = couldUse = true; + canUse = couldUse = true; + return true; } canUse = couldUse = false; @@ -58,9 +59,10 @@ internal static class ConsolePatches { if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { - if (CustomGameModeManager.ActiveMode != null && CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance)) + if (CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance)) { - return canUse = couldUse = true; + canUse = couldUse = true; + return true; } canUse = couldUse = false; @@ -77,7 +79,7 @@ internal static class ConsolePatches if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { return CustomGameModeManager.ActiveMode == null || - CustomGameModeManager.ActiveMode!.CanUseMapConsole(__instance); + CustomGameModeManager.ActiveMode.CanUseMapConsole(__instance); } return true; @@ -89,7 +91,7 @@ internal static class ConsolePatches if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { return CustomGameModeManager.ActiveMode == null || - CustomGameModeManager.ActiveMode!.CanUseSystemConsole(__instance); + CustomGameModeManager.ActiveMode.CanUseSystemConsole(__instance); } return true; @@ -101,7 +103,7 @@ internal static class ConsolePatches if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && ShipStatus.Instance) { return CustomGameModeManager.ActiveMode == null || - CustomGameModeManager.ActiveMode!.CanUseTasks(__instance); + CustomGameModeManager.ActiveMode.CanUseTasks(__instance); } return true;