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()); return false; } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 3c1e645..c66ea52 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -5,6 +5,7 @@ using System.Linq; using AmongUs.GameOptions; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; @@ -74,7 +75,24 @@ public static class RoleSettingMenuPatches dividerImage.gameObject.SetActive(false); } - if (MenuState.Instance.FinishedRoleMenus.TryGetValue(MenuState.Instance.CurrentModIdx, out var finished) && finished) + var queuedRefresh = false; + if (MenuState.Instance.QueuedRoleMenuRefresh.TryGetValue(MenuState.Instance.CurrentModIdx, out var queued) && queued) + { + queuedRefresh = true; + var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); + var headers = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); + foreach (var child in roleOptionSettings) + { + Object.Destroy(child.gameObject); + } + foreach (var child in headers) + { + Object.Destroy(child.gameObject); + } + yield return new WaitForEndOfFrame(); + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; + } + if (!queuedRefresh && MenuState.Instance.FinishedRoleMenus.TryGetValue(MenuState.Instance.CurrentModIdx, out var finished) && finished) { var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(true); foreach (var r in roleOptionSettings) @@ -153,6 +171,17 @@ public static class RoleSettingMenuPatches ScrollerNum = 0.522f; var num4 = 0; + var roleOptionSettings = roleMenu.scrollBar.Inner.GetComponentsInChildren(false); + var headers = roleMenu.scrollBar.Inner.GetComponentsInChildren(false); + foreach (var child in roleOptionSettings) + { + Object.Destroy(child.gameObject); + } + foreach (var child in headers) + { + Object.Destroy(child.gameObject); + } + yield return new WaitForEndOfFrame(); var roleGroups = MenuState.Instance.CurrentMod.InternalRoles.Values.OfType() .ToLookup(x => x.RoleOptionsGroup); @@ -176,7 +205,7 @@ public static class RoleSettingMenuPatches foreach (var grouping in sortedRoleGroups) { if (!grouping.Any() || - grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings())) + grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings() || !x.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode))) { continue; } @@ -307,6 +336,7 @@ public static class RoleSettingMenuPatches RoleGroupHidden[group] = !groupHidden; } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = false; foreach (var child in roleMenu.RoleChancesSettings.transform) @@ -326,6 +356,7 @@ public static class RoleSettingMenuPatches } } } + MenuState.Instance.QueuedRoleMenuRefresh[MenuState.Instance.CurrentModIdx] = false; MenuState.Instance.FinishedRoleMenus[MenuState.Instance.CurrentModIdx] = true; } @@ -516,6 +547,26 @@ public static class RoleSettingMenuPatches CurrentRoleOptions = filteredOptions; } + [HarmonyPrefix] + [HarmonyPatch(nameof(RolesSettingsMenu.ChangeTab))] + public static void ChangeTabPatch(RolesSettingsMenu __instance) + { + var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); + categoryHeaderMasked.Title.text = TranslationController.Instance.GetString(StringNames.RoleSettingsLabel); + var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground"); + var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); + imgBg.gameObject.SetActive(true); + __instance.roleScreenshot.gameObject.SetActive(true); + __instance.roleDescriptionText.transform.parent.localPosition = new Vector3(2.5176f, -0.2731f, -1f); + __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.0675f, 0.1494f, 0.5687f); + labelBg.transform.localPosition = new Vector3(1.082f, 0.1054f, -2.5f); + __instance.roleScreenshot.drawMode = SpriteDrawMode.Simple; + foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren()) + { + optBehaviour.gameObject.Destroy(); + } + } + private static void ChangeTab(RoleBehaviour role, RolesSettingsMenu __instance) { if (role is not ICustomRole customRole) diff --git a/MiraAPI/Patches/Roles/GameStartupPatch.cs b/MiraAPI/Patches/Roles/GameStartupPatch.cs index 9f88bdd..94a7ec1 100644 --- a/MiraAPI/Patches/Roles/GameStartupPatch.cs +++ b/MiraAPI/Patches/Roles/GameStartupPatch.cs @@ -1,6 +1,8 @@ using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Utilities.Assets; namespace MiraAPI.Patches.Roles; @@ -22,6 +24,13 @@ public static class GameStartupPatch } _runOnce = true; + foreach (var mode in CustomGameModeManager.IdToModeMap.Values) + { + if (mode.Icon != null) + { + mode.TmpIcon = TmpSpriteUtils.CreateSpriteAsset(mode.Icon.LoadAsset(), $"MiraApi.Gamemode.{mode.Name.Replace(" ", string.Empty)}.png", 1.35f); + } + } if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) { diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index 8d13595..99359a6 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -3,6 +3,7 @@ using AmongUs.GameOptions; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameModes; using MiraAPI.Modifiers; using MiraAPI.Roles; using UnityEngine; @@ -65,14 +66,20 @@ public static class RoleManagerPatches [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static void ModifierSelectionPatches(RoleManager __instance) { - if (!AmongUsClient.Instance.AmHost || !ModifierManager.MiraAssignsModifiers) + if (AmongUsClient.Instance.AmHost && ModifierManager.MiraAssignsModifiers) + { + ModifierManager.AssignModifiers( + PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) + .ToList()); + } + + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (CustomGameModeManager.ActiveMode == null || roleSelection == null) { return; } - ModifierManager.AssignModifiers( - PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected) - .ToList()); + CustomGameModeManager.ActiveMode.PostAssignRoles(roleSelection); } [HarmonyPrefix] diff --git a/MiraAPI/Patches/Roles/SelectRolesPatch.cs b/MiraAPI/Patches/Roles/SelectRolesPatch.cs index dc51f03..1d17765 100644 --- a/MiraAPI/Patches/Roles/SelectRolesPatch.cs +++ b/MiraAPI/Patches/Roles/SelectRolesPatch.cs @@ -3,6 +3,7 @@ using AmongUs.GameOptions; using HarmonyLib; using System.Linq; using InnerNet; +using MiraAPI.GameModes; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -15,6 +16,21 @@ public static class SelectRolesPatch [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static bool SelectRoles() { + var roleSelection = GameManager.Instance.LogicRoleSelection.Cast(); + if (!AmongUsClient.Instance.AmHost || roleSelection == null) + { + return true; + } + + if (CustomGameModeManager.ActiveMode != null) + { + CustomGameModeManager.ActiveMode.AssignRoles(out var runOriginal, roleSelection); + + if (!runOriginal) + { + return false; + } + } if (!ApiHandlesRoleSelect) { return true; diff --git a/MiraAPI/PluginLoading/IMiraPlugin.cs b/MiraAPI/PluginLoading/IMiraPlugin.cs index a29dd4c..6fb13ed 100644 --- a/MiraAPI/PluginLoading/IMiraPlugin.cs +++ b/MiraAPI/PluginLoading/IMiraPlugin.cs @@ -1,5 +1,7 @@ using System.Linq; using BepInEx.Configuration; +using MiraAPI.Utilities.Assets; +using UnityEngine; namespace MiraAPI.PluginLoading; @@ -17,7 +19,7 @@ public interface IMiraPlugin /// Gets the name to display on the options menu. /// string OptionsTitleText { get; } - + /// /// Gets the abbreviated name to display for other mods to pick up. /// diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index db030b0..a48cceb 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -44,6 +44,15 @@ public class MiraPluginInfo /// public ReadOnlyDictionary Roles { get; private set; } = null!; + /// + /// Gets a read only dictionary of Game Mode IDs and the AbstractGameMode object they are associated with. + /// + /// Read only dictionary of IDs and Game Modes. + public ReadOnlyDictionary GetModes() + { + return new ReadOnlyDictionary(GameModes); + } + /// /// Gets a of this 's . /// @@ -72,10 +81,9 @@ public class MiraPluginInfo internal List InternalModifiers { get; } = []; + internal Dictionary GameModes { get; } = []; internal Dictionary InternalRoles { get; } = []; - internal Dictionary InternalGameModes { get; } = []; - internal List InternalButtons { get; } = []; /// diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index f9439b9..543b78d 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -7,9 +7,11 @@ using System.Reflection; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using Il2CppInterop.Runtime.Injection; using MiraAPI.Colors; using MiraAPI.Events; using MiraAPI.GameEnd; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; @@ -21,6 +23,7 @@ using MiraAPI.Patches.Options; using MiraAPI.Presets; using MiraAPI.Roles; using MiraAPI.Utilities; +using Reactor.Localization.Utilities; using Reactor.Networking; using Reactor.Utilities; @@ -33,15 +36,117 @@ public sealed class MiraPluginManager { private readonly Dictionary _registeredPlugins = []; - internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!; - internal MiraPluginInfo[] RegisteredPluginsWithOptions { get; private set; } = null!; + internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = []; + internal MiraPluginInfo[] RegisteredPluginsWithOptions { get; private set; } = []; + internal MiraPluginInfo[] PluginsWithOptionsOrGameModes { get; private set; } = []; internal Dictionary> QueuedRoleRegistrations { get; } = []; internal static MiraPluginManager Instance { get; private set; } = new(); + public static ConfigFile MiraApiConfig { get; private set; } = null!; + public static MiraPluginInfo MiraApiPluginInfo { get; private set; } = null!; - internal void Initialize() + internal void Initialize(BasePlugin miraApiPlugin, IMiraPlugin apiPlugin) { Instance = this; + CustomGameModeManager.RegisterDefaultMode(); + CustomGameModeManager.GetAndSetGameMode(); + if (IL2CPPChainloader.Instance.Plugins.TryGetValue(MiraApiPlugin.Id, out var newPlugin)) + { + var assembly = Assembly.GetCallingAssembly(); + var info = new MiraPluginInfo(apiPlugin, newPlugin); + var roles = new List(); + + var oldConfigSetting = info.PluginConfig.SaveOnConfigSet; + info.PluginConfig.SaveOnConfigSet = false; + + foreach (var type in assembly.GetTypes()) + { + if (type.IsAbstract || type.GetCustomAttribute() != null) + { + continue; + } + + foreach (var method in AccessTools.GetDeclaredMethods(type)) + { + var eventAttribute = method.GetCustomAttribute(); + if (eventAttribute == null) + { + continue; + } + + if (!method.IsStatic) + { + Error($"Event method {method.Name} in {type.Name} must be static."); + continue; + } + + var parameters = method.GetParameters(); + if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) + { + Error($"Invalid event registration method {method.Name} in {type.Name}"); + continue; + } + + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + } + + if (RegisterModifier(type, info)) + { + continue; + } + + if (RegisterOptions(type, info)) + { + continue; + } + + if (RegisterLocalTabs(type, miraApiPlugin)) + { + continue; + } + + if (RegisterRole(type, info, out var role)) + { + roles.Add(role); + continue; + } + + if (RegisterButton(type, info)) + { + continue; + } + + if (RegisterGameOver(type)) + { + continue; + } + + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + + RegisterColorClasses(type); + RegisterKeybinds(type, miraApiPlugin); + } + + info.PluginConfig.Save(); + info.PluginConfig.SaveOnConfigSet = oldConfigSetting; + MiraApiConfig = info.PluginConfig; + + info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + QueuedRoleRegistrations.Add(info, roles); + + _registeredPlugins.Add(assembly, info); + MiraApiPluginInfo = info; + + info.SavePublicCollections(); + PresetManager.CreateDefaultPreset(info); + PresetManager.LoadPresets(info); + Info($"Registering Mira API as mod."); + } + IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => { if (plugin is not IMiraPlugin miraPlugin) @@ -118,6 +223,11 @@ public sealed class MiraPluginManager continue; } + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + RegisterColorClasses(type); RegisterKeybinds(type, plugin); } @@ -146,8 +256,24 @@ public sealed class MiraPluginManager // Cache all the registered plugins into an array for easy access RegisteredPlugins = [.. _registeredPlugins.Values]; RegisteredPluginsWithOptions = [.. RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu)]; + PluginsWithOptionsOrGameModes = [..RegisteredPlugins.Where(m => m.MiraPlugin.DisplayOnOptionsMenu || m.GameModes.Count > 0)]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); + + var dict = new Dictionary(); + foreach (var (key, value) in CustomGameModeManager.IdToModeMap) + { + if (value is ClassicMode) + { + continue; + } + dict.Add(value.Name, key); + if (((AmongUs.GameOptions.GameModes) key) == AmongUs.GameOptions.GameModes.Normal) + return; + // 'Default' was still being registered twice and idk why + GameModesHelpers.ModeToName.Add((AmongUs.GameOptions.GameModes)key, CustomStringName.CreateAndRegister(value.Name)); + } + EnumInjector.InjectEnumValues(dict); }; RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); @@ -323,6 +449,20 @@ public sealed class MiraPluginManager return false; } + private static bool RegisterGameModeAttribute(Type type, MiraPluginInfo pluginInfo) + { + try + { + if (typeof(AbstractGameMode).IsAssignableFrom(type)) + return CustomGameModeManager.RegisterGameMode(type, pluginInfo); + } + catch (Exception e) + { + Error($"Failed to register gamemode {type.Name}: {e}"); + } + return false; + } + private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo) { try diff --git a/MiraAPI/Resources/ClassicGamemodeIcon.png b/MiraAPI/Resources/ClassicGamemodeIcon.png new file mode 100644 index 0000000..163ac1a Binary files /dev/null and b/MiraAPI/Resources/ClassicGamemodeIcon.png differ diff --git a/MiraAPI/Resources/HnSGamemodeIcon.png b/MiraAPI/Resources/HnSGamemodeIcon.png new file mode 100644 index 0000000..8b60c3d Binary files /dev/null and b/MiraAPI/Resources/HnSGamemodeIcon.png differ diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index f756fed..0829eaa 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,6 +1,7 @@ -using AmongUs.GameOptions; using System; +using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameModes; using MiraAPI.Patches.Freeplay; using MiraAPI.Utilities.Assets; using TMPro; @@ -100,6 +101,11 @@ public record struct CustomRoleConfiguration [HideFromIl2Cpp] public LoadableAsset? IntroSound { get; set; } = null; + /// + /// Gets or sets the associated game mode for this role. This is used to determine if the role should be available in a specific game mode. + /// + public Type AssociatedGameMode { get; set; } = typeof(ClassicMode); + /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. /// diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index 08c5cd4..659076a 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Linq; using System.Text; using AmongUs.GameOptions; +using MiraAPI.GameModes; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -23,14 +25,14 @@ public static class CustomRoleUtils { if (role is ICustomRole custom) { - return custom.CanSpawnOnCurrentMode(); + return custom.CanSpawnOnCurrentMode() && custom.Configuration.AssociatedGameMode.IsInstanceOfType(CustomGameModeManager.ActiveMode); } - if (GameManager.Instance.IsHideAndSeek()) + if (CustomGameModeManager.ActiveMode is HideAndSeekMode) { return role.Role is RoleTypes.Engineer || role.Role is RoleTypes.Impostor; } - return true; + return !Helpers.IsRoleBlacklisted(role); } public static List<(ushort RoleType, int Chance)> GetPossibleRoles( diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 82402b7..bec994a 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -130,6 +130,16 @@ public static class MiraAssets /// public static LoadableResourceAsset ModifierFile { get; } = new("MiraAPI.Resources.ModifierFile.png"); + /// + /// Gets the sprite used for the Classic gamemode. + /// + public static LoadableResourceAsset ClassicGamemodeIcon { get; } = new("MiraAPI.Resources.ClassicGamemodeIcon.png"); + + /// + /// Gets the sprite used for the Hide n Seek gamemode. + /// + public static LoadableResourceAsset HnSGamemodeIcon { get; } = new("MiraAPI.Resources.HnSGamemodeIcon.png"); + /// /// Gets the sprite used for timed modifier file in TaskAdderGame. /// diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 0bd42eb..c31b9e5 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using HarmonyLib; +using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; @@ -676,7 +677,8 @@ public static class Extensions public static bool IsCustom(this OptionBehaviour optionBehaviour) { return ModdedOptionsManager.ModdedOptions.Values.Any( - opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour); + opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour) + || optionBehaviour.Equals(GameModeOption.OptionBehaviour); } ///