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