diff --git a/AmongUs.props b/AmongUs.props
index ba7e7f2..0819a88 100644
--- a/AmongUs.props
+++ b/AmongUs.props
@@ -11,4 +11,4 @@
-
\ No newline at end of file
+
diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs
index b20ec41..dc0783b 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 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
new file mode 100644
index 0000000..c15e1d5
--- /dev/null
+++ b/MiraAPI.Example/GameModes/ExampleMode.cs
@@ -0,0 +1,13 @@
+using MiraAPI.GameModes;
+using MiraAPI.Utilities.Assets;
+using UnityEngine;
+
+namespace MiraAPI.Example.GameModes;
+
+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/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/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs
new file mode 100644
index 0000000..6fa2d8c
--- /dev/null
+++ b/MiraAPI/GameModes/AbstractGameMode.cs
@@ -0,0 +1,296 @@
+using System.Collections;
+using System.Collections.Generic;
+using AmongUs.GameOptions;
+using MiraAPI.GameOptions;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using Reactor.Utilities.Extensions;
+using TMPro;
+using UnityEngine;
+
+namespace MiraAPI.GameModes;
+
+///
+/// Base class for custom gamemodes.
+///
+[MiraIgnore]
+public abstract class AbstractGameMode : IOptionable
+{
+ ///
+ /// Gets the game mode name.
+ ///
+ 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.
+ ///
+ public abstract string Description { get; }
+
+ ///
+ /// Gets the game mode description.
+ ///
+ 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 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.
+ ///
+ public string ColoredName => $"{this.Name}";
+
+ ///
+ /// Gets the game mode id.
+ ///
+ public uint ID { get; internal set; }
+
+ ///
+ /// Gets a value indicating whether a specific body type is used by the game mode.
+ ///
+ public virtual bool GameModeBodyTypeOverride => false;
+
+ ///
+ /// Gets a value indicating whether the gamemode removes the normal game settings.
+ ///
+ 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().
+ ///
+ /// 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.
+ ///
+ public virtual bool ShowGameModeIntroCutscene => false;
+
+ ///
+ /// Called when Intro Cutscene is destroyed.
+ ///
+ public virtual void Initialize()
+ {
+ }
+
+ ///
+ /// Called on HudManager.Start().
+ ///
+ /// Instance of HudManager.
+ public virtual void HudStart(HudManager instance)
+ {
+ }
+
+ ///
+ /// Called every frame on HudManager.Update().
+ ///
+ /// Instance of HudManager.
+ public virtual void HudUpdate(HudManager instance)
+ {
+ }
+
+ ///
+ /// Called when a player is killed.
+ ///
+ /// PlayerControl that was killed.
+ /// Determines whether to assign a ghost role on the player.
+ public virtual void OnPlayerDeath(PlayerControl player, bool assignGhostRole)
+ {
+ GameManager.Instance.LogicRoleSelection.OnPlayerDeath(player, assignGhostRole);
+ }
+
+ ///
+ /// Calculate Game End logic.
+ ///
+ /// Should original game end logic be used.
+ /// Instance of LogicGameFlowNormal.
+ public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance)
+ {
+ runOriginal = true;
+ }
+
+ ///
+ /// Custom role assignment.
+ ///
+ /// Should original role assignment be used.
+ /// Instance of LogicRoleSelection.
+ public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance)
+ {
+ 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.
+ ///
+ /// Should the original murder logic be used.
+ /// Result of custom murder logic.
+ /// Target player for murder.
+ public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target)
+ {
+ result = false;
+ runOriginal = true;
+ }
+
+ ///
+ /// Custom winner selection.
+ ///
+ /// List of winners or null.
+ public virtual List? CalculateWinners() => null;
+
+ ///
+ /// The IEnumerator that plays the intro cutscene for this gamemode.
+ ///
+ /// 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.
+ ///
+ /// Admin Console.
+ /// True if Admin console is enabled.
+ public virtual bool CanUseMapConsole(MapConsole console) => true;
+
+ ///
+ /// Can a body be reported in this gamemode.
+ ///
+ /// Target body for reporting.
+ /// True if dead bodies can be reported.
+ public virtual bool CanReport(DeadBody body) => true;
+
+ ///
+ /// Can system consoles be used in this gamemode.
+ ///
+ /// System Console.
+ /// True if system consoles are enabled in this mode.
+ public virtual bool CanUseSystemConsole(SystemConsole console) => true;
+
+ ///
+ /// Can tasks be interacted with in this gamemode.
+ ///
+ /// Task console.
+ /// True if tasks are enabled in this mode.
+ public virtual bool CanUseTasks(Console console) => true;
+
+ ///
+ /// Should the sabotage map be used when attempting to open sabotage overlay.
+ ///
+ /// MapBehaviour object.
+ /// 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.
+ ///
+ /// Target vent.
+ /// Player attempting to vent.
+ /// 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/ClassicMode.cs b/MiraAPI/GameModes/ClassicMode.cs
new file mode 100644
index 0000000..44b43da
--- /dev/null
+++ b/MiraAPI/GameModes/ClassicMode.cs
@@ -0,0 +1,20 @@
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using UnityEngine;
+
+namespace MiraAPI.GameModes;
+
+///
+/// The classic game mode.
+///
+[MiraIgnore]
+public class ClassicMode : AbstractGameMode
+{
+ ///
+ public override string Name => "Classic";
+
+ ///
+ 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/CustomGameMode.cs b/MiraAPI/GameModes/CustomGameMode.cs
deleted file mode 100644
index f4dc43d..0000000
--- a/MiraAPI/GameModes/CustomGameMode.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using System.Collections.Generic;
-
-namespace MiraAPI.GameModes;
-
-///
-/// Base class for custom gamemodes.
-///
-public abstract class CustomGameMode
-{
- ///
- /// Gets the game mode name.
- ///
- public abstract string Name { get; }
-
- ///
- /// Gets the game mode description.
- ///
- public abstract string Description { get; }
-
- ///
- /// Gets the game mode ID.
- ///
- public abstract int Id { get; }
-
- ///
- /// Called when is destroyed.
- ///
- public virtual void Initialize()
- {
- }
-
- ///
- /// Called on .
- ///
- /// Instance of .
- public virtual void HudStart(HudManager instance)
- {
- }
-
- ///
- /// Called every frame on .
- ///
- /// Instance of .
- public virtual void HudUpdate(HudManager instance)
- {
- }
-
- ///
- /// Called when a player is killed.
- ///
- /// that was killed.
- public virtual void OnDeath(PlayerControl player)
- {
- }
-
- ///
- /// Calculate Game End logic.
- ///
- /// Should original game end logic be used.
- /// Instance of .
- public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance)
- {
- runOriginal = true;
- }
-
- ///
- /// Custom role assignment.
- ///
- /// Should original role assignment be used.
- /// Instance of .
- public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance)
- {
- runOriginal = true;
- }
-
- ///
- /// Custom murder logic.
- ///
- /// Should the original murder logic be used.
- /// Result of custom murder logic.
- /// Target for murder.
- public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target)
- {
- result = false;
- runOriginal = true;
- }
-
- ///
- /// Should Roles Settings be available when this gamemode is selected.
- ///
- /// if Role Settings are enabled in this game mode.
- public virtual bool AreRoleSettingsEnabled() => true;
-
- ///
- /// Should Game Settings be available when this gamemode is selected.
- ///
- /// if Game Settings are enabled in this mode.
- public virtual bool AreGameSettingsEnabled() => true;
-
- ///
- /// Custom winner selection.
- ///
- /// List of winners or .
- public virtual List? CalculateWinners() => null;
-
- ///
- /// Show gamemode in .
- ///
- /// if the game mode should be shown in the intro cutscene.
- public virtual bool ShowGameModeIntroCutscene() => false;
-
- ///
- /// Can Admin be used in this gamemode.
- ///
- /// Admin Console.
- /// if Admin console is enabled.
- public virtual bool CanUseMapConsole(MapConsole console) => true;
-
- ///
- /// Can a be reported in this gamemode.
- ///
- /// Target for reporting.
- /// if s can be reported.
- public virtual bool CanReport(DeadBody body) => true;
-
- ///
- /// Can s be used in this gamemode.
- ///
- /// System Console.
- /// if s are enabled in this mode.
- public virtual bool CanUseSystemConsole(SystemConsole console) => true;
-
- ///
- /// Can tasks be interacted with in this gamemode.
- ///
- /// Task console.
- /// if tasks are enabled in this mode.
- public virtual bool CanUseTasks(Console console) => true;
-
- ///
- /// Should the sabotage map be used when attempting to open sabotage overlay.
- ///
- /// object.
- /// if the sabotage map should be shown.
- public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true;
-
- ///
- /// Can a player vent in this gamemode.
- ///
- /// Target vent.
- /// Player attempting to vent.
- /// if venting is enabled in this mode.
- public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true;
-}
diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs
index 3edd028..30877c3 100644
--- a/MiraAPI/GameModes/CustomGameModeManager.cs
+++ b/MiraAPI/GameModes/CustomGameModeManager.cs
@@ -1,70 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using MiraAPI.PluginLoading;
+using Reactor.Utilities;
namespace MiraAPI.GameModes;
///
-/// Manages s.
+/// Manages custom gamemodes.
///
public static class CustomGameModeManager
{
- ///
- /// List of registered .
- ///
- internal static readonly Dictionary GameModes = [];
+ internal static readonly Dictionary IdToModeMap = [];
- public static bool IsDefault()
+ private static uint GetNextId() => ++LastId;
+
+ private static uint LastId { get; set; }
+
+ ///
+ /// Register gamemode from type.
+ ///
+ /// Type of gamemode class, should inherit from .
+ /// The custom plugin info of the mod.
+ /// Whether the gamemode was successfully registered.
+ internal static bool RegisterGameMode(Type gameModeType, MiraPluginInfo pluginInfo)
{
- return ActiveMode?.Id == 0;
- }
-
- ///
- /// Gets the current .
- ///
- public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode();
-
- ///
- /// Set current .
- ///
- /// ID.
- public static void SetGameMode(int id)
- {
- if (GameModes.TryGetValue(id, out var gameMode))
- {
- ActiveMode = gameMode;
- return;
- }
-
- Error($"No gamemode with id {id} found!");
- }
-
- ///
- /// Register from type.
- ///
- /// Type of , should inherit from .
- internal static void RegisterGameMode(Type gameModeType)
- {
- if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType))
+ if (!typeof(AbstractGameMode).IsAssignableFrom(gameModeType))
{
Warning($"{gameModeType.Name} does not inherit CustomGameMode!");
- return;
+ return false;
}
- var modeObj = Activator.CreateInstance(gameModeType);
+ var instance = Activator.CreateInstance(gameModeType);
- if (modeObj is not CustomGameMode gameMode)
+ if (instance is not AbstractGameMode mode)
{
Error($"Failed to create instance of {gameModeType.Name}");
- return;
+ return false;
}
- if (GameModes.Any(x => x.Key == gameMode.Id))
+ if (!mode.HideMode)
{
- Error($"ID for gamemode {gameMode.Name} already exists!");
+ IdToModeMap.Add(GetNextId(), mode);
+ pluginInfo.GameModes.Add(LastId, mode);
+ mode.ID = LastId;
+ GameModeOption.AddOption(mode);
+ }
+ return true;
+ }
+
+ ///
+ /// 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() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(ClassicMode));
+
+ ///
+ /// 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() => ActiveMode != null && ActiveMode.GetType().IsAssignableTo(typeof(HideAndSeekMode));
+
+ ///
+ /// 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; private 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];
+
+ ///
+ /// 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();
+ 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;
+ var hnsMode = new HideAndSeekMode();
+ if (!hnsMode.HideMode)
+ {
+ IdToModeMap.Add(1, hnsMode);
+ hnsMode.ID = 1;
+ GameModeOption.AddOption(hnsMode);
+ LastId++;
+ }
+ }
+
+ internal static void GetAndSetGameMode()
+ {
+ var id = (uint)GameModeOption.Value;
+
+ if (IdToModeMap.TryGetValue(id, out var mode))
+ {
+ ActiveMode = mode;
return;
}
- GameModes.Add(gameMode.Id, gameMode);
+ 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
deleted file mode 100644
index 83c11ba..0000000
--- a/MiraAPI/GameModes/DefaultMode.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace MiraAPI.GameModes;
-
-public class DefaultMode : CustomGameMode
-{
- 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..95acd9c
--- /dev/null
+++ b/MiraAPI/GameModes/GameModeOption.cs
@@ -0,0 +1,160 @@
+using System.Collections.Generic;
+using System.Linq;
+using HarmonyLib;
+using Il2CppInterop.Runtime.InteropTypes.Arrays;
+using MiraAPI.GameOptions;
+using MiraAPI.Networking;
+using MiraAPI.Patches.GameModes;
+using MiraAPI.Patches.Options;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using Reactor.Localization.Utilities;
+using Reactor.Networking.Attributes;
+using Reactor.Utilities.Extensions;
+using TMPro;
+using UnityEngine;
+using UnityEngine.Events;
+using UnityEngine.ProBuilder;
+using Object = UnityEngine.Object;
+
+namespace MiraAPI.GameModes;
+
+///
+/// The game mode option.
+///
+[HarmonyPatch]
+public static class GameModeOption
+{
+ ///
+ /// Gets the current index of the Game Mode Option
+ /// For the value as an , see .
+ ///
+ public static int Value
+ {
+ get =>
+ OptionBehaviour != null
+ ? OptionBehaviour.GetInt()
+ : _lastValue;
+ private set
+ {
+ _lastValue = value;
+ if (OptionBehaviour == null)
+ return;
+ OptionBehaviour.Value = value;
+ OptionBehaviour.UpdateValue();
+ _lastValue = value;
+ }
+ }
+ internal static StringOption OptionBehaviour { get; set; } = null!;
+
+ 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"),
+ };
+
+ internal static void AddOption(AbstractGameMode mode)
+ {
+ if (!Values.ContainsKey(mode.ID))
+ Values.Add(mode.ID, CustomStringName.CreateAndRegister(mode.ColoredName));
+ }
+ /*[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))]
+ [HarmonyPostfix]
+ private static void CreateSettingsPatch(GameOptionsMenu __instance)
+ {
+ if (MenuState.Instance.CurrentModIdx != 0 || GameManager.Instance.IsHideAndSeek() || CustomGameModeManager.ActiveMode == null)
+ {
+ return;
+ }
+
+ var num = 0.713f;
+
+ foreach (var category in __instance.settingsContainer.GetComponentsInChildren())
+ {
+ 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;
+ categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f);
+ OptionBehaviour = Object.Instantiate(
+ __instance.stringOptionOrigin,
+ Vector3.zero,
+ Quaternion.identity,
+ __instance.settingsContainer);
+ num -= 0.63f;
+ OptionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f);
+ OptionBehaviour.SetClickMask(__instance.ButtonClickMask);
+ StringGameSetting setting = ScriptableObject.CreateInstance();
+ setting.Type = OptionTypes.MultipleChoice;
+ setting.Title = GamemodeName;
+ setting.Index = _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);
+ 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);
+ }*/
+
+ 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!;
+ 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
+ }
+
+ [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))]
+ [HarmonyPrefix]
+ private static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option)
+ {
+ if (OptionBehaviour.Equals(option))
+ {
+ Info($"Game mode changed to {option.GetInt()}");
+ 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;
+ }
+ return true;
+ }
+
+ [MethodRpc((uint) MiraRpc.SyncGamemodeOption)]
+ internal static void RpcSyncGamemode(PlayerControl host, int data)
+ {
+ Set(data);
+ }
+}
diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs
new file mode 100644
index 0000000..d5c90e8
--- /dev/null
+++ b/MiraAPI/GameModes/HideAndSeekMode.cs
@@ -0,0 +1,534 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using AmongUs.Data;
+using AmongUs.GameOptions;
+using HarmonyLib;
+using InnerNet;
+using MiraAPI.GameOptions;
+using MiraAPI.HnsReimplemented;
+using MiraAPI.HnsReimplemented.Options;
+using MiraAPI.PluginLoading;
+using MiraAPI.Roles;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using PowerTools;
+using UnityEngine;
+
+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!\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;
+ public override bool ShouldShowSabotageMap(MapBehaviour map) => false;
+ 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 override bool ShowTaskBar => false;
+ public static int ImpostorPlayerID()
+ {
+ return OptionGroupSingleton.Instance.SelectedSeeker.Value;
+ }
+
+ 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;
+ Il2CppSystem.Collections.Generic.List list = new();
+ AmongUsClient.Instance.GetAllClients(list);
+ 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)
+ {
+ list2.Add(networkedPlayerInfo);
+ }
+ }
+ IGameOptions currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions;
+ int adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count);
+ AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, Math.Max(adjustedNumImpostors, 1), RoleTypes.Impostor);
+ AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Engineer);
+ }
+
+ public static void AssignRolesForTeam(
+ List players,
+ IGameOptions opts,
+ RoleTeamTypes team,
+ int teamMax,
+ RoleTypes 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()
+ .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 (HasImpostorPlayerID() &&
+ ValidateImpostorPlayerID(players) &&
+ !AmongUsClient.Instance.IsGamePublic)
+ {
+ NetworkedPlayerInfo networkedPlayerInfo = players.ToArray()
+ .First(p => p.PlayerId == 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.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);
+ }
+ }
+
+ 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)
+ {
+ SoundManager.Instance.PlaySound(__instance.IntroStinger, false, 1f, null);
+ 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);
+ HnsMusicHandler.Instance.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
+ {
+ HideAndSeekHudHelper.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();
+ HnsMusicHandler.Instance.OnGameStart();
+ HnsDangerMeter.Instance.OnGameStart();
+ UnityEngine.Object.Destroy(__instance.gameObject);
+ }
+ public override void Initialize()
+ {
+ deadPlayerCount = 0;
+ ShipStatus.Instance.BreakEmergencyButton();
+ PlayerControl.LocalPlayer.SetKillTimer(0.01f);
+ if (HudManager.InstanceExists)
+ {
+ HudManager.Instance.gameObject.AddComponent();
+ HudManager.Instance.gameObject.AddComponent();
+ 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)
+ {
+ 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 (HideAndSeekHudHelper.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)
+ {
+ 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;
+ }
+ }
+ 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)
+ {
+ base.OnPlayerDeath(player, assignGhostRole);
+ HudManager.Instance.NotifyOfDeath();
+ 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/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs
index 02daa25..3d48453 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/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 7dd26ee..d91d079 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -5,6 +5,8 @@ using System.Linq;
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 +28,8 @@ public static class ModdedOptionsManager
private static readonly Dictionary TypeToGroup = [];
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 = [];
@@ -34,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)
{
@@ -62,6 +66,29 @@ public static class ModdedOptionsManager
notif.SettingsChangeMessageLogic(key, item, playSound);
}
+ 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)
@@ -77,6 +104,17 @@ public static class ModdedOptionsManager
Groups.Add(group);
TypeToGroup.Add(type, group);
+ if (group.OptionableType?.IsAssignableTo(typeof(AbstractGameMode)) == true)
+ {
+ if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList))
+ {
+ oldList.Add(group);
+ }
+ else
+ {
+ GameModeOptionGroups.Add(group.OptionableType, new List() { group });
+ }
+ }
pluginInfo.InternalOptionGroups.Add(group);
typeof(OptionGroupSingleton<>).MakeGenericType(type)
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/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
index 4300c4f..1df4921 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using BepInEx.Configuration;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs
new file mode 100644
index 0000000..62d07cf
--- /dev/null
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+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)
+ 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)
+ {
+ 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 Array.IndexOf(Values, Value);
+ }
+
+ ///
+ 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);
+ }
+ }
+}
diff --git a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs
new file mode 100644
index 0000000..d41b445
--- /dev/null
+++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs
@@ -0,0 +1,285 @@
+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!;
+ }
+ 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;
+ 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/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"
+ },
+ };
+}
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..7be2864
--- /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,
+ 4,
+ 1,
+ "#",
+ "#",
+ MiraNumberSuffixes.None);
+
+ public ModdedNumberOption LongTasks { get; set; } = new(
+ "# Long Tasks",
+ 1,
+ 0,
+ 4,
+ 1,
+ "#",
+ "#",
+ MiraNumberSuffixes.None);
+
+ public ModdedNumberOption ShortTasks { get; set; } = new(
+ "# Short Tasks",
+ 2,
+ 0,
+ 8,
+ 1,
+ "#",
+ "#",
+ MiraNumberSuffixes.None);
+}
diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs
index 52abe60..d7c2c64 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 14b20ac..36b1747 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,20 @@ namespace MiraAPI;
[BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)]
[ReactorModFlags(ModFlags.RequireOnAllClients)]
-public partial class MiraApiPlugin : BasePlugin
+public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
{
+ ///
+ public ConfigFile GetConfigFile()
+ {
+ return Config;
+ }
+
+ ///
+ public string OptionsTitleText => "MiraAPI";
+
+ ///
+ public bool DisplayOnOptionsMenu => false;
+
///
/// Gets a value indicating whether the current device is running Starlight (on mobile).
///
@@ -54,7 +67,7 @@ public partial class MiraApiPlugin : BasePlugin
ReactorCredits.Register("Mira API", Version, IsDevBuild, ReactorCredits.AlwaysShow);
PluginManager = new MiraPluginManager();
- PluginManager.Initialize();
+ PluginManager.Initialize(this, this);
IL2CPPChainloader.Instance.Finished +=
ModCompatibility
diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs
index f5ae883..051ae07 100644
--- a/MiraAPI/Modifiers/Types/GameModifier.cs
+++ b/MiraAPI/Modifiers/Types/GameModifier.cs
@@ -1,8 +1,12 @@
-namespace MiraAPI.Modifiers.Types;
+using MiraAPI.GameModes;
+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.
///
+[MiraIgnore]
public abstract class GameModifier : BaseModifier
{
///
@@ -51,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();
}
diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs
index 472b3cf..cbcb13d 100644
--- a/MiraAPI/Modifiers/Types/TimedModifier.cs
+++ b/MiraAPI/Modifiers/Types/TimedModifier.cs
@@ -1,4 +1,6 @@
using System;
+using MiraAPI.PluginLoading;
+using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Modifiers.Types;
@@ -6,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.
///
+[MiraIgnore]
public abstract class TimedModifier : BaseModifier
{
///
diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs
index 0622492..e840d00 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;
@@ -629,7 +630,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
@@ -666,7 +670,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/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs
index 7228904..836e822 100644
--- a/MiraAPI/Networking/MiraRpc.cs
+++ b/MiraAPI/Networking/MiraRpc.cs
@@ -79,4 +79,9 @@ public enum MiraRpc : uint
/// Custom RPC to replace Among Us QueueOverruleVotes.
///
QueueOverruleVotes,
+
+ ///
+ /// Custom RPC to sync the Gamemode option.
+ ///
+ SyncGamemodeOption,
}
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]
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/ConsolePatches.cs b/MiraAPI/Patches/GameModes/ConsolePatches.cs
index e2f24b2..4427b16 100644
--- a/MiraAPI/Patches/GameModes/ConsolePatches.cs
+++ b/MiraAPI/Patches/GameModes/ConsolePatches.cs
@@ -1,14 +1,18 @@
-/*
+using HarmonyLib;
+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))]
+ [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,15 +35,15 @@ 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)
{
- 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;
@@ -50,15 +54,15 @@ 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)
{
- 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;
@@ -68,4 +72,40 @@ public 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;
+ }
+}
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/Patches/GameModes/DeadBodyPatch.cs b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs
index 07a2899..fff7e40 100644
--- a/MiraAPI/Patches/GameModes/DeadBodyPatch.cs
+++ b/MiraAPI/Patches/GameModes/DeadBodyPatch.cs
@@ -1,11 +1,14 @@
-/*
+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))]
+ [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..742a56c 100644
--- a/MiraAPI/Patches/GameModes/GameLogicPatches.cs
+++ b/MiraAPI/Patches/GameModes/GameLogicPatches.cs
@@ -1,20 +1,78 @@
-/*
+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)
+ [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidHumansWin))]
+ public static bool DidHumansWin(GameOverReason reason, ref bool __result)
{
- var runOriginal = true;
- CustomGameModeManager.ActiveMode?.CheckGameEnd(out runOriginal, __instance);
- return runOriginal;
+ __result = reason == GameOverReason.CrewmatesByTask || reason == GameOverReason.CrewmatesByVote ||
+ reason == GameOverReason.ImpostorDisconnect || reason == GameOverReason.HideAndSeek_CrewmatesByTimer;
+ return false;
}
- [HarmonyPrefix, HarmonyPatch(typeof(LogicRoleSelectionNormal), nameof(LogicRoleSelectionNormal.AssignRolesFromList))]
- public static bool AssignRolesPatch(LogicRoleSelectionNormal __instance)
+ [HarmonyPrefix, HarmonyPatch(typeof(GameManager), nameof(GameManager.DidImpostorsWin))]
+ public static bool DidImpostorsWin(GameOverReason reason, ref bool __result)
{
- var runOriginal = true;
- CustomGameModeManager.ActiveMode?.AssignRoles(out runOriginal, __instance);
- return runOriginal;
+ __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)
+ {
+ if (CustomGameModeManager.ActiveMode == null || !CustomGameModeManager.ActiveMode.GameModeBodyTypeOverride)
+ {
+ return true;
+ }
+ __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;
+ }
+ }
+}
diff --git a/MiraAPI/Patches/GameModes/GameManagerPatches.cs b/MiraAPI/Patches/GameModes/GameManagerPatches.cs
new file mode 100644
index 0000000..3f56e82
--- /dev/null
+++ b/MiraAPI/Patches/GameModes/GameManagerPatches.cs
@@ -0,0 +1,56 @@
+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 is HideAndSeekMode;
+ return false;
+ }
+
+ 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/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs
new file mode 100644
index 0000000..735d198
--- /dev/null
+++ b/MiraAPI/Patches/GameModes/HudPatches.cs
@@ -0,0 +1,62 @@
+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.Update))]
+ public static void HudUpdatePatch(HudManager __instance)
+ {
+ if (GameManager.Instance != null && GameManager.Instance.GameHasStarted &&
+ CustomGameModeManager.ActiveMode != null)
+ {
+ 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())
+ 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");
+ 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 = CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic";
+ CustomGameModeManager.ActiveMode?.HudStart(instance);
+ }
+
+ private static TextMeshPro? _text;
+ internal static void SetGameModeText(string text)
+ {
+ if (_text != null)
+ _text.text = text;
+ }
+}
diff --git a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs
index ad0d9e0..de9bbf1 100644
--- a/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs
+++ b/MiraAPI/Patches/GameModes/ImpostorTargetPatch.cs
@@ -1,9 +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)
{
@@ -16,9 +20,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/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/MapBehaviourPatch.cs b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs
index 9a4bd8b..06ea282 100644
--- a/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs
+++ b/MiraAPI/Patches/GameModes/MapBehaviourPatch.cs
@@ -1,9 +1,12 @@
-/*
+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))]
+ [HarmonyPrefix, HarmonyPatch(nameof(MapBehaviour.ShowSabotageMap))]
public static bool ShowSabotagePatch(MapBehaviour __instance)
{
var shouldShow = CustomGameModeManager.ActiveMode?.ShouldShowSabotageMap(__instance);
@@ -15,4 +18,4 @@ public static class MapBehaviourPatch
return true;
}
-}*/
+}
diff --git a/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs
new file mode 100644
index 0000000..e7c4f8b
--- /dev/null
+++ b/MiraAPI/Patches/GameModes/NormalGameManagerPatches.cs
@@ -0,0 +1,19 @@
+/*
+using HarmonyLib;
+using MiraAPI.GameModes;
+
+namespace MiraAPI.Patches.GameModes;
+
+[HarmonyPatch(typeof(NormalGameManager))]
+internal static class NormalGameManagerPatches
+{
+ [HarmonyPrefix, HarmonyPatch(nameof(NormalGameManager.IsNormal))]
+ public static void IsNormal(ref bool __result)
+ {
+ if (CustomGameModeManager.ActiveMode != null)
+ {
+ __result = !CustomGameModeManager.ActiveMode.IsHideAndSeek;
+ }
+ }
+}
+*/
diff --git a/MiraAPI/Patches/GameModes/OnDeathPatch.cs b/MiraAPI/Patches/GameModes/OnDeathPatch.cs
deleted file mode 100644
index 99f9211..0000000
--- a/MiraAPI/Patches/GameModes/OnDeathPatch.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
-[HarmonyPatch(typeof(KillAnimation))]
-public static class OnDeathPatch
-{
- [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/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..30a30cc
--- /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 (__instance != Roles.HudManagerPatches.RoleTab && CustomGameModeManager.ActiveMode != null)
+ {
+ CustomGameModeManager.ActiveMode.UpdateTaskPanel(__instance);
+ return false;
+ }
+ return true;
+ }
+}
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/GameModes/WinnersPatch.cs b/MiraAPI/Patches/GameModes/WinnersPatch.cs
new file mode 100644
index 0000000..c7c9385
--- /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 ClassicMode)
+ 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/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs
index 4ee990c..ae227bd 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;
@@ -29,6 +30,11 @@ public static class IntroCutscenePatches
{
var @event = new IntroBeginEvent(__instance);
MiraEventManager.InvokeEvent(@event);
+
+ if (CustomGameModeManager.ActiveMode != null)
+ {
+ CustomGameModeManager.ActiveMode.Initialize();
+ }
}
[HarmonyPatch]
@@ -121,8 +127,6 @@ public static class IntroCutscenePatches
introCutscene = __instance.Cast();
}
- Info("IntroCutscene ended");
-
MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene));
var @event = new BeforeRoundStartEvent(true);
diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs
index 6e96d5f..0092103 100644
--- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs
+++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs
@@ -7,6 +7,8 @@ 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/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
index 2bc615d..ba3d268 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,15 @@ 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 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!;
+ private static SpriteRenderer _modIcon = null!;
+
[HarmonyPrefix]
[HarmonyPatch(nameof(GameOptionsMenu.Initialize))]
// ReSharper disable once InconsistentNaming
@@ -41,15 +54,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 +77,314 @@ internal static class GameOptionsMenuPatch
return false;
}
+ public static float AdditionalVanillaScrollNum;
+ internal static void ToggleGamemodeOptions(AbstractGameMode gameMode, GameOptionsMenu instance)
+ {
+ float num = -1.217f;
+ instance.Children.Clear();
+ foreach (var opt in _mainOptions)
+ {
+ instance.Children.Add(opt);
+ }
+
+ 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 = icon.LoadAsset().texture;
+ var scale = Mathf.Min(128f / texture.width, 128f / texture.height);
+
+ var newSprite = Sprite.Create(
+ texture,
+ new Rect(0, 0, texture.width, texture.height),
+ Vector2.one / 2,
+ 100f / scale
+ );
+
+ _modIcon.sprite = newSprite;
+ _modIcon.drawMode = SpriteDrawMode.Simple;
+ }
+ 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)
+ {
+ 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);
+ }
+ }
+
+ 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())
+ {
+ 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();
+ _gameModeGroups.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);
+ }
+
+ 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.45f, 0.45f, 1),
+ },
+ layer = gamemodeBg.layer,
+ };
+ _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);
+ gamemodeTextObj.transform.localScale = new Vector3(2.12f, 2.12f, 1);
+ _gamemodeDescription = gamemodeTextObj.GetComponentInChildren();
+ _gamemodeDescription.fontSizeMin = 0.65f;
+ _gamemodeDescription.fontSizeMax = 0.9f;
+ _gamemodeDescription.alignment = TextAlignmentOptions.Left;
+
+ num -= 1.3f;
+ AdditionalVanillaScrollNum = 0f;
+ 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;
+ AdditionalVanillaScrollNum -= 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;
+ AdditionalVanillaScrollNum -= 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);
+ }
+
+ 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);
+ group.Ready = true;
+ }
+ _gameModeHeaders.Add(mode, categoryHeaders);
+ _gameModeOptions.Add(mode, optionBehaviours);
+ }
+
+ 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)
@@ -88,11 +404,10 @@ internal static class GameOptionsMenuPatch
// ReSharper disable once InconsistentNaming
public static void UpdatePatch(GameOptionsMenu __instance)
{
- if (MenuState.Instance.CurrentModIdx == 0)
+ if (MenuState.Instance.CurrentModIdx == 0 && _gameModeGroups.Count <= 0)
{
return;
}
-
var num = 2.1f;
switch (MenuState.Instance.CurrentMenu)
@@ -107,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)
@@ -122,8 +443,100 @@ 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);
- private static void UpdateGroup(AbstractOptionGroup? group, ref float num)
+ 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)
{
if (group is null || !group.Ready || group.Options.Count == 0 || group.Header is null)
{
@@ -132,18 +545,24 @@ internal static class GameOptionsMenuPatch
if (!group.GroupVisible.Invoke())
{
- group.Header.gameObject.SetActive(false);
+ 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;
}
- 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;
@@ -175,6 +594,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
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;
diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
index 51e2e92..a048c33 100644
--- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
+++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
@@ -1,8 +1,9 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
+using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
@@ -23,14 +24,51 @@ public static class LobbyViewPanePatches
{
private static int SelectedModIdx { get; set; }
+ private static MiraPluginInfo[] Plugins => MiraPluginManager.Instance.RegisteredPluginsWithOptions;
+
private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0
? null
- : MiraPluginManager.Instance.RegisteredPluginsWithOptions[SelectedModIdx - 1];
+ : Plugins[SelectedModIdx - 1];
private static PassiveButton? ModifiersTabButton { get; set; }
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)
@@ -75,7 +113,7 @@ public static class LobbyViewPanePatches
(UnityAction)(() =>
{
SelectedModIdx += 1;
- if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length)
+ if (SelectedModIdx > Plugins.Length)
{
SelectedModIdx = 0;
}
@@ -99,7 +137,7 @@ public static class LobbyViewPanePatches
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
{
- SelectedModIdx = MiraPluginManager.Instance.RegisteredPluginsWithOptions.Length;
+ SelectedModIdx = Plugins.Length;
}
Refresh(__instance);
@@ -108,8 +146,25 @@ public static class LobbyViewPanePatches
private static void Refresh(LobbyViewSettingsPane menu)
{
- ModifiersTabButton?.gameObject.SetActive(SelectedModIdx != 0);
- menu.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Main";
+ if (SelectedMod == null)
+ {
+ var cgm = GetCustomGamemode();
+ ModifiersTabButton?.gameObject.SetActive(false);
+ menu.rolesTabButton.gameObject.SetActive(cgm is null or { ShowNormalRoleSettings: true });
+ menu.gameModeText.text = cgm is null ? "Main" : cgm.ColoredName;
+ }
+ 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;
+ }
+
menu.RefreshTab();
menu.scrollBar.ScrollToTop();
}
@@ -141,8 +196,15 @@ public static class LobbyViewPanePatches
[HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))]
public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance)
{
- if (SelectedModIdx == 0 || SelectedMod == null)
+ if (SelectedMod == null)
{
+ var gamemodeOpts = GetCustomGamemodeOptions();
+ if (gamemodeOpts != null)
+ {
+ DrawOptions(__instance, gamemodeOpts);
+ return false;
+ }
+
return true;
}
@@ -349,6 +411,13 @@ 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(
+ data.Title, CustomGameModeManager.ActiveMode != null ? $"{CustomGameModeManager.ActiveMode.Name}" : "Classic"
+ );
+ }
}
menu.settingsInfo.Add(viewSettingsInfoPanel.gameObject);
diff --git a/MiraAPI/Patches/Options/MenuState.cs b/MiraAPI/Patches/Options/MenuState.cs
index d459202..b52bd95 100644
--- a/MiraAPI/Patches/Options/MenuState.cs
+++ b/MiraAPI/Patches/Options/MenuState.cs
@@ -1,10 +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;
@@ -60,6 +63,8 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
internal Dictionary FinishedRoleMenus { get; } = [];
+ [HideFromIl2Cpp]
+ internal Dictionary QueuedRoleMenuRefresh { get; } = [];
public void Awake()
{
@@ -404,14 +409,16 @@ 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
{
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);
diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs
index 36651f9..fbcbe38 100644
--- a/MiraAPI/Patches/Options/OptionsPatches.cs
+++ b/MiraAPI/Patches/Options/OptionsPatches.cs
@@ -1,8 +1,10 @@
using HarmonyLib;
using Il2CppSystem;
+using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Roles;
using MiraAPI.Utilities;
+using Reactor.Utilities.Extensions;
using UnityEngine;
using Object = Il2CppSystem.Object;
@@ -199,8 +201,8 @@ public static class OptionsPatches
{
return true;
}
+ __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title);
- __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty