From ce15e93e596996d674ca219ea7f644ce15672061 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:40:03 -0500 Subject: [PATCH 01/86] Fix DefaultKeybind name + remove requirement By default, buttons will use no keybind, and will not add a new keybind option in doing so. --- MiraAPI.Example/Buttons/Freezer/FreezeButton.cs | 2 +- MiraAPI.Example/Buttons/MeetingButton.cs | 2 +- MiraAPI.Example/Buttons/NeutralKillerButton.cs | 2 +- MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs | 2 +- MiraAPI/Hud/CustomActionButton.cs | 2 +- MiraAPI/PluginLoading/MiraPluginManager.cs | 9 +++++++-- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 4edd4f8..ddefcc2 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -20,7 +20,7 @@ public class FreezeButton : CustomActionButton public override int MaxUses => (int)OptionGroupSingleton.Instance.FreezeUses; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.T; + public override KeyboardKeyCode DefaultKeybind => KeyboardKeyCode.T; public override ModifierKey Modifier1 => ModifierKey.Control; protected override void OnClick() diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index c0acc20..900d8a2 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -16,7 +16,7 @@ public class MeetingButton : CustomActionButton public override int MaxUses => 3; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.P; + public override KeyboardKeyCode DefaultKeybind => KeyboardKeyCode.P; public override ModifierKey Modifier1 => ModifierKey.Control; public override ModifierKey Modifier2 => ModifierKey.Shift; diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example/Buttons/NeutralKillerButton.cs index 02fc908..79fe06b 100644 --- a/MiraAPI.Example/Buttons/NeutralKillerButton.cs +++ b/MiraAPI.Example/Buttons/NeutralKillerButton.cs @@ -13,7 +13,7 @@ public class NeutralKillerButton : CustomActionButton public override string Name => "Win Game"; public override float Cooldown => 0f; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.K; + public override KeyboardKeyCode DefaultKeybind => KeyboardKeyCode.K; protected override void OnClick() { CustomGameOver.Trigger([PlayerControl.LocalPlayer.Data]); diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index c4af2ca..55f773b 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -22,7 +22,7 @@ public class TeleportButton : CustomActionButton public override LoadableAsset Sprite => ExampleAssets.TeleportButton; public override Color TextOutlineColor => new Color32(221, 176, 152, 255); public static bool IsZoom { get; private set; } - public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.M; + public override KeyboardKeyCode DefaultKeybind => KeyboardKeyCode.M; public override bool Enabled(RoleBehaviour? role) { return role is TeleporterRole; diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index ac48c4a..81ac5bd 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -56,7 +56,7 @@ public abstract class CustomActionButton /// /// Gets or sets the current key binding for this button. /// - public abstract KeyboardKeyCode Defaultkeybind { get; } + public virtual KeyboardKeyCode DefaultKeybind { get; } = KeyboardKeyCode.None; /// /// The first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key to activate the button. diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 02e9102..49adbda 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -18,6 +18,7 @@ using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; +using Rewired; using UnityEngine; namespace MiraAPI.PluginLoading; @@ -137,9 +138,13 @@ public sealed class MiraPluginManager foreach (var button in CustomButtonManager.Buttons) { - KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", button.Defaultkeybind, button.ClickHandler, modifier1: button.Modifier1, modifier2: button.Modifier2, modifier3: button.Modifier3); + if (button.DefaultKeybind is KeyboardKeyCode.None) + { + continue; + } + KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", button.DefaultKeybind, button.ClickHandler, modifier1: button.Modifier1, modifier2: button.Modifier2, modifier3: button.Modifier3); - Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {button.Defaultkeybind}."); + Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {button.DefaultKeybind}."); } }; } From f1a5aef581829259a812ab5539de5c00a97a3c1c Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:52:46 -0500 Subject: [PATCH 02/86] Remove requirement but allow blank keybind --- MiraAPI.Example/Buttons/Freezer/FreezeButton.cs | 2 +- MiraAPI.Example/Buttons/MeetingButton.cs | 2 +- MiraAPI.Example/Buttons/NeutralKillerButton.cs | 2 +- MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs | 2 +- MiraAPI/Hud/CustomActionButton.cs | 10 +++++----- MiraAPI/PluginLoading/MiraPluginManager.cs | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index ddefcc2..1f10c6f 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -20,7 +20,7 @@ public class FreezeButton : CustomActionButton public override int MaxUses => (int)OptionGroupSingleton.Instance.FreezeUses; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode DefaultKeybind => KeyboardKeyCode.T; + public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.T; public override ModifierKey Modifier1 => ModifierKey.Control; protected override void OnClick() diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index 900d8a2..89e722b 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -16,7 +16,7 @@ public class MeetingButton : CustomActionButton public override int MaxUses => 3; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode DefaultKeybind => KeyboardKeyCode.P; + public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.P; public override ModifierKey Modifier1 => ModifierKey.Control; public override ModifierKey Modifier2 => ModifierKey.Shift; diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example/Buttons/NeutralKillerButton.cs index 79fe06b..58d8bee 100644 --- a/MiraAPI.Example/Buttons/NeutralKillerButton.cs +++ b/MiraAPI.Example/Buttons/NeutralKillerButton.cs @@ -13,7 +13,7 @@ public class NeutralKillerButton : CustomActionButton public override string Name => "Win Game"; public override float Cooldown => 0f; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode DefaultKeybind => KeyboardKeyCode.K; + public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.K; protected override void OnClick() { CustomGameOver.Trigger([PlayerControl.LocalPlayer.Data]); diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index 55f773b..78beb0b 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -22,7 +22,7 @@ public class TeleportButton : CustomActionButton public override LoadableAsset Sprite => ExampleAssets.TeleportButton; public override Color TextOutlineColor => new Color32(221, 176, 152, 255); public static bool IsZoom { get; private set; } - public override KeyboardKeyCode DefaultKeybind => KeyboardKeyCode.M; + public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.M; public override bool Enabled(RoleBehaviour? role) { return role is TeleporterRole; diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 81ac5bd..4bd8c8e 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -54,23 +54,23 @@ public abstract class CustomActionButton public virtual int MaxUses => 0; /// - /// Gets or sets the current key binding for this button. + /// Gets the current key binding for this button. If null, no keybind will be added. /// - public virtual KeyboardKeyCode DefaultKeybind { get; } = KeyboardKeyCode.None; + public virtual KeyboardKeyCode? DefaultKeybind { get; } /// - /// The first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key to activate the button. + /// Gets the first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key to activate the button. /// public virtual ModifierKey Modifier1 => ModifierKey.None; /// - /// The second optional modifier key. + /// Gets the second optional modifier key. /// Set to if unused. /// public virtual ModifierKey Modifier2 => ModifierKey.None; /// - /// The third optional modifier key. + /// Gets the third optional modifier key. /// Set to if unused. /// public virtual ModifierKey Modifier3 => ModifierKey.None; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 49adbda..759a0fd 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -138,13 +138,13 @@ public sealed class MiraPluginManager foreach (var button in CustomButtonManager.Buttons) { - if (button.DefaultKeybind is KeyboardKeyCode.None) + if (button.DefaultKeybind == null) { continue; } - KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", button.DefaultKeybind, button.ClickHandler, modifier1: button.Modifier1, modifier2: button.Modifier2, modifier3: button.Modifier3); + KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", (KeyboardKeyCode)button.DefaultKeybind, button.ClickHandler, modifier1: button.Modifier1, modifier2: button.Modifier2, modifier3: button.Modifier3); - Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {button.DefaultKeybind}."); + Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {(KeyboardKeyCode)button.DefaultKeybind}."); } }; } From f006deab3faada833693f5db3669fc5831d4be4f Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 24 Aug 2025 18:33:52 -0500 Subject: [PATCH 03/86] Fix Send Immediately warning from Reactor --- MiraAPI/Modifiers/ModifierExtensions.cs | 2 +- MiraAPI/Networking/CustomMurderRpc.cs | 2 +- MiraAPI/Voting/VotingUtils.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index 309a7d2..9c6f741 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -63,7 +63,7 @@ public static class ModifierExtensions /// /// The player to remove the modifier from. /// The unique ID of the modifier. - [MethodRpc((uint)MiraRpc.RemoveModifier, SendImmediately = true)] + [MethodRpc((uint)MiraRpc.RemoveModifier)] public static void RpcRemoveModifier(this PlayerControl target, Guid uniqueId) { target.RemoveModifier(uniqueId); diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 4a7bc30..209afcd 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -29,7 +29,7 @@ public static class CustomMurderRpc /// Should the killer be snapped to the dead player. /// Should the kill animation be shown. /// Should the kill sound be played. - [MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before, SendImmediately = true)] + [MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before)] public static void RpcCustomMurder( this PlayerControl source, PlayerControl target, diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index 159b576..38fbf16 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -71,7 +71,7 @@ public static class VotingUtils /// The player who is sending the RPC. Should be the host. /// The player who voted. /// The player who the voter voted for. - [MethodRpc((uint)MiraRpc.RemoveVote, SendImmediately = true)] + [MethodRpc((uint)MiraRpc.RemoveVote)] public static void RpcRemoveVote(PlayerControl source, byte voterId, byte votedFor) { if (!source.IsHost()) return; @@ -107,7 +107,7 @@ public static class VotingUtils /// The player who sent this RPC. /// The id of the player who casted the vote. /// The voted player's id. - [MethodRpc((uint)MiraRpc.CastVote, SendImmediately = true)] + [MethodRpc((uint)MiraRpc.CastVote)] public static void RpcCastVote(PlayerControl source, byte srcPlayerId, byte suspectPlayerId) { CustomCastVote(srcPlayerId, suspectPlayerId); From 85a0275eeac01309c9f2bbb6282940a7fdfab091 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 24 Aug 2025 19:48:43 -0500 Subject: [PATCH 04/86] Add IntroRoleRevealEvent Also needs to be changed for the android branch --- .../Vanilla/Gameplay/IntroRoleRevealEvent.cs | 21 +++++++++++++++++++ MiraAPI/Patches/IntroCutscenePatches.cs | 10 +++++++++ 2 files changed, 31 insertions(+) create mode 100644 MiraAPI/Events/Vanilla/Gameplay/IntroRoleRevealEvent.cs diff --git a/MiraAPI/Events/Vanilla/Gameplay/IntroRoleRevealEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/IntroRoleRevealEvent.cs new file mode 100644 index 0000000..809ce11 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Gameplay/IntroRoleRevealEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla.Gameplay; + +/// +/// The event that is invoked when the player's role is shown on the intro cutscene. Non cancelable. +/// +public class IntroRoleRevealEvent : MiraEvent +{ + /// + /// Gets the instance of the IntroCutscene. + /// + public IntroCutscene IntroCutscene { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The intro cutscene. + public IntroRoleRevealEvent(IntroCutscene introCutscene) + { + IntroCutscene = introCutscene; + } +} diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index af7ce8c..41217e6 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -29,6 +29,16 @@ public static class IntroCutscenePatches MiraEventManager.InvokeEvent(@event); } + [HarmonyPostfix] + [HarmonyPatch(typeof(IntroCutscene._ShowRole_d__41), nameof(IntroCutscene._ShowRole_d__41.MoveNext))] + [HarmonyPriority(Priority.Last)] + public static void ShowRolePatch(IntroCutscene._ShowRole_d__41 __instance) + { + var realInstance = __instance.__4__this; + var @event = new IntroRoleRevealEvent(realInstance); + MiraEventManager.InvokeEvent(@event); + } + [HarmonyPrefix] [HarmonyPatch(nameof(IntroCutscene.BeginImpostor))] [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] From e708c23f81133781520a5937bcb617b146c0c0d5 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Thu, 28 Aug 2025 11:26:45 -0400 Subject: [PATCH 05/86] Fix checks and positions for keybinds --- MiraAPI/Hud/CustomActionButton.cs | 3 ++- MiraAPI/Patches/HudManagerPatches.cs | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 29a76b3..1400f4c 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -205,7 +205,8 @@ public abstract class CustomActionButton KeybindIcon.transform.GetChild(0).GetComponent().text = KeybindManager.GetEntries().First(x => x.Id == $"{Name}_Keybind").Key.ToString(); KeybindIcon.name = "KeybindIcon"; - KeybindIcon.transform.localPosition = new(0.341f, 0.374f, -0.1f); + KeybindIcon.transform.localPosition = new(0.5f, 0.5f, -0.1f); + Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.5f, -0.1f); } /// diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index acb4482..ce244db 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -132,9 +132,6 @@ public static class HudManagerPatches [HarmonyPostfix] public static void StartPostfix() { - if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return; - if (PlayerControl.LocalPlayer.Data.IsDead) return; - foreach (var entry in KeybindManager.GetEntries()) { var player = ReInput.players.GetPlayer(0); From 2237d3a2dfbd41909f3f29b4a34b416b37c0520a Mon Sep 17 00:00:00 2001 From: ang-xd Date: Thu, 28 Aug 2025 12:33:19 -0400 Subject: [PATCH 06/86] Additional changes to keybind icons --- .../Buttons/Freezer/FreezeButton.cs | 2 -- MiraAPI/Hud/CustomActionButton.cs | 36 ++++++++++++++----- MiraAPI/Hud/CustomButtonManager.cs | 1 + MiraAPI/Patches/HudManagerPatches.cs | 30 ++++++++++++++++ MiraAPI/Utilities/Extensions.cs | 2 +- MiraAPI/Utilities/Helpers.cs | 22 ++++++++++++ 6 files changed, 82 insertions(+), 11 deletions(-) diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 1f10c6f..726c16f 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -17,8 +17,6 @@ public class FreezeButton : CustomActionButton public override float Cooldown => OptionGroupSingleton.Instance.FreezeDuration; - public override int MaxUses => (int)OptionGroupSingleton.Instance.FreezeUses; - public override LoadableAsset Sprite => ExampleAssets.ExampleButton; public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.T; public override ModifierKey Modifier1 => ModifierKey.Control; diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 1400f4c..ffaa315 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -126,7 +126,17 @@ public abstract class CustomActionButton /// /// Gets the gameObject used for the keybind icon. /// - public GameObject KeybindIcon { get; set; } + public GameObject? KeybindIcon { get; private set; } + + /// + /// Gets or sets the keybind entry for the button. + /// + private KeybindManager.KeybindEntry? KeybindEntry { get; set; } + + /// + /// Gets or sets the keybind icon text. + /// + private TextMeshPro? KeybindText { get; set; } /// /// The method used to create the button. @@ -200,13 +210,18 @@ public abstract class CustomActionButton } })); - KeybindIcon = Object.Instantiate(Button.usesRemainingSprite.gameObject, Button.transform); - KeybindIcon.GetComponent().sprite = MiraAssets.KeybindButton.LoadAsset(); - KeybindIcon.transform.GetChild(0).GetComponent().text = - KeybindManager.GetEntries().First(x => x.Id == $"{Name}_Keybind").Key.ToString(); - KeybindIcon.name = "KeybindIcon"; - KeybindIcon.transform.localPosition = new(0.5f, 0.5f, -0.1f); - Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.5f, -0.1f); + if (DefaultKeybind != KeyboardKeyCode.None) + { + KeybindEntry = KeybindManager.GetEntries().First(x => x.Id == $"{Name}_Keybind"); + KeybindIcon = + Helpers.CreateKeybindIcon( + Button.gameObject, + KeybindEntry.Key, + new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f) + ); + KeybindText = KeybindIcon.transform.GetChild(0).GetComponent(); + Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f); + } } /// @@ -465,6 +480,11 @@ public abstract class CustomActionButton /// The local PlayerControl. public virtual void FixedUpdateHandler(PlayerControl playerControl) { + if (KeybindText != null && KeybindEntry != null) + { + KeybindText.text = KeybindEntry.Key.ToString(); + } + if (Timer >= 0) { if (!TimerPaused) diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs index e576595..8e57c40 100644 --- a/MiraAPI/Hud/CustomButtonManager.cs +++ b/MiraAPI/Hud/CustomButtonManager.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Reflection; using MiraAPI.Events.Mira; using MiraAPI.PluginLoading; +using UnityEngine; namespace MiraAPI.Hud; diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index ce244db..fa848de 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,11 +1,14 @@ using System; using System.Collections; +using System.Collections.Generic; using HarmonyLib; using MiraAPI.Hud; using MiraAPI.Keybinds; +using MiraAPI.Utilities; using Reactor.Utilities; using Reactor.Utilities.Extensions; using Rewired; +using TMPro; using UnityEngine; using Object = UnityEngine.Object; @@ -22,6 +25,8 @@ public static class HudManagerPatches public static Transform? BottomRight { get; private set; } public static Transform? Buttons { get; private set; } + private static Dictionary VanillaKeybindIcons = new(); + /* /// /// Trigger hudstart on current custom gamemode @@ -96,6 +101,26 @@ public static class HudManagerPatches gridArrange.ArrangeChilds(); aspectPosition.AdjustPosition(); + + var keybindIconPos = new Vector3(-0.4f, 0.4f, -9f); + var vanillaButtons = new Dictionary + { + { __instance.KillButton.gameObject, 8 }, + { __instance.UseButton.gameObject, 6 }, + { __instance.ReportButton.gameObject, 7 }, + { __instance.ImpostorVentButton.gameObject, 50 }, + { __instance.SabotageButton.gameObject, 4 }, + }; + + foreach (var kvp in vanillaButtons) + { + var buttonObj = kvp.Key; + var actionId = kvp.Value; + + var key = Helpers.GetKeybindByActionId(actionId); + var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); + VanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); + } } /// @@ -132,6 +157,11 @@ public static class HudManagerPatches [HarmonyPostfix] public static void StartPostfix() { + foreach (var btnIcon in VanillaKeybindIcons) + { + btnIcon.Key.text = Helpers.GetKeybindByActionId(btnIcon.Value).ToString(); + } + foreach (var entry in KeybindManager.GetEntries()) { var player = ReInput.players.GetPlayer(0); diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 863df73..e5f991f 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -94,7 +94,7 @@ public static class Extensions /// The current timer value. /// The maximum timer value. /// The format string to use for the timer text. - public static void SetCooldownFormat(this ActionButton button, float timer, float maxTimer, string format = "0") + public static void SetCooldownFormat(this ActionButton? button, float timer, float maxTimer, string format = "0") { var num = Mathf.Clamp(timer / maxTimer, 0f, 1f); button.isCoolingDown = num > 0f; diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 2bf7b37..6c0bf4c 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using Il2CppSystem.IO; using MiraAPI.Keybinds; +using MiraAPI.Utilities.Assets; using Rewired; using TMPro; using UnityEngine; @@ -25,6 +26,27 @@ public static class Helpers { return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x => x.Object)]; } + internal static GameObject CreateKeybindIcon(GameObject button, KeyboardKeyCode keyCode, Vector3 localPos) + { + var keybindIcon = Object.Instantiate(HudManager.Instance.AbilityButton.usesRemainingSprite.gameObject, button.transform); + keybindIcon.GetComponent().sprite = MiraAssets.KeybindButton.LoadAsset(); + keybindIcon.transform.GetChild(0).GetComponent().text = keyCode.ToString(); + keybindIcon.name = "KeybindIcon"; + keybindIcon.transform.localPosition = localPos; + return keybindIcon; + } + + /// + /// Gets the keybind for an action with ReInput. + /// + /// The action ID. + /// The keyboard key code. + public static KeyboardKeyCode GetKeybindByActionId(int actionId) + { + var player = ReInput.players.GetPlayer(0); + return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false) + .keyboardKeyCode; + } /// /// Determines whether a given probability check succeeds. From b4e30b2b68814e798ccc4c6e6a095efd8176eb9d Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 29 Aug 2025 13:04:18 +0200 Subject: [PATCH 07/86] wip --- MiraAPI.Example/ExampleLocalSetings.cs | 19 ++ MiraAPI.Example/ExamplePlugin.cs | 1 + .../Attributes/LocalSettingAttribute.cs | 11 ++ .../Attributes/LocalSettingToggleAttribute.cs | 11 ++ MiraAPI/LocalSettings/ILocalSetting.cs | 16 ++ MiraAPI/LocalSettings/LocalSettingsTab.cs | 164 ++++++++++++++++++ .../SettingTypes/LocalSetingToggle.cs | 117 +++++++++++++ .../SettingTypes/LocalSettingBase.cs | 54 ++++++ 8 files changed, 393 insertions(+) create mode 100644 MiraAPI.Example/ExampleLocalSetings.cs create mode 100644 MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs create mode 100644 MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs create mode 100644 MiraAPI/LocalSettings/ILocalSetting.cs create mode 100644 MiraAPI/LocalSettings/LocalSettingsTab.cs create mode 100644 MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs create mode 100644 MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs new file mode 100644 index 0000000..0feb0f0 --- /dev/null +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -0,0 +1,19 @@ +using BepInEx.Configuration; +using MiraAPI.LocalSettings; +using MiraAPI.LocalSettings.Attributes; +using Reactor.Utilities; + +namespace MiraAPI.Example; + +public static class ExampleLocalSetings +{ + public static LocalSettingsTab ModTab { get; } = new LocalSettingsTab("Example Tab"); + + [LocalSettingToggle] + public static ConfigEntry ExampleToggleSetting { get; set; } + + internal static void BindSettings(ConfigFile config) + { + ExampleToggleSetting = config.Bind("General", "Example Toggle", false); + } +} \ No newline at end of file diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index b20ec41..19d23e7 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -21,6 +21,7 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin public ConfigFile GetConfigFile() => Config; public override void Load() { + ExampleLocalSetings.BindSettings(Config); ExampleEventHandlers.Initialize(); Harmony.PatchAll(); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs new file mode 100644 index 0000000..3d72d3c --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Base class for all local settings attributes. +/// +[AttributeUsage(AttributeTargets.Property)] +public abstract class LocalSettingAttribute : Attribute +{ +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs new file mode 100644 index 0000000..428acf1 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates a toggle and binds it with the config entry. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalSettingToggleAttribute : LocalSettingAttribute +{ +} diff --git a/MiraAPI/LocalSettings/ILocalSetting.cs b/MiraAPI/LocalSettings/ILocalSetting.cs new file mode 100644 index 0000000..78d3647 --- /dev/null +++ b/MiraAPI/LocalSettings/ILocalSetting.cs @@ -0,0 +1,16 @@ +using BepInEx.Configuration; +using UnityEngine; + +namespace MiraAPI.LocalSettings; + +/// +/// Interface for all local settings +/// +public interface ILocalSetting +{ + string Name { get; } + string Description { get; } + GameObject? Setting { get; } + ConfigEntryBase ConfigEntry { get; } + GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs new file mode 100644 index 0000000..8a6b2dc --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.LocalSettings; + +/// +/// Represents a local settings tab. Gets a button by default +/// +public class LocalSettingsTab +{ + public string TabName { get; } + public Color TabColor { get; } + public Color TabHoverColor { get; } + public Color TabActiveColor { get; } + public LoadableAsset? TabIcon { get; } + + public TabGroup? TabButton { get; protected set; } + private bool ShouldCreateButton { get; set; } + + public List Settings { get; } + + /// + public LocalSettingsTab(string tabName) : this( + tabName, + Color.white, + Palette.AcceptedGreen, + Palette.AcceptedGreen, + createButton: true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the tab. + /// The color of the tab. + /// The hover color of the tab. + /// The active color of the tab. + /// The tab icon. Defaults to no icon. + /// Should the tab button be created. Without it the tab needs to be opened manually. + public LocalSettingsTab(string tabName, Color tabColor, Color tabHoverColor, Color tabActiveColor, LoadableAsset? tabIcon = null, bool createButton = false) + { + TabName = tabName; + TabColor = tabColor; + TabHoverColor = tabHoverColor; + TabActiveColor = tabActiveColor; + TabIcon = tabIcon; + ShouldCreateButton = createButton; + Settings = new(); + } + + public GameObject? CreateTab(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + { + var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); + tab.name = $"{TabName}Tab"; + tab.transform.DestroyChildren(); + tab.gameObject.SetActive(false); + + if (TabButton != null) + { + TabButton.Content = tab.gameObject; + } + + var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent(); + var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent(); + + float contentOffset = 0; + var contentOrder = 1; + var contentIndex = 1; + foreach (var setting in Settings) + { + setting.CreateOption( + toggle, + slider, + tab.transform, + ref contentOffset, + ref contentOrder, + contentIndex == Settings.Count); + contentIndex++; + } + + if (ShouldCreateButton) + { + var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); + tabButtonObject.name = $"{TabName} Button"; + tabButtonObject.transform.localPosition = new Vector3(2.4f, 2.1f - offset, 5.5f); + tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1); + tabButtonObject.Button.color = TabColor; + TabButton = tabButtonObject; + + var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent(); + tabButtonText.GetComponent().Destroy(); // i hate text translators + tabButtonText.transform.localPosition = new Vector3(0.078f, 0, 0); + tabButtonText.transform.localScale = new Vector3(0.9f, 0.9f); + tabButtonText.alignment = TextAlignmentOptions.Right; + tabButtonText.text = $"{GetShortName(TabName)}"; + + var tabButton = tabButtonObject.GetComponent(); + var rollover = tabButtonObject.Rollover; + rollover.OverColor = TabColor; + rollover.OutColor = TabColor; + + SpriteRenderer tabButtonRend = null; + if (TabIcon != null) + { + tabButtonRend = new GameObject("sprite").AddComponent(); + tabButtonRend.gameObject.layer = 5; // ui layer + tabButtonRend.transform.SetParent(tabButtonObject.transform); + tabButtonRend.transform.localPosition = new Vector3(0.5f, 0, -2); + tabButtonRend.transform.localScale = new Vector3(0.5f, 0.5f, 1); + tabButtonRend.sprite = TabIcon.LoadAsset(); + tabButtonText.gameObject.SetActive(false); + } + + int tabIndex = tabIdx; // Local copies of the tab variables so we can use them in the lambdas + float tabOffset = offset; + tabButton.OnClick.AddListener((UnityAction)(() => + { + instance.OpenTabGroup(tabIndex + 10); + })); + tabButton.OnMouseOver.AddListener((UnityAction)(() => + { + tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f); + + if (tabButtonRend != null) + { + tabButtonRend.gameObject.SetActive(false); + } + tabButtonText.gameObject.SetActive(true); + tabButtonText.transform.localPosition = new Vector3(0.045f, 0, 0); + tabButtonText.maxVisibleCharacters = int.MaxValue; + tabButtonText.text = TabName; + })); + tabButton.OnMouseOut.AddListener((UnityAction)(() => + { + tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f); + + if (tabButtonRend != null) + { + tabButtonRend.gameObject.SetActive(true); + tabButtonText.gameObject.SetActive(false); + } + tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0); + tabButtonText.maxVisibleCharacters = 4; + tabButtonText.text = $"{GetShortName(TabName)}"; + })); + } + + return tab; + } + + private static string GetShortName(string name) + { + string shortName = string.Empty; + name.Split(' ').Do(x => shortName += x[0]); + return shortName; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs new file mode 100644 index 0000000..a2443dd --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs @@ -0,0 +1,117 @@ +using BepInEx.Configuration; +using Reactor.Localization.Utilities; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.LocalSettings.SettingTypes; + +public class LocalSetingToggle : LocalSettingBase +{ + /// + /// Gets the color of the toggle. + /// + public Color ToggleColor { get; } + + /// + /// Gets the color of the toggle when hovered. + /// + public Color ToggleHoverColor { get; } + + /// + /// Gets the color of the toggle when enabled. + /// + public Color ToggleActiveColor { get; } + + /// + public LocalSetingToggle(LocalSettingsTab tab, ConfigEntryBase configEntry, string name = null, string description = null) : this( + tab, + configEntry, + name, + description, + Color.white, + Palette.AcceptedGreen, + Palette.AcceptedGreen) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The binded config entry. + /// The name of the setting. Defaults to ConfigEntry key. + /// The desription of the setting. No description by default. + /// The color of the toggle. + /// The hover color of the toggle. + /// The active color of the toggle. + public LocalSetingToggle( + LocalSettingsTab tab, + ConfigEntryBase configEntry, + string name, + string description, + Color toggleColor, + Color toggleHoverColor, + Color toggleActiveColor) + : base(tab, configEntry, name, description) + { + ToggleColor = toggleColor; + ToggleHoverColor = toggleHoverColor; + ToggleActiveColor = toggleActiveColor; + } + + public override GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + { + var toggleObject = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = toggleObject.transform.FindChild("Text_TMP").GetComponent(); + var passiveButton = toggleObject.GetComponent(); + var rollover = toggleObject.GetComponent(); + toggleObject.gameObject.SetActive(true); + + if (last && order == 1) + { + // Toggle in the middle + toggleObject.transform.localPosition = new Vector3(0, 1.85f - offset); + } + else + { + toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.35f : 1.28f, 1.85f - offset); + } + + toggleObject.BaseText = CustomStringName.CreateAndRegister(Name); + toggleObject.UpdateText(GetValue()); + toggleObject.name = Name; + toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + passiveButton.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OverColor = ToggleHoverColor; + + passiveButton.OnClick.AddListener((UnityAction)(() => + { + SetValue(!GetValue()); + toggleObject.UpdateText(GetValue()); + toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + })); + passiveButton.OnMouseOver.AddListener((UnityAction)(() => + { + if (!Description.IsNullOrWhiteSpace()) + { + tmp.text = Description; + } + })); + passiveButton.OnMouseOut.AddListener((UnityAction)(() => + { + toggleObject.UpdateText(GetValue()); + toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + })); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return toggleObject.gameObject; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs new file mode 100644 index 0000000..5634ecf --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -0,0 +1,54 @@ +using BepInEx.Configuration; +using TMPro; +using UnityEngine; + +namespace MiraAPI.LocalSettings.SettingTypes; + +public abstract class LocalSettingBase : ILocalSetting +{ + public string Name { get; } + public string Description { get; } + + public GameObject? Setting { get; } + public ConfigEntryBase ConfigEntry { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The config entry. + /// The name of the setting. + /// The description of the setting. + protected LocalSettingBase(LocalSettingsTab tab, ConfigEntryBase configEntry, string name = null, string description = null) + { + ConfigEntry = configEntry; + Name = name ?? ConfigEntry.Definition.Key; + Description = description; + + tab.Settings.Add(this); + } + + /// + /// Creates an instance of the setting. + /// + /// The transform parent of the setting. + /// The created setting. + public abstract GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); + + /// + /// Gets the value of the config entry, casted to T. + /// + /// The value. + public T GetValue() + { + return (T)ConfigEntry.BoxedValue; + } + + /// + /// Sets the value of the config entry + /// + /// The value to set to. + public void SetValue(T value) + { + ConfigEntry.BoxedValue = value; + } +} From 5f67960b28d4a3c270d444f0f0375f47609f997b Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 29 Aug 2025 13:38:35 +0200 Subject: [PATCH 08/86] cancelable abilities --- MiraAPI.Example/Buttons/ThinkButton.cs | 29 ++++++++++++++++++++++++++ MiraAPI/Hud/CustomActionButton.cs | 27 +++++++++++++++++------- 2 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 MiraAPI.Example/Buttons/ThinkButton.cs diff --git a/MiraAPI.Example/Buttons/ThinkButton.cs b/MiraAPI.Example/Buttons/ThinkButton.cs new file mode 100644 index 0000000..95ce73a --- /dev/null +++ b/MiraAPI.Example/Buttons/ThinkButton.cs @@ -0,0 +1,29 @@ +using MiraAPI.Hud; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.Buttons; + +public class ThinkButton : CustomActionButton +{ + public override string Name => "Think"; + public override float Cooldown => 10f; + public override float EffectDuration => 10f; + public override bool EffectCancelable => true; + public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + protected override void OnClick() + { + // This button does absolutely nothing besides changing its text + Button!.OverrideText("Thinking..."); + } + + public override void OnEffectEnd() + { + Button!.OverrideText("Think"); + } + + public override bool Enabled(RoleBehaviour? role) + { + return role != null && role.IsImpostor; + } +} diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index ffaa315..78ebd4d 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -47,10 +47,20 @@ public abstract class CustomActionButton public virtual string CooldownTimerFormatString => "0"; /// - /// Gets the button's effect duration in seconds. If the button has no effect, set to 0. + /// Gets a value indicating whether the button has an effect ability. By default true if is greater than 0. + /// + public virtual bool HasEffect => EffectDuration > 0; + + /// + /// Gets the button's effect duration in seconds. If 0, the effect needs to be ended manually. /// public virtual float EffectDuration => 0; + /// + /// Gets a value indicating whether the effect can be ended early by the player. + /// + public virtual bool EffectCancelable => false; + /// /// Gets the maximum amount of uses the button has. If the button has infinite uses, set to 0. /// @@ -88,11 +98,6 @@ public abstract class CustomActionButton /// public virtual ButtonLocation Location { get; set; } = ButtonLocation.BottomLeft; - /// - /// Gets a value indicating whether the button has an effect ability. - /// - public bool HasEffect => EffectDuration > 0; - /// /// Gets a value indicating whether the button has limited uses. /// @@ -416,7 +421,7 @@ public abstract class CustomActionButton /// A value that represents whether the button can be clicked or not. public virtual bool CanClick() { - return Timer <= 0 && !EffectActive && CanUse(); + return (EffectActive ? EffectCancelable : Timer <= 0) && CanUse(); } /// @@ -426,7 +431,7 @@ public abstract class CustomActionButton /// A value that represents whether the button should light up or not. public virtual bool CanUse() { - return PlayerControl.LocalPlayer.moveable && (!LimitedUses || UsesLeft > 0); + return PlayerControl.LocalPlayer.moveable && (EffectActive || !LimitedUses || UsesLeft > 0); } /// @@ -448,6 +453,12 @@ public abstract class CustomActionButton /// public virtual void ClickHandler() { + if (EffectActive && EffectCancelable) + { + ResetCooldownAndOrEffect(); + return; + } + if (!CanClick()) { return; From b97ada22bc16511c8adc900f3b5dedc3b427471b Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 29 Aug 2025 13:58:06 +0200 Subject: [PATCH 09/86] allow the duration to be 0 --- MiraAPI.Example/Buttons/ThinkButton.cs | 2 +- MiraAPI/Hud/CustomActionButton.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/MiraAPI.Example/Buttons/ThinkButton.cs b/MiraAPI.Example/Buttons/ThinkButton.cs index 95ce73a..505365d 100644 --- a/MiraAPI.Example/Buttons/ThinkButton.cs +++ b/MiraAPI.Example/Buttons/ThinkButton.cs @@ -7,7 +7,7 @@ namespace MiraAPI.Example.Buttons; public class ThinkButton : CustomActionButton { public override string Name => "Think"; - public override float Cooldown => 10f; + public override float Cooldown => 15f; public override float EffectDuration => 10f; public override bool EffectCancelable => true; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 78ebd4d..4985aab 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -431,7 +431,8 @@ public abstract class CustomActionButton /// A value that represents whether the button should light up or not. public virtual bool CanUse() { - return PlayerControl.LocalPlayer.moveable && (EffectActive || !LimitedUses || UsesLeft > 0); + return PlayerControl.LocalPlayer.moveable && + ((EffectActive && EffectCancelable) || (!EffectActive && (!LimitedUses || UsesLeft > 0))); } /// @@ -503,7 +504,7 @@ public abstract class CustomActionButton Timer -= Time.deltaTime; } } - else if (HasEffect && EffectActive) + else if (HasEffect && EffectActive && EffectDuration > 0) { EffectActive = false; Timer = Cooldown; @@ -521,7 +522,7 @@ public abstract class CustomActionButton Button!.SetDisabled(); } - if (EffectActive) + if (EffectActive && EffectDuration > 0) { Button.SetFillUp(Timer, EffectDuration); From e6d38195176ff9f9bd6ab407227d91ca40045b65 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 29 Aug 2025 14:20:19 +0200 Subject: [PATCH 10/86] add uses mode for buttons --- MiraAPI.Example/Buttons/ThinkButton.cs | 2 ++ MiraAPI/Hud/ButtonUsesMode.cs | 17 +++++++++++++++++ MiraAPI/Hud/CustomActionButton.cs | 5 +++++ MiraAPI/Patches/Hud/ButtonResetPatches.cs | 5 +++++ 4 files changed, 29 insertions(+) create mode 100644 MiraAPI/Hud/ButtonUsesMode.cs diff --git a/MiraAPI.Example/Buttons/ThinkButton.cs b/MiraAPI.Example/Buttons/ThinkButton.cs index 505365d..b1d7164 100644 --- a/MiraAPI.Example/Buttons/ThinkButton.cs +++ b/MiraAPI.Example/Buttons/ThinkButton.cs @@ -10,6 +10,8 @@ public class ThinkButton : CustomActionButton public override float Cooldown => 15f; public override float EffectDuration => 10f; public override bool EffectCancelable => true; + public override int MaxUses => 1; + public override ButtonUsesMode UsesMode => ButtonUsesMode.PerRound; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; protected override void OnClick() { diff --git a/MiraAPI/Hud/ButtonUsesMode.cs b/MiraAPI/Hud/ButtonUsesMode.cs new file mode 100644 index 0000000..2c43300 --- /dev/null +++ b/MiraAPI/Hud/ButtonUsesMode.cs @@ -0,0 +1,17 @@ +namespace MiraAPI.Hud; + +/// +/// The uses mode for custom buttons. +/// +public enum ButtonUsesMode +{ + /// + /// Uses per game. + /// + PerGame, + + /// + /// Uses per round. + /// + PerRound, +} diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 4985aab..1c93188 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -66,6 +66,11 @@ public abstract class CustomActionButton /// public virtual int MaxUses => 0; + /// + /// Gets the value indicating uses mode. + /// + public virtual ButtonUsesMode UsesMode => ButtonUsesMode.PerGame; + /// /// Gets the current key binding for this button. If null, no keybind will be added. /// diff --git a/MiraAPI/Patches/Hud/ButtonResetPatches.cs b/MiraAPI/Patches/Hud/ButtonResetPatches.cs index 29b190d..b6f88f4 100644 --- a/MiraAPI/Patches/Hud/ButtonResetPatches.cs +++ b/MiraAPI/Patches/Hud/ButtonResetPatches.cs @@ -20,6 +20,11 @@ public static class ButtonResetPatches { Logger.Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}"); } + + if (customActionButton.UsesMode == ButtonUsesMode.PerRound) + { + customActionButton.SetUses(customActionButton.MaxUses); + } } } } From a6f7171b71d8857a2f8fb8c6edf226faca684fd0 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 29 Aug 2025 14:32:13 +0200 Subject: [PATCH 11/86] add PauseTimerInVent --- MiraAPI.Example/Buttons/Freezer/FreezeButton.cs | 1 + MiraAPI/Hud/CustomActionButton.cs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 726c16f..85edad2 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -16,6 +16,7 @@ public class FreezeButton : CustomActionButton public override string Name => "Freeze"; public override float Cooldown => OptionGroupSingleton.Instance.FreezeDuration; + public override bool PauseTimerInVent => true; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.T; diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 1c93188..4510e43 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -36,6 +36,12 @@ public abstract class CustomActionButton /// public abstract float Cooldown { get; } + /// + /// Gets a value indicating whether the timer should stop decreasing when the player is venting. + /// Note that both cooldown decrease and effect timer decrease is disabled. + /// + public virtual bool PauseTimerInVent => false; + /// /// Gets the sprite of the button. Use to load a sprite from a resource path. Use to load a sprite from an asset bundle. /// @@ -504,7 +510,7 @@ public abstract class CustomActionButton if (Timer >= 0) { - if (!TimerPaused) + if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent)) { Timer -= Time.deltaTime; } From 45972f71a911336492514f519e6ca13ce8bf9ccf Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sat, 30 Aug 2025 21:13:42 +0200 Subject: [PATCH 12/86] major rework of the keybinds api --- .../Buttons/Freezer/FreezeButton.cs | 5 +- MiraAPI.Example/Buttons/MeetingButton.cs | 6 +- .../Buttons/NeutralKillerButton.cs | 5 +- .../Buttons/Teleporter/TeleportButton.cs | 5 +- MiraAPI.Example/ExampleKeybinds.cs | 10 ++ MiraAPI/Hud/CustomActionButton.cs | 47 ++---- MiraAPI/Keybinds/KeybindManager.cs | 143 ++++++------------ MiraAPI/Keybinds/KeybindUtils.cs | 61 ++++++++ MiraAPI/Keybinds/MiraGlobalKeybinds.cs | 41 +++++ MiraAPI/Keybinds/MiraKeybind.cs | 111 ++++++++++++++ .../RegisterCustomKeybindsAttribute.cs | 9 ++ MiraAPI/Patches/GameStartManagerPatch.cs | 36 ----- MiraAPI/Patches/HudManagerPatches.cs | 33 ++-- .../Patches/Keybinds/ControlMapperPatches.cs | 38 +++++ MiraAPI/Patches/Keybinds/KeybindsPatch.cs | 50 ++++-- .../Keybinds/OptionsMenuBehaviourPatch.cs | 40 +++++ MiraAPI/PluginLoading/MiraPluginManager.cs | 59 ++++++-- MiraAPI/Utilities/Extensions.cs | 30 ++-- MiraAPI/Utilities/Helpers.cs | 40 +---- 19 files changed, 506 insertions(+), 263 deletions(-) create mode 100644 MiraAPI.Example/ExampleKeybinds.cs create mode 100644 MiraAPI/Keybinds/KeybindUtils.cs create mode 100644 MiraAPI/Keybinds/MiraGlobalKeybinds.cs create mode 100644 MiraAPI/Keybinds/MiraKeybind.cs create mode 100644 MiraAPI/Keybinds/RegisterCustomKeybindsAttribute.cs delete mode 100644 MiraAPI/Patches/GameStartManagerPatch.cs create mode 100644 MiraAPI/Patches/Keybinds/ControlMapperPatches.cs create mode 100644 MiraAPI/Patches/Keybinds/OptionsMenuBehaviourPatch.cs diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 85edad2..4da744d 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -3,10 +3,10 @@ using MiraAPI.Example.Options.Roles; using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.Keybinds; using MiraAPI.Modifiers; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; -using Rewired; using UnityEngine; namespace MiraAPI.Example.Buttons.Freezer; @@ -19,8 +19,7 @@ public class FreezeButton : CustomActionButton public override bool PauseTimerInVent => true; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.T; - public override ModifierKey Modifier1 => ModifierKey.Control; + public override MiraKeybind? Keybind => MiraGlobalKeybinds.PrimaryAbility; protected override void OnClick() { diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index 89e722b..8ca1b82 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -1,8 +1,8 @@ using MiraAPI.Example.Modifiers; using MiraAPI.Hud; +using MiraAPI.Keybinds; using MiraAPI.Modifiers; using MiraAPI.Utilities.Assets; -using Rewired; using UnityEngine; namespace MiraAPI.Example.Buttons; @@ -16,9 +16,7 @@ public class MeetingButton : CustomActionButton public override int MaxUses => 3; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.P; - public override ModifierKey Modifier1 => ModifierKey.Control; - public override ModifierKey Modifier2 => ModifierKey.Shift; + public override MiraKeybind? Keybind => MiraGlobalKeybinds.ModifierPrimaryAbility; public override bool Enabled(RoleBehaviour? role) { diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example/Buttons/NeutralKillerButton.cs index 58d8bee..3763577 100644 --- a/MiraAPI.Example/Buttons/NeutralKillerButton.cs +++ b/MiraAPI.Example/Buttons/NeutralKillerButton.cs @@ -2,8 +2,8 @@ using MiraAPI.Example.Roles; using MiraAPI.GameEnd; using MiraAPI.Hud; +using MiraAPI.Keybinds; using MiraAPI.Utilities.Assets; -using Rewired; using UnityEngine; namespace MiraAPI.Example.Buttons; @@ -13,7 +13,8 @@ public class NeutralKillerButton : CustomActionButton public override string Name => "Win Game"; public override float Cooldown => 0f; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.K; + public override MiraKeybind Keybind => ExampleKeybinds.NeutralWinKeybind; + protected override void OnClick() { CustomGameOver.Trigger([PlayerControl.LocalPlayer.Data]); diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index 78beb0b..b59d387 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -3,9 +3,9 @@ using MiraAPI.Example.Options.Roles; using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.Keybinds; using MiraAPI.Utilities.Assets; using Reactor.Utilities; -using Rewired; using UnityEngine; namespace MiraAPI.Example.Buttons.Teleporter; @@ -22,7 +22,8 @@ public class TeleportButton : CustomActionButton public override LoadableAsset Sprite => ExampleAssets.TeleportButton; public override Color TextOutlineColor => new Color32(221, 176, 152, 255); public static bool IsZoom { get; private set; } - public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.M; + public override MiraKeybind? Keybind => MiraGlobalKeybinds.PrimaryAbility; + public override bool Enabled(RoleBehaviour? role) { return role is TeleporterRole; diff --git a/MiraAPI.Example/ExampleKeybinds.cs b/MiraAPI.Example/ExampleKeybinds.cs new file mode 100644 index 0000000..dba6b67 --- /dev/null +++ b/MiraAPI.Example/ExampleKeybinds.cs @@ -0,0 +1,10 @@ +using MiraAPI.Keybinds; +using Rewired; + +namespace MiraAPI.Example; + +[RegisterCustomKeybinds] +public static class ExampleKeybinds +{ + public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, [ModifierKey.Shift]); +} diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 4510e43..8651fd1 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using System.Linq; using MiraAPI.Events; using MiraAPI.Events.Mira; using MiraAPI.Keybinds; @@ -78,26 +77,9 @@ public abstract class CustomActionButton public virtual ButtonUsesMode UsesMode => ButtonUsesMode.PerGame; /// - /// Gets the current key binding for this button. If null, no keybind will be added. + /// Gets the keybind for this button. If null, no keybind will be added. /// - public virtual KeyboardKeyCode? DefaultKeybind { get; } - - /// - /// Gets the first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key to activate the button. - /// - public virtual ModifierKey Modifier1 => ModifierKey.None; - - /// - /// Gets the second optional modifier key. - /// Set to if unused. - /// - public virtual ModifierKey Modifier2 => ModifierKey.None; - - /// - /// Gets the third optional modifier key. - /// Set to if unused. - /// - public virtual ModifierKey Modifier3 => ModifierKey.None; + public virtual MiraKeybind? Keybind => null; /// /// Gets the button's text outline color. @@ -144,11 +126,6 @@ public abstract class CustomActionButton /// public GameObject? KeybindIcon { get; private set; } - /// - /// Gets or sets the keybind entry for the button. - /// - private KeybindManager.KeybindEntry? KeybindEntry { get; set; } - /// /// Gets or sets the keybind icon text. /// @@ -226,13 +203,20 @@ public abstract class CustomActionButton } })); - if (DefaultKeybind != KeyboardKeyCode.None) + if (Keybind != null && Keybind.CurrentKey != KeyboardKeyCode.None) { - KeybindEntry = KeybindManager.GetEntries().First(x => x.Id == $"{Name}_Keybind"); + Keybind.OnActivate(() => + { + if (Enabled(PlayerControl.LocalPlayer.Data.Role)) + { + ClickHandler(); + } + }); + KeybindIcon = Helpers.CreateKeybindIcon( Button.gameObject, - KeybindEntry.Key, + Keybind.CurrentKey, new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f) ); KeybindText = KeybindIcon.transform.GetChild(0).GetComponent(); @@ -503,9 +487,10 @@ public abstract class CustomActionButton /// The local PlayerControl. public virtual void FixedUpdateHandler(PlayerControl playerControl) { - if (KeybindText != null && KeybindEntry != null) + if (Keybind != null && KeybindText != null) { - KeybindText.text = KeybindEntry.Key.ToString(); + KeybindText.text = Keybind.CurrentKey.ToString(); + KeybindIcon?.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard); } if (Timer >= 0) @@ -617,4 +602,4 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB Target = null; SetOutline(false); } -} \ No newline at end of file +} diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs index 3f95f4f..b7b6c1b 100644 --- a/MiraAPI/Keybinds/KeybindManager.cs +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -1,116 +1,63 @@ -using System; using System.Collections.Generic; using Rewired; -namespace MiraAPI.Keybinds +namespace MiraAPI.Keybinds; + +/// +/// Lets you register and manage mod keybinds with conflict checks and rebinding support. +/// +public static class KeybindManager { /// - /// Lets you register and manage mod keybinds with conflict checks and rebinding support. + /// Gets a list of all registered keybinds. /// - public static class KeybindManager + public static List Keybinds { get; } = new(); + + /// + /// Returns all conflicts where keybinds use the same key. + /// + /// A list of pairs of keybinds that conflict. + public static Dictionary> GetConflicts() { - /// - /// Stores all info for a registered keybind. - /// - public class KeybindEntry + var conflicts = new Dictionary>(); + foreach (var keybind in Keybinds) { - /// Unique identifier for this keybind. - public string Id; - - /// Text shown in the Among Us rebinding menu to describe this keybind. - public string Description; - - /// - /// The method that runs when this keybind is pressed. - /// - public Action Handler; - - /// - /// The key currently assigned to this keybind. - /// - public KeyboardKeyCode Key; - - /// - /// If true, this keybind will be checked for conflicts with other exclusive keybinds using the same key. - /// - public bool Exclusive; - - /// - /// The first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key. - /// - public ModifierKey Modifier1 = ModifierKey.None; - - /// - /// The second optional modifier key, if needed. Set to ModifierKey.None if unused. - /// - public ModifierKey Modifier2 = ModifierKey.None; - - /// - /// The third optional modifier key, if needed. Set to ModifierKey.None if unused. - /// - public ModifierKey Modifier3 = ModifierKey.None; - } - - private static readonly List Registered = new(); - - /// - /// Registers a new mod keybind. - /// - /// Unique identifier for the keybind. - /// Description to display in UI. - /// Default key to use if no rebind exists. - /// Method to invoke when the keybind is pressed. - /// If true, conflicts with other exclusive keybinds on the same key. - /// Optional first modifier key (e.g., Control, Shift, Alt) that must be held with the main key. - /// Optional second modifier key. Use ModifierKey.None if unused. - /// Optional third modifier key. Use ModifierKey.None if unused. - /// Returns the key that was registered for this keybind. - public static KeyboardKeyCode Register(string id, string description, KeyboardKeyCode key, Action handler, bool exclusive = true, ModifierKey modifier1 = ModifierKey.None, ModifierKey modifier2 = ModifierKey.None, ModifierKey modifier3 = ModifierKey.None) - { - Registered.Add(new KeybindEntry + var all = GetKeybindsForKey(keybind.CurrentKey, true); + if (all.Length > 1) { - Id = id, - Description = description, - Key = key, - Handler = handler, - Exclusive = exclusive, - Modifier1 = modifier1, - Modifier2 = modifier2, - Modifier3 = modifier3, - }); - - return key; - } - - /// - /// Returns all conflicts where exclusive keybinds use the same key. - /// - /// A list of pairs of keybinds that conflict. - public static List<(KeybindEntry, KeybindEntry)> GetConflicts() - { - var conflicts = new List<(KeybindEntry, KeybindEntry)>(); - for (int i = 0; i < Registered.Count; i++) - { - for (int j = i + 1; j < Registered.Count; j++) + if (!conflicts.TryGetValue(keybind.CurrentKey, out var group)) { - var a = Registered[i]; - var b = Registered[j]; - if (a.Key == b.Key && a.Exclusive && b.Exclusive) - { - conflicts.Add((a, b)); - } + conflicts.Add(keybind.CurrentKey, new List()); + group = conflicts[keybind.CurrentKey]; + } + + if (!group.Contains(keybind)) + { + group.Add(keybind); } } - return conflicts; } - /// - /// Returns all currently registered keybind entries. - /// - /// A list of all registered keybind entries. - public static List GetEntries() + return conflicts; + } + + /// + /// Returns all keybinds for a specified keycode. + /// + /// The key to look for. + /// /// Should only exlusive keybinds be present. + /// The list of keybinds. + public static MiraKeybind[] GetKeybindsForKey(KeyboardKeyCode keyCode, bool exclusiveCheck = false) + { + var all = new List(); + foreach (var keybind in Keybinds) { - return Registered; + if (keybind.CurrentKey == keyCode && (keybind.Exclusive || !exclusiveCheck)) + { + all.Add(keybind); + } } + + return all.ToArray(); } } diff --git a/MiraAPI/Keybinds/KeybindUtils.cs b/MiraAPI/Keybinds/KeybindUtils.cs new file mode 100644 index 0000000..3d342d6 --- /dev/null +++ b/MiraAPI/Keybinds/KeybindUtils.cs @@ -0,0 +1,61 @@ +using System; +using Rewired; + +namespace MiraAPI.Keybinds; + +/// +/// Collection of keybind-related utility classes. +/// +public static class KeybindUtils +{ + /// + /// Gets the Rewired instance. + /// + public static InputManager_Base? RewiredInputManager { get; internal set; } + + /// + /// Returns the currently assigned keycode of a keybind. + /// + /// The keybind to get the keycode from. + /// The currently assigned keycode. + public static KeyboardKeyCode? GetKeycodeByKeybind(MiraKeybind keybind) + { + if (keybind.RewiredInputAction == null) + { + return null; + } + + return GetKeycodeByActionId(keybind.RewiredInputAction.id); + } + + /// + /// Gets the keycode for an action with ReInput. + /// + /// The action ID. + /// The keyboard keycode. + public static KeyboardKeyCode? GetKeycodeByActionId(int actionId) + { + var player = ReInput.players.GetPlayer(0); + return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false)? + .keyboardKeyCode; + } + + /// + /// Finds and returns an unused KeyCode that is not equal to the excluded key. + /// + /// The KeyCode to skip during the search. + /// + /// The first available KeyCode not currently used by any registered keybind, + /// or KeyCode.None if none are available. + /// + public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude) + { + foreach (KeyboardKeyCode key in Enum.GetValues()) + { + if (key == exclude) continue; + bool used = KeybindManager.Keybinds.Exists(e => e.DefaultKey == key); + if (!used) return key; + } + return KeyboardKeyCode.None; + } +} diff --git a/MiraAPI/Keybinds/MiraGlobalKeybinds.cs b/MiraAPI/Keybinds/MiraGlobalKeybinds.cs new file mode 100644 index 0000000..00e89c2 --- /dev/null +++ b/MiraAPI/Keybinds/MiraGlobalKeybinds.cs @@ -0,0 +1,41 @@ +using Rewired; + +namespace MiraAPI.Keybinds; + +/// +/// Class containing all default keybinds. +/// Recommended for cross-mod compatibility. +/// +[RegisterCustomKeybinds] +public static class MiraGlobalKeybinds +{ + /// + /// Gets the keybind for primary abilities. + /// + public static MiraKeybind PrimaryAbility { get; } = new("Primary Ability", KeyboardKeyCode.T); + + /// + /// Gets the keybind for secondary abilities. + /// + public static MiraKeybind SecondaryAbility { get; } = new("Secondary Ability", KeyboardKeyCode.Y); + + /// + /// Gets the keybind for Tetiary abilities. + /// + public static MiraKeybind TertiaryAbility { get; } = new("Tertiary Ability", KeyboardKeyCode.U); + + /// + /// Gets the keybind for primary modifier abilities. + /// + public static MiraKeybind ModifierPrimaryAbility { get; } = new("Modifier Primary Ability", KeyboardKeyCode.M); + + /// + /// Gets the keybind for secondary modifier abilities. + /// + public static MiraKeybind ModifierSecondaryAbility { get; } = new("Modifier Secondary Ability", KeyboardKeyCode.N); + + /// + /// Gets the keybind for Tetiary modifier abilities. + /// + public static MiraKeybind ModifierTertiaryAbility { get; } = new("Modifier Tertiary Ability", KeyboardKeyCode.T); +} diff --git a/MiraAPI/Keybinds/MiraKeybind.cs b/MiraAPI/Keybinds/MiraKeybind.cs new file mode 100644 index 0000000..aa03d89 --- /dev/null +++ b/MiraAPI/Keybinds/MiraKeybind.cs @@ -0,0 +1,111 @@ +using System; +using System.Globalization; +using BepInEx.Unity.IL2CPP; +using Reactor.Utilities; +using Rewired; + +namespace MiraAPI.Keybinds; + +/// +/// Represents a registered keybind. +/// +public class MiraKeybind +{ + /// + /// Gets name of the keybind + /// + public string Name { get; } + + /// + /// Gets the unique identifier for this keybind. Used in Rewired. + /// + public string Id { get; } + + /// + /// Gets the default keycode of this keybind. + /// + public KeyboardKeyCode DefaultKey { get; } + + /// + /// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key. + /// + public bool Exclusive { get; } + + /// + /// Gets the modifier keys assinged to this keybind. + /// Due to Rewired limitations, there can only be 3 modifier keys. + /// + public ModifierKey[] ModifierKeys { get; } + + /// + /// Gets the Rewired assinged for this keybind. + /// + public InputAction? RewiredInputAction { get; internal set; } + + /// + /// Gets the currently assigned keycode. + /// + public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this) ?? KeyboardKeyCode.None; + + /// + /// Gets or sets the handler of the keybind. Invoked when the keybind is activated. + /// + private Action Handler { get; set; } + + internal BasePlugin? SourcePlugin { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the keybind. + /// The default keycode. + /// Up to 3 optional modifier keys. + /// Is exclusive. + public MiraKeybind( + string name, + KeyboardKeyCode? defaultKeycode, + ModifierKey[]? modifierKeys = null, + bool exclusive = true) + { + Name = name; + Id = name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_'); + DefaultKey = defaultKeycode ?? KeyboardKeyCode.None; + ModifierKeys = modifierKeys ?? Array.Empty(); + Exclusive = exclusive; + Handler = () => { }; + } + + /// + public override string ToString() + { + return $"{Name} ({Id})"; + } + + /// + /// Triggers the keybind, invoking its handler. + /// + public void Invoke() + { + Handler.Invoke(); + Logger.Warning($"Keybind invoked: {Name} ({Id}) - {DefaultKey.ToString()}"); + } + + /// + /// Adds an that will be invoked when the keybind is used. + /// + /// The to add. + public void OnActivate(Action action) + { + Handler += action; + } + + /// + /// Removes an that would be invoked when the keybind is used. + /// Not recommended to use - add checks in the method added in itself rather than removing it from the keybind. + /// + /// The to remove. + public void RemoveOnActivate(Action action) + { + Handler -= action; + } +} diff --git a/MiraAPI/Keybinds/RegisterCustomKeybindsAttribute.cs b/MiraAPI/Keybinds/RegisterCustomKeybindsAttribute.cs new file mode 100644 index 0000000..3af9f4e --- /dev/null +++ b/MiraAPI/Keybinds/RegisterCustomKeybindsAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiraAPI.Keybinds; + +/// +/// Used to mark a class for custom keybinds registration. +/// +[AttributeUsage(AttributeTargets.Class)] +public class RegisterCustomKeybindsAttribute : Attribute; diff --git a/MiraAPI/Patches/GameStartManagerPatch.cs b/MiraAPI/Patches/GameStartManagerPatch.cs deleted file mode 100644 index 7ebb05b..0000000 --- a/MiraAPI/Patches/GameStartManagerPatch.cs +++ /dev/null @@ -1,36 +0,0 @@ -using HarmonyLib; -using MiraAPI.Keybinds; -using UnityEngine; - -namespace MiraAPI.Patches; - -/// -/// Checks keybind conflicts in the lobby and disables secondary ones. -/// -[HarmonyPatch(typeof(GameStartManager))] -public static class GameStartManagerPatch -{ - public static bool conflictShown = false; - - [HarmonyPatch(nameof(GameStartManager.Start))] - [HarmonyPostfix] - public static void StartPostfix(GameStartManager __instance) - { - if (conflictShown) return; - - var conflicts = KeybindManager.GetConflicts(); - if (conflicts.Count == 0) return; - - conflictShown = true; - - var message = "The following keybinds are disabled due to conflicts:\n\n"; - - foreach (var (a, b) in conflicts) - { - message += $"• {b.Description} (conflicts with {a.Description})\n"; - b.Handler = null; - } - - HudManager.Instance.ShowPopUp(message.TrimEnd()); - } -} diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index fa848de..829556b 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; using HarmonyLib; using MiraAPI.Hud; using MiraAPI.Keybinds; @@ -25,7 +24,7 @@ public static class HudManagerPatches public static Transform? BottomRight { get; private set; } public static Transform? Buttons { get; private set; } - private static Dictionary VanillaKeybindIcons = new(); + private static Dictionary vanillaKeybindIcons = new(); /* /// @@ -117,9 +116,13 @@ public static class HudManagerPatches var buttonObj = kvp.Key; var actionId = kvp.Value; - var key = Helpers.GetKeybindByActionId(actionId); + var key = KeybindUtils.GetKeycodeByActionId(actionId) ?? KeyboardKeyCode.None; + if (key == KeyboardKeyCode.None) + { + continue; + } var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); - VanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); + vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); } } @@ -155,24 +158,24 @@ public static class HudManagerPatches [HarmonyPatch(nameof(HudManager.Update))] [HarmonyPostfix] - public static void StartPostfix() + public static void UpdatePostfix() { - foreach (var btnIcon in VanillaKeybindIcons) + foreach (var btnIcon in vanillaKeybindIcons) { - btnIcon.Key.text = Helpers.GetKeybindByActionId(btnIcon.Value).ToString(); + btnIcon.Key.text = KeybindUtils.GetKeycodeByActionId(btnIcon.Value).ToString(); + btnIcon.Key.transform.parent.gameObject.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard); } - foreach (var entry in KeybindManager.GetEntries()) + foreach (var entry in KeybindManager.Keybinds) { var player = ReInput.players.GetPlayer(0); var keyboard = player.controllers.Keyboard; - if (player.GetButtonDown(entry.Id) && - (entry.Modifier1 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier1)) && - (entry.Modifier2 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier2)) && - (entry.Modifier3 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier3))) + bool modifierKeysPressed = entry.ModifierKeys.All(k => keyboard.GetModifierKey(k)) || + entry.ModifierKeys.Length <= 0; + if (player.GetButtonDown(entry.Id) && modifierKeysPressed) { - entry.Handler?.Invoke(); + entry.Invoke(); } } } diff --git a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs new file mode 100644 index 0000000..c08175b --- /dev/null +++ b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs @@ -0,0 +1,38 @@ +using System.Linq; +using HarmonyLib; +using MiraAPI.Keybinds; +using Rewired.UI.ControlMapper; +using UnityEngine; +using UnityEngine.UI; + +namespace MiraAPI.Patches.Keybinds; + +[HarmonyPatch(typeof(ControlMapper))] +public static class ControlMapperPatches +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(ControlMapper.Update))] + private static void UpdatePrefix(ControlMapper __instance) + { + foreach (var element in __instance.themedElements) + { + var info = element.GetComponent(); + if (info == null) + { + continue; + } + + var key = info.glyphOrText?.actionElementMap?.keyboardKeyCode; + if (key == null) + { + continue; + } + + var conflicts = KeybindManager.GetConflicts(); + if (conflicts.Any(x => x.Key.ToString() == key.ToString())) + { + element.GetComponent().color = Color.red; + } + } + } +} diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs index e609086..022aa81 100644 --- a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs +++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs @@ -1,20 +1,52 @@ +using System.Linq; +using BepInEx; +using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.Keybinds; +using MiraAPI.PluginLoading; using MiraAPI.Utilities; +using Reactor.Utilities; using Rewired; -namespace MiraAPI.Patches.Keybinds +namespace MiraAPI.Patches.Keybinds; + +[HarmonyPatch(typeof(InputManager_Base), nameof(InputManager_Base.Awake))] +public static class KeybindMenuPatch { - [HarmonyPatch(typeof(InputManager_Base), nameof(InputManager_Base.Awake))] - public static class KeybindMenuPatch + private static bool _registered; + + [HarmonyPrefix] + private static void AwakePrefix(InputManager_Base __instance) { - [HarmonyPrefix] - private static void StartPrefix(InputManager_Base __instance) + if (_registered) { - foreach (var entry in KeybindManager.GetEntries()) - { - __instance.userData.RegisterModBind(entry.Id, entry.Description, entry.Key, modifier1: entry.Modifier1, modifier2: entry.Modifier2, modifier3: entry.Modifier3); - } + return; } + + KeybindUtils.RewiredInputManager = __instance; + foreach (var keybind in KeybindManager.Keybinds) + { + if (__instance.userData.actions.ToArray().Any(x => x.name == keybind.Id)) + { + Logger.Warning($"Keybind of id {keybind.Id} already exists. Skipping it"); + continue; + } + + PluginInfo? info = IL2CPPChainloader.Instance.Plugins.Values + .FirstOrDefault(p => p.Instance == keybind.SourcePlugin); + string group = info!.Metadata.Name; + if (keybind.SourcePlugin is IMiraPlugin miraPlugin) + { + group = miraPlugin.OptionsTitleText; + } + + keybind.RewiredInputAction = __instance.userData.RegisterModBind( + keybind.Id, + keybind.Name, + group, + keybind.DefaultKey, + modifiers: keybind.ModifierKeys); + } + _registered = true; } } diff --git a/MiraAPI/Patches/Keybinds/OptionsMenuBehaviourPatch.cs b/MiraAPI/Patches/Keybinds/OptionsMenuBehaviourPatch.cs new file mode 100644 index 0000000..2511168 --- /dev/null +++ b/MiraAPI/Patches/Keybinds/OptionsMenuBehaviourPatch.cs @@ -0,0 +1,40 @@ +using HarmonyLib; +using MiraAPI.Keybinds; +using UnityEngine; + +namespace MiraAPI.Patches.Keybinds; + +[HarmonyPatch(typeof(OptionsMenuBehaviour))] +public static class OptionsMenuBehaviourPatch +{ + private static ButtonRolloverHandler? _remap_rollover; + private static SpriteRenderer? _remap_background; + private static bool _conflicts => KeybindManager.GetConflicts().Count > 0; + + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Open))] + private static void OpenPostfix(OptionsMenuBehaviour __instance) + { + _remap_rollover = GameObject.Find("Remap Controls")?.GetComponent()!; + _remap_background = _remap_rollover.transform.FindChild("Background").GetComponent(); + _remap_background.color = _conflicts ? Color.red : Color.white; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Update))] + private static void UpdatePostfix(OptionsMenuBehaviour __instance) + { + if (_remap_rollover == null) + { + return; + } + if (_remap_background == null) + { + return; + } + + _remap_rollover.OutColor = _conflicts ? Color.red : Color.white; + _remap_rollover.UnselectedColor = _conflicts ? Color.red : Color.white; + _remap_rollover.OverColor = _conflicts ? new Color32(255, 55, 55, 255) : Palette.AcceptedGreen; + } +} diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 759a0fd..21f7d3a 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -18,8 +18,6 @@ using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; -using Rewired; -using UnityEngine; namespace MiraAPI.PluginLoading; @@ -110,6 +108,7 @@ public sealed class MiraPluginManager } RegisterColorClasses(type); + RegisterKeybindClasses(type, plugin); } info.PluginConfig.Save(); @@ -135,18 +134,9 @@ public sealed class MiraPluginManager RegisteredPlugins = [.. _registeredPlugins.Values]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); - - foreach (var button in CustomButtonManager.Buttons) - { - if (button.DefaultKeybind == null) - { - continue; - } - KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", (KeyboardKeyCode)button.DefaultKeybind, button.ClickHandler, modifier1: button.Modifier1, modifier2: button.Modifier2, modifier3: button.Modifier3); - - Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {(KeyboardKeyCode)button.DefaultKeybind}."); - } }; + + RegisterKeybindClasses(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); } /// @@ -317,4 +307,47 @@ public sealed class MiraPluginManager return false; } + + private static void RegisterKeybindClasses(Type type, BasePlugin source) + { + try + { + if (type.GetCustomAttribute() == null) + { + return; + } + + if (!type.IsStatic()) + { + Logger.Error($"Keybinds class {type.Name} must be static."); + return; + } + + foreach (var property in type.GetProperties()) + { + if (property.PropertyType != typeof(MiraKeybind)) + { + continue; + } + + if (property.GetValue(null) is not MiraKeybind keybind) + { + Logger.Error($"Keybind property {property.Name} in {type.Name} is not a MiraKeybind."); + continue; + } + + KeybindManager.Keybinds.Add(keybind); + keybind.SourcePlugin = source; + } + + foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(MiraKeybind)))) + { + Logger.Error($"{field.Name} is a field, not a property. Use properties for keybinds."); + } + } + catch (Exception e) + { + Logger.Error($"Failed to register keybind class {type.Name}: {e}"); + } + } } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index e5f991f..5f4523c 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -554,24 +554,22 @@ public static class Extensions /// Registers a new mod keybind as a user-assignable button action in Rewired. /// /// The Rewired user data to add the action to. - /// The internal name of the action. - /// Text shown in the rebinding UI. + /// The internal name of the action. + /// Text shown in the rebinding UI. + /// /// Group shown above the label. /// The default key to assign to this action. /// Category ID to group actions in Rewired (default is 0). /// The element identifier ID (default is -1, meaning none specified). /// The for this action (default is Button). - /// The first optional modifier key (e.g., Control, Shift, Alt) that must be held together with the main key. - /// The second optional modifier key. Set to if not used. - /// The third optional modifier key. Set to if not used. + /// Optional modifier keys (e.g., Control, Shift, Alt) that must be held together with the main key. /// The action ID of the newly registered action. - public static int RegisterModBind(this UserData userData, string actionName, string description, KeyboardKeyCode key, int category = 0, int elementIdentifierId = -1, InputActionType type = InputActionType.Button, ModifierKey modifier1 = ModifierKey.None, ModifierKey modifier2 = ModifierKey.None, ModifierKey modifier3 = ModifierKey.None) + public static InputAction RegisterModBind(this UserData userData, string id, string name, string group, KeyboardKeyCode key, int category = 0, int elementIdentifierId = -1, InputActionType type = InputActionType.Button, ModifierKey[]? modifiers = null) { userData.AddAction(category); - var action = userData.GetAction(userData.actions.Count - 1)!; - action.name = actionName; - action.descriptiveName = description; + action.name = id; + action.descriptiveName = $"{Palette.CrewmateRoleHeaderDarkBlue.ToTextColor()}{group.ReplaceLineEndings(" ")}\n{name}"; action.categoryId = category; action.type = type; action.userAssignable = true; @@ -583,13 +581,17 @@ public static class Extensions _elementType = ControllerElementType.Button, _axisContribution = Pole.Positive, _keyboardKeyCode = key, - _modifierKey1 = modifier1, - _modifierKey2 = modifier2, - _modifierKey3 = modifier3, }; + + if (modifiers != null) + { + if (modifiers.Length > 0) map._modifierKey1 = modifiers[0]; + if (modifiers.Length > 1) map._modifierKey2 = modifiers[1]; + if (modifiers.Length > 2) map._modifierKey3 = modifiers[2]; + } + userData.keyboardMaps[0].actionElementMaps.Add(map); userData.joystickMaps[0].actionElementMaps.Add(map); - - return action.id; + return action; } } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 6c0bf4c..7731aac 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Il2CppSystem.IO; -using MiraAPI.Keybinds; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; @@ -26,28 +23,17 @@ public static class Helpers { return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x => x.Object)]; } + internal static GameObject CreateKeybindIcon(GameObject button, KeyboardKeyCode keyCode, Vector3 localPos) { var keybindIcon = Object.Instantiate(HudManager.Instance.AbilityButton.usesRemainingSprite.gameObject, button.transform); keybindIcon.GetComponent().sprite = MiraAssets.KeybindButton.LoadAsset(); - keybindIcon.transform.GetChild(0).GetComponent().text = keyCode.ToString(); + keybindIcon.transform.GetComponentInChildren().text = keyCode.ToString(); keybindIcon.name = "KeybindIcon"; keybindIcon.transform.localPosition = localPos; return keybindIcon; } - /// - /// Gets the keybind for an action with ReInput. - /// - /// The action ID. - /// The keyboard key code. - public static KeyboardKeyCode GetKeybindByActionId(int actionId) - { - var player = ReInput.players.GetPlayer(0); - return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false) - .keyboardKeyCode; - } - /// /// Determines whether a given probability check succeeds. /// @@ -426,23 +412,5 @@ public static class Helpers return new string(Enumerable.Repeat(chars, length) .Select(s => s[UnityEngine.Random.RandomRangeInt(0, s.Length)]).ToArray()); } - - /// - /// Finds and returns an unused KeyCode that is not equal to the excluded key. - /// - /// The KeyCode to skip during the search. - /// - /// The first available KeyCode not currently used by any registered keybind, - /// or KeyCode.None if none are available. - /// - public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude) - { - foreach (KeyboardKeyCode key in Enum.GetValues(typeof(KeyboardKeyCode))) - { - if (key == exclude) continue; - bool used = KeybindManager.GetEntries().Exists(e => e.Key == key); - if (!used) return key; - } - return KeyboardKeyCode.None; - } } + From 45291b0e98ddef64241c8c3ca33e8504c700532b Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sat, 30 Aug 2025 21:31:34 +0200 Subject: [PATCH 13/86] change default keybinds to make sense --- MiraAPI/Keybinds/MiraGlobalKeybinds.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MiraAPI/Keybinds/MiraGlobalKeybinds.cs b/MiraAPI/Keybinds/MiraGlobalKeybinds.cs index 00e89c2..ab41c2c 100644 --- a/MiraAPI/Keybinds/MiraGlobalKeybinds.cs +++ b/MiraAPI/Keybinds/MiraGlobalKeybinds.cs @@ -27,15 +27,15 @@ public static class MiraGlobalKeybinds /// /// Gets the keybind for primary modifier abilities. /// - public static MiraKeybind ModifierPrimaryAbility { get; } = new("Modifier Primary Ability", KeyboardKeyCode.M); + public static MiraKeybind ModifierPrimaryAbility { get; } = new("Modifier Primary Ability", KeyboardKeyCode.I); /// /// Gets the keybind for secondary modifier abilities. /// - public static MiraKeybind ModifierSecondaryAbility { get; } = new("Modifier Secondary Ability", KeyboardKeyCode.N); + public static MiraKeybind ModifierSecondaryAbility { get; } = new("Modifier Secondary Ability", KeyboardKeyCode.O); /// /// Gets the keybind for Tetiary modifier abilities. /// - public static MiraKeybind ModifierTertiaryAbility { get; } = new("Modifier Tertiary Ability", KeyboardKeyCode.T); + public static MiraKeybind ModifierTertiaryAbility { get; } = new("Modifier Tertiary Ability", KeyboardKeyCode.P); } From 43d7f80660dbae2ac97463d667a1fd0f79653553 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sat, 30 Aug 2025 21:41:04 +0200 Subject: [PATCH 14/86] add show keybinds setting --- MiraAPI/Hud/CustomActionButton.cs | 7 +++-- MiraAPI/MiraApiConfig.cs | 16 ++++------- MiraAPI/Patches/HudManagerPatches.cs | 3 +- .../Patches/Keybinds/ControlMapperPatches.cs | 28 +++++++++++++++++++ 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 8651fd1..dd84316 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -6,7 +6,7 @@ using MiraAPI.Keybinds; using MiraAPI.Patches; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; -using Rewired; +using Reactor.Utilities; using TMPro; using UnityEngine; using UnityEngine.Events; @@ -203,7 +203,7 @@ public abstract class CustomActionButton } })); - if (Keybind != null && Keybind.CurrentKey != KeyboardKeyCode.None) + if (Keybind != null) { Keybind.OnActivate(() => { @@ -490,7 +490,8 @@ 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 == ActiveInputManager.InputType.Keyboard && + PluginSingleton.Instance.MiraConfig!.ShowKeybinds.Value); } if (Timer >= 0) diff --git a/MiraAPI/MiraApiConfig.cs b/MiraAPI/MiraApiConfig.cs index 10c9776..3db12a9 100644 --- a/MiraAPI/MiraApiConfig.cs +++ b/MiraAPI/MiraApiConfig.cs @@ -1,13 +1,4 @@ -using BepInEx; -using BepInEx.Configuration; -using BepInEx.Unity.IL2CPP; -using HarmonyLib; -using MiraAPI.PluginLoading; -using Reactor; -using Reactor.Networking; -using Reactor.Networking.Attributes; -using Reactor.Utilities; -using UnityEngine; +using BepInEx.Configuration; namespace MiraAPI; @@ -20,4 +11,9 @@ public class MiraApiConfig(ConfigFile config) /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. /// public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false); + + /// + /// Gets whether the keybind icons should be visible. + /// + public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Displays", "Show Keybinds on buttons", true); } diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 829556b..bfdcce5 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -163,7 +163,8 @@ public static class HudManagerPatches foreach (var btnIcon in vanillaKeybindIcons) { btnIcon.Key.text = KeybindUtils.GetKeycodeByActionId(btnIcon.Value).ToString(); - btnIcon.Key.transform.parent.gameObject.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard); + btnIcon.Key.transform.parent.gameObject.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard && + PluginSingleton.Instance.MiraConfig!.ShowKeybinds.Value); } foreach (var entry in KeybindManager.Keybinds) diff --git a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs index c08175b..4a5fe90 100644 --- a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs +++ b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs @@ -1,8 +1,12 @@ using System.Linq; using HarmonyLib; using MiraAPI.Keybinds; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; using Rewired.UI.ControlMapper; +using TMPro; using UnityEngine; +using UnityEngine.Events; using UnityEngine.UI; namespace MiraAPI.Patches.Keybinds; @@ -10,6 +14,30 @@ namespace MiraAPI.Patches.Keybinds; [HarmonyPatch(typeof(ControlMapper))] public static class ControlMapperPatches { + [HarmonyPrefix] + [HarmonyPatch(nameof(ControlMapper.Start))] + private static void StartPrefix(ControlMapper __instance) + { + var doneButton = GameObject.Find("DoneButton")?.GetComponent(); + if (doneButton == null) + { + return; + } + + var toggle = Object.Instantiate(doneButton, doneButton.transform.parent); + var text = toggle.GetComponentInChildren(); + var entry = PluginSingleton.Instance!.MiraConfig!.ShowKeybinds; + toggle.gameObject.name = "KeybindsVisibleToggle"; + text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}"; + toggle.onClick.RemoveAllListeners(); + toggle.GetComponent().identifier = "KeybindsVisibleToggle"; + toggle.onClick.AddListener((UnityAction)(() => + { + entry.Value = !entry.Value; + text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}"; + })); + } + [HarmonyPrefix] [HarmonyPatch(nameof(ControlMapper.Update))] private static void UpdatePrefix(ControlMapper __instance) From 5fb59408fa4ec388cd3a1ca93505a2846628b457 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 31 Aug 2025 17:51:02 +0200 Subject: [PATCH 15/86] wip --- MiraAPI.Example/ExampleLocalSetings.cs | 13 ++---- MiraAPI.Example/ExamplePlugin.cs | 6 ++- .../Attributes/LocalSettingAttribute.cs | 15 ++++++- .../Attributes/LocalSettingToggleAttribute.cs | 17 ++++++- MiraAPI/LocalSettings/LocalSettingsTab.cs | 20 +++------ .../SettingTypes/LocalSettingBase.cs | 8 ++-- ...lSetingToggle.cs => LocalSettingToggle.cs} | 36 ++++++--------- MiraAPI/PluginLoading/MiraPluginManager.cs | 45 +++++++++++++++++++ 8 files changed, 105 insertions(+), 55 deletions(-) rename MiraAPI/LocalSettings/SettingTypes/{LocalSetingToggle.cs => LocalSettingToggle.cs} (83%) diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index 0feb0f0..c682617 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -7,13 +7,8 @@ namespace MiraAPI.Example; public static class ExampleLocalSetings { - public static LocalSettingsTab ModTab { get; } = new LocalSettingsTab("Example Tab"); + public static LocalSettingsTab ModTab { get; } = new("Example Tab"); - [LocalSettingToggle] - public static ConfigEntry ExampleToggleSetting { get; set; } - - internal static void BindSettings(ConfigFile config) - { - ExampleToggleSetting = config.Bind("General", "Example Toggle", false); - } -} \ No newline at end of file + [LocalSetting] + public static ConfigEntry ExampleToggleSetting { get; private set; } +} diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index 19d23e7..3819347 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -1,4 +1,5 @@ -using BepInEx; +using System.Linq; +using BepInEx; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; @@ -21,8 +22,9 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin public ConfigFile GetConfigFile() => Config; public override void Load() { - ExampleLocalSetings.BindSettings(Config); ExampleEventHandlers.Initialize(); Harmony.PatchAll(); + + Config.Keys. } } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs index 3d72d3c..6735d4e 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -1,11 +1,22 @@ using System; +using BepInEx.Configuration; +using UnityEngine; namespace MiraAPI.LocalSettings.Attributes; /// /// Base class for all local settings attributes. /// +/// The local setting type the attribute handles. [AttributeUsage(AttributeTargets.Property)] -public abstract class LocalSettingAttribute : Attribute +public abstract class LocalSettingAttribute( + LocalSettingsTab tab, + string? name = null, + string? description = null) : Attribute { -} + /// + /// Returns the created setting object. + /// + /// The created class. + protected abstract GameObject CreateSetting(); +} \ No newline at end of file diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs index 428acf1..7a8cd34 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs @@ -1,4 +1,7 @@ using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; +using UnityEngine; namespace MiraAPI.LocalSettings.Attributes; @@ -6,6 +9,18 @@ namespace MiraAPI.LocalSettings.Attributes; /// Creates a toggle and binds it with the config entry. /// [AttributeUsage(AttributeTargets.Property)] -public class LocalSettingToggleAttribute : LocalSettingAttribute +public class LocalSettingToggleAttribute( + LocalSettingsTab tab, + string? name = null, + string? description = null, + Color? toggleColor = null, + Color? toggleHoverColor = null, + Color? toggleActiveColor = null + ) : LocalSettingAttribute(tab, name, description) { + /// + protected override LocalSettingToggle CreateSetting(ConfigEntryBase configEntry) + { + return new LocalSettingToggle(tab, configEntry, name, description, toggleColor, toggleHoverColor, toggleActiveColor); + } } diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index 8a6b2dc..e956589 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -25,16 +25,6 @@ public class LocalSettingsTab public List Settings { get; } - /// - public LocalSettingsTab(string tabName) : this( - tabName, - Color.white, - Palette.AcceptedGreen, - Palette.AcceptedGreen, - createButton: true) - { - } - /// /// Initializes a new instance of the class. /// @@ -44,18 +34,18 @@ public class LocalSettingsTab /// The active color of the tab. /// The tab icon. Defaults to no icon. /// Should the tab button be created. Without it the tab needs to be opened manually. - public LocalSettingsTab(string tabName, Color tabColor, Color tabHoverColor, Color tabActiveColor, LoadableAsset? tabIcon = null, bool createButton = false) + public LocalSettingsTab(string tabName, Color? tabColor = null, Color? tabHoverColor = null, Color? tabActiveColor = null, LoadableAsset? tabIcon = null, bool createButton = true) { TabName = tabName; - TabColor = tabColor; - TabHoverColor = tabHoverColor; - TabActiveColor = tabActiveColor; + TabColor = tabColor ?? Color.white; + TabHoverColor = tabHoverColor ?? Palette.AcceptedGreen; + TabActiveColor = tabActiveColor ?? Palette.AcceptedGreen; TabIcon = tabIcon; ShouldCreateButton = createButton; Settings = new(); } - public GameObject? CreateTab(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + internal GameObject? CreateTab(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) { var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); tab.name = $"{TabName}Tab"; diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index 5634ecf..e93131a 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -15,14 +15,15 @@ public abstract class LocalSettingBase : ILocalSetting /// /// Initializes a new instance of the class. /// + /// The tab to create the setting in. /// The config entry. /// The name of the setting. /// The description of the setting. - protected LocalSettingBase(LocalSettingsTab tab, ConfigEntryBase configEntry, string name = null, string description = null) + protected LocalSettingBase(LocalSettingsTab tab, ConfigEntryBase configEntry, string? name = null, string? description = null) { ConfigEntry = configEntry; Name = name ?? ConfigEntry.Definition.Key; - Description = description; + Description = description ?? ConfigEntry.Description.Description; tab.Settings.Add(this); } @@ -30,12 +31,11 @@ public abstract class LocalSettingBase : ILocalSetting /// /// Creates an instance of the setting. /// - /// The transform parent of the setting. /// The created setting. public abstract GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); /// - /// Gets the value of the config entry, casted to T. + /// Gets the value of the config entry, casted to the setting type. /// /// The value. public T GetValue() diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs similarity index 83% rename from MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs rename to MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs index a2443dd..00babc1 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs @@ -6,7 +6,10 @@ using UnityEngine.Events; namespace MiraAPI.LocalSettings.SettingTypes; -public class LocalSetingToggle : LocalSettingBase +/// +/// Local setting class for toggles. +/// +public class LocalSettingToggle : LocalSettingBase { /// /// Gets the color of the toggle. @@ -23,40 +26,29 @@ public class LocalSetingToggle : LocalSettingBase /// public Color ToggleActiveColor { get; } - /// - public LocalSetingToggle(LocalSettingsTab tab, ConfigEntryBase configEntry, string name = null, string description = null) : this( - tab, - configEntry, - name, - description, - Color.white, - Palette.AcceptedGreen, - Palette.AcceptedGreen) - { - } - /// /// Initializes a new instance of the class. /// + /// /// Tab that the toggle belongs to. /// The binded config entry. /// The name of the setting. Defaults to ConfigEntry key. /// The desription of the setting. No description by default. /// The color of the toggle. /// The hover color of the toggle. /// The active color of the toggle. - public LocalSetingToggle( + public LocalSettingToggle( LocalSettingsTab tab, ConfigEntryBase configEntry, - string name, - string description, - Color toggleColor, - Color toggleHoverColor, - Color toggleActiveColor) + string? name = null, + string? description = null, + Color? toggleColor = null, + Color? toggleHoverColor = null, + Color? toggleActiveColor = null) : base(tab, configEntry, name, description) { - ToggleColor = toggleColor; - ToggleHoverColor = toggleHoverColor; - ToggleActiveColor = toggleActiveColor; + ToggleColor = toggleColor ?? Color.white; + ToggleHoverColor = toggleHoverColor ?? Palette.AcceptedGreen; + ToggleActiveColor = toggleActiveColor ?? Palette.AcceptedGreen; } public override GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 759a0fd..aa6dfcd 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using MiraAPI.Colors; using MiraAPI.Events; @@ -12,6 +13,7 @@ using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; using MiraAPI.Keybinds; +using MiraAPI.LocalSettings.Attributes; using MiraAPI.Modifiers; using MiraAPI.Presets; using MiraAPI.Roles; @@ -109,6 +111,7 @@ public sealed class MiraPluginManager continue; } + RegisterLocalSettings(type); RegisterColorClasses(type); } @@ -317,4 +320,46 @@ public sealed class MiraPluginManager return false; } + + private static void RegisterLocalSettings(Type type) + { + try + { + if (!type.IsStatic()) + { + Logger.Error($"The type {type.Name} has to be static for local settings."); + return; + } + + foreach (var property in type.GetProperties()) + { + if (property.PropertyType.IsAssignableTo(typeof(ConfigEntryBase))) + { + continue; + } + + if (property.GetValue(null) is not ConfigEntryBase configEntry) + { + continue; + } + + var attribute = property.GetCustomAttribute(); + if (attribute == null) + { + continue; + } + + attribute.Create(configEntry); + } + + foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute() != null)) + { + Logger.Error($"{field.Name} is a field, not a property. Use properties for local settings."); + } + } + catch (Exception e) + { + Logger.Error($"Failed to register local settings from {type.Name}: {e}"); + } + } } From 2f5ca324757d1d496f072507dacc3f675ea52a46 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 31 Aug 2025 17:59:38 +0200 Subject: [PATCH 16/86] multitarget buttons --- MiraAPI/Hud/MultiTargetButton.cs | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 MiraAPI/Hud/MultiTargetButton.cs diff --git a/MiraAPI/Hud/MultiTargetButton.cs b/MiraAPI/Hud/MultiTargetButton.cs new file mode 100644 index 0000000..a700233 --- /dev/null +++ b/MiraAPI/Hud/MultiTargetButton.cs @@ -0,0 +1,70 @@ +using System.Linq; +using HarmonyLib; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Hud; + +/// +/// Custom action button that has multiple target objects. +/// +/// The type of the target objects. +public abstract class MultiTargetButton : CustomActionButton where T : MonoBehaviour +{ + /// + /// Gets or sets the list of targets of the button. + /// + public T[] Targets { get; protected set; } = []; + + /// + public virtual float Distance => PlayerControl.LocalPlayer.Data.Role.GetAbilityDistance(); + + /// + public virtual bool IsTargetValid(T target) + { + return target != null; + } + + /// + /// The method used to get the target objects. + /// + /// The list of target objects. + public abstract T[] GetTargets(); + + /// + /// The target object to set the oultine. + public abstract void SetOutline(T target, bool active); + + /// + public override bool CanUse() + { + var newTargets = GetTargets(); + foreach (var target in Targets) + { + if (!newTargets.Contains(target)) + { + SetOutline(target, false); + } + } + + Targets = newTargets.Where(IsTargetValid).ToArray(); + Targets.Do(t => SetOutline(t, true)); + + return base.CanUse() && Targets.Length > 0; + } + + /// + public override bool CanClick() + { + return base.CanClick() && Targets.Length > 0; + } + + /// + /// Use this to reset the button's targets after used. + /// + public virtual void ResetTarget() + { + Targets.Do(t => SetOutline(t, false)); + Targets = []; + } +} From a2a1e914399cb3420bd4734a8453cd1a6f0577ba Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 31 Aug 2025 18:03:17 +0200 Subject: [PATCH 17/86] replace EffectCancelable with a method --- MiraAPI/Hud/CustomActionButton.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index dd84316..4d88284 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -61,11 +61,6 @@ public abstract class CustomActionButton /// public virtual float EffectDuration => 0; - /// - /// Gets a value indicating whether the effect can be ended early by the player. - /// - public virtual bool EffectCancelable => false; - /// /// Gets the maximum amount of uses the button has. If the button has infinite uses, set to 0. /// @@ -408,6 +403,13 @@ public abstract class CustomActionButton { } + /// + /// Returns a value indicating whether the effect can be ended early by the player. + /// Always false by default + /// + /// Can the effect be canceled. + public virtual bool IsEffectCancellable() => false; + /// /// When the button is usable, this method is called to determine if the button can be clicked. /// By default, it takes into account the timer, effect, and uses. @@ -416,7 +418,7 @@ public abstract class CustomActionButton /// A value that represents whether the button can be clicked or not. public virtual bool CanClick() { - return (EffectActive ? EffectCancelable : Timer <= 0) && CanUse(); + return (EffectActive ? IsEffectCancellable() : Timer <= 0) && CanUse(); } /// @@ -427,7 +429,7 @@ public abstract class CustomActionButton public virtual bool CanUse() { return PlayerControl.LocalPlayer.moveable && - ((EffectActive && EffectCancelable) || (!EffectActive && (!LimitedUses || UsesLeft > 0))); + ((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0))); } /// @@ -449,7 +451,7 @@ public abstract class CustomActionButton /// public virtual void ClickHandler() { - if (EffectActive && EffectCancelable) + if (EffectActive && IsEffectCancellable()) { ResetCooldownAndOrEffect(); return; From 47394c6afef29ac1ab4d3e8c5912e6e8d966700e Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 31 Aug 2025 18:06:53 +0200 Subject: [PATCH 18/86] fix example mod --- MiraAPI.Example/Buttons/ThinkButton.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MiraAPI.Example/Buttons/ThinkButton.cs b/MiraAPI.Example/Buttons/ThinkButton.cs index b1d7164..a6f6855 100644 --- a/MiraAPI.Example/Buttons/ThinkButton.cs +++ b/MiraAPI.Example/Buttons/ThinkButton.cs @@ -9,7 +9,6 @@ public class ThinkButton : CustomActionButton public override string Name => "Think"; public override float Cooldown => 15f; public override float EffectDuration => 10f; - public override bool EffectCancelable => true; public override int MaxUses => 1; public override ButtonUsesMode UsesMode => ButtonUsesMode.PerRound; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; @@ -24,6 +23,11 @@ public class ThinkButton : CustomActionButton Button!.OverrideText("Think"); } + public override bool IsEffectCancellable() + { + return true; + } + public override bool Enabled(RoleBehaviour? role) { return role != null && role.IsImpostor; From 809e7a07abd21ba8e7a3562a9cce9c25fcfd9a79 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 31 Aug 2025 20:21:50 +0200 Subject: [PATCH 19/86] literally one dot --- MiraAPI/Hud/CustomActionButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 4d88284..ca7dd6a 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -405,7 +405,7 @@ public abstract class CustomActionButton /// /// Returns a value indicating whether the effect can be ended early by the player. - /// Always false by default + /// Always false by default. /// /// Can the effect be canceled. public virtual bool IsEffectCancellable() => false; From 8a71f138743b94ac8f04eab82fe75bce790084f2 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 31 Aug 2025 20:26:22 +0200 Subject: [PATCH 20/86] fix for keybind loading --- MiraAPI/Keybinds/KeybindManager.cs | 31 ++++++++++++++++++++++ MiraAPI/Keybinds/MiraKeybind.cs | 3 +-- MiraAPI/Patches/Keybinds/KeybindsPatch.cs | 30 +-------------------- MiraAPI/PluginLoading/MiraPluginManager.cs | 15 ++++++++--- MiraAPI/Utilities/Extensions.cs | 6 +++-- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs index b7b6c1b..57c87e7 100644 --- a/MiraAPI/Keybinds/KeybindManager.cs +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -1,4 +1,8 @@ +using System; using System.Collections.Generic; +using System.Linq; +using MiraAPI.Utilities; +using Reactor.Utilities; using Rewired; namespace MiraAPI.Keybinds; @@ -13,6 +17,33 @@ public static class KeybindManager /// public static List Keybinds { get; } = new(); + internal static void RewiredInit() + { + try + { + var instance = KeybindUtils.RewiredInputManager!; + foreach (var keybind in KeybindManager.Keybinds) + { + if (instance.userData.actions.ToArray().Any(x => x.name == keybind.Id)) + { + Logger.Warning($"Keybind of id {keybind.Id} already exists. Skipping it"); + continue; + } + + keybind.RewiredInputAction = instance.userData.RegisterModBind( + keybind.Id, + keybind.Name, + keybind.SourcePluginName, + keybind.DefaultKey, + modifiers: keybind.ModifierKeys); + } + } + catch (Exception e) + { + Logger.Error($"Error while registering keybinds in Rewired: {e}"); + } + } + /// /// Returns all conflicts where keybinds use the same key. /// diff --git a/MiraAPI/Keybinds/MiraKeybind.cs b/MiraAPI/Keybinds/MiraKeybind.cs index aa03d89..efdd1e0 100644 --- a/MiraAPI/Keybinds/MiraKeybind.cs +++ b/MiraAPI/Keybinds/MiraKeybind.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using BepInEx.Unity.IL2CPP; using Reactor.Utilities; using Rewired; @@ -52,7 +51,7 @@ public class MiraKeybind /// private Action Handler { get; set; } - internal BasePlugin? SourcePlugin { get; set; } + internal string? SourcePluginName { get; set; } /// /// Initializes a new instance of the class. diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs index 022aa81..1f60df0 100644 --- a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs +++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs @@ -1,11 +1,5 @@ -using System.Linq; -using BepInEx; -using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.Keybinds; -using MiraAPI.PluginLoading; -using MiraAPI.Utilities; -using Reactor.Utilities; using Rewired; namespace MiraAPI.Patches.Keybinds; @@ -24,29 +18,7 @@ public static class KeybindMenuPatch } KeybindUtils.RewiredInputManager = __instance; - foreach (var keybind in KeybindManager.Keybinds) - { - if (__instance.userData.actions.ToArray().Any(x => x.name == keybind.Id)) - { - Logger.Warning($"Keybind of id {keybind.Id} already exists. Skipping it"); - continue; - } - - PluginInfo? info = IL2CPPChainloader.Instance.Plugins.Values - .FirstOrDefault(p => p.Instance == keybind.SourcePlugin); - string group = info!.Metadata.Name; - if (keybind.SourcePlugin is IMiraPlugin miraPlugin) - { - group = miraPlugin.OptionsTitleText; - } - - keybind.RewiredInputAction = __instance.userData.RegisterModBind( - keybind.Id, - keybind.Name, - group, - keybind.DefaultKey, - modifiers: keybind.ModifierKeys); - } + KeybindManager.RewiredInit(); _registered = true; } } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 21f7d3a..34e2033 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -108,7 +108,7 @@ public sealed class MiraPluginManager } RegisterColorClasses(type); - RegisterKeybindClasses(type, plugin); + RegisterKeybinds(type, plugin); } info.PluginConfig.Save(); @@ -136,7 +136,7 @@ public sealed class MiraPluginManager ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); }; - RegisterKeybindClasses(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); + RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); } /// @@ -308,7 +308,7 @@ public sealed class MiraPluginManager return false; } - private static void RegisterKeybindClasses(Type type, BasePlugin source) + private static void RegisterKeybinds(Type type, BasePlugin source) { try { @@ -337,7 +337,14 @@ public sealed class MiraPluginManager } KeybindManager.Keybinds.Add(keybind); - keybind.SourcePlugin = source; + if (source is IMiraPlugin miraPlugin) + { + keybind.SourcePluginName = miraPlugin.OptionsTitleText; + } + else if (source is MiraApiPlugin) + { + keybind.SourcePluginName = "MiraAPI"; + } } foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(MiraKeybind)))) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 5f4523c..16cfffd 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -563,13 +563,15 @@ public static class Extensions /// The for this action (default is Button). /// Optional modifier keys (e.g., Control, Shift, Alt) that must be held together with the main key. /// The action ID of the newly registered action. - public static InputAction RegisterModBind(this UserData userData, string id, string name, string group, KeyboardKeyCode key, int category = 0, int elementIdentifierId = -1, InputActionType type = InputActionType.Button, ModifierKey[]? modifiers = null) + public static InputAction RegisterModBind(this UserData userData, string id, string name, string? group, KeyboardKeyCode key, int category = 0, int elementIdentifierId = -1, InputActionType type = InputActionType.Button, ModifierKey[]? modifiers = null) { userData.AddAction(category); var action = userData.GetAction(userData.actions.Count - 1)!; action.name = id; - action.descriptiveName = $"{Palette.CrewmateRoleHeaderDarkBlue.ToTextColor()}{group.ReplaceLineEndings(" ")}\n{name}"; + action.descriptiveName = group != null + ? $"{Palette.CrewmateRoleHeaderDarkBlue.ToTextColor()}{group.ReplaceLineEndings(" ")}\n{name}" + : name; action.categoryId = category; action.type = type; action.userAssignable = true; From b19a0178ad6ad7f0499e343eb5feeaac859e290d Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 31 Aug 2025 23:41:35 +0200 Subject: [PATCH 21/86] add a limit to multitarget buttons --- MiraAPI/Hud/MultiTargetButton.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Hud/MultiTargetButton.cs b/MiraAPI/Hud/MultiTargetButton.cs index a700233..8c1ae34 100644 --- a/MiraAPI/Hud/MultiTargetButton.cs +++ b/MiraAPI/Hud/MultiTargetButton.cs @@ -19,6 +19,11 @@ public abstract class MultiTargetButton : CustomActionButton where T : MonoBe /// public virtual float Distance => PlayerControl.LocalPlayer.Data.Role.GetAbilityDistance(); + /// + /// Gets the maximum amount of targets the button can have. + /// + public virtual int MaxTargets { get; } = int.MaxValue; + /// public virtual bool IsTargetValid(T target) { @@ -47,7 +52,10 @@ public abstract class MultiTargetButton : CustomActionButton where T : MonoBe } } - Targets = newTargets.Where(IsTargetValid).ToArray(); + Targets = newTargets + .Where(IsTargetValid) + .Take(MaxTargets) + .ToArray(); Targets.Do(t => SetOutline(t, true)); return base.CanUse() && Targets.Length > 0; From b4e6e448f2b024f3505838698d285a1d8a31edec Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sun, 31 Aug 2025 18:04:52 -0400 Subject: [PATCH 22/86] Major work/rewrite on local settings --- MiraAPI.Example/ExampleLocalSetings.cs | 35 +- MiraAPI.Example/ExamplePlugin.cs | 2 - .../Attributes/LocalSettingAttribute.cs | 9 +- .../Attributes/LocalSettingToggleAttribute.cs | 12 +- MiraAPI/LocalSettings/ILocalSetting.cs | 26 ++ MiraAPI/LocalSettings/LocalSettingsManager.cs | 42 +++ MiraAPI/LocalSettings/LocalSettingsTab.cs | 309 +++++++++++------- .../LocalSettingsTabSingleton.cs | 20 ++ .../SettingTypes/LocalSettingBase.cs | 16 +- .../SettingTypes/LocalSettingToggle.cs | 11 +- MiraAPI/MiraApiPlugin.cs | 5 - .../{MiraApiConfig.cs => MiraApiSettings.cs} | 12 +- MiraAPI/Modifiers/ModifierComponent.cs | 3 +- .../ModifierDisplayComponent.cs | 3 +- .../LocalSettings/OptionsMenuPatches.cs | 116 +++++++ .../LocalSettings/ToggleBehaviourPatch.cs | 37 +++ .../Patches/Options/GamePresetsTabPatches.cs | 21 +- MiraAPI/PluginLoading/MiraPluginInfo.cs | 1 + MiraAPI/PluginLoading/MiraPluginManager.cs | 47 ++- MiraAPI/Resources/Settings.png | Bin 0 -> 9598 bytes MiraAPI/Utilities/Assets/MiraAssets.cs | 5 + MiraAPI/Utilities/Helpers.cs | 51 +++ 22 files changed, 604 insertions(+), 179 deletions(-) create mode 100644 MiraAPI/LocalSettings/LocalSettingsManager.cs create mode 100644 MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs rename MiraAPI/{MiraApiConfig.cs => MiraApiSettings.cs} (62%) create mode 100644 MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs create mode 100644 MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs create mode 100644 MiraAPI/Resources/Settings.png diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index c682617..fb07869 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -5,10 +5,37 @@ using Reactor.Utilities; namespace MiraAPI.Example; -public static class ExampleLocalSetings +public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) { - public static LocalSettingsTab ModTab { get; } = new("Example Tab"); + public override string TabName => "Mira Example"; - [LocalSetting] - public static ConfigEntry ExampleToggleSetting { get; private set; } + [LocalSettingToggle] + public ConfigEntry PlayStartupSound { get; private set; } = config.Bind("Audio", "Play Startup Sound", true); + + [LocalSettingToggle] + public ConfigEntry MuteInBackground { get; private set; } = config.Bind("Audio", "Mute In Background", false); + + [LocalSettingToggle] + public ConfigEntry AutoSaveEnabled { get; private set; } = config.Bind("General", "Auto Save Enabled", true); + + [LocalSettingToggle] + public ConfigEntry EnableCloudSync { get; private set; } = config.Bind("General", "Enable Cloud Sync", false); + + [LocalSettingToggle] + public ConfigEntry ShowAdvancedTooltips { get; private set; } = config.Bind("UI", "Show Advanced Tooltips", true); + + [LocalSettingToggle] + public ConfigEntry CompactUI { get; private set; } = config.Bind("UI", "Compact UI Layout", false); + + [LocalSettingToggle] + public ConfigEntry EnableControllerSupport { get; private set; } = config.Bind("Input", "Enable Controller Support", true); + + [LocalSettingToggle] + public ConfigEntry EnableVibration { get; private set; } = config.Bind("Input", "Enable Controller Vibration", true); + + [LocalSettingToggle] + public ConfigEntry ShowPlayerNames { get; private set; } = config.Bind("Gameplay", "Show Player Names", true); + + [LocalSettingToggle] + public ConfigEntry EnableHardMode { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); } diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index 3819347..0aed9c4 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -24,7 +24,5 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin { ExampleEventHandlers.Initialize(); Harmony.PatchAll(); - - Config.Keys. } } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs index 6735d4e..1b101c5 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -1,22 +1,21 @@ using System; using BepInEx.Configuration; -using UnityEngine; namespace MiraAPI.LocalSettings.Attributes; /// /// Base class for all local settings attributes. /// -/// The local setting type the attribute handles. [AttributeUsage(AttributeTargets.Property)] public abstract class LocalSettingAttribute( - LocalSettingsTab tab, string? name = null, string? description = null) : Attribute { /// /// Returns the created setting object. /// + /// Gets the tab where the setting is located. + /// Gets the config entry the setting is attached to. /// The created class. - protected abstract GameObject CreateSetting(); -} \ No newline at end of file + internal abstract ILocalSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase); +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs index 7a8cd34..7b44f7f 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs @@ -10,17 +10,13 @@ namespace MiraAPI.LocalSettings.Attributes; /// [AttributeUsage(AttributeTargets.Property)] public class LocalSettingToggleAttribute( - LocalSettingsTab tab, string? name = null, - string? description = null, - Color? toggleColor = null, - Color? toggleHoverColor = null, - Color? toggleActiveColor = null - ) : LocalSettingAttribute(tab, name, description) + string? description = null + ) : LocalSettingAttribute(name, description) { /// - protected override LocalSettingToggle CreateSetting(ConfigEntryBase configEntry) + internal override LocalSettingToggle CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalSettingToggle(tab, configEntry, name, description, toggleColor, toggleHoverColor, toggleActiveColor); + return new LocalSettingToggle(tab, configEntryBase, name, description); } } diff --git a/MiraAPI/LocalSettings/ILocalSetting.cs b/MiraAPI/LocalSettings/ILocalSetting.cs index 78d3647..e6649bb 100644 --- a/MiraAPI/LocalSettings/ILocalSetting.cs +++ b/MiraAPI/LocalSettings/ILocalSetting.cs @@ -8,9 +8,35 @@ namespace MiraAPI.LocalSettings; /// public interface ILocalSetting { + /// + /// Gets the preferred name of a setting. + /// string Name { get; } + + /// + /// Gets the description of a setting. + /// string Description { get; } + + /// + /// Gets the setting's gameobject. + /// GameObject? Setting { get; } + + /// + /// Gets the setting's config entry. + /// ConfigEntryBase ConfigEntry { get; } + + /// + /// Used to create the setting + /// + /// Toggle template. + /// Slider template. + /// The parent of the setting. + /// The offset. + /// The order. + /// Whether it's the last on the row. + /// The created setting. GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); } diff --git a/MiraAPI/LocalSettings/LocalSettingsManager.cs b/MiraAPI/LocalSettings/LocalSettingsManager.cs new file mode 100644 index 0000000..3b75d05 --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using BepInEx.Unity.IL2CPP; +using MiraAPI.GameOptions; +using Reactor.Utilities; + +namespace MiraAPI.LocalSettings; + +/// +/// Manages local settings. +/// +public static class LocalSettingsManager +{ + internal static readonly Dictionary TypeToTab = []; + internal static readonly List Tabs = []; + + internal static bool RegisterTab(Type type, BasePlugin pluginInfo) + { + if (Activator.CreateInstance(type, pluginInfo.Config) is not LocalSettingsTab tab) + { + return false; + } + + if (TypeToTab.ContainsKey(type)) + { + Logger.Error($"Local settings tab {type.Name} already exists."); + return false; + } + + Tabs.Add(tab); + TypeToTab.Add(type, tab); + + typeof(LocalSettingsTabSingleton<>).MakeGenericType(type) +#pragma warning disable S3011 + .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! +#pragma warning restore S3011 + .SetValue(null, tab); + + return true; + } +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index e956589..a7e2ea7 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -1,154 +1,231 @@ -using System.Collections.Generic; -using System.Linq; +using System; +using System.Collections.Generic; +using BepInEx.Configuration; using HarmonyLib; +using MiraAPI.Patches.LocalSettings; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using Object = UnityEngine.Object; namespace MiraAPI.LocalSettings; /// /// Represents a local settings tab. Gets a button by default /// -public class LocalSettingsTab +public abstract class LocalSettingsTab(ConfigFile config) { - public string TabName { get; } - public Color TabColor { get; } - public Color TabHoverColor { get; } - public Color TabActiveColor { get; } - public LoadableAsset? TabIcon { get; } - - public TabGroup? TabButton { get; protected set; } - private bool ShouldCreateButton { get; set; } - - public List Settings { get; } + /// + /// Gets the name of the tab + /// + public abstract string TabName { get; } /// - /// Initializes a new instance of the class. + /// Gets the tab button icon. /// - /// The name of the tab. - /// The color of the tab. - /// The hover color of the tab. - /// The active color of the tab. - /// The tab icon. Defaults to no icon. - /// Should the tab button be created. Without it the tab needs to be opened manually. - public LocalSettingsTab(string tabName, Color? tabColor = null, Color? tabHoverColor = null, Color? tabActiveColor = null, LoadableAsset? tabIcon = null, bool createButton = true) - { - TabName = tabName; - TabColor = tabColor ?? Color.white; - TabHoverColor = tabHoverColor ?? Palette.AcceptedGreen; - TabActiveColor = tabActiveColor ?? Palette.AcceptedGreen; - TabIcon = tabIcon; - ShouldCreateButton = createButton; - Settings = new(); - } + public virtual LoadableAsset? TabIcon => null; + + /// + /// Gets the color of the tab button. + /// + public virtual Color TabColor => Color.white; + + /// + /// Gets the color of the tab button when hovered. + /// + public Color TabHoverColor => Palette.AcceptedGreen; + + /// + /// Gets the color of the tab button when active. + /// + public virtual Color TabActiveColor => Palette.AcceptedGreen; + + /// + /// Gets a value indicating whether a button should be created for the tab. + /// + protected virtual bool ShouldCreateButton => true; + + /// + /// Gets the mod's config file. Automatically found in the plugin loader. + /// + public ConfigFile Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); + + internal TabGroup? TabButton { get; set; } + internal List Settings { get; } = []; + private Scroller? Scroller { get; set; } internal GameObject? CreateTab(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) { - var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); - tab.name = $"{TabName}Tab"; - tab.transform.DestroyChildren(); - tab.gameObject.SetActive(false); + if (ShouldCreateButton) + { + CreateTabButton(instance, ref tabIdx, ref offset); + } - if (TabButton != null) + var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); + tab.name = $"{TabName}Tab"; + tab.transform.DestroyChildren(); + tab.gameObject.SetActive(false); + + if (Scroller == null) + { + Scroller = Helpers.CreateScroller(tab.transform, OptionsMenuPatches.MaskCollider); + } + + if (TabButton != null) + { + TabButton.Content = tab.gameObject; + } + + var generalLabel = instance.transform.FindChild("GeneralTab").FindChild("ControlGroup") + .FindChild("ControlText_TMP").gameObject; + var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent(); + var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent(); + + Dictionary> entriesByGroup = new(); + foreach (var entry in Settings) + { + var group = entry.ConfigEntry.Definition.Section; + if (!entriesByGroup.ContainsKey(group)) + entriesByGroup.Add(group, []); + + entriesByGroup[group].Add(entry); + } + + float contentOffset = 0; + var contentOrder = 1; + var contentIndex = 1; + + foreach (var pair in entriesByGroup) + { + CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset); + + foreach (var setting in pair.Value) + { + var obj = setting.CreateOption( + toggle, + slider, + Scroller.Inner, + ref contentOffset, + ref contentOrder, + contentIndex == pair.Value.Count); + + obj!.GetComponentsInChildren().Do(x => + { + x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + x.sortingOrder = 150; + x.sortingLayerName = "Default"; + }); + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); + + contentIndex++; + } + + contentOrder = 1; + } + + Scroller.SetBounds(new FloatRange(0, Settings.Count * 0.5f - 5f), new FloatRange(0, 0)); + return tab; + } + + private void CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + { + var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); + tabButtonObject.name = $"{TabName} Button"; + tabButtonObject.transform.localPosition = new Vector3(2.4f, 2.1f - offset, 5.5f); + tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1); + tabButtonObject.Button.color = TabColor; + TabButton = tabButtonObject; + + var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent(); + tabButtonText.GetComponent().Destroy(); // i hate text translators + tabButtonText.transform.localPosition = new Vector3(0.078f, 0, 0); + tabButtonText.transform.localScale = new Vector3(0.9f, 0.9f); + tabButtonText.alignment = TextAlignmentOptions.Right; + tabButtonText.text = $"{GetShortName(TabName)}"; + + var tabButton = tabButtonObject.GetComponent(); + var rollover = tabButtonObject.Rollover; + rollover.OverColor = TabColor; + rollover.OutColor = TabColor; + + SpriteRenderer? tabButtonRend = null; + if (TabIcon != null) { - TabButton.Content = tab.gameObject; + tabButtonRend = new GameObject("sprite").AddComponent(); + tabButtonRend.gameObject.layer = 5; // ui layer + tabButtonRend.transform.SetParent(tabButtonObject.transform); + tabButtonRend.transform.localPosition = new Vector3(0.5f, 0, -2); + tabButtonRend.transform.localScale = new Vector3(0.2f, 0.2f, 1); + tabButtonRend.sprite = TabIcon.LoadAsset(); + tabButtonText.gameObject.SetActive(false); } - var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent(); - var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent(); - - float contentOffset = 0; - var contentOrder = 1; - var contentIndex = 1; - foreach (var setting in Settings) + var tabIndex = tabIdx; // Local copies of the tab variables so we can use them in the lambdas + var tabOffset = offset; + tabButton.OnClick.AddListener((UnityAction)(() => { instance.OpenTabGroup(tabIndex + 10); })); + tabButton.OnMouseOver.AddListener((UnityAction)(() => { - setting.CreateOption( - toggle, - slider, - tab.transform, - ref contentOffset, - ref contentOrder, - contentIndex == Settings.Count); - contentIndex++; - } - - if (ShouldCreateButton) + tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f); + tabButtonObject.Button.color = TabHoverColor; + tabButtonObject.Rollover.OutColor = TabHoverColor; + tabButtonText.gameObject.SetActive(true); + tabButtonText.transform.localPosition = new Vector3(-0.024f, 0, 0); + tabButtonText.maxVisibleCharacters = int.MaxValue; + tabButtonText.text = TabName; + tabButtonText.alignment = TextAlignmentOptions.Left; + })); + tabButton.OnMouseOut.AddListener((UnityAction)(() => { - var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); - tabButtonObject.name = $"{TabName} Button"; - tabButtonObject.transform.localPosition = new Vector3(2.4f, 2.1f - offset, 5.5f); - tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1); - tabButtonObject.Button.color = TabColor; - TabButton = tabButtonObject; - - var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent(); - tabButtonText.GetComponent().Destroy(); // i hate text translators - tabButtonText.transform.localPosition = new Vector3(0.078f, 0, 0); - tabButtonText.transform.localScale = new Vector3(0.9f, 0.9f); - tabButtonText.alignment = TextAlignmentOptions.Right; - tabButtonText.text = $"{GetShortName(TabName)}"; - - var tabButton = tabButtonObject.GetComponent(); - var rollover = tabButtonObject.Rollover; - rollover.OverColor = TabColor; - rollover.OutColor = TabColor; - - SpriteRenderer tabButtonRend = null; - if (TabIcon != null) + if (!tabButtonObject.Content.gameObject.activeSelf) { - tabButtonRend = new GameObject("sprite").AddComponent(); - tabButtonRend.gameObject.layer = 5; // ui layer - tabButtonRend.transform.SetParent(tabButtonObject.transform); - tabButtonRend.transform.localPosition = new Vector3(0.5f, 0, -2); - tabButtonRend.transform.localScale = new Vector3(0.5f, 0.5f, 1); - tabButtonRend.sprite = TabIcon.LoadAsset(); + tabButtonObject.Button.color = TabColor; + tabButtonObject.Rollover.OutColor = TabColor; + } + + tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f); + tabButtonText.alignment = TextAlignmentOptions.Right; + if (tabButtonRend != null) + { + tabButtonRend.gameObject.SetActive(true); tabButtonText.gameObject.SetActive(false); } - int tabIndex = tabIdx; // Local copies of the tab variables so we can use them in the lambdas - float tabOffset = offset; - tabButton.OnClick.AddListener((UnityAction)(() => - { - instance.OpenTabGroup(tabIndex + 10); - })); - tabButton.OnMouseOver.AddListener((UnityAction)(() => - { - tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f); + tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0); + tabButtonText.maxVisibleCharacters = 4; + tabButtonText.text = $"{GetShortName(TabName)}"; + })); - if (tabButtonRend != null) - { - tabButtonRend.gameObject.SetActive(false); - } - tabButtonText.gameObject.SetActive(true); - tabButtonText.transform.localPosition = new Vector3(0.045f, 0, 0); - tabButtonText.maxVisibleCharacters = int.MaxValue; - tabButtonText.text = TabName; - })); - tabButton.OnMouseOut.AddListener((UnityAction)(() => - { - tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f); - - if (tabButtonRend != null) - { - tabButtonRend.gameObject.SetActive(true); - tabButtonText.gameObject.SetActive(false); - } - tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0); - tabButtonText.maxVisibleCharacters = 4; - tabButtonText.text = $"{GetShortName(TabName)}"; - })); - } - - return tab; + offset += 0.6f; + tabIdx++; } + private static void CreateLabel(GameObject template, Transform parent, string text, ref float offset) + { + var label = Object.Instantiate(template, parent); + label.transform.localPosition = new Vector3(0, 1.85f - offset); + label.GetComponent().Destroy(); + label.name = text; + + var tmp = label.GetComponent(); + tmp.text = $"{text}"; + tmp.alignment = TextAlignmentOptions.Center; + + var meshRend = label.GetComponent(); + meshRend.sortingLayerName = "Default"; + meshRend.sortingOrder = 151; + + offset += 0.5f; + } + + private static string GetShortName(string name) { - string shortName = string.Empty; + var shortName = string.Empty; name.Split(' ').Do(x => shortName += x[0]); return shortName; } } + diff --git a/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs new file mode 100644 index 0000000..1994235 --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs @@ -0,0 +1,20 @@ +using System.Linq; +using MiraAPI.LocalSettings; + +namespace MiraAPI.GameOptions; + +/// +/// Singleton for local settings tabs. +/// +/// The settings tab type. +public static class LocalSettingsTabSingleton where T : LocalSettingsTab +{ + private static T? _instance; + + /// + /// Gets the instance of the setting tab. + /// +#pragma warning disable CA1000 + public static T Instance => _instance ??= LocalSettingsManager.Tabs.OfType().Single(); +#pragma warning restore CA1000 +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index e93131a..e1ebe10 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -1,15 +1,23 @@ -using BepInEx.Configuration; +using System; +using BepInEx.Configuration; using TMPro; using UnityEngine; namespace MiraAPI.LocalSettings.SettingTypes; +/// public abstract class LocalSettingBase : ILocalSetting { + /// public string Name { get; } + + /// public string Description { get; } - public GameObject? Setting { get; } + /// + public GameObject? Setting { get; } = null!; + + /// public ConfigEntryBase ConfigEntry { get; } /// @@ -19,13 +27,13 @@ public abstract class LocalSettingBase : ILocalSetting /// The config entry. /// The name of the setting. /// The description of the setting. - protected LocalSettingBase(LocalSettingsTab tab, ConfigEntryBase configEntry, string? name = null, string? description = null) + protected LocalSettingBase(Type tab, ConfigEntryBase configEntry, string? name = null, string? description = null) { ConfigEntry = configEntry; Name = name ?? ConfigEntry.Definition.Key; Description = description ?? ConfigEntry.Description.Description; - tab.Settings.Add(this); + LocalSettingsManager.TypeToTab[tab].Settings.Add(this); } /// diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs index 00babc1..fa36333 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs @@ -1,8 +1,11 @@ -using BepInEx.Configuration; +using System; +using BepInEx.Configuration; +using MiraAPI.Utilities; using Reactor.Localization.Utilities; using TMPro; using UnityEngine; using UnityEngine.Events; +using Object = UnityEngine.Object; namespace MiraAPI.LocalSettings.SettingTypes; @@ -37,7 +40,7 @@ public class LocalSettingToggle : LocalSettingBase /// The hover color of the toggle. /// The active color of the toggle. public LocalSettingToggle( - LocalSettingsTab tab, + Type tab, ConfigEntryBase configEntry, string? name = null, string? description = null, @@ -66,7 +69,7 @@ public class LocalSettingToggle : LocalSettingBase } else { - toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.35f : 1.28f, 1.85f - offset); + toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset); } toggleObject.BaseText = CustomStringName.CreateAndRegister(Name); @@ -95,6 +98,8 @@ public class LocalSettingToggle : LocalSettingBase toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; })); + Helpers.DivideSize(toggleObject.gameObject, 1.1f); + order++; if (order > 2 && !last) { diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index aa7f3c6..8654cf6 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -30,10 +30,6 @@ public partial class MiraApiPlugin : BasePlugin /// public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255); - /// - /// Gets the Mira Api Config Handler. - /// - public MiraApiConfig? MiraConfig { get; private set; } private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); @@ -44,7 +40,6 @@ public partial class MiraApiPlugin : BasePlugin ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow); - MiraConfig = new MiraApiConfig(Config); PluginManager = new MiraPluginManager(); PluginManager.Initialize(); } diff --git a/MiraAPI/MiraApiConfig.cs b/MiraAPI/MiraApiSettings.cs similarity index 62% rename from MiraAPI/MiraApiConfig.cs rename to MiraAPI/MiraApiSettings.cs index 10c9776..e5fe303 100644 --- a/MiraAPI/MiraApiConfig.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -2,7 +2,10 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.LocalSettings; +using MiraAPI.LocalSettings.Attributes; using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; @@ -14,10 +17,17 @@ namespace MiraAPI; /// /// Mira API Config File Handler /// -public class MiraApiConfig(ConfigFile config) +public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) { + /// + public override string TabName => "Mira API"; + + /// + public override LoadableAsset? TabIcon => MiraAssets.SettingsIcon; + /// /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. /// + [LocalSettingToggle] public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false); } diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 4f556f5..f4003c9 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameOptions; using MiraAPI.Modifiers.ModifierDisplay; using MiraAPI.Modifiers.Types; using MiraAPI.Patches.Roles; @@ -129,7 +130,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) { var taskPanelOpen = HudManager.Instance.TaskPanel.open; var roleTabOpen = HudManagerPatches.RoleTab != null && HudManagerPatches.RoleTab.open; - var leftSideHud = PluginSingleton.Instance.MiraConfig!.ModifiersHudLeftSide.Value; + var leftSideHud = LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value; if (leftSideHud && (taskPanelOpen || roleTabOpen)) { diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs index 0836935..0cbb8c3 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameOptions; using MiraAPI.Patches.Roles; using MiraAPI.Utilities.Assets; using Reactor.Utilities; @@ -98,7 +99,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) IsOpen = false; _children.gameObject.SetActive(false); - if (PluginSingleton.Instance.MiraConfig!.ModifiersHudLeftSide.Value) + if (LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value) { var aspect = GetComponent(); _toggleButton.transform.localPosition = new Vector3(-1f, 2.7f, 0f); diff --git a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs new file mode 100644 index 0000000..0b4ad9c --- /dev/null +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using MiraAPI.LocalSettings; +using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.Patches.LocalSettings; + +[HarmonyPatch(typeof(OptionsMenuBehaviour))] +public static class OptionsMenuPatches +{ + internal static BoxCollider2D MaskCollider; + /// + /// Creates the tabs and their content + /// + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Start))] + public static void StartPostfix(OptionsMenuBehaviour __instance) + { + // Fix for tabs not being clickable in the main menu + if (!AmongUsClient.Instance.IsInGame) + { + __instance.Background.GetComponent().enabled = false; + __instance.transform.FindChild("Tint").SetLocalZ(5.6f); + } + + var maskObj = new GameObject + { + layer = 5, + name = "SpriteMask", + }; + maskObj.transform.SetParent(__instance.transform); + maskObj.transform.localPosition = new Vector3(0, -0.3f, 0); + maskObj.transform.localScale = new Vector3(500, 120, 1); + + var blank = Sprite.Create( + Texture2D.whiteTexture, + new Rect(0, 0, Texture2D.whiteTexture.width, Texture2D.whiteTexture.height), + new Vector2(0.5f, 0.5f)); + + var mask = maskObj.AddComponent(); + mask.sprite = blank; + mask.isCustomRangeActive = true; + mask.sortingLayerName = "Default"; + mask.backSortingOrder = 100; + mask.frontSortingOrder = 500; + + MaskCollider = maskObj.AddComponent(); + MaskCollider.size = new Vector2(0.01f, 0.1f); + MaskCollider.isTrigger = true; + MaskCollider.enabled = true; + + float yOffset = 0; + var i = 0; + foreach (var settings in LocalSettingsManager.Tabs) + { + settings.CreateTab(__instance, ref i, ref yOffset); + } + } + + /// + /// Modifies how opening tabs is handled for the custom ones to work + /// + [HarmonyPrefix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.OpenTabGroup))] + public static bool OpenTabGroupPrefix(OptionsMenuBehaviour __instance, ref int index) + { + __instance.Tabs.ToList().ForEach(x => x.Close()); // Close all vanilla tabs + LocalSettingsManager.Tabs.ForEach(CustomClose); // Close all mod tabs + + if (index >= 10) // Tabs with index 10 and above are the mod settings tabs + { + CustomOpen(LocalSettingsManager.Tabs[index - 10]); + return false; + } + + return true; + } + + /// + /// Makes the custom tabs close when opening the options menu + /// + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Open))] + public static void OpenPostfix(OptionsMenuBehaviour __instance) + { + LocalSettingsManager.Tabs.ForEach(CustomClose); + } + + private static void CustomOpen(LocalSettingsTab tab) + { + if (tab.TabButton?.Button) + tab.TabButton!.Button.color = tab.TabHoverColor; + + if (tab.TabButton?.Rollover) + tab.TabButton!.Rollover.OutColor = tab.TabHoverColor; + + if (tab.TabButton?.Content) + tab.TabButton!.Content.SetActive(true); + } + private static void CustomClose(LocalSettingsTab tab) + { + if (tab.TabButton?.Button) + tab.TabButton!.Button.color = tab.TabColor; + + if (tab.TabButton?.Rollover) + tab.TabButton!.Rollover.OutColor = tab.TabColor; + + if (tab.TabButton?.Content) + tab.TabButton!.Content.SetActive(false); + } +} diff --git a/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs new file mode 100644 index 0000000..5022d1c --- /dev/null +++ b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs @@ -0,0 +1,37 @@ +using HarmonyLib; +using UnityEngine; + +namespace MiraAPI.Patches.LocalSettings; + +[HarmonyPatch(typeof(ToggleButtonBehaviour))] +public static class ToggleBehaviourPatch +{ + /// + /// ResetText was inlined ofc + /// + [HarmonyPrefix] + [HarmonyPatch(nameof(ToggleButtonBehaviour.UpdateText))] + public static bool ResetTextPrefix(ToggleButtonBehaviour __instance, bool on) + { + __instance.onState = on; + var color = on ? new Color(0f, 1f, 0.16470589f, 1f) : Color.white; + __instance.Background.color = color; + + // Used to mask the text + __instance.Text.richText = true; + __instance.Text.text = $"" + + $"{DestroyableSingleton.Instance.GetString(__instance.BaseText)}: " + + $"{DestroyableSingleton.Instance.GetString(__instance.onState ? StringNames.SettingsOn : StringNames.SettingsOff)}"; + + __instance.Text.sortingOrder = 151; + __instance.Text.renderer.sortingLayerName = "Default"; + __instance.Text.renderer.sortingOrder = 151; + + if (__instance.Rollover) + { + __instance.Rollover.ChangeOutColor(color); + } + + return false; + } +} diff --git a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs index 2b4f77d..ed80bb1 100644 --- a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs +++ b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs @@ -6,6 +6,7 @@ using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Presets; using MiraAPI.Roles; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -28,22 +29,6 @@ internal static class GamePresetsTabPatches private static readonly object Lock = new(); - public static void DivideSize(GameObject obj, float amount) - { - foreach (var collider in obj.GetComponentsInChildren(true)) - { - if (collider.TryCast() is { } col) - { - col.size = new Vector2(col.size.x / amount, col.size.y); - } - } - - foreach (var rend in obj.GetComponentsInChildren(true)) - { - rend.size = new Vector2(rend.size.x / amount, rend.size.y); - } - } - [HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnEnable))] public static class GamePresetsOnEnablePatch { @@ -207,7 +192,7 @@ internal static class GamePresetsTabPatches saveText.transform.parent.localPosition.y, saveText.transform.parent.localPosition.z); - DivideSize(saveButton.gameObject, 2f); + Helpers.DivideSize(saveButton.gameObject, 2f); saveButton.OnClick = new Button.ButtonClickedEvent(); saveButton.OnClick.AddListener( @@ -276,7 +261,7 @@ internal static class GamePresetsTabPatches var iconSpriteRend = icon.AddComponent(); iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset(); - DivideSize(refreshButton.gameObject, 2f); + Helpers.DivideSize(refreshButton.gameObject, 2f); refreshButton.buttonText.gameObject.Destroy(); refreshButton.OnClick = new Button.ButtonClickedEvent(); diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 4a454a6..891c3ba 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -5,6 +5,7 @@ using BepInEx.Configuration; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Presets; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index aa6dfcd..98b8db5 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -13,6 +13,7 @@ using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; using MiraAPI.Keybinds; +using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Modifiers; using MiraAPI.Presets; @@ -95,6 +96,11 @@ public sealed class MiraPluginManager continue; } + if (RegisterLocalTabs(type, plugin)) + { + continue; + } + if (RegisterRole(type, info, out var role)) { roles.Add(role); @@ -111,7 +117,6 @@ public sealed class MiraPluginManager continue; } - RegisterLocalSettings(type); RegisterColorClasses(type); } @@ -150,6 +155,8 @@ public sealed class MiraPluginManager Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {(KeyboardKeyCode)button.DefaultKeybind}."); } }; + + RegisterLocalTabs(typeof(MiraApiSettings), PluginSingleton.Instance); } /// @@ -321,45 +328,63 @@ public sealed class MiraPluginManager return false; } - private static void RegisterLocalSettings(Type type) + private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo) { try { - if (!type.IsStatic()) + if (!type.IsAssignableTo(typeof(LocalSettingsTab))) { - Logger.Error($"The type {type.Name} has to be static for local settings."); - return; + return false; + } + + if (!LocalSettingsManager.RegisterTab(type, pluginInfo)) + { + return false; } foreach (var property in type.GetProperties()) { - if (property.PropertyType.IsAssignableTo(typeof(ConfigEntryBase))) + if (!typeof(ConfigEntryBase).IsAssignableFrom(property.PropertyType)) { continue; } - if (property.GetValue(null) is not ConfigEntryBase configEntry) + if (property.GetMethod?.IsStatic == true) + { + Logger.Error($"Option property {property.Name} in {type.Name} must not be static."); + continue; + } + + if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance) { continue; } - var attribute = property.GetCustomAttribute(); + if (property.GetValue(tabInstance) is not ConfigEntryBase configEntry) + { + continue; + } + + var attribute = property.GetCustomAttribute(); if (attribute == null) { continue; } - attribute.Create(configEntry); + attribute.CreateSetting(type, configEntry); } - foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute() != null)) + foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute() != null)) { Logger.Error($"{field.Name} is a field, not a property. Use properties for local settings."); } + + return true; } catch (Exception e) { - Logger.Error($"Failed to register local settings from {type.Name}: {e}"); + Logger.Error($"Failed to register options for {type.Name}: {e.ToString()}"); } + return false; } } diff --git a/MiraAPI/Resources/Settings.png b/MiraAPI/Resources/Settings.png new file mode 100644 index 0000000000000000000000000000000000000000..6f0da713c35a5771e03c581ac0520a5f91629211 GIT binary patch literal 9598 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 148c65a..3f50a79 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -69,6 +69,11 @@ public static class MiraAssets /// Gets the Next Button sprite. /// public static LoadableResourceAsset NextButton { get; } = new("MiraAPI.Resources.NextButton.png"); + + /// + /// Gets the Mira settings icon.. + /// + public static LoadableResourceAsset SettingsIcon { get; } = new("MiraAPI.Resources.Settings.png"); /// /// Gets the highlighted Next Button sprite. diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 6c0bf4c..06ddc76 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -3,8 +3,10 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem.IO; using MiraAPI.Keybinds; +using MiraAPI.Patches.LocalSettings; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; @@ -26,6 +28,7 @@ public static class Helpers { return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x => x.Object)]; } + internal static GameObject CreateKeybindIcon(GameObject button, KeyboardKeyCode keyCode, Vector3 localPos) { var keybindIcon = Object.Instantiate(HudManager.Instance.AbilityButton.usesRemainingSprite.gameObject, button.transform); @@ -36,6 +39,54 @@ public static class Helpers return keybindIcon; } + /// + /// Creates a draggable scroller. Add items into the scroller.Inner transform. This does not automatically position children. + /// + /// Where the scroller should be parented to. + /// The collider of the scroller. Used to drag. + /// The created scroller object. + public static Scroller CreateScroller(Transform parent, BoxCollider2D hitBoxCollider) + { + var scrollObj = new GameObject("Scroller"); + scrollObj.transform.SetParent(parent); + scrollObj.transform.localScale = new Vector3(1, 1, 1); + + var inner = new GameObject("Inner"); + inner.transform.SetParent(scrollObj.transform); + inner.transform.localScale = new Vector3(1, 1, 1); + inner.transform.localPosition = new Vector3(0, 0, 2); + + var scroller = scrollObj.AddComponent(); + scroller.allowX = false; + scroller.allowY = true; + scroller.DragScrollSpeed = 1f; + scroller.Colliders = new Il2CppReferenceArray([hitBoxCollider]); + scroller.Inner = inner.transform; + + return scroller; + } + + /// + /// Divides a button/etc by a certain amount by resizing colliders and renderer sizes. + /// + /// The object you want to divide. + /// How much you want to divide by. + public static void DivideSize(GameObject obj, float amount) + { + foreach (var collider in obj.GetComponentsInChildren(true)) + { + if (collider.TryCast() is { } col) + { + col.size = new Vector2(col.size.x / amount, col.size.y); + } + } + + foreach (var rend in obj.GetComponentsInChildren(true)) + { + rend.size = new Vector2(rend.size.x / amount, rend.size.y); + } + } + /// /// Gets the keybind for an action with ReInput. /// From 04d5e72caa3d13d33df02dcb299ca6cb7aac64d8 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sun, 31 Aug 2025 18:45:22 -0400 Subject: [PATCH 23/86] Add slider float value --- MiraAPI.Example/ExampleLocalSetings.cs | 12 ++ .../Attributes/LocalSettingFloatAttribute.cs | 28 ++++ MiraAPI/LocalSettings/LocalSettingsTab.cs | 9 +- .../SettingTypes/LocalSettingFloat.cs | 127 ++++++++++++++++++ .../SettingTypes/LocalSettingToggle.cs | 5 +- 5 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 MiraAPI/LocalSettings/Attributes/LocalSettingFloatAttribute.cs create mode 100644 MiraAPI/LocalSettings/SettingTypes/LocalSettingFloat.cs diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index fb07869..fcbc455 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -1,6 +1,7 @@ using BepInEx.Configuration; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; +using MiraAPI.Utilities; using Reactor.Utilities; namespace MiraAPI.Example; @@ -9,6 +10,9 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Mira Example"; + [LocalSettingFloat(min: 0, max: 15, suffixType: MiraNumberSuffixes.Multiplier, roundValue: false)] + public ConfigEntry ExampleFloatEntry { get; private set; } = config.Bind("General", "Example Float Setting", 50f); + [LocalSettingToggle] public ConfigEntry PlayStartupSound { get; private set; } = config.Bind("Audio", "Play Startup Sound", true); @@ -38,4 +42,12 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) [LocalSettingToggle] public ConfigEntry EnableHardMode { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); + [LocalSettingToggle] + public ConfigEntry EnableHardMode2 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); + [LocalSettingToggle] + public ConfigEntry EnableHardMode3 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); + [LocalSettingToggle] + public ConfigEntry EnableHardMode4 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); + [LocalSettingToggle] + public ConfigEntry EnableHardMode5 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingFloatAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingFloatAttribute.cs new file mode 100644 index 0000000..1414ad8 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingFloatAttribute.cs @@ -0,0 +1,28 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates a float and binds it with the config entry. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalSettingFloatAttribute( + string? name = null, + float min = 0, + float max = 100, + string? description = null, + string? formatString = null, + bool roundValue = true, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None + ) : LocalSettingAttribute(name, description) +{ + /// + internal override LocalSettingFloat CreateSetting(Type tab, ConfigEntryBase configEntryBase) + { + return new LocalSettingFloat(tab, configEntryBase, name, description, new FloatRange(min, max), suffixType, formatString, roundValue); + } +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index a7e2ea7..f6bb88c 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -112,12 +112,19 @@ public abstract class LocalSettingsTab(ConfigFile config) ref contentOrder, contentIndex == pair.Value.Count); - obj!.GetComponentsInChildren().Do(x => + obj!.GetComponentsInChildren(true).Do(x => { x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; x.sortingOrder = 150; x.sortingLayerName = "Default"; }); + + obj!.GetComponentsInChildren(true).Do(x => + { + x.sortingOrder = 151; + x.sortingLayerName = "Default"; + }); + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); contentIndex++; diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingFloat.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingFloat.cs new file mode 100644 index 0000000..ca5781a --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingFloat.cs @@ -0,0 +1,127 @@ +using System; +using System.Globalization; +using BepInEx.Configuration; +using MiraAPI.Utilities; +using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings.SettingTypes; + +/// +/// Local setting class for toggles. +/// +public class LocalSettingFloat : LocalSettingBase +{ + /// + /// Gets the color of the slider. + /// + public Color SliderColor { get; } + + /// + /// Gets the range of the slider. + /// + public FloatRange SliderRange { get; } + + /// + /// Gets a format for the text to use to format the number. + /// + public string FormatString { get; } + + /// + /// Gets a value indicating whether the value should be rounded. + /// + public bool RoundValue { get; } + + /// + /// Gets the suffix for the number value. + /// + public MiraNumberSuffixes SuffixType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// /// Tab that the toggle belongs to. + /// The binded config entry. + /// The name of the setting. Defaults to ConfigEntry key. + /// The desription of the setting. No description by default. + /// Gets the suffix for the number value. + /// Gets a format for the text to use to format the number. + /// Gets the color of the slider. + /// Gets the range of the slider. + /// Gets whether the value should be rounded. + public LocalSettingFloat( + Type tab, + ConfigEntryBase configEntry, + string? name = null, + string? description = null, + FloatRange? sliderRange = null, + MiraNumberSuffixes? suffixType = null, + string? formatString = null, + bool roundValue = true) + : base(tab, configEntry, name, description) + { + SliderColor = Palette.AcceptedGreen; + SuffixType = suffixType ?? MiraNumberSuffixes.None; + SliderRange = sliderRange ?? new FloatRange(0, 100); + RoundValue = roundValue; + FormatString = formatString ?? "0.0"; + } + + /// + public override GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + { + var newSlider = Object.Instantiate(slider, parent).GetComponent(); + var rollover = newSlider.GetComponent(); + newSlider.Title = newSlider.transform.FindChild("Text_TMP").GetComponent(); // Why the hell slider has a title property that is not even assigned??? + newSlider.Title.GetComponent().Destroy(); + newSlider.gameObject.SetActive(true); + + if (order == 2) + offset += 0.5f; + + newSlider.Bar.transform.localPosition = new Vector3(2.85f, 0, 0); + newSlider.transform.localPosition = new Vector3(-2.12f, 1.85f - offset, -7); + newSlider.name = Name; + newSlider.Range = new FloatRange(-1.5f, 1.5f); + newSlider.SetValue(Mathf.InverseLerp(SliderRange.min, SliderRange.max, GetValue())); + rollover.OverColor = SliderColor; + newSlider.Title.transform.localPosition = new Vector3(0.5f, 0, -1f); + newSlider.Title.horizontalAlignment = HorizontalAlignmentOptions.Right; + newSlider.Title.text = GetValueText(); + + newSlider.OnValueChange.AddListener((UnityAction)(() => + { + SetValue(RoundValue + ? Mathf.Round(Mathf.Lerp(SliderRange.min, SliderRange.max, newSlider.Value)) + : Mathf.Lerp(SliderRange.min, SliderRange.max, newSlider.Value)); + + newSlider.Title.text = GetValueText(); + })); + + order = 1; + offset += 0.5f; + return newSlider.gameObject; + } + + private string FormatValue(float value) + { + return SuffixType switch + { + MiraNumberSuffixes.None => value.ToString(FormatString, NumberFormatInfo.InvariantInfo), + MiraNumberSuffixes.Multiplier => value.ToString(FormatString, NumberFormatInfo.InvariantInfo) + "x", + MiraNumberSuffixes.Percent => value.ToString(FormatString, NumberFormatInfo.InvariantInfo) + "%", + _ => TranslationController.Instance.GetString( + StringNames.GameSecondsAbbrev, + (Il2CppSystem.Object[])[value.ToString(FormatString, CultureInfo.InvariantCulture)]), + }; + } + private string GetValueText() + { + var value = GetValue(); + return $"{Name}: {FormatValue(value)} / {FormatValue(SliderRange.max)}"; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs index fa36333..0624345 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs @@ -54,6 +54,7 @@ public class LocalSettingToggle : LocalSettingBase ToggleActiveColor = toggleActiveColor ?? Palette.AcceptedGreen; } + /// public override GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) { var toggleObject = Object.Instantiate(toggle, parent).GetComponent(); @@ -65,11 +66,11 @@ public class LocalSettingToggle : LocalSettingBase if (last && order == 1) { // Toggle in the middle - toggleObject.transform.localPosition = new Vector3(0, 1.85f - offset); + toggleObject.transform.localPosition = new Vector3(0, 1.85f - offset, -7); } else { - toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset); + toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); } toggleObject.BaseText = CustomStringName.CreateAndRegister(Name); From 57bab258e5b7c9a6c68913a47d8b30ada8883b51 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 1 Sep 2025 21:54:13 +0200 Subject: [PATCH 24/86] enum and int options and lots of other stuff i forgot --- MiraAPI.Example/ExampleLocalSetings.cs | 36 ++++- .../Attributes/LocalEnumSettingAttribute.cs | 24 +++ .../Attributes/LocalNumberSettingAttribute.cs | 33 ++++ .../Attributes/LocalSettingAttribute.cs | 5 +- .../Attributes/LocalSettingFloatAttribute.cs | 28 ---- .../Attributes/LocalSettingToggleAttribute.cs | 8 +- .../Attributes/LocalSliderSettingAttribute.cs | 32 ++++ MiraAPI/LocalSettings/ILocalSetting.cs | 4 +- MiraAPI/LocalSettings/LocalSettingsTab.cs | 68 +++------ .../LocalSettingsTabAppearance.cs | 81 ++++++++++ .../LocalSettingsTabSingleton.cs | 3 +- .../SettingTypes/LocalEnumSetting.cs | 126 +++++++++++++++ .../SettingTypes/LocalNumberSetting.cs | 144 ++++++++++++++++++ .../SettingTypes/LocalSettingBase.cs | 18 ++- ...lSettingFloat.cs => LocalSliderSetting.cs} | 57 +++---- ...SettingToggle.cs => LocalToggleSetting.cs} | 49 ++---- MiraAPI/MiraApiSettings.cs | 6 +- MiraAPI/Modifiers/ModifierComponent.cs | 2 +- .../ModifierDisplayComponent.cs | 7 +- .../LocalSettings/OptionsMenuPatches.cs | 132 ++++++++++++++-- .../LocalSettings/ToggleBehaviourPatch.cs | 8 +- MiraAPI/Utilities/Helpers.cs | 20 +++ 22 files changed, 708 insertions(+), 183 deletions(-) create mode 100644 MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs create mode 100644 MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs delete mode 100644 MiraAPI/LocalSettings/Attributes/LocalSettingFloatAttribute.cs create mode 100644 MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs create mode 100644 MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs create mode 100644 MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs create mode 100644 MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs rename MiraAPI/LocalSettings/SettingTypes/{LocalSettingFloat.cs => LocalSliderSetting.cs} (57%) rename MiraAPI/LocalSettings/SettingTypes/{LocalSettingToggle.cs => LocalToggleSetting.cs} (53%) diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index fcbc455..da7e4f5 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -3,14 +3,22 @@ using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Utilities; using Reactor.Utilities; +using UnityEngine; namespace MiraAPI.Example; public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Mira Example"; + public override LocalSettingTabAppearance TabAppearance => new() + { + TabColor = Color.cyan, + ToggleActiveColor = Color.blue, + ToggleInactiveColor = Color.red, + SliderHoverColor = Color.magenta, + }; - [LocalSettingFloat(min: 0, max: 15, suffixType: MiraNumberSuffixes.Multiplier, roundValue: false)] + [LocalSliderSetting(min: 0, max: 15, suffixType: MiraNumberSuffixes.Multiplier)] public ConfigEntry ExampleFloatEntry { get; private set; } = config.Bind("General", "Example Float Setting", 50f); [LocalSettingToggle] @@ -50,4 +58,30 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) public ConfigEntry EnableHardMode4 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); [LocalSettingToggle] public ConfigEntry EnableHardMode5 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); + + [LocalNumberSetting] + public ConfigEntry NumberExample { get; private set; } = config.Bind("Gameplay", "Number of cheeseburgers", 3); + + [LocalEnumSetting] + public ConfigEntry EnumExample { get; private set; } = config.Bind("Gameplay", "Anything else", ExampleSettingEnum.Fries); } + +public enum ExampleSettingEnum +{ + Fries, + OnionRings, + ChickenNuggets, + Gun, +} + +public class Tab1(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab2(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab4(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab3(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab5(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab6(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab7(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab8(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab9(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab10(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +public class Tab11(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs new file mode 100644 index 0000000..a4a22e2 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs @@ -0,0 +1,24 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates an enum setting for the . +/// +/// Optional custom enum names. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalEnumSettingAttribute( + string? name = null, + string? description = null, + string[]? names = null + ) : LocalSettingAttribute(name, description) +{ + /// + internal override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + { + return new LocalEnumSetting(tab, configEntryBase, configEntryBase.SettingType, name, description, names); + } +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs new file mode 100644 index 0000000..20a55f3 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs @@ -0,0 +1,33 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates a number setting for the . +/// +/// Minimum range. +/// Maximum range. +/// Increment per use. +/// Suffix for the value. +/// Format string used when formating. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalNumberSettingAttribute( + string? name = null, + string? description = null, + int min = 1, + int max = 5, + int increment = 1, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null + ) : LocalSettingAttribute(name, description) +{ + /// + internal override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + { + return new LocalNumberSetting(tab, configEntryBase, name, description, new IntRange(min, max), increment, suffixType, formatString); + } +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs index 1b101c5..86dfbc9 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -6,10 +6,13 @@ namespace MiraAPI.LocalSettings.Attributes; /// /// Base class for all local settings attributes. /// +/// The name of the setting. Defaults to entry key. +/// The description of the setting. Defalts to entry description. [AttributeUsage(AttributeTargets.Property)] public abstract class LocalSettingAttribute( string? name = null, - string? description = null) : Attribute + string? description = null + ) : Attribute { /// /// Returns the created setting object. diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingFloatAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingFloatAttribute.cs deleted file mode 100644 index 1414ad8..0000000 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingFloatAttribute.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using BepInEx.Configuration; -using MiraAPI.LocalSettings.SettingTypes; -using MiraAPI.Utilities; -using UnityEngine; - -namespace MiraAPI.LocalSettings.Attributes; - -/// -/// Creates a float and binds it with the config entry. -/// -[AttributeUsage(AttributeTargets.Property)] -public class LocalSettingFloatAttribute( - string? name = null, - float min = 0, - float max = 100, - string? description = null, - string? formatString = null, - bool roundValue = true, - MiraNumberSuffixes suffixType = MiraNumberSuffixes.None - ) : LocalSettingAttribute(name, description) -{ - /// - internal override LocalSettingFloat CreateSetting(Type tab, ConfigEntryBase configEntryBase) - { - return new LocalSettingFloat(tab, configEntryBase, name, description, new FloatRange(min, max), suffixType, formatString, roundValue); - } -} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs index 7b44f7f..2add91d 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs @@ -1,13 +1,13 @@ using System; using BepInEx.Configuration; using MiraAPI.LocalSettings.SettingTypes; -using UnityEngine; namespace MiraAPI.LocalSettings.Attributes; /// -/// Creates a toggle and binds it with the config entry. +/// Creates a toggle setting for the . /// +/// [AttributeUsage(AttributeTargets.Property)] public class LocalSettingToggleAttribute( string? name = null, @@ -15,8 +15,8 @@ public class LocalSettingToggleAttribute( ) : LocalSettingAttribute(name, description) { /// - internal override LocalSettingToggle CreateSetting(Type tab, ConfigEntryBase configEntryBase) + internal override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalSettingToggle(tab, configEntryBase, name, description); + return new LocalToggleSetting(tab, configEntryBase, name, description); } } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs new file mode 100644 index 0000000..53555fa --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs @@ -0,0 +1,32 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates a slider setting for the . +/// +/// +/// Minimum range. +/// Maximum range. +/// Should the value be rounded. +/// Suffix for the value. +[AttributeUsage(AttributeTargets.Property)] +public class LocalSliderSettingAttribute( + string? name = null, + float min = 0, + float max = 100, + string? description = null, + string? formatString = null, + bool roundValue = false, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None + ) : LocalSettingAttribute(name, description) +{ + /// + internal override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + { + return new LocalSliderSetting(tab, configEntryBase, name, description, new FloatRange(min, max), suffixType, formatString, roundValue); + } +} diff --git a/MiraAPI/LocalSettings/ILocalSetting.cs b/MiraAPI/LocalSettings/ILocalSetting.cs index e6649bb..a94e102 100644 --- a/MiraAPI/LocalSettings/ILocalSetting.cs +++ b/MiraAPI/LocalSettings/ILocalSetting.cs @@ -4,7 +4,7 @@ using UnityEngine; namespace MiraAPI.LocalSettings; /// -/// Interface for all local settings +/// Interface for all local settings. /// public interface ILocalSetting { @@ -29,7 +29,7 @@ public interface ILocalSetting ConfigEntryBase ConfigEntry { get; } /// - /// Used to create the setting + /// Used to create the setting. /// /// Toggle template. /// Slider template. diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index f6bb88c..5516d05 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -4,7 +4,6 @@ using BepInEx.Configuration; using HarmonyLib; using MiraAPI.Patches.LocalSettings; using MiraAPI.Utilities; -using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -14,39 +13,29 @@ using Object = UnityEngine.Object; namespace MiraAPI.LocalSettings; /// -/// Represents a local settings tab. Gets a button by default +/// Represents a local settings tab. Gets a button by default. /// public abstract class LocalSettingsTab(ConfigFile config) { /// - /// Gets the name of the tab + /// Gets the name of the tab. /// public abstract string TabName { get; } /// - /// Gets the tab button icon. + /// Gets the tab appearance. /// - public virtual LoadableAsset? TabIcon => null; - - /// - /// Gets the color of the tab button. - /// - public virtual Color TabColor => Color.white; - - /// - /// Gets the color of the tab button when hovered. - /// - public Color TabHoverColor => Palette.AcceptedGreen; - - /// - /// Gets the color of the tab button when active. - /// - public virtual Color TabActiveColor => Palette.AcceptedGreen; + public virtual LocalSettingTabAppearance TabAppearance { get; } = new(); /// /// Gets a value indicating whether a button should be created for the tab. /// - protected virtual bool ShouldCreateButton => true; + public virtual bool ShouldCreateButton => true; + + /// + /// Gets a value indicating whether the tab should have category labels. + /// + protected virtual bool ShouldCreateLabels => true; /// /// Gets the mod's config file. Automatically found in the plugin loader. @@ -57,13 +46,8 @@ public abstract class LocalSettingsTab(ConfigFile config) internal List Settings { get; } = []; private Scroller? Scroller { get; set; } - internal GameObject? CreateTab(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + internal GameObject CreateTab(OptionsMenuBehaviour instance) { - if (ShouldCreateButton) - { - CreateTabButton(instance, ref tabIdx, ref offset); - } - var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); tab.name = $"{TabName}Tab"; tab.transform.DestroyChildren(); @@ -100,7 +84,10 @@ public abstract class LocalSettingsTab(ConfigFile config) foreach (var pair in entriesByGroup) { - CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset); + if (ShouldCreateLabels) + { + CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset); + } foreach (var setting in pair.Value) { @@ -119,7 +106,7 @@ public abstract class LocalSettingsTab(ConfigFile config) x.sortingLayerName = "Default"; }); - obj!.GetComponentsInChildren(true).Do(x => + obj.GetComponentsInChildren(true).Do(x => { x.sortingOrder = 151; x.sortingLayerName = "Default"; @@ -137,13 +124,13 @@ public abstract class LocalSettingsTab(ConfigFile config) return tab; } - private void CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + internal GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) { var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); tabButtonObject.name = $"{TabName} Button"; tabButtonObject.transform.localPosition = new Vector3(2.4f, 2.1f - offset, 5.5f); tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1); - tabButtonObject.Button.color = TabColor; + tabButtonObject.Button.color = TabAppearance.TabButtonColor; TabButton = tabButtonObject; var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent(); @@ -155,18 +142,18 @@ public abstract class LocalSettingsTab(ConfigFile config) var tabButton = tabButtonObject.GetComponent(); var rollover = tabButtonObject.Rollover; - rollover.OverColor = TabColor; - rollover.OutColor = TabColor; + rollover.OverColor = TabAppearance.TabButtonHoverColor; + rollover.OutColor = TabAppearance.TabButtonColor; SpriteRenderer? tabButtonRend = null; - if (TabIcon != null) + if (TabAppearance.TabIcon != null) { tabButtonRend = new GameObject("sprite").AddComponent(); tabButtonRend.gameObject.layer = 5; // ui layer tabButtonRend.transform.SetParent(tabButtonObject.transform); tabButtonRend.transform.localPosition = new Vector3(0.5f, 0, -2); tabButtonRend.transform.localScale = new Vector3(0.2f, 0.2f, 1); - tabButtonRend.sprite = TabIcon.LoadAsset(); + tabButtonRend.sprite = TabAppearance.TabIcon.LoadAsset(); tabButtonText.gameObject.SetActive(false); } @@ -176,8 +163,6 @@ public abstract class LocalSettingsTab(ConfigFile config) tabButton.OnMouseOver.AddListener((UnityAction)(() => { tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f); - tabButtonObject.Button.color = TabHoverColor; - tabButtonObject.Rollover.OutColor = TabHoverColor; tabButtonText.gameObject.SetActive(true); tabButtonText.transform.localPosition = new Vector3(-0.024f, 0, 0); tabButtonText.maxVisibleCharacters = int.MaxValue; @@ -188,8 +173,8 @@ public abstract class LocalSettingsTab(ConfigFile config) { if (!tabButtonObject.Content.gameObject.activeSelf) { - tabButtonObject.Button.color = TabColor; - tabButtonObject.Rollover.OutColor = TabColor; + //tabButtonObject.Button.color = TabColor; + //tabButtonObject.Rollover.OutColor = TabColor; } tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f); @@ -205,8 +190,7 @@ public abstract class LocalSettingsTab(ConfigFile config) tabButtonText.text = $"{GetShortName(TabName)}"; })); - offset += 0.6f; - tabIdx++; + return tabButtonObject.gameObject; } private static void CreateLabel(GameObject template, Transform parent, string text, ref float offset) @@ -227,7 +211,6 @@ public abstract class LocalSettingsTab(ConfigFile config) offset += 0.5f; } - private static string GetShortName(string name) { var shortName = string.Empty; @@ -235,4 +218,3 @@ public abstract class LocalSettingsTab(ConfigFile config) return shortName; } } - diff --git a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs new file mode 100644 index 0000000..2cad514 --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs @@ -0,0 +1,81 @@ +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.LocalSettings; + +/// +/// Struct for modyfing tab apparance. +/// Defines how the settings look. +/// +public record struct LocalSettingTabAppearance() +{ + /// + /// Gets or sets main color of the tab. + /// + public Color TabColor { get; set; } = Color.white; + + /// + /// Gets or sets tab button icon. No icon by default. + /// + public LoadableAsset? TabIcon { get; set; } + + /// + /// Gets or sets color of the tab button. + /// + public Color TabButtonColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of the tab button. + /// + public Color TabButtonHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets active color of the tab button. + /// + public Color TabButtonActiveColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets hover color of toggles. + /// + public Color ToggleHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets active color of toggles. + /// + public Color ToggleActiveColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets inactive color of toggles. + /// + public Color ToggleInactiveColor { get; set; } = Color.red; + + /// + /// Gets or sets color of number buttons. + /// + public Color NumberColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of number buttons. + /// + public Color NumberHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets color of sliders. + /// + public Color SliderColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of sliders. + /// + public Color SliderHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets color of enum buttons. + /// + public Color EnumColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of enum buttons. + /// + public Color EnumHoverColor { get; set; } = Palette.AcceptedGreen; +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs index 1994235..eec81a2 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs @@ -1,7 +1,6 @@ using System.Linq; -using MiraAPI.LocalSettings; -namespace MiraAPI.GameOptions; +namespace MiraAPI.LocalSettings; /// /// Singleton for local settings tabs. diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs new file mode 100644 index 0000000..c435a58 --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs @@ -0,0 +1,126 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings.SettingTypes; + +/// +/// Local setting class for enums. +/// +public class LocalEnumSetting : LocalSettingBase +{ + /// + /// Gets the enum type of the setting. + /// + public Type EnumType { get; } + + /// + /// Gets the enum values. + /// + public string[] Values { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The enum type. + /// The optional values array to replace the enum names. + public LocalEnumSetting( + Type tab, + ConfigEntryBase configEntry, + Type enumType, + string? name = null, + string? description = null, + string[]? values = null) + : base(tab, configEntry, name, description) + { + EnumType = enumType; + Values = values ?? Enum.GetNames(EnumType); + } + + /// + public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + { + var button = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + var rollover = button.GetComponent(); + tmp.GetComponent().Destroy(); + button.gameObject.SetActive(true); + + var toggleComp = button.GetComponent(); + var background = toggleComp.Background; + var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); + if (highlight != null) + { + highlight.color = Tab!.TabAppearance.EnumHoverColor; + } + toggleComp.Destroy(); + + if (last && order == 1) + { + // Button in the middle + button.transform.localPosition = new Vector3(0, 1.85f - offset, -7); + } + else + { + button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); + } + + tmp.text = GetValueText(); + button.name = Name; + button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OutColor = Tab!.TabAppearance.EnumColor; + rollover.OverColor = Tab!.TabAppearance.EnumHoverColor; + background.color = Tab!.TabAppearance.EnumColor; + + button.OnClick.AddListener((UnityAction)(() => + { + int value = GetValue(); + value++; + if (value >= Values.Length) + { + value = 0; + } + + SetValue(value); + tmp.text = GetValueText(); + })); + button.OnMouseOver.AddListener((UnityAction)(() => + { + if (!Description.IsNullOrWhiteSpace()) + { + tmp.text = Description; + } + highlight?.gameObject.SetActive(true); + })); + button.OnMouseOut.AddListener((UnityAction)(() => + { + tmp.text = GetValueText(); + highlight?.gameObject.SetActive(false); + })); + + Helpers.DivideSize(button.gameObject, 1.1f); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return button.gameObject; + } + + /// + protected override string GetValueText() + { + return $"{Name}: {Values[GetValue()]}"; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs new file mode 100644 index 0000000..1473889 --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs @@ -0,0 +1,144 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings.SettingTypes; + +/// +/// Local setting class for numbers. +/// +public class LocalNumberSetting : LocalSettingBase +{ + /// + /// Gets the range of the button. + /// + public IntRange NumberRange { get; } + + /// + /// Gets the increment of the value when button is pressed. + /// + public int Increment { get; } + + /// + /// Gets a format for the text to use to format the number. + /// + public string FormatString { get; } + + /// + /// Gets the suffix for the number value. + /// + public MiraNumberSuffixes SuffixType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The value range. + /// The increment per click. + /// The suffix used for formating. + /// The format string used for formating. + public LocalNumberSetting( + Type tab, + ConfigEntryBase configEntry, + string? name = null, + string? description = null, + IntRange? numberRange = null, + int? increment = null, + MiraNumberSuffixes? suffixType = null, + string? formatString = null) + : base(tab, configEntry, name, description) + { + SuffixType = suffixType ?? MiraNumberSuffixes.None; + NumberRange = numberRange ?? new IntRange(1, 5); + Increment = increment ?? 1; + FormatString = formatString ?? "0"; + } + + /// + public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + { + var button = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + var rollover = button.GetComponent(); + tmp.GetComponent().Destroy(); + button.gameObject.SetActive(true); + + var toggleComp = button.GetComponent(); + var background = toggleComp.Background; + var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); + if (highlight != null) + { + highlight.color = Tab!.TabAppearance.NumberHoverColor; + } + toggleComp.Destroy(); + + if (last && order == 1) + { + // Button in the middle + button.transform.localPosition = new Vector3(0, 1.85f - offset, -7); + } + else + { + button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); + } + + tmp.text = GetValueText(); + button.name = Name; + button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OutColor = Tab!.TabAppearance.NumberColor; + rollover.OverColor = Tab!.TabAppearance.NumberHoverColor; + background.color = Tab!.TabAppearance.NumberColor; + + button.OnClick.AddListener((UnityAction)(() => + { + int value = GetValue(); + value += Increment; + if (value > NumberRange.max) + { + value = NumberRange.min; + } + + SetValue(value); + tmp.text = GetValueText(); + })); + button.OnMouseOver.AddListener((UnityAction)(() => + { + if (!Description.IsNullOrWhiteSpace()) + { + tmp.text = Description; + } + highlight?.gameObject.SetActive(true); + })); + button.OnMouseOut.AddListener((UnityAction)(() => + { + tmp.text = GetValueText(); + highlight?.gameObject.SetActive(false); + })); + + Helpers.DivideSize(button.gameObject, 1.1f); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return button.gameObject; + } + + /// + protected override string GetValueText() + { + var value = GetValue(); + var formated = Helpers.FormatValue(value, SuffixType, FormatString); + return $"{Name}: {formated}"; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index e1ebe10..934b33e 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -1,6 +1,6 @@ using System; +using System.Linq; using BepInEx.Configuration; -using TMPro; using UnityEngine; namespace MiraAPI.LocalSettings.SettingTypes; @@ -20,6 +20,8 @@ public abstract class LocalSettingBase : ILocalSetting /// public ConfigEntryBase ConfigEntry { get; } + internal LocalSettingsTab? Tab => LocalSettingsManager.Tabs.FirstOrDefault(x => x.Settings.Contains(this)); + /// /// Initializes a new instance of the class. /// @@ -32,7 +34,6 @@ public abstract class LocalSettingBase : ILocalSetting ConfigEntry = configEntry; Name = name ?? ConfigEntry.Definition.Key; Description = description ?? ConfigEntry.Description.Description; - LocalSettingsManager.TypeToTab[tab].Settings.Add(this); } @@ -40,7 +41,16 @@ public abstract class LocalSettingBase : ILocalSetting /// Creates an instance of the setting. /// /// The created setting. - public abstract GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); + public abstract GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); + + /// + /// Returns the formated string to use in the text of the setting. + /// + /// The value text. + protected virtual string GetValueText() + { + return GetValue()?.ToString() ?? string.Empty; + } /// /// Gets the value of the config entry, casted to the setting type. @@ -52,7 +62,7 @@ public abstract class LocalSettingBase : ILocalSetting } /// - /// Sets the value of the config entry + /// Sets the value of the config entry. /// /// The value to set to. public void SetValue(T value) diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingFloat.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs similarity index 57% rename from MiraAPI/LocalSettings/SettingTypes/LocalSettingFloat.cs rename to MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs index ca5781a..8535baa 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingFloat.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs @@ -1,8 +1,6 @@ using System; -using System.Globalization; using BepInEx.Configuration; using MiraAPI.Utilities; -using Reactor.Localization.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -12,15 +10,10 @@ using Object = UnityEngine.Object; namespace MiraAPI.LocalSettings.SettingTypes; /// -/// Local setting class for toggles. +/// Local setting class for sliders. /// -public class LocalSettingFloat : LocalSettingBase +public class LocalSliderSetting : LocalSettingBase { - /// - /// Gets the color of the slider. - /// - public Color SliderColor { get; } - /// /// Gets the range of the slider. /// @@ -42,18 +35,14 @@ public class LocalSettingFloat : LocalSettingBase public MiraNumberSuffixes SuffixType { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// /// Tab that the toggle belongs to. - /// The binded config entry. - /// The name of the setting. Defaults to ConfigEntry key. - /// The desription of the setting. No description by default. - /// Gets the suffix for the number value. - /// Gets a format for the text to use to format the number. - /// Gets the color of the slider. - /// Gets the range of the slider. - /// Gets whether the value should be rounded. - public LocalSettingFloat( + /// + /// The value range. + /// The suffix used for formating. + /// The format string used for formating. + /// Should the value be rounded. + public LocalSliderSetting( Type tab, ConfigEntryBase configEntry, string? name = null, @@ -61,10 +50,9 @@ public class LocalSettingFloat : LocalSettingBase FloatRange? sliderRange = null, MiraNumberSuffixes? suffixType = null, string? formatString = null, - bool roundValue = true) + bool roundValue = false) : base(tab, configEntry, name, description) { - SliderColor = Palette.AcceptedGreen; SuffixType = suffixType ?? MiraNumberSuffixes.None; SliderRange = sliderRange ?? new FloatRange(0, 100); RoundValue = roundValue; @@ -72,13 +60,14 @@ public class LocalSettingFloat : LocalSettingBase } /// - public override GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) { var newSlider = Object.Instantiate(slider, parent).GetComponent(); var rollover = newSlider.GetComponent(); newSlider.Title = newSlider.transform.FindChild("Text_TMP").GetComponent(); // Why the hell slider has a title property that is not even assigned??? newSlider.Title.GetComponent().Destroy(); newSlider.gameObject.SetActive(true); + newSlider.Bar.color = Tab!.TabAppearance.SliderColor; if (order == 2) offset += 0.5f; @@ -88,7 +77,8 @@ public class LocalSettingFloat : LocalSettingBase newSlider.name = Name; newSlider.Range = new FloatRange(-1.5f, 1.5f); newSlider.SetValue(Mathf.InverseLerp(SliderRange.min, SliderRange.max, GetValue())); - rollover.OverColor = SliderColor; + rollover.OutColor = Tab!.TabAppearance.SliderColor; + rollover.OverColor = Tab!.TabAppearance.SliderHoverColor; newSlider.Title.transform.localPosition = new Vector3(0.5f, 0, -1f); newSlider.Title.horizontalAlignment = HorizontalAlignmentOptions.Right; newSlider.Title.text = GetValueText(); @@ -107,21 +97,12 @@ public class LocalSettingFloat : LocalSettingBase return newSlider.gameObject; } - private string FormatValue(float value) - { - return SuffixType switch - { - MiraNumberSuffixes.None => value.ToString(FormatString, NumberFormatInfo.InvariantInfo), - MiraNumberSuffixes.Multiplier => value.ToString(FormatString, NumberFormatInfo.InvariantInfo) + "x", - MiraNumberSuffixes.Percent => value.ToString(FormatString, NumberFormatInfo.InvariantInfo) + "%", - _ => TranslationController.Instance.GetString( - StringNames.GameSecondsAbbrev, - (Il2CppSystem.Object[])[value.ToString(FormatString, CultureInfo.InvariantCulture)]), - }; - } - private string GetValueText() + /// + protected override string GetValueText() { var value = GetValue(); - return $"{Name}: {FormatValue(value)} / {FormatValue(SliderRange.max)}"; + var formated = Helpers.FormatValue(value, SuffixType, FormatString); + var maxFormated = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString); + return $"{Name}: {formated} / {maxFormated}"; } } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs similarity index 53% rename from MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs rename to MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs index 0624345..54cd8ee 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs @@ -12,50 +12,23 @@ namespace MiraAPI.LocalSettings.SettingTypes; /// /// Local setting class for toggles. /// -public class LocalSettingToggle : LocalSettingBase +public class LocalToggleSetting : LocalSettingBase { /// - /// Gets the color of the toggle. + /// Initializes a new instance of the class. /// - public Color ToggleColor { get; } - - /// - /// Gets the color of the toggle when hovered. - /// - public Color ToggleHoverColor { get; } - - /// - /// Gets the color of the toggle when enabled. - /// - public Color ToggleActiveColor { get; } - - /// - /// Initializes a new instance of the class. - /// - /// /// Tab that the toggle belongs to. - /// The binded config entry. - /// The name of the setting. Defaults to ConfigEntry key. - /// The desription of the setting. No description by default. - /// The color of the toggle. - /// The hover color of the toggle. - /// The active color of the toggle. - public LocalSettingToggle( + /// + public LocalToggleSetting( Type tab, ConfigEntryBase configEntry, string? name = null, - string? description = null, - Color? toggleColor = null, - Color? toggleHoverColor = null, - Color? toggleActiveColor = null) - : base(tab, configEntry, name, description) + string? description = null + ) : base(tab, configEntry, name, description) { - ToggleColor = toggleColor ?? Color.white; - ToggleHoverColor = toggleHoverColor ?? Palette.AcceptedGreen; - ToggleActiveColor = toggleActiveColor ?? Palette.AcceptedGreen; } /// - public override GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) { var toggleObject = Object.Instantiate(toggle, parent).GetComponent(); var tmp = toggleObject.transform.FindChild("Text_TMP").GetComponent(); @@ -76,15 +49,15 @@ public class LocalSettingToggle : LocalSettingBase toggleObject.BaseText = CustomStringName.CreateAndRegister(Name); toggleObject.UpdateText(GetValue()); toggleObject.name = Name; - toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; passiveButton.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); - rollover.OverColor = ToggleHoverColor; + rollover.OverColor = Tab!.TabAppearance.ToggleHoverColor; passiveButton.OnClick.AddListener((UnityAction)(() => { SetValue(!GetValue()); toggleObject.UpdateText(GetValue()); - toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; })); passiveButton.OnMouseOver.AddListener((UnityAction)(() => { @@ -96,7 +69,7 @@ public class LocalSettingToggle : LocalSettingBase passiveButton.OnMouseOut.AddListener((UnityAction)(() => { toggleObject.UpdateText(GetValue()); - toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; })); Helpers.DivideSize(toggleObject.gameObject, 1.1f); diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index e5fe303..4ff4d04 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -23,7 +23,11 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) public override string TabName => "Mira API"; /// - public override LoadableAsset? TabIcon => MiraAssets.SettingsIcon; + public override LocalSettingTabAppearance TabAppearance => new() + { + TabButtonHoverColor = MiraApiPlugin.MiraColor, + TabIcon = MiraAssets.SettingsIcon, + }; /// /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index f4003c9..ef812a5 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -4,7 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Il2CppInterop.Runtime.Attributes; -using MiraAPI.GameOptions; +using MiraAPI.LocalSettings; using MiraAPI.Modifiers.ModifierDisplay; using MiraAPI.Modifiers.Types; using MiraAPI.Patches.Roles; diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs index 0cbb8c3..2982f67 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.Attributes; -using MiraAPI.GameOptions; -using MiraAPI.Patches.Roles; +using MiraAPI.LocalSettings; using MiraAPI.Utilities.Assets; -using Reactor.Utilities; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using TMPro; diff --git a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs index 0b4ad9c..3c73e1a 100644 --- a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -2,11 +2,12 @@ using System.Linq; using HarmonyLib; using MiraAPI.LocalSettings; -using Reactor.Localization.Utilities; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities; using Reactor.Utilities.Extensions; -using TMPro; using UnityEngine; using UnityEngine.Events; +using UnityEngine.UI; namespace MiraAPI.Patches.LocalSettings; @@ -14,6 +15,11 @@ namespace MiraAPI.Patches.LocalSettings; public static class OptionsMenuPatches { internal static BoxCollider2D MaskCollider; + private static SpriteRenderer? background; + + private static int currentPage = 1; + private static Dictionary> tabButtons = new(); + /// /// Creates the tabs and their content /// @@ -28,6 +34,8 @@ public static class OptionsMenuPatches __instance.transform.FindChild("Tint").SetLocalZ(5.6f); } + background = __instance.Background; + var maskObj = new GameObject { layer = 5, @@ -54,16 +62,101 @@ public static class OptionsMenuPatches MaskCollider.isTrigger = true; MaskCollider.enabled = true; + currentPage = 1; + tabButtons.Clear(); float yOffset = 0; - var i = 0; + int i = 0; + int tabIdx = 0; + int page = 1; foreach (var settings in LocalSettingsManager.Tabs) { - settings.CreateTab(__instance, ref i, ref yOffset); + var tab = settings.CreateTab(__instance); + if (settings.ShouldCreateButton) + { + var button = settings.CreateTabButton(__instance, ref tabIdx, ref yOffset); + button.GetComponent().Content = tab; + button.SetActive(page == currentPage); + + if (!tabButtons.TryGetValue(page, out var list)) + { + tabButtons.Add(page, new()); + list = tabButtons[page]; + } + list.Add(button); + yOffset += 0.6f; + tabIdx++; + i++; + } + if (i > 7) + { + i = 0; + yOffset = 0; + page++; + } } + + void ChangePage(int increase) + { + currentPage += increase; + if (currentPage > tabButtons.Count) + { + currentPage = 1; + } + if (currentPage < 1) + { + currentPage = tabButtons.Count; + } + } + + // Create page buttons + // TODO: In-game menu has no close button. I tried making one from scratch but failed. Add one later plz + if (tabButtons.Count <= 1) + { + return; + } + + var nextButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent(); + nextButton.transform.localPosition = new Vector3(3.2f, 2.6f, -10); + nextButton.transform.localScale = new Vector2(0.25f, 0.25f); + var nextRend = nextButton.GetComponent(); + nextRend.sprite = MiraAssets.NextButton.LoadAsset(); + nextButton.gameObject.GetComponent().DestroyImmediate(); + + var nextRollover = nextButton.gameObject.AddComponent(); + nextRollover.Target = nextRend; + nextRollover.OverColor = Palette.AcceptedGreen; + + nextButton.OnClick = new Button.ButtonClickedEvent(); + nextButton.OnClick.AddListener( + (UnityAction)(() => + { + ChangePage(1); + UpdatePages(); + })); + + var backButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent(); + backButton.transform.localPosition = new Vector3(2.9f, 2.6f, -10); + backButton.transform.localScale = new Vector2(0.25f, 0.25f); + var backRend = backButton.GetComponent(); + backRend.sprite = MiraAssets.NextButton.LoadAsset(); + backRend.flipX = true; + backButton.gameObject.GetComponent().DestroyImmediate(); + + var backRollover = backButton.gameObject.AddComponent(); + backRollover.Target = backRend; + backRollover.OverColor = Palette.AcceptedGreen; + + backButton.OnClick = new Button.ButtonClickedEvent(); + backButton.OnClick.AddListener( + (UnityAction)(() => + { + ChangePage(-1); + UpdatePages(); + })); } /// - /// Modifies how opening tabs is handled for the custom ones to work + /// Modifies how opening tabs is handled for the custom ones to work. /// [HarmonyPrefix] [HarmonyPatch(nameof(OptionsMenuBehaviour.OpenTabGroup))] @@ -72,7 +165,8 @@ public static class OptionsMenuPatches __instance.Tabs.ToList().ForEach(x => x.Close()); // Close all vanilla tabs LocalSettingsManager.Tabs.ForEach(CustomClose); // Close all mod tabs - if (index >= 10) // Tabs with index 10 and above are the mod settings tabs + // Tabs with index 10 and above are the mod settings tabs + if (index >= 10) { CustomOpen(LocalSettingsManager.Tabs[index - 10]); return false; @@ -82,35 +176,51 @@ public static class OptionsMenuPatches } /// - /// Makes the custom tabs close when opening the options menu + /// Makes the custom tabs close when opening the options menu and reset pages. /// [HarmonyPostfix] [HarmonyPatch(nameof(OptionsMenuBehaviour.Open))] public static void OpenPostfix(OptionsMenuBehaviour __instance) { LocalSettingsManager.Tabs.ForEach(CustomClose); + currentPage = 1; + UpdatePages(); + } + + private static void UpdatePages() + { + foreach (var pages in tabButtons) + { + pages.Value.Do(x => x.SetActive(pages.Key == currentPage)); + } } private static void CustomOpen(LocalSettingsTab tab) { if (tab.TabButton?.Button) - tab.TabButton!.Button.color = tab.TabHoverColor; + tab.TabButton!.Button.color = tab.TabAppearance.TabButtonHoverColor; if (tab.TabButton?.Rollover) - tab.TabButton!.Rollover.OutColor = tab.TabHoverColor; + tab.TabButton!.Rollover.OutColor = tab.TabAppearance.TabButtonHoverColor; if (tab.TabButton?.Content) tab.TabButton!.Content.SetActive(true); + + if (background != null) + background.color = tab.TabAppearance.TabColor; } private static void CustomClose(LocalSettingsTab tab) { if (tab.TabButton?.Button) - tab.TabButton!.Button.color = tab.TabColor; + tab.TabButton!.Button.color = tab.TabAppearance.TabButtonColor; if (tab.TabButton?.Rollover) - tab.TabButton!.Rollover.OutColor = tab.TabColor; + tab.TabButton!.Rollover.OutColor = tab.TabAppearance.TabButtonColor; if (tab.TabButton?.Content) tab.TabButton!.Content.SetActive(false); + + if (background != null) + background.color = Color.white; } } diff --git a/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs index 5022d1c..9a8321d 100644 --- a/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs +++ b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs @@ -7,14 +7,15 @@ namespace MiraAPI.Patches.LocalSettings; public static class ToggleBehaviourPatch { /// - /// ResetText was inlined ofc + /// ResetText was inlined ofc. + /// Skill issue /// [HarmonyPrefix] [HarmonyPatch(nameof(ToggleButtonBehaviour.UpdateText))] - public static bool ResetTextPrefix(ToggleButtonBehaviour __instance, bool on) + public static bool UpdateTextPrefix(ToggleButtonBehaviour __instance, bool on) { - __instance.onState = on; var color = on ? new Color(0f, 1f, 0.16470589f, 1f) : Color.white; + __instance.onState = on; __instance.Background.color = color; // Used to mask the text @@ -22,7 +23,6 @@ public static class ToggleBehaviourPatch __instance.Text.text = $"" + $"{DestroyableSingleton.Instance.GetString(__instance.BaseText)}: " + $"{DestroyableSingleton.Instance.GetString(__instance.onState ? StringNames.SettingsOn : StringNames.SettingsOff)}"; - __instance.Text.sortingOrder = 151; __instance.Text.renderer.sortingLayerName = "Default"; __instance.Text.renderer.sortingOrder = 151; diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 06ddc76..4717189 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -496,4 +496,24 @@ public static class Helpers } return KeyboardKeyCode.None; } + + /// + /// Returns the formated value using the specified suffix and format string. + /// + /// The value to format. + /// The suffix to add. + /// The format string to use to format. + /// The formated value. + public static string FormatValue(float value, MiraNumberSuffixes suffix = MiraNumberSuffixes.None, string formatString = "0.0") + { + return suffix switch + { + MiraNumberSuffixes.None => value.ToString(formatString, NumberFormatInfo.InvariantInfo), + MiraNumberSuffixes.Multiplier => value.ToString(formatString, NumberFormatInfo.InvariantInfo) + "x", + MiraNumberSuffixes.Percent => value.ToString(formatString, NumberFormatInfo.InvariantInfo) + "%", + _ => TranslationController.Instance.GetString( + StringNames.GameSecondsAbbrev, + (Il2CppSystem.Object[])[value.ToString(formatString, CultureInfo.InvariantCulture)]), + }; + } } From 8b2d24ccc103da68f2d2e3cf22ee33ce0bedd9d7 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 1 Sep 2025 22:22:32 +0200 Subject: [PATCH 25/86] more stuff --- MiraAPI.Example/ExampleLocalSetings.cs | 59 +++++-------------- .../Attributes/LocalSliderSettingAttribute.cs | 3 +- ...bute.cs => LocalToggleSettingAttribute.cs} | 2 +- .../SettingTypes/LocalEnumSetting.cs | 8 ++- .../SettingTypes/LocalNumberSetting.cs | 1 + .../SettingTypes/LocalSettingBase.cs | 2 +- .../SettingTypes/LocalSliderSetting.cs | 26 +++++--- MiraAPI/MiraApiSettings.cs | 2 +- 8 files changed, 46 insertions(+), 57 deletions(-) rename MiraAPI/LocalSettings/Attributes/{LocalSettingToggleAttribute.cs => LocalToggleSettingAttribute.cs} (93%) diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index da7e4f5..258a970 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -10,68 +10,37 @@ namespace MiraAPI.Example; public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Mira Example"; + protected override bool ShouldCreateLabels => false; + public override LocalSettingTabAppearance TabAppearance => new() { TabColor = Color.cyan, ToggleActiveColor = Color.blue, ToggleInactiveColor = Color.red, - SliderHoverColor = Color.magenta, }; - [LocalSliderSetting(min: 0, max: 15, suffixType: MiraNumberSuffixes.Multiplier)] - public ConfigEntry ExampleFloatEntry { get; private set; } = config.Bind("General", "Example Float Setting", 50f); + [LocalToggleSetting] + public ConfigEntry ExampleToggle { get; private set; } = config.Bind("General", "Example Bool", true); - [LocalSettingToggle] - public ConfigEntry PlayStartupSound { get; private set; } = config.Bind("Audio", "Play Startup Sound", true); + [LocalToggleSetting] + public ConfigEntry ExampleToggle2 { get; private set; } = config.Bind("General", "Example Bool 2", false); - [LocalSettingToggle] - public ConfigEntry MuteInBackground { get; private set; } = config.Bind("Audio", "Mute In Background", false); + [LocalSliderSetting(suffixType: MiraNumberSuffixes.Percent, formatString: "0", roundValue: true)] + public ConfigEntry ExampleSlider { get; private set; } = config.Bind("General", "Slider", 50f); - [LocalSettingToggle] - public ConfigEntry AutoSaveEnabled { get; private set; } = config.Bind("General", "Auto Save Enabled", true); - - [LocalSettingToggle] - public ConfigEntry EnableCloudSync { get; private set; } = config.Bind("General", "Enable Cloud Sync", false); - - [LocalSettingToggle] - public ConfigEntry ShowAdvancedTooltips { get; private set; } = config.Bind("UI", "Show Advanced Tooltips", true); - - [LocalSettingToggle] - public ConfigEntry CompactUI { get; private set; } = config.Bind("UI", "Compact UI Layout", false); - - [LocalSettingToggle] - public ConfigEntry EnableControllerSupport { get; private set; } = config.Bind("Input", "Enable Controller Support", true); - - [LocalSettingToggle] - public ConfigEntry EnableVibration { get; private set; } = config.Bind("Input", "Enable Controller Vibration", true); - - [LocalSettingToggle] - public ConfigEntry ShowPlayerNames { get; private set; } = config.Bind("Gameplay", "Show Player Names", true); - - [LocalSettingToggle] - public ConfigEntry EnableHardMode { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); - [LocalSettingToggle] - public ConfigEntry EnableHardMode2 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); - [LocalSettingToggle] - public ConfigEntry EnableHardMode3 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); - [LocalSettingToggle] - public ConfigEntry EnableHardMode4 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); - [LocalSettingToggle] - public ConfigEntry EnableHardMode5 { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); - - [LocalNumberSetting] - public ConfigEntry NumberExample { get; private set; } = config.Bind("Gameplay", "Number of cheeseburgers", 3); + [LocalNumberSetting(suffixType: MiraNumberSuffixes.Seconds)] + public ConfigEntry ExampleNumber { get; private set; } = config.Bind("General", "Example Number", 4); [LocalEnumSetting] - public ConfigEntry EnumExample { get; private set; } = config.Bind("Gameplay", "Anything else", ExampleSettingEnum.Fries); + public ConfigEntry ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries); } -public enum ExampleSettingEnum +public enum ExampleEnumSetting { + Cheeseburger, + Sandwich, Fries, - OnionRings, ChickenNuggets, - Gun, } public class Tab1(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs index 53555fa..4a211d5 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs @@ -19,6 +19,7 @@ public class LocalSliderSettingAttribute( float min = 0, float max = 100, string? description = null, + bool displayValue = false, string? formatString = null, bool roundValue = false, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None @@ -27,6 +28,6 @@ public class LocalSliderSettingAttribute( /// internal override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalSliderSetting(tab, configEntryBase, name, description, new FloatRange(min, max), suffixType, formatString, roundValue); + return new LocalSliderSetting(tab, configEntryBase, name, description, new FloatRange(min, max), displayValue, suffixType, formatString, roundValue); } } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs similarity index 93% rename from MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs rename to MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs index 2add91d..31c9b09 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs @@ -9,7 +9,7 @@ namespace MiraAPI.LocalSettings.Attributes; /// /// [AttributeUsage(AttributeTargets.Property)] -public class LocalSettingToggleAttribute( +public class LocalToggleSettingAttribute( string? name = null, string? description = null ) : LocalSettingAttribute(name, description) diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs index c435a58..437eed1 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using BepInEx.Configuration; using MiraAPI.Utilities; using Reactor.Utilities.Extensions; @@ -40,7 +41,11 @@ public class LocalEnumSetting : LocalSettingBase : base(tab, configEntry, name, description) { EnumType = enumType; - Values = values ?? Enum.GetNames(EnumType); + Values = values ?? Enum + .GetValues(configEntry.SettingType) + .Cast() + .Select(x => x.ToDisplayString()) + .ToArray(); } /// @@ -58,6 +63,7 @@ public class LocalEnumSetting : LocalSettingBase if (highlight != null) { highlight.color = Tab!.TabAppearance.EnumHoverColor; + highlight.gameObject.SetActive(false); } toggleComp.Destroy(); diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs index 1473889..70be2aa 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs @@ -74,6 +74,7 @@ public class LocalNumberSetting : LocalSettingBase if (highlight != null) { highlight.color = Tab!.TabAppearance.NumberHoverColor; + highlight.gameObject.SetActive(false); } toggleComp.Destroy(); diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index 934b33e..7d2bc5b 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -49,7 +49,7 @@ public abstract class LocalSettingBase : ILocalSetting /// The value text. protected virtual string GetValueText() { - return GetValue()?.ToString() ?? string.Empty; + return string.Empty; } /// diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs index 8535baa..ae6f500 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs @@ -19,6 +19,11 @@ public class LocalSliderSetting : LocalSettingBase /// public FloatRange SliderRange { get; } + /// + /// Gets a value indicating whether the value should be displayed next to name. + /// + public bool DisplayValue { get; } + /// /// Gets a format for the text to use to format the number. /// @@ -48,15 +53,17 @@ public class LocalSliderSetting : LocalSettingBase string? name = null, string? description = null, FloatRange? sliderRange = null, + bool displayValue = false, MiraNumberSuffixes? suffixType = null, string? formatString = null, bool roundValue = false) : base(tab, configEntry, name, description) { - SuffixType = suffixType ?? MiraNumberSuffixes.None; SliderRange = sliderRange ?? new FloatRange(0, 100); - RoundValue = roundValue; + DisplayValue = displayValue; + SuffixType = suffixType ?? MiraNumberSuffixes.None; FormatString = formatString ?? "0.0"; + RoundValue = roundValue; } /// @@ -80,7 +87,7 @@ public class LocalSliderSetting : LocalSettingBase rollover.OutColor = Tab!.TabAppearance.SliderColor; rollover.OverColor = Tab!.TabAppearance.SliderHoverColor; newSlider.Title.transform.localPosition = new Vector3(0.5f, 0, -1f); - newSlider.Title.horizontalAlignment = HorizontalAlignmentOptions.Right; + newSlider.Title.horizontalAlignment = DisplayValue ? HorizontalAlignmentOptions.Left : HorizontalAlignmentOptions.Center; newSlider.Title.text = GetValueText(); newSlider.OnValueChange.AddListener((UnityAction)(() => @@ -100,9 +107,14 @@ public class LocalSliderSetting : LocalSettingBase /// protected override string GetValueText() { - var value = GetValue(); - var formated = Helpers.FormatValue(value, SuffixType, FormatString); - var maxFormated = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString); - return $"{Name}: {formated} / {maxFormated}"; + if (DisplayValue) + { + var value = GetValue(); + var formated = Helpers.FormatValue(value, SuffixType, FormatString); + var maxFormated = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString); + return $"{Name}: {formated} / {maxFormated}"; + } + + return $"{Name}"; } } diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 4ff4d04..6a659f1 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -32,6 +32,6 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) /// /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. /// - [LocalSettingToggle] + [LocalToggleSetting] public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false); } From f515516937b8bc3c363767c38d472f8f2acbd4d6 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 1 Sep 2025 22:24:58 +0200 Subject: [PATCH 26/86] remove the two million test tabs --- MiraAPI.Example/ExampleLocalSetings.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index 258a970..a258967 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -2,7 +2,6 @@ using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Utilities; -using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Example; @@ -41,16 +40,4 @@ public enum ExampleEnumSetting Sandwich, Fries, ChickenNuggets, -} - -public class Tab1(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab2(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab4(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab3(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab5(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab6(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab7(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab8(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab9(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab10(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } -public class Tab11(ConfigFile config) : LocalSettingsTab(config) { public override string TabName => "Tab"; public override LocalSettingTabAppearance TabAppearance => new(); } +} \ No newline at end of file From 6b37b9c09c72646970bd4f6cf6bee765ddb1075b Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 1 Sep 2025 22:28:30 +0200 Subject: [PATCH 27/86] add an error --- MiraAPI/PluginLoading/MiraPluginManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 98b8db5..27d8e92 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -362,6 +362,7 @@ public sealed class MiraPluginManager if (property.GetValue(tabInstance) is not ConfigEntryBase configEntry) { + Logger.Error($"Option property {property.Name} in {type.Name} has to be a config entry."); continue; } From 1f6962efe9fa63e9e2ac2b45140050954377489d Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 1 Sep 2025 23:34:17 +0200 Subject: [PATCH 28/86] add local setting buttons --- MiraAPI.Example/ExampleLocalSetings.cs | 12 ++- .../LocalSettingsButtonAttribute.cs | 12 +++ MiraAPI/LocalSettings/LocalSettingsButton.cs | 80 +++++++++++++++++++ MiraAPI/LocalSettings/LocalSettingsTab.cs | 60 ++++++++++++-- .../LocalSettingsTabAppearance.cs | 10 +++ .../SettingTypes/LocalSettingBase.cs | 1 + MiraAPI/PluginLoading/MiraPluginManager.cs | 19 +++-- 7 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs create mode 100644 MiraAPI/LocalSettings/LocalSettingsButton.cs diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index a258967..21b68f0 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -2,6 +2,7 @@ using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Utilities; +using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Example; @@ -13,11 +14,13 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) public override LocalSettingTabAppearance TabAppearance => new() { - TabColor = Color.cyan, ToggleActiveColor = Color.blue, ToggleInactiveColor = Color.red, }; + [LocalSettingsButton] + public LocalSettingsButton ExampleButton { get; private set; } = new("Example Button", OnExampleButtonClick); + [LocalToggleSetting] public ConfigEntry ExampleToggle { get; private set; } = config.Bind("General", "Example Bool", true); @@ -32,6 +35,11 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) [LocalEnumSetting] public ConfigEntry ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries); + + private static void OnExampleButtonClick(LocalSettingsTab instance) + { + Logger.Warning("Button clicked"); + } } public enum ExampleEnumSetting @@ -40,4 +48,4 @@ public enum ExampleEnumSetting Sandwich, Fries, ChickenNuggets, -} \ No newline at end of file +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs new file mode 100644 index 0000000..043a81d --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs @@ -0,0 +1,12 @@ +using System; +using BepInEx.Configuration; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Attribute for local setting buttons. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalSettingsButtonAttribute : Attribute +{ +} diff --git a/MiraAPI/LocalSettings/LocalSettingsButton.cs b/MiraAPI/LocalSettings/LocalSettingsButton.cs new file mode 100644 index 0000000..2876dac --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsButton.cs @@ -0,0 +1,80 @@ +using System; +using MiraAPI.Utilities; +using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings; + +/// +/// Represents a local settings button. +/// +public class LocalSettingsButton(string text, Action onClick) +{ + /// + /// Gets or sets the button text. + /// + public string Text { get; set; } = text; + + /// + /// Gets or sets the button on-click action. + /// + public Action OnClick { get; set; } = onClick; + + /// + /// Gets the tab instance this button belongs to. + /// + public LocalSettingsTab Tab { get; internal set; } + + internal GameObject CreateButton(ToggleButtonBehaviour toggle, Transform parent, ref float offset, ref int order, bool last) + { + var button = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + var rollover = button.GetComponent(); + tmp.GetComponent().Destroy(); + button.gameObject.SetActive(true); + + var toggleComp = button.GetComponent(); + var background = toggleComp.Background; + button.transform.FindChild("ButtonHighlight")?.gameObject.DestroyImmediate(); + toggleComp.Destroy(); + + if (last && order == 1) + { + // Button in the middle + button.transform.localPosition = new Vector3(0, 1.85f - offset, -7); + } + else + { + button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); + } + + tmp.text = Text; + button.name = Text; + button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OutColor = Tab!.TabAppearance.ButtonColor; + rollover.OverColor = Tab!.TabAppearance.ButtonHoverColor; + rollover.Target = background; + background.color = Tab!.TabAppearance.ButtonColor; + + var tab = Tab; + var click = OnClick; + button.OnClick.AddListener((UnityAction)(() => click(tab))); + + Helpers.DivideSize(button.gameObject, 1.1f); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return button.gameObject; + } +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index 5516d05..7f30306 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -42,11 +42,32 @@ public abstract class LocalSettingsTab(ConfigFile config) /// public ConfigFile Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); - internal TabGroup? TabButton { get; set; } - internal List Settings { get; } = []; + /// + /// Gets the tab button instance. + /// + public TabGroup? TabButton { get; internal set; } + + /// + /// Gets the list of settings. + /// + public List Settings { get; } = []; + + /// + /// Gets the list of buttons. + /// + public List Buttons { get; } = []; + private Scroller? Scroller { get; set; } - internal GameObject CreateTab(OptionsMenuBehaviour instance) + /// + /// Invoked when an option value is changed. + /// + /// The config entry which was changed. + public virtual void OnOptionChanged(ConfigEntryBase configEntry) + { + } + + public virtual GameObject CreateTab(OptionsMenuBehaviour instance) { var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); tab.name = $"{TabName}Tab"; @@ -120,11 +141,40 @@ public abstract class LocalSettingsTab(ConfigFile config) contentOrder = 1; } - Scroller.SetBounds(new FloatRange(0, Settings.Count * 0.5f - 5f), new FloatRange(0, 0)); + contentIndex = 0; + contentOrder = 1; + foreach (var button in Buttons) + { + var obj = button.CreateButton( + toggle, + Scroller.Inner, + ref contentOffset, + ref contentOrder, + contentIndex == Buttons.Count); + + obj!.GetComponentsInChildren(true).Do(x => + { + x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + x.sortingOrder = 150; + x.sortingLayerName = "Default"; + }); + + obj.GetComponentsInChildren(true).Do(x => + { + x.sortingOrder = 151; + x.sortingLayerName = "Default"; + }); + + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); + + contentIndex++; + } + + Scroller.SetBounds(new FloatRange(0, (Settings.Count + Buttons.Count) * 0.5f - 5f), new FloatRange(0, 0)); return tab; } - internal GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + public virtual GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) { var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); tabButtonObject.name = $"{TabName} Button"; diff --git a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs index 2cad514..26fd726 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs @@ -78,4 +78,14 @@ public record struct LocalSettingTabAppearance() /// Gets or sets hover color of enum buttons. /// public Color EnumHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets color of buttons. + /// + public Color ButtonColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of buttons. + /// + public Color ButtonHoverColor { get; set; } = Palette.AcceptedGreen; } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index 7d2bc5b..f991376 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -68,5 +68,6 @@ public abstract class LocalSettingBase : ILocalSetting public void SetValue(T value) { ConfigEntry.BoxedValue = value; + Tab?.OnOptionChanged(ConfigEntry); } } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 27d8e92..0c987cd 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -344,6 +344,20 @@ public sealed class MiraPluginManager foreach (var property in type.GetProperties()) { + if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance) + { + continue; + } + + if (property.GetCustomAttribute() != null && + property.PropertyType.IsAssignableTo(typeof(LocalSettingsButton))) + { + var button = property.GetValue(tabInstance) as LocalSettingsButton; + button!.Tab = tabInstance; + tabInstance.Buttons.Add(button); + continue; + } + if (!typeof(ConfigEntryBase).IsAssignableFrom(property.PropertyType)) { continue; @@ -355,11 +369,6 @@ public sealed class MiraPluginManager continue; } - if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance) - { - continue; - } - if (property.GetValue(tabInstance) is not ConfigEntryBase configEntry) { Logger.Error($"Option property {property.Name} in {type.Name} has to be a config entry."); From ac113b6a7f13e148f9bb7388fcbf7e9e1d348159 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Tue, 2 Sep 2025 18:55:27 +0200 Subject: [PATCH 29/86] add tab open method and fixes --- MiraAPI.Example/ExampleLocalSetings.cs | 4 +-- MiraAPI/LocalSettings/LocalSettingsButton.cs | 12 ++++---- MiraAPI/LocalSettings/LocalSettingsTab.cs | 28 ++++++++++++++++++- .../LocalSettings/OptionsMenuPatches.cs | 10 +++++-- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index 21b68f0..e7c292f 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -36,9 +36,9 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) [LocalEnumSetting] public ConfigEntry ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries); - private static void OnExampleButtonClick(LocalSettingsTab instance) + private static void OnExampleButtonClick() { - Logger.Warning("Button clicked"); + LocalSettingsTabSingleton.Instance.Open(); } } diff --git a/MiraAPI/LocalSettings/LocalSettingsButton.cs b/MiraAPI/LocalSettings/LocalSettingsButton.cs index 2876dac..fa0c0bf 100644 --- a/MiraAPI/LocalSettings/LocalSettingsButton.cs +++ b/MiraAPI/LocalSettings/LocalSettingsButton.cs @@ -12,7 +12,7 @@ namespace MiraAPI.LocalSettings; /// /// Represents a local settings button. /// -public class LocalSettingsButton(string text, Action onClick) +public class LocalSettingsButton(string text, Action onClick) { /// /// Gets or sets the button text. @@ -22,12 +22,12 @@ public class LocalSettingsButton(string text, Action onClick) /// /// Gets or sets the button on-click action. /// - public Action OnClick { get; set; } = onClick; + public Action OnClick { get; set; } = onClick; /// /// Gets the tab instance this button belongs to. /// - public LocalSettingsTab Tab { get; internal set; } + public LocalSettingsTab? Tab { get; internal set; } internal GameObject CreateButton(ToggleButtonBehaviour toggle, Transform parent, ref float offset, ref int order, bool last) { @@ -59,11 +59,9 @@ public class LocalSettingsButton(string text, Action onClick) rollover.OverColor = Tab!.TabAppearance.ButtonHoverColor; rollover.Target = background; background.color = Tab!.TabAppearance.ButtonColor; + button.OnClick.AddListener((UnityAction)OnClick); - var tab = Tab; - var click = OnClick; - button.OnClick.AddListener((UnityAction)(() => click(tab))); - + background.transform.localScale = new Vector3(1, 1.25f, 1); Helpers.DivideSize(button.gameObject, 1.1f); order++; diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index 7f30306..a04830d 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -57,6 +57,11 @@ public abstract class LocalSettingsTab(ConfigFile config) /// public List Buttons { get; } = []; + /// + /// Gets the index of this tab. + /// + protected int TabIndex => LocalSettingsManager.Tabs.IndexOf(this) + 10; + private Scroller? Scroller { get; set; } /// @@ -67,6 +72,20 @@ public abstract class LocalSettingsTab(ConfigFile config) { } + /// + /// Attempts to open the tab. + /// Override for custom behavior. + /// + public virtual void Open() + { + OptionsMenuPatches.Instance?.OpenTabGroup(TabIndex); + } + + /// + /// Creates the tab and it's content. + /// + /// The instace. + /// The created tab . public virtual GameObject CreateTab(OptionsMenuBehaviour instance) { var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); @@ -141,7 +160,7 @@ public abstract class LocalSettingsTab(ConfigFile config) contentOrder = 1; } - contentIndex = 0; + contentIndex = 1; contentOrder = 1; foreach (var button in Buttons) { @@ -174,6 +193,13 @@ public abstract class LocalSettingsTab(ConfigFile config) return tab; } + /// + /// Creates the tab button. + /// + /// The instance. + /// The tab index. + /// The current button offset. + /// The created button . public virtual GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) { var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); diff --git a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs index 3c73e1a..574070f 100644 --- a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -14,6 +14,7 @@ namespace MiraAPI.Patches.LocalSettings; [HarmonyPatch(typeof(OptionsMenuBehaviour))] public static class OptionsMenuPatches { + internal static OptionsMenuBehaviour? Instance { get; private set; } internal static BoxCollider2D MaskCollider; private static SpriteRenderer? background; @@ -27,6 +28,7 @@ public static class OptionsMenuPatches [HarmonyPatch(nameof(OptionsMenuBehaviour.Start))] public static void StartPostfix(OptionsMenuBehaviour __instance) { + Instance = __instance; // Fix for tabs not being clickable in the main menu if (!AmongUsClient.Instance.IsInGame) { @@ -176,7 +178,7 @@ public static class OptionsMenuPatches } /// - /// Makes the custom tabs close when opening the options menu and reset pages. + /// Makes the custom tabs close when opening the options menu and resets pages. /// [HarmonyPostfix] [HarmonyPatch(nameof(OptionsMenuBehaviour.Open))] @@ -191,7 +193,11 @@ public static class OptionsMenuPatches { foreach (var pages in tabButtons) { - pages.Value.Do(x => x.SetActive(pages.Key == currentPage)); + pages.Value? + .Where(x => x != null) + .Do( + x => x?.SetActive( + pages.Key == currentPage)); } } From ebdb4a09cab35e1e32046f60181da28556003cde Mon Sep 17 00:00:00 2001 From: xChipseq Date: Tue, 2 Sep 2025 19:24:34 +0200 Subject: [PATCH 30/86] make number options accept floats --- MiraAPI.Example/ExampleLocalSetings.cs | 8 +++++--- .../Attributes/LocalNumberSettingAttribute.cs | 8 ++++---- .../SettingTypes/LocalNumberSetting.cs | 14 +++++++------- .../LocalSettings/SettingTypes/LocalSettingBase.cs | 4 ++-- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index e7c292f..bac0507 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -2,7 +2,6 @@ using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Utilities; -using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Example; @@ -30,8 +29,11 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) [LocalSliderSetting(suffixType: MiraNumberSuffixes.Percent, formatString: "0", roundValue: true)] public ConfigEntry ExampleSlider { get; private set; } = config.Bind("General", "Slider", 50f); - [LocalNumberSetting(suffixType: MiraNumberSuffixes.Seconds)] - public ConfigEntry ExampleNumber { get; private set; } = config.Bind("General", "Example Number", 4); + [LocalNumberSetting] + public ConfigEntry ExampleNumber { get; private set; } = config.Bind("General", "Example Number", 4f); + + [LocalNumberSetting(min: 0.1f, max: 2.5f, increment: 0.1f, formatString: "0.0", suffixType: MiraNumberSuffixes.Multiplier)] + public ConfigEntry ExampleFloatNumber { get; private set; } = config.Bind("General", "Example Multplier", 1f); [LocalEnumSetting] public ConfigEntry ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries); diff --git a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs index 20a55f3..febd711 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs @@ -18,9 +18,9 @@ namespace MiraAPI.LocalSettings.Attributes; public class LocalNumberSettingAttribute( string? name = null, string? description = null, - int min = 1, - int max = 5, - int increment = 1, + float min = 1, + float max = 5, + float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, string? formatString = null ) : LocalSettingAttribute(name, description) @@ -28,6 +28,6 @@ public class LocalNumberSettingAttribute( /// internal override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalNumberSetting(tab, configEntryBase, name, description, new IntRange(min, max), increment, suffixType, formatString); + return new LocalNumberSetting(tab, configEntryBase, name, description, new FloatRange(min, max), increment, suffixType, formatString); } } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs index 70be2aa..fb86fdc 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs @@ -12,17 +12,17 @@ namespace MiraAPI.LocalSettings.SettingTypes; /// /// Local setting class for numbers. /// -public class LocalNumberSetting : LocalSettingBase +public class LocalNumberSetting : LocalSettingBase { /// /// Gets the range of the button. /// - public IntRange NumberRange { get; } + public FloatRange NumberRange { get; } /// /// Gets the increment of the value when button is pressed. /// - public int Increment { get; } + public float Increment { get; } /// /// Gets a format for the text to use to format the number. @@ -47,14 +47,14 @@ public class LocalNumberSetting : LocalSettingBase ConfigEntryBase configEntry, string? name = null, string? description = null, - IntRange? numberRange = null, - int? increment = null, + FloatRange? numberRange = null, + float? increment = null, MiraNumberSuffixes? suffixType = null, string? formatString = null) : base(tab, configEntry, name, description) { SuffixType = suffixType ?? MiraNumberSuffixes.None; - NumberRange = numberRange ?? new IntRange(1, 5); + NumberRange = numberRange ?? new FloatRange(1, 5); Increment = increment ?? 1; FormatString = formatString ?? "0"; } @@ -97,7 +97,7 @@ public class LocalNumberSetting : LocalSettingBase button.OnClick.AddListener((UnityAction)(() => { - int value = GetValue(); + float value = GetValue(); value += Increment; if (value > NumberRange.max) { diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index f991376..d144ce4 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -56,7 +56,7 @@ public abstract class LocalSettingBase : ILocalSetting /// Gets the value of the config entry, casted to the setting type. /// /// The value. - public T GetValue() + public virtual T GetValue() { return (T)ConfigEntry.BoxedValue; } @@ -65,7 +65,7 @@ public abstract class LocalSettingBase : ILocalSetting /// Sets the value of the config entry. /// /// The value to set to. - public void SetValue(T value) + public virtual void SetValue(T value) { ConfigEntry.BoxedValue = value; Tab?.OnOptionChanged(ConfigEntry); From 705ef64b4d2dd506597796ec9f3b7841e0c6a7bb Mon Sep 17 00:00:00 2001 From: xChipseq Date: Tue, 2 Sep 2025 22:27:04 +0200 Subject: [PATCH 31/86] fix helper method returning null if no keybind --- MiraAPI/Keybinds/KeybindUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Keybinds/KeybindUtils.cs b/MiraAPI/Keybinds/KeybindUtils.cs index 3d342d6..fbca1d6 100644 --- a/MiraAPI/Keybinds/KeybindUtils.cs +++ b/MiraAPI/Keybinds/KeybindUtils.cs @@ -33,11 +33,11 @@ public static class KeybindUtils /// /// The action ID. /// The keyboard keycode. - public static KeyboardKeyCode? GetKeycodeByActionId(int actionId) + public static KeyboardKeyCode GetKeycodeByActionId(int actionId) { var player = ReInput.players.GetPlayer(0); return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false)? - .keyboardKeyCode; + .keyboardKeyCode ?? KeyboardKeyCode.None; } /// From ffe0db0e9eb2c12c4d1672576aa43c121caad476 Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:14:07 +0100 Subject: [PATCH 32/86] Add role icons in role settings menu --- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 80f96c9..6215b26 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using HarmonyLib; @@ -528,7 +528,7 @@ public static class RoleSettingMenuPatches roleOptionSetting.CountPlusBtn.SetInteractable(true); __instance.roleChances.Add(roleOptionSetting); - roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.5376f, -0.2923f, 0f); + roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.25f, -0.2923f, 0f); roleOptionSetting.titleText.color = customRole.OptionsMenuColor.FindAlternateColor(); roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left; @@ -551,6 +551,16 @@ public static class RoleSettingMenuPatches passiveButton.interactableHoveredColor = Color.white; passiveButton.OnClick.AddListener((UnityAction)(() => { ChangeTab(role, __instance); })); + + if (customRole.Configuration.Icon != null) + { + var roleIcon = new GameObject("RoleIcon"); + roleIcon.transform.parent = roleOptionSetting.transform; + roleIcon.transform.localScale = new(.25f, .25f, 1); + roleIcon.layer = LayerMask.NameToLayer("UI"); + roleIcon.transform.localPosition = new Vector3(-1.3f, -0.3f, -2f); + roleIcon.AddComponent().sprite = customRole.Configuration.Icon.LoadAsset(); + } } if (customRole.Configuration is { MaxRoleCount: 0 }) From e6043b6f5f2eb1a1fb4d8a2935fa2261ee008fb2 Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:27:38 +0100 Subject: [PATCH 33/86] Fix Masking issue --- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 6215b26..d99d2e5 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -559,7 +559,9 @@ public static class RoleSettingMenuPatches roleIcon.transform.localScale = new(.25f, .25f, 1); roleIcon.layer = LayerMask.NameToLayer("UI"); roleIcon.transform.localPosition = new Vector3(-1.3f, -0.3f, -2f); - roleIcon.AddComponent().sprite = customRole.Configuration.Icon.LoadAsset(); + var rend = roleIcon.AddComponent(); + rend.sprite = customRole.Configuration.Icon.LoadAsset(); + rend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; } } From b2fdaaf89f46dbb903c0520cd1cc61c4488d0a91 Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:29:19 +0100 Subject: [PATCH 34/86] Timed Modifier Enhancements in TaskAdderGame --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 6 ++++++ MiraAPI/Resources/TimedModifierFile.png | Bin 0 -> 5246 bytes MiraAPI/Utilities/Assets/MiraAssets.cs | 5 +++++ 3 files changed, 11 insertions(+) create mode 100644 MiraAPI/Resources/TimedModifierFile.png diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index ae49ed4..db4c5b0 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -355,6 +355,12 @@ internal static class TaskAdderPatches } })); + if (Modifier is TimedModifer timed) + { + taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset(); + taskAddButton.Text.text += $"\n {timed.Duration}s"; + } + instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); diff --git a/MiraAPI/Resources/TimedModifierFile.png b/MiraAPI/Resources/TimedModifierFile.png new file mode 100644 index 0000000000000000000000000000000000000000..666bd48f31afd7cc804d2c0b55c43d588baa8a1c GIT binary patch literal 5246 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 Skj(#nqJh-))EZT6qyGo?l+QH) literal 0 HcmV?d00001 diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 148c65a..0da66ae 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -99,4 +99,9 @@ public static class MiraAssets /// Gets the sprite used for the Keybind icons. /// public static LoadableResourceAsset KeybindButton { get; } = new("MiraAPI.Resources.KeybindButton.png"); + + /// + /// Gets the sprite used for timed modifier file in TaskAdderGame. + /// + public static LoadableResourceAsset TimedModifierFile { get; } = new("MiraAPI.Resources.TimedModifierFile.png"); } From ef85fab6a6c3c653e22f9b61061b07657d3ae4ce Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:35:40 +0100 Subject: [PATCH 35/86] Update TaskAdderPatches.cs --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index db4c5b0..4a584a3 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -4,6 +4,7 @@ using System.Linq; using AmongUs.GameOptions; using HarmonyLib; using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -355,7 +356,7 @@ internal static class TaskAdderPatches } })); - if (Modifier is TimedModifer timed) + if (modifier is TimedModifer timed) { taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset(); taskAddButton.Text.text += $"\n {timed.Duration}s"; From ea389b986facefd9ece6c1c82735167927869387 Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:58:09 +0100 Subject: [PATCH 36/86] Update TaskAdderPatches.cs --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 4a584a3..af04927 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -356,7 +356,7 @@ internal static class TaskAdderPatches } })); - if (modifier is TimedModifer timed) + if (modifier is TimedModifier timed) { taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset(); taskAddButton.Text.text += $"\n {timed.Duration}s"; From 31a9c0c98df4c47753fe25897ea6230b8c14020e Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:23:31 +0100 Subject: [PATCH 37/86] Update TaskAdderPatches.cs --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index af04927..725fb4f 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -359,7 +359,7 @@ internal static class TaskAdderPatches if (modifier is TimedModifier timed) { taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset(); - taskAddButton.Text.text += $"\n {timed.Duration}s"; + taskAddButton.Text.text += $" ({timed.Duration}s)"; } instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); From 5ad53f695fb4cd669bcb19938550ecba1eacd0e0 Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:29:39 +0100 Subject: [PATCH 38/86] Update TaskAdderPatches.cs --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 725fb4f..7f6a8b5 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Linq; using AmongUs.GameOptions; @@ -337,6 +337,7 @@ internal static class TaskAdderPatches taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; taskAddButton.Text.text = modifier.ModifierName; + taskAddButton.Text.fontSizeMin = 1; taskAddButton.Text.EnableMasking(); taskAddButton.FileImage.color = modifier.FreeplayFileColor; taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor; @@ -359,7 +360,7 @@ internal static class TaskAdderPatches if (modifier is TimedModifier timed) { taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset(); - taskAddButton.Text.text += $" ({timed.Duration}s)"; + taskAddButton.Text.text += $"({timed.Duration}s)"; } instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); From ecadfa86dd2aeb2d2868e812aa930b7bb8bafd64 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Thu, 4 Sep 2025 21:51:26 +0200 Subject: [PATCH 39/86] remove invoke log --- MiraAPI/Keybinds/MiraKeybind.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MiraAPI/Keybinds/MiraKeybind.cs b/MiraAPI/Keybinds/MiraKeybind.cs index efdd1e0..9007a0d 100644 --- a/MiraAPI/Keybinds/MiraKeybind.cs +++ b/MiraAPI/Keybinds/MiraKeybind.cs @@ -86,7 +86,6 @@ public class MiraKeybind public void Invoke() { Handler.Invoke(); - Logger.Warning($"Keybind invoked: {Name} ({Id}) - {DefaultKey.ToString()}"); } /// From bebd648691a597b5fc702d1d6ac1cdb440e0cca4 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Thu, 4 Sep 2025 21:51:45 +0200 Subject: [PATCH 40/86] remove null check, it's always none when null --- MiraAPI/Patches/HudManagerPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index bfdcce5..e90ecac 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -116,7 +116,7 @@ public static class HudManagerPatches var buttonObj = kvp.Key; var actionId = kvp.Value; - var key = KeybindUtils.GetKeycodeByActionId(actionId) ?? KeyboardKeyCode.None; + var key = KeybindUtils.GetKeycodeByActionId(actionId); if (key == KeyboardKeyCode.None) { continue; From f3614b9a5c5210422e3ed65498c8e8f89595ee5e Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 5 Sep 2025 23:33:01 +0200 Subject: [PATCH 41/86] fix null errors --- MiraAPI/Patches/HudManagerPatches.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index e90ecac..b1ec33a 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -101,6 +101,7 @@ public static class HudManagerPatches aspectPosition.AdjustPosition(); + vanillaKeybindIcons = []; var keybindIconPos = new Vector3(-0.4f, 0.4f, -9f); var vanillaButtons = new Dictionary { From 449192f0ef81835a553170613f6dc8d2f722338a Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sat, 6 Sep 2025 19:02:40 +0200 Subject: [PATCH 42/86] remove this annoying log --- MiraAPI/Patches/Voting/DummyBehaviourPatch.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs index 7652d4f..9e5c8a4 100644 --- a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs +++ b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs @@ -40,7 +40,6 @@ internal static class DummyBehaviourPatches private static IEnumerator CoDoVote(DummyBehaviour dummy) { - Logger.Warning(dummy.voted); dummy.voted = true; yield return new WaitForSeconds(dummy.voteTime.Next()); From d73c21936c7402c14222ba5a3cfe87c038174ba8 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:19:17 -0500 Subject: [PATCH 43/86] Add Before and After vote events Useful for local checks before a vote is placed (like with TOU Mira Prosecutor) --- .../Vanilla/Meeting/Voting/AfterVoteEvent.cs | 30 +++++++++++++++++++ .../Vanilla/Meeting/Voting/BeforeVoteEvent.cs | 30 +++++++++++++++++++ MiraAPI/Patches/Voting/MeetingHudPatches.cs | 18 +++++++++++ 3 files changed, 78 insertions(+) create mode 100644 MiraAPI/Events/Vanilla/Meeting/Voting/AfterVoteEvent.cs create mode 100644 MiraAPI/Events/Vanilla/Meeting/Voting/BeforeVoteEvent.cs diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/AfterVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/AfterVoteEvent.cs new file mode 100644 index 0000000..714d3b8 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/AfterVoteEvent.cs @@ -0,0 +1,30 @@ +using MiraAPI.Voting; + +namespace MiraAPI.Events.Vanilla.Meeting.Voting; + +/// +/// Event that is invoked after the local player successfully votes a player or skips. This event is not cancelable. +/// +public class AfterVoteEvent : MiraEvent +{ + /// + /// Gets the instance of the voter's vote data. + /// + public PlayerVoteArea VoteArea { get; } + + /// + /// Gets the player who voted. + /// + public PlayerControl Player { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player vote area that was voted on. + /// The player who voted. + public AfterVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter) + { + VoteArea = playerVoteArea; + Player = voter; + } +} diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/BeforeVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/BeforeVoteEvent.cs new file mode 100644 index 0000000..1d8d907 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/BeforeVoteEvent.cs @@ -0,0 +1,30 @@ +using MiraAPI.Voting; + +namespace MiraAPI.Events.Vanilla.Meeting.Voting; + +/// +/// Event that is invoked before the local player confirms a vote on a player or when skipping. This event is cancelable. +/// +public class BeforeVoteEvent : MiraCancelableEvent +{ + /// + /// Gets the instance of the voter's vote data. + /// + public PlayerVoteArea VoteArea { get; } + + /// + /// Gets the player who voted. + /// + public PlayerControl Player { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player vote area that was voted on. + /// The player who voted. + public BeforeVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter) + { + VoteArea = playerVoteArea; + Player = voter; + } +} diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 919170d..2aa9a82 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -15,6 +15,24 @@ namespace MiraAPI.Patches.Voting; [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony Convention")] internal static class MeetingHudPatches { + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.VoteForMe))] + public static bool VotePatch(PlayerVoteArea __instance) + { + var beforeVoteEvent = new BeforeVoteEvent(__instance, PlayerControl.LocalPlayer); + MiraEventManager.InvokeEvent(beforeVoteEvent); + + if (beforeVoteEvent.IsCancelled) + { + return false; + } + + var afterVoteEvent = new AfterVoteEvent(__instance, PlayerControl.LocalPlayer); + MiraEventManager.InvokeEvent(afterVoteEvent); + + return true; + } + [HarmonyPostfix] [HarmonyPatch(nameof(MeetingHud.Start))] public static void MeetingHudStartPatch(MeetingHud __instance) From 9d130a53de165d845d72bf95ea8404afe0399de7 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 7 Sep 2025 00:54:44 +0200 Subject: [PATCH 44/86] fix typo --- MiraAPI/Keybinds/MiraKeybind.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Keybinds/MiraKeybind.cs b/MiraAPI/Keybinds/MiraKeybind.cs index 9007a0d..2d72f0b 100644 --- a/MiraAPI/Keybinds/MiraKeybind.cs +++ b/MiraAPI/Keybinds/MiraKeybind.cs @@ -31,7 +31,7 @@ public class MiraKeybind public bool Exclusive { get; } /// - /// Gets the modifier keys assinged to this keybind. + /// Gets the modifier keys assigned to this keybind. /// Due to Rewired limitations, there can only be 3 modifier keys. /// public ModifierKey[] ModifierKeys { get; } From 94197e38ddfffdba1e83c560b6f490a7ec6ead5a Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 7 Sep 2025 20:47:59 +0200 Subject: [PATCH 45/86] cleanup --- MiraAPI/MiraApiSettings.cs | 11 +---------- MiraAPI/PluginLoading/MiraPluginManager.cs | 4 ++-- MiraAPI/Utilities/Helpers.cs | 2 -- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 6a659f1..3014411 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -1,16 +1,7 @@ -using BepInEx; -using BepInEx.Configuration; -using BepInEx.Unity.IL2CPP; -using HarmonyLib; +using BepInEx.Configuration; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; -using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; -using Reactor; -using Reactor.Networking; -using Reactor.Networking.Attributes; -using Reactor.Utilities; -using UnityEngine; namespace MiraAPI; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 0c987cd..4fbb203 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -22,7 +22,6 @@ using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; using Rewired; -using UnityEngine; namespace MiraAPI.PluginLoading; @@ -155,7 +154,7 @@ public sealed class MiraPluginManager Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {(KeyboardKeyCode)button.DefaultKeybind}."); } }; - + RegisterLocalTabs(typeof(MiraApiSettings), PluginSingleton.Instance); } @@ -395,6 +394,7 @@ public sealed class MiraPluginManager { Logger.Error($"Failed to register options for {type.Name}: {e.ToString()}"); } + return false; } } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 4717189..7b4bac6 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -4,9 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Il2CppInterop.Runtime.InteropTypes.Arrays; -using Il2CppSystem.IO; using MiraAPI.Keybinds; -using MiraAPI.Patches.LocalSettings; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; From af3d99eafe87a3c16d2e0c1b3b97c9bb3260c10c Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 7 Sep 2025 21:25:15 +0200 Subject: [PATCH 46/86] wip --- MiraAPI/Keybinds/KeybindManager.cs | 12 ++++++++++++ MiraAPI/Keybinds/MiraKeybind.cs | 4 ++-- MiraAPI/Keybinds/VanillaKeybind.cs | 18 ++++++++++++++++++ MiraAPI/Keybinds/VanillaKeybindSingleton.cs | 15 +++++++++++++++ MiraAPI/Patches/HudManagerPatches.cs | 1 + 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 MiraAPI/Keybinds/VanillaKeybind.cs create mode 100644 MiraAPI/Keybinds/VanillaKeybindSingleton.cs diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs index 57c87e7..630fadb 100644 --- a/MiraAPI/Keybinds/KeybindManager.cs +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -17,6 +17,8 @@ public static class KeybindManager /// public static List Keybinds { get; } = new(); + internal static List VanillaKeybinds { get; } = new(); + internal static void RewiredInit() { try @@ -91,4 +93,14 @@ public static class KeybindManager return all.ToArray(); } + + internal static void RegisterVanillaKeybind(AbilityButton button, InputAction inputAction) + { + var keybind = new VanillaKeybind(button, inputAction); + VanillaKeybinds.Add(keybind); + typeof(VanillaKeybindSingleton<>) + .MakeGenericType(button.GetType()) + .GetField("_instance")! + .SetValue(null, keybind); + } } diff --git a/MiraAPI/Keybinds/MiraKeybind.cs b/MiraAPI/Keybinds/MiraKeybind.cs index 2d72f0b..a0e2c45 100644 --- a/MiraAPI/Keybinds/MiraKeybind.cs +++ b/MiraAPI/Keybinds/MiraKeybind.cs @@ -8,7 +8,7 @@ namespace MiraAPI.Keybinds; /// /// Represents a registered keybind. /// -public class MiraKeybind +public class MiraKeybind() { /// /// Gets name of the keybind @@ -64,7 +64,7 @@ public class MiraKeybind string name, KeyboardKeyCode? defaultKeycode, ModifierKey[]? modifierKeys = null, - bool exclusive = true) + bool exclusive = true) : this() { Name = name; Id = name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_'); diff --git a/MiraAPI/Keybinds/VanillaKeybind.cs b/MiraAPI/Keybinds/VanillaKeybind.cs new file mode 100644 index 0000000..56a3121 --- /dev/null +++ b/MiraAPI/Keybinds/VanillaKeybind.cs @@ -0,0 +1,18 @@ +using Rewired; +using TMPro; +using UnityEngine; + +namespace MiraAPI.Keybinds; + +public class VanillaKeybind +{ + public AbilityButton Ability { get; } + public InputAction RewiredInputAction { get; } + public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByActionId(RewiredInputAction.id); + + public VanillaKeybind(AbilityButton ability, InputAction inputAction) + { + Ability = ability; + RewiredInputAction = inputAction; + } +} diff --git a/MiraAPI/Keybinds/VanillaKeybindSingleton.cs b/MiraAPI/Keybinds/VanillaKeybindSingleton.cs new file mode 100644 index 0000000..99b34a7 --- /dev/null +++ b/MiraAPI/Keybinds/VanillaKeybindSingleton.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace MiraAPI.Keybinds; + +public static class VanillaKeybindSingleton where T : AbilityButton +{ + internal static VanillaKeybind _instance; + + /// + /// Gets the instance of the option group. + /// +#pragma warning disable CA1000 + public static VanillaKeybind Instance => _instance; +#pragma warning restore CA1000 +} \ No newline at end of file diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index b1ec33a..99dea2b 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -124,6 +124,7 @@ public static class HudManagerPatches } var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); + KeybindManager.RegisterVanillaKeybind(); } } From a1611f5f7a81443d86326bea21aa32967f18de7a Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 7 Sep 2025 16:34:57 -0400 Subject: [PATCH 47/86] fix merge errors --- MiraAPI/Hud/CustomActionButton.cs | 3 ++- .../Attributes/LocalEnumSettingAttribute.cs | 5 ++++- .../Attributes/LocalNumberSettingAttribute.cs | 5 ++++- .../Attributes/LocalSliderSettingAttribute.cs | 5 ++++- .../Attributes/LocalToggleSettingAttribute.cs | 5 ++++- MiraAPI/MiraApiSettings.cs | 6 ++++++ MiraAPI/Patches/HudManagerPatches.cs | 3 ++- .../Patches/Keybinds/ControlMapperPatches.cs | 3 ++- MiraAPI/Utilities/Assets/MiraAssets.cs | 2 +- MiraAPI/Utilities/Helpers.cs | 19 ------------------- 10 files changed, 29 insertions(+), 27 deletions(-) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index ca7dd6a..b7d4085 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -3,6 +3,7 @@ using System.Globalization; using MiraAPI.Events; using MiraAPI.Events.Mira; using MiraAPI.Keybinds; +using MiraAPI.LocalSettings; using MiraAPI.Patches; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; @@ -493,7 +494,7 @@ public abstract class CustomActionButton { KeybindText.text = Keybind.CurrentKey.ToString(); KeybindIcon?.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard && - PluginSingleton.Instance.MiraConfig!.ShowKeybinds.Value); + LocalSettingsTabSingleton.Instance.ShowKeybinds.Value); } if (Timer >= 0) diff --git a/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs index a4a22e2..24ea47f 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs @@ -16,9 +16,12 @@ public class LocalEnumSettingAttribute( string[]? names = null ) : LocalSettingAttribute(name, description) { + private readonly string? _name = name; + private readonly string? _description = description; + /// internal override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalEnumSetting(tab, configEntryBase, configEntryBase.SettingType, name, description, names); + return new LocalEnumSetting(tab, configEntryBase, configEntryBase.SettingType, _name, _description, names); } } diff --git a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs index febd711..3008755 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs @@ -25,9 +25,12 @@ public class LocalNumberSettingAttribute( string? formatString = null ) : LocalSettingAttribute(name, description) { + private readonly string? _name = name; + private readonly string? _description = description; + /// internal override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalNumberSetting(tab, configEntryBase, name, description, new FloatRange(min, max), increment, suffixType, formatString); + return new LocalNumberSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), increment, suffixType, formatString); } } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs index 4a211d5..2db56b4 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs @@ -25,9 +25,12 @@ public class LocalSliderSettingAttribute( MiraNumberSuffixes suffixType = MiraNumberSuffixes.None ) : LocalSettingAttribute(name, description) { + private readonly string? _name = name; + private readonly string? _description = description; + /// internal override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalSliderSetting(tab, configEntryBase, name, description, new FloatRange(min, max), displayValue, suffixType, formatString, roundValue); + return new LocalSliderSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), displayValue, suffixType, formatString, roundValue); } } diff --git a/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs index 31c9b09..022fb38 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs @@ -14,9 +14,12 @@ public class LocalToggleSettingAttribute( string? description = null ) : LocalSettingAttribute(name, description) { + private readonly string? _name = name; + private readonly string? _description = description; + /// internal override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalToggleSetting(tab, configEntryBase, name, description); + return new LocalToggleSetting(tab, configEntryBase, _name, _description); } } diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 3014411..55b5913 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -25,4 +25,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) /// [LocalToggleSetting] 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. + /// + [LocalToggleSetting] + public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true); } diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index b1ec33a..a06e170 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -3,6 +3,7 @@ using System.Linq; using HarmonyLib; using MiraAPI.Hud; using MiraAPI.Keybinds; +using MiraAPI.LocalSettings; using MiraAPI.Utilities; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -165,7 +166,7 @@ public static class HudManagerPatches { btnIcon.Key.text = KeybindUtils.GetKeycodeByActionId(btnIcon.Value).ToString(); btnIcon.Key.transform.parent.gameObject.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard && - PluginSingleton.Instance.MiraConfig!.ShowKeybinds.Value); + LocalSettingsTabSingleton.Instance.ShowKeybinds.Value); } foreach (var entry in KeybindManager.Keybinds) diff --git a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs index 4a5fe90..ae46734 100644 --- a/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs +++ b/MiraAPI/Patches/Keybinds/ControlMapperPatches.cs @@ -1,6 +1,7 @@ using System.Linq; using HarmonyLib; using MiraAPI.Keybinds; +using MiraAPI.LocalSettings; using Reactor.Utilities; using Reactor.Utilities.Extensions; using Rewired.UI.ControlMapper; @@ -26,7 +27,7 @@ public static class ControlMapperPatches var toggle = Object.Instantiate(doneButton, doneButton.transform.parent); var text = toggle.GetComponentInChildren(); - var entry = PluginSingleton.Instance!.MiraConfig!.ShowKeybinds; + var entry = LocalSettingsTabSingleton.Instance.ShowKeybinds; toggle.gameObject.name = "KeybindsVisibleToggle"; text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}"; toggle.onClick.RemoveAllListeners(); diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index ddbf354..1e39c28 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -69,7 +69,7 @@ public static class MiraAssets /// Gets the Next Button sprite. /// public static LoadableResourceAsset NextButton { get; } = new("MiraAPI.Resources.NextButton.png"); - + /// /// Gets the Mira settings icon.. /// diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 6a00751..416cd28 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -476,25 +476,6 @@ public static class Helpers .Select(s => s[UnityEngine.Random.RandomRangeInt(0, s.Length)]).ToArray()); } - /// - /// Finds and returns an unused KeyCode that is not equal to the excluded key. - /// - /// The KeyCode to skip during the search. - /// - /// The first available KeyCode not currently used by any registered keybind, - /// or KeyCode.None if none are available. - /// - public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude) - { - foreach (KeyboardKeyCode key in Enum.GetValues(typeof(KeyboardKeyCode))) - { - if (key == exclude) continue; - bool used = KeybindManager.GetEntries().Exists(e => e.Key == key); - if (!used) return key; - } - return KeyboardKeyCode.None; - } - /// /// Returns the formated value using the specified suffix and format string. /// From ffc7a1a4fd83748501415cb344512dc87373a2fe Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 7 Sep 2025 22:43:08 +0200 Subject: [PATCH 48/86] fix keybind icon clipping --- MiraAPI/Patches/HudManagerPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index a06e170..09ee13e 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -103,7 +103,7 @@ public static class HudManagerPatches aspectPosition.AdjustPosition(); vanillaKeybindIcons = []; - var keybindIconPos = new Vector3(-0.4f, 0.4f, -9f); + var keybindIconPos = new Vector3(-0.4f, 0.4f, -9.5f); var vanillaButtons = new Dictionary { { __instance.KillButton.gameObject, 8 }, From b7173d18ab81ce08d93767ad6e27c0d27f482b1b Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 7 Sep 2025 22:45:06 +0200 Subject: [PATCH 49/86] change mira config --- MiraAPI/MiraApiConfig.cs | 19 ------------------- MiraAPI/MiraApiSettings.cs | 1 - 2 files changed, 20 deletions(-) delete mode 100644 MiraAPI/MiraApiConfig.cs diff --git a/MiraAPI/MiraApiConfig.cs b/MiraAPI/MiraApiConfig.cs deleted file mode 100644 index 3db12a9..0000000 --- a/MiraAPI/MiraApiConfig.cs +++ /dev/null @@ -1,19 +0,0 @@ -using BepInEx.Configuration; - -namespace MiraAPI; - -/// -/// Mira API Config File Handler -/// -public class MiraApiConfig(ConfigFile config) -{ - /// - /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. - /// - public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false); - - /// - /// Gets whether the keybind icons should be visible. - /// - public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Displays", "Show Keybinds on buttons", true); -} diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 55b5913..aa4b29c 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -29,6 +29,5 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) /// /// Gets whether to show keybinds in the control mapper. /// - [LocalToggleSetting] public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true); } From 4b7fa906b9f24a4b26241b7985dd5a3d1a9f63a0 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sun, 7 Sep 2025 22:49:23 +0200 Subject: [PATCH 50/86] fix misaligned options after labels --- MiraAPI/LocalSettings/LocalSettingsTab.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index a04830d..dee9ae1 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -127,6 +127,7 @@ public abstract class LocalSettingsTab(ConfigFile config) if (ShouldCreateLabels) { CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset); + contentOrder = 1; } foreach (var setting in pair.Value) From 6872ca6866b20ae75817c10975d01e2d3107bad7 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 8 Sep 2025 18:37:09 +0200 Subject: [PATCH 51/86] basekeybind class, vanilla keybindings --- MiraAPI/Hud/CustomActionButton.cs | 6 +- MiraAPI/Keybinds/BaseKeybind.cs | 67 +++++++++++++++++++++ MiraAPI/Keybinds/KeybindManager.cs | 13 ++-- MiraAPI/Keybinds/KeybindUtils.cs | 27 +++++++-- MiraAPI/Keybinds/MiraKeybind.cs | 63 ++----------------- MiraAPI/Keybinds/VanillaKeybind.cs | 25 +++++--- MiraAPI/Keybinds/VanillaKeybindSingleton.cs | 16 ++--- MiraAPI/Patches/HudManagerPatches.cs | 63 ++++++++++--------- 8 files changed, 164 insertions(+), 116 deletions(-) create mode 100644 MiraAPI/Keybinds/BaseKeybind.cs diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index ca7dd6a..c0e1b65 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -7,6 +7,7 @@ using MiraAPI.Patches; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities; +using Rewired; using TMPro; using UnityEngine; using UnityEngine.Events; @@ -74,7 +75,7 @@ public abstract class CustomActionButton /// /// Gets the keybind for this button. If null, no keybind will be added. /// - public virtual MiraKeybind? Keybind => null; + public virtual BaseKeybind? Keybind => null; /// /// Gets the button's text outline color. @@ -493,7 +494,8 @@ public abstract class CustomActionButton { KeybindText.text = Keybind.CurrentKey.ToString(); KeybindIcon?.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard && - PluginSingleton.Instance.MiraConfig!.ShowKeybinds.Value); + PluginSingleton.Instance.MiraConfig!.ShowKeybinds.Value && + Keybind.CurrentKey != KeyboardKeyCode.None); } if (Timer >= 0) diff --git a/MiraAPI/Keybinds/BaseKeybind.cs b/MiraAPI/Keybinds/BaseKeybind.cs new file mode 100644 index 0000000..21d636e --- /dev/null +++ b/MiraAPI/Keybinds/BaseKeybind.cs @@ -0,0 +1,67 @@ +using System; +using Rewired; + +namespace MiraAPI.Keybinds; + +/// +/// Base keybind class. +/// +public class BaseKeybind +{ + /// + /// Gets the Rewired assinged for this keybind. + /// + public InputAction? RewiredInputAction { get; internal set; } + + /// + /// Gets the unique identifier for this keybind. Used in Rewired. + /// + public string Id { get; } + + /// + /// Gets the currently assigned keycode. + /// + public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this); + + /// + /// Gets or sets the handler of the keybind. Invoked when the keybind is activated. + /// + protected Action Handler { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier for this keybind. + public BaseKeybind(string id) + { + Id = id; + Handler = () => { }; + } + + /// + /// Triggers the keybind, invoking its handler. + /// + public void Invoke() + { + Handler.Invoke(); + } + + /// + /// Adds an that will be invoked when the keybind is used. + /// + /// The to add. + public void OnActivate(Action action) + { + Handler += action; + } + + /// + /// Removes an that would be invoked when the keybind is used. + /// Not recommended to use - add checks in the method added in itself rather than removing it from the keybind. + /// + /// The to remove. + public void RemoveOnActivate(Action action) + { + Handler -= action; + } +} \ No newline at end of file diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs index 630fadb..a123647 100644 --- a/MiraAPI/Keybinds/KeybindManager.cs +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -16,8 +16,7 @@ public static class KeybindManager /// Gets a list of all registered keybinds. /// public static List Keybinds { get; } = new(); - - internal static List VanillaKeybinds { get; } = new(); + internal static Dictionary VanillaKeybinds { get; } = new(); internal static void RewiredInit() { @@ -94,13 +93,9 @@ public static class KeybindManager return all.ToArray(); } - internal static void RegisterVanillaKeybind(AbilityButton button, InputAction inputAction) + internal static void RegisterVanillaKeybind(ActionButton button, int id) { - var keybind = new VanillaKeybind(button, inputAction); - VanillaKeybinds.Add(keybind); - typeof(VanillaKeybindSingleton<>) - .MakeGenericType(button.GetType()) - .GetField("_instance")! - .SetValue(null, keybind); + var keybind = new VanillaKeybind(button, KeybindUtils.GetInputActionById(id)!); + VanillaKeybinds.Add(button.GetType(), keybind); } } diff --git a/MiraAPI/Keybinds/KeybindUtils.cs b/MiraAPI/Keybinds/KeybindUtils.cs index fbca1d6..f4371b3 100644 --- a/MiraAPI/Keybinds/KeybindUtils.cs +++ b/MiraAPI/Keybinds/KeybindUtils.cs @@ -18,11 +18,11 @@ public static class KeybindUtils /// /// The keybind to get the keycode from. /// The currently assigned keycode. - public static KeyboardKeyCode? GetKeycodeByKeybind(MiraKeybind keybind) + public static KeyboardKeyCode GetKeycodeByKeybind(BaseKeybind keybind) { if (keybind.RewiredInputAction == null) { - return null; + return KeyboardKeyCode.None; } return GetKeycodeByActionId(keybind.RewiredInputAction.id); @@ -34,10 +34,29 @@ public static class KeybindUtils /// The action ID. /// The keyboard keycode. public static KeyboardKeyCode GetKeycodeByActionId(int actionId) + { + return GetActionElementMap(actionId)?.keyboardKeyCode ?? KeyboardKeyCode.None; + } + + /// + /// Gets a by id with ReInput. + /// + /// The action ID. + /// The keyboard keycode. + public static ActionElementMap? GetActionElementMap(int actionId) { var player = ReInput.players.GetPlayer(0); - return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false)? - .keyboardKeyCode ?? KeyboardKeyCode.None; + return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false); + } + + /// + /// Gets an by id. + /// + /// The action identifier name. + /// The keyboard keycode. + public static InputAction? GetInputActionById(int id) + { + return RewiredInputManager?.userData?.GetActionById(id); } /// diff --git a/MiraAPI/Keybinds/MiraKeybind.cs b/MiraAPI/Keybinds/MiraKeybind.cs index a0e2c45..52d64a1 100644 --- a/MiraAPI/Keybinds/MiraKeybind.cs +++ b/MiraAPI/Keybinds/MiraKeybind.cs @@ -1,6 +1,4 @@ -using System; -using System.Globalization; -using Reactor.Utilities; +using System.Globalization; using Rewired; namespace MiraAPI.Keybinds; @@ -8,28 +6,18 @@ namespace MiraAPI.Keybinds; /// /// Represents a registered keybind. /// -public class MiraKeybind() +public class MiraKeybind : BaseKeybind { /// /// Gets name of the keybind /// public string Name { get; } - /// - /// Gets the unique identifier for this keybind. Used in Rewired. - /// - public string Id { get; } - /// /// Gets the default keycode of this keybind. /// public KeyboardKeyCode DefaultKey { get; } - /// - /// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key. - /// - public bool Exclusive { get; } - /// /// Gets the modifier keys assigned to this keybind. /// Due to Rewired limitations, there can only be 3 modifier keys. @@ -37,19 +25,9 @@ public class MiraKeybind() public ModifierKey[] ModifierKeys { get; } /// - /// Gets the Rewired assinged for this keybind. + /// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key. /// - public InputAction? RewiredInputAction { get; internal set; } - - /// - /// Gets the currently assigned keycode. - /// - public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this) ?? KeyboardKeyCode.None; - - /// - /// Gets or sets the handler of the keybind. Invoked when the keybind is activated. - /// - private Action Handler { get; set; } + public bool Exclusive { get; } internal string? SourcePluginName { get; set; } @@ -64,14 +42,12 @@ public class MiraKeybind() string name, KeyboardKeyCode? defaultKeycode, ModifierKey[]? modifierKeys = null, - bool exclusive = true) : this() + bool exclusive = true) : base(name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_')) { Name = name; - Id = name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_'); DefaultKey = defaultKeycode ?? KeyboardKeyCode.None; - ModifierKeys = modifierKeys ?? Array.Empty(); + ModifierKeys = modifierKeys ?? []; Exclusive = exclusive; - Handler = () => { }; } /// @@ -79,31 +55,4 @@ public class MiraKeybind() { return $"{Name} ({Id})"; } - - /// - /// Triggers the keybind, invoking its handler. - /// - public void Invoke() - { - Handler.Invoke(); - } - - /// - /// Adds an that will be invoked when the keybind is used. - /// - /// The to add. - public void OnActivate(Action action) - { - Handler += action; - } - - /// - /// Removes an that would be invoked when the keybind is used. - /// Not recommended to use - add checks in the method added in itself rather than removing it from the keybind. - /// - /// The to remove. - public void RemoveOnActivate(Action action) - { - Handler -= action; - } } diff --git a/MiraAPI/Keybinds/VanillaKeybind.cs b/MiraAPI/Keybinds/VanillaKeybind.cs index 56a3121..ad1bbb5 100644 --- a/MiraAPI/Keybinds/VanillaKeybind.cs +++ b/MiraAPI/Keybinds/VanillaKeybind.cs @@ -1,18 +1,25 @@ using Rewired; -using TMPro; -using UnityEngine; namespace MiraAPI.Keybinds; -public class VanillaKeybind +/// +/// Represents a vanilla keybind. +/// +public class VanillaKeybind : BaseKeybind { - public AbilityButton Ability { get; } - public InputAction RewiredInputAction { get; } - public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByActionId(RewiredInputAction.id); + /// + /// Gets the this keybind is binded to. + /// + public ActionButton? Button { get; } - public VanillaKeybind(AbilityButton ability, InputAction inputAction) + /// + /// Initializes a new instance of the class. + /// + /// The keybind action button. + /// The rewired input action. + public VanillaKeybind(ActionButton button, InputAction action) : base(action.name) { - Ability = ability; - RewiredInputAction = inputAction; + Button = button; + RewiredInputAction = action; } } diff --git a/MiraAPI/Keybinds/VanillaKeybindSingleton.cs b/MiraAPI/Keybinds/VanillaKeybindSingleton.cs index 99b34a7..980da77 100644 --- a/MiraAPI/Keybinds/VanillaKeybindSingleton.cs +++ b/MiraAPI/Keybinds/VanillaKeybindSingleton.cs @@ -1,15 +1,17 @@ -using System.Linq; +namespace MiraAPI.Keybinds; -namespace MiraAPI.Keybinds; - -public static class VanillaKeybindSingleton where T : AbilityButton +/// +/// Singleton for getting a vanilla keybind by . +/// +/// ActionButton type. +public static class VanillaKeybinding where T : ActionButton { - internal static VanillaKeybind _instance; + private static VanillaKeybind? _instance; /// /// Gets the instance of the option group. /// #pragma warning disable CA1000 - public static VanillaKeybind Instance => _instance; + public static VanillaKeybind Instance => _instance ??= KeybindManager.VanillaKeybinds[typeof(T)]; #pragma warning restore CA1000 -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 99dea2b..6183d5c 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -45,6 +45,33 @@ public static class HudManagerPatches [HarmonyPatch(nameof(HudManager.Start))] public static void StartPostfix(HudManager __instance) { + vanillaKeybindIcons = []; + var keybindIconPos = new Vector3(-0.4f, 0.4f, -9f); + var vanillaButtons = new Dictionary + { + { __instance.KillButton.gameObject, 8 }, + { __instance.UseButton.gameObject, 6 }, + { __instance.ReportButton.gameObject, 7 }, + { __instance.ImpostorVentButton.gameObject, 50 }, + { __instance.SabotageButton.gameObject, 4 }, + }; + + KeybindManager.VanillaKeybinds.Clear(); + foreach (var kvp in vanillaButtons) + { + var buttonObj = kvp.Key; + var actionId = kvp.Value; + + var key = KeybindUtils.GetKeycodeByActionId(actionId); + if (key == KeyboardKeyCode.None) + { + continue; + } + var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); + vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); + KeybindManager.RegisterVanillaKeybind(buttonObj.GetComponent(), actionId); + } + if (Buttons == null) { Buttons = __instance.transform.Find("Buttons"); @@ -100,32 +127,6 @@ public static class HudManagerPatches gridArrange.ArrangeChilds(); aspectPosition.AdjustPosition(); - - vanillaKeybindIcons = []; - var keybindIconPos = new Vector3(-0.4f, 0.4f, -9f); - var vanillaButtons = new Dictionary - { - { __instance.KillButton.gameObject, 8 }, - { __instance.UseButton.gameObject, 6 }, - { __instance.ReportButton.gameObject, 7 }, - { __instance.ImpostorVentButton.gameObject, 50 }, - { __instance.SabotageButton.gameObject, 4 }, - }; - - foreach (var kvp in vanillaButtons) - { - var buttonObj = kvp.Key; - var actionId = kvp.Value; - - var key = KeybindUtils.GetKeycodeByActionId(actionId); - if (key == KeyboardKeyCode.None) - { - continue; - } - var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); - vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); - KeybindManager.RegisterVanillaKeybind(); - } } /// @@ -169,11 +170,10 @@ public static class HudManagerPatches PluginSingleton.Instance.MiraConfig!.ShowKeybinds.Value); } + var player = ReInput.players.GetPlayer(0); foreach (var entry in KeybindManager.Keybinds) { - var player = ReInput.players.GetPlayer(0); var keyboard = player.controllers.Keyboard; - bool modifierKeysPressed = entry.ModifierKeys.All(k => keyboard.GetModifierKey(k)) || entry.ModifierKeys.Length <= 0; if (player.GetButtonDown(entry.Id) && modifierKeysPressed) @@ -181,5 +181,12 @@ public static class HudManagerPatches entry.Invoke(); } } + foreach (var entry in KeybindManager.VanillaKeybinds.Values) + { + if (player.GetButtonDown(entry.Id)) + { + entry.Invoke(); + } + } } } From c41dd27f8812f073964c4653fc7f78921336f310 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 8 Sep 2025 19:38:07 +0200 Subject: [PATCH 52/86] init vanilla keybinds after input manager init --- MiraAPI/Keybinds/BaseKeybind.cs | 7 ++- MiraAPI/Keybinds/KeybindManager.cs | 9 +--- MiraAPI/Keybinds/VanillaKeybind.cs | 11 ++--- MiraAPI/Patches/HudManagerPatches.cs | 56 +++++++++++------------ MiraAPI/Patches/Keybinds/KeybindsPatch.cs | 11 +++++ 5 files changed, 52 insertions(+), 42 deletions(-) diff --git a/MiraAPI/Keybinds/BaseKeybind.cs b/MiraAPI/Keybinds/BaseKeybind.cs index 21d636e..e4e6ac8 100644 --- a/MiraAPI/Keybinds/BaseKeybind.cs +++ b/MiraAPI/Keybinds/BaseKeybind.cs @@ -16,7 +16,7 @@ public class BaseKeybind /// /// Gets the unique identifier for this keybind. Used in Rewired. /// - public string Id { get; } + public string Id { get; protected set; } /// /// Gets the currently assigned keycode. @@ -38,6 +38,11 @@ public class BaseKeybind Handler = () => { }; } + public BaseKeybind() + { + Handler = () => { }; + } + /// /// Triggers the keybind, invoking its handler. /// diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs index a123647..a47a20a 100644 --- a/MiraAPI/Keybinds/KeybindManager.cs +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -16,7 +16,8 @@ public static class KeybindManager /// Gets a list of all registered keybinds. /// public static List Keybinds { get; } = new(); - internal static Dictionary VanillaKeybinds { get; } = new(); + + internal static Dictionary VanillaKeybinds { get; set; } internal static void RewiredInit() { @@ -92,10 +93,4 @@ public static class KeybindManager return all.ToArray(); } - - internal static void RegisterVanillaKeybind(ActionButton button, int id) - { - var keybind = new VanillaKeybind(button, KeybindUtils.GetInputActionById(id)!); - VanillaKeybinds.Add(button.GetType(), keybind); - } } diff --git a/MiraAPI/Keybinds/VanillaKeybind.cs b/MiraAPI/Keybinds/VanillaKeybind.cs index ad1bbb5..c04b83b 100644 --- a/MiraAPI/Keybinds/VanillaKeybind.cs +++ b/MiraAPI/Keybinds/VanillaKeybind.cs @@ -10,16 +10,15 @@ public class VanillaKeybind : BaseKeybind /// /// Gets the this keybind is binded to. /// - public ActionButton? Button { get; } + public ActionButton? Button { get; internal set; } /// /// Initializes a new instance of the class. /// - /// The keybind action button. - /// The rewired input action. - public VanillaKeybind(ActionButton button, InputAction action) : base(action.name) + /// The keybind id. + public VanillaKeybind(int id) { - Button = button; - RewiredInputAction = action; + RewiredInputAction = KeybindUtils.GetInputActionById(id); + Id = RewiredInputAction?.name; } } diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 6183d5c..c1a9bf2 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -45,33 +45,6 @@ public static class HudManagerPatches [HarmonyPatch(nameof(HudManager.Start))] public static void StartPostfix(HudManager __instance) { - vanillaKeybindIcons = []; - var keybindIconPos = new Vector3(-0.4f, 0.4f, -9f); - var vanillaButtons = new Dictionary - { - { __instance.KillButton.gameObject, 8 }, - { __instance.UseButton.gameObject, 6 }, - { __instance.ReportButton.gameObject, 7 }, - { __instance.ImpostorVentButton.gameObject, 50 }, - { __instance.SabotageButton.gameObject, 4 }, - }; - - KeybindManager.VanillaKeybinds.Clear(); - foreach (var kvp in vanillaButtons) - { - var buttonObj = kvp.Key; - var actionId = kvp.Value; - - var key = KeybindUtils.GetKeycodeByActionId(actionId); - if (key == KeyboardKeyCode.None) - { - continue; - } - var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); - vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); - KeybindManager.RegisterVanillaKeybind(buttonObj.GetComponent(), actionId); - } - if (Buttons == null) { Buttons = __instance.transform.Find("Buttons"); @@ -125,8 +98,35 @@ public static class HudManagerPatches gridArrange.Start(); gridArrange.ArrangeChilds(); - aspectPosition.AdjustPosition(); + + vanillaKeybindIcons = []; + var keybindIconPos = new Vector3(-0.4f, 0.4f, -9f); + var vanillaButtons = new Dictionary + { + { __instance.KillButton.gameObject, 8 }, + { __instance.UseButton.gameObject, 6 }, + { __instance.ReportButton.gameObject, 7 }, + { __instance.ImpostorVentButton.gameObject, 50 }, + { __instance.SabotageButton.gameObject, 4 }, + }; + + KeybindManager.VanillaKeybinds.Clear(); + foreach (var kvp in vanillaButtons) + { + var buttonObj = kvp.Key; + var actionId = kvp.Value; + + var key = KeybindUtils.GetKeycodeByActionId(actionId); + if (key == KeyboardKeyCode.None) + { + continue; + } + var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos); + vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent(), actionId); + var comp = buttonObj.GetComponent(); + KeybindManager.VanillaKeybinds[comp.GetType()].Button = comp; + } } /// diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs index 1f60df0..3a6cf24 100644 --- a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs +++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs @@ -1,5 +1,6 @@ using HarmonyLib; using MiraAPI.Keybinds; +using MonoMod.Utils; using Rewired; namespace MiraAPI.Patches.Keybinds; @@ -20,5 +21,15 @@ public static class KeybindMenuPatch KeybindUtils.RewiredInputManager = __instance; KeybindManager.RewiredInit(); _registered = true; + + // Set these here right after input manager initializes + KeybindManager.VanillaKeybinds = new() + { + { typeof(KillButton), new VanillaKeybind(8) }, + { typeof(UseButton), new VanillaKeybind(6) }, + { typeof(ReportButton), new VanillaKeybind(7) }, + { typeof(VentButton), new VanillaKeybind(50) }, + { typeof(SabotageButton), new VanillaKeybind(4) }, + }; } } From b94e20c6f72142dab9071fb27ebe5140d61f7e28 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:29:33 -0500 Subject: [PATCH 53/86] Add Ability Button bind --- MiraAPI/Patches/Keybinds/KeybindsPatch.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs index 3a6cf24..c08a47f 100644 --- a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs +++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs @@ -30,6 +30,7 @@ public static class KeybindMenuPatch { typeof(ReportButton), new VanillaKeybind(7) }, { typeof(VentButton), new VanillaKeybind(50) }, { typeof(SabotageButton), new VanillaKeybind(4) }, + { typeof(AbilityButton), new VanillaKeybind(49) }, }; } } From 3d274c2f6157d97e69442b3b498556e75ed778de Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Tue, 9 Sep 2025 05:05:56 -0500 Subject: [PATCH 54/86] KEYBINDS WORK NOW --- MiraAPI/Keybinds/KeybindManager.cs | 4 +++- MiraAPI/Patches/HudManagerPatches.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs index a47a20a..ce212f0 100644 --- a/MiraAPI/Keybinds/KeybindManager.cs +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.Utilities; using Reactor.Utilities; using Rewired; @@ -17,7 +18,8 @@ public static class KeybindManager /// public static List Keybinds { get; } = new(); - internal static Dictionary VanillaKeybinds { get; set; } + [HideFromIl2Cpp] + internal static Dictionary VanillaKeybinds { get; set; } = []; internal static void RewiredInit() { diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 09ecf66..54e7fae 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -110,9 +110,9 @@ public static class HudManagerPatches { __instance.ReportButton.gameObject, 7 }, { __instance.ImpostorVentButton.gameObject, 50 }, { __instance.SabotageButton.gameObject, 4 }, + { __instance.AbilityButton.gameObject, 49 }, }; - KeybindManager.VanillaKeybinds.Clear(); foreach (var kvp in vanillaButtons) { var buttonObj = kvp.Key; From 03a37339662c3243363301708096f8e612bf85c3 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Tue, 9 Sep 2025 22:57:19 +0200 Subject: [PATCH 55/86] fix body prefab --- MiraAPI/Networking/CustomMurderRpc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 209afcd..e3005c9 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -222,7 +222,7 @@ public static class CustomMurderRpc if (createDeadBody) { - deadBody = Object.Instantiate(GameManager.Instance.DeadBodyPrefab); + deadBody = Object.Instantiate(GameManager.Instance.deadBodyPrefab[0]); deadBody.enabled = false; deadBody.ParentId = target.PlayerId; deadBody.bodyRenderers.ToList().ForEach(target.SetPlayerMaterialColors); From 70f63f8b16aa08d60152f95126933b6db8b4947b Mon Sep 17 00:00:00 2001 From: xChipseq Date: Tue, 9 Sep 2025 22:58:02 +0200 Subject: [PATCH 56/86] custom role fixes --- MiraAPI/Patches/Roles/RoleManagerPatches.cs | 2 +- MiraAPI/Roles/CustomRoleManager.cs | 10 +++++++++- MiraAPI/Roles/CustomRoleUtils.cs | 3 +-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index fa0cb5a..93daa82 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -33,7 +33,7 @@ public static class RoleManagerPatches data.Role.Deinitialize(targetPlayer); Object.Destroy(data.Role.gameObject); } - var roleBehaviour = Object.Instantiate(__instance.AllRoles.First(r => r.Role == roleType), data.gameObject.transform); + var roleBehaviour = Object.Instantiate(__instance.AllRoles.ToArray().First(r => r.Role == roleType), data.gameObject.transform); roleBehaviour.Initialize(targetPlayer); targetPlayer.Data.Role = roleBehaviour; targetPlayer.Data.RoleType = roleType; diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 5e1c3ae..d16153b 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -35,6 +35,11 @@ public static class CustomRoleManager public static readonly LoadableAsset ImpostorIntroSound = CustomRoleUtils.GetIntroSound(RoleTypes.Impostor)!; + /// + /// Gets the list of all roles from vanilla RoleManager. + /// + public static RoleBehaviour[] AllRoles => RoleManager.Instance.AllRoles.ToArray(); + /// /// Gets the list of custom roles as RoleBehaviour objects. /// @@ -57,7 +62,10 @@ public static class CustomRoleManager internal static void RegisterInRoleManager() { - RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray(); + foreach (var role in CustomRoles.Values) + { + RoleManager.Instance.AllRoles.Add(role); + } foreach (var role in CustomRoles.Values.Where(x => x.IsDead)) { diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index 01b402d..cb4dbfb 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text; using AmongUs.GameOptions; using MiraAPI.Utilities.Assets; -using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Roles; @@ -56,7 +55,7 @@ public static class CustomRoleUtils /// The intro sound. public static LoadableAsset? GetIntroSound(RoleTypes roleType) { - var role = RoleManager.Instance.AllRoles.FirstOrDefault(role => role.Role == roleType); + var role = CustomRoleManager.AllRoles.FirstOrDefault(role => role.Role == roleType); if (role is ICustomRole customRole) { return customRole.Configuration.IntroSound; From 2c0042e6a84e9fdd121c4880efb26abfddaeb23e Mon Sep 17 00:00:00 2001 From: xChipseq Date: Tue, 9 Sep 2025 22:58:18 +0200 Subject: [PATCH 57/86] bump to 2025.9.9 --- AmongUs.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AmongUs.props b/AmongUs.props index e0d7f3c..6f6e0f9 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -2,12 +2,12 @@ - + - + From dfa8b2971087baa3ee31c864c910ba4f8194684b Mon Sep 17 00:00:00 2001 From: xChipseq Date: Wed, 10 Sep 2025 22:07:47 +0200 Subject: [PATCH 58/86] update task adder (and improvements) --- MiraAPI/MiraApiSettings.cs | 6 + MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 480 ++++++++----------- MiraAPI/Resources/ModifierFile.png | Bin 0 -> 4664 bytes MiraAPI/Resources/RoleFile.png | Bin 0 -> 6070 bytes MiraAPI/Resources/TimedModifierFile.png | Bin 5246 -> 5220 bytes MiraAPI/Roles/CustomRoleConfiguration.cs | 11 + MiraAPI/Utilities/Assets/MiraAssets.cs | 10 + 7 files changed, 230 insertions(+), 277 deletions(-) create mode 100644 MiraAPI/Resources/ModifierFile.png create mode 100644 MiraAPI/Resources/RoleFile.png diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index aa4b29c..574212c 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -30,4 +30,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) /// Gets whether to show keybinds in the control mapper. /// public ConfigEntry ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true); + + /// + /// Gets whether to apply cosmetic changes to the TaskAdder. + /// + [LocalToggleSetting] + public ConfigEntry PrettyTaskAdder { get; private set; } = config.Bind("Freeplay", "Pretty Task Laptop", true); } diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 7f6a8b5..c96c609 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -1,15 +1,15 @@ -using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; -using AmongUs.GameOptions; +using System.Text; using HarmonyLib; +using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Roles; -using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; -using TMPro; +using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -21,6 +21,8 @@ namespace MiraAPI.Patches.Freeplay; internal static class TaskAdderPatches { private static Scroller _scroller = null!; + private static Dictionary folders = new(); + [HarmonyPostfix] [HarmonyPatch(nameof(TaskAdderGame.Begin))] public static void AddRolesFolder(TaskAdderGame __instance) @@ -60,46 +62,80 @@ internal static class TaskAdderPatches __instance.RoleButton.GetComponent().ClickMask = collider; __instance.RootFolderPrefab.GetComponent().ClickMask = collider; __instance.RootFolderPrefab.gameObject.SetActive(false); - __instance.TaskParent = inner.transform; - var rolesFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); - rolesFolder.gameObject.SetActive(false); - rolesFolder.FolderName = "Roles"; - rolesFolder.name = "RolesFolder"; + var crewmateFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == "Crewmate")!; + var impostorFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == "Impostor")!; + var neutralFolder = __instance.CreateFolder("Neutral", __instance.Root, 2, Color.gray); + var modifiersFolder = __instance.CreateFolder("Modifiers", __instance.Root, 0, Color.blue); - var modifiersFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); - modifiersFolder.gameObject.SetActive(false); - modifiersFolder.FolderName = "Modifiers"; - modifiersFolder.name = "ModifiersFolder"; + folders.Clear(); + folders.Add("Crewmate", crewmateFolder); + folders.Add("Impostor", impostorFolder); + folders.Add("Neutral", neutralFolder); + folders.Add("Modifiers", modifiersFolder); + int folderIdx = 3; foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins) { - if (plugin.InternalRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true })) + var pluginFolders = new Dictionary(); + + foreach (var role in plugin.InternalRoles + .Where(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true }) + .Select(x => x.Value)) { - var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); - newFolder.name = plugin.PluginId+":Roles"; - newFolder.FolderName = plugin.PluginInfo.Metadata.Name; - newFolder.gameObject.SetActive(false); - rolesFolder.SubFolders.Add(newFolder); + var customRole = role as ICustomRole; + var folderName = customRole!.Configuration.FreeplayFolder; + if (!folders.TryGetValue(folderName, out var teamFolder)) + { + teamFolder = __instance.CreateFolder(folderName, __instance.Root, folderIdx++, customRole.RoleColor); + folders.Add(folderName, teamFolder); + } + + if (!pluginFolders.TryGetValue(folderName, out var pluginFolder)) + { + pluginFolder = __instance.CreateFolder(plugin.PluginInfo.Metadata.Name, teamFolder); + pluginFolders.Add(folderName, teamFolder); + } + + pluginFolder.RoleChildren.Add(role); } if (plugin.InternalModifiers.Exists(x => x.ShowInFreeplay)) { - var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); - newFolder.name = plugin.PluginId+":Modifiers"; - newFolder.FolderName = plugin.PluginInfo.Metadata.Name; - newFolder.gameObject.SetActive(false); - modifiersFolder.SubFolders.Add(newFolder); + __instance.CreateFolder(plugin.PluginInfo.Metadata.Name, modifiersFolder) + .gameObject.name = plugin.PluginId; } } - __instance.Root.SubFolders.Add(rolesFolder); - __instance.Root.SubFolders.Add(modifiersFolder); + // Modifier folder at the end + __instance.Root.SubFolders.Insert(folderIdx, modifiersFolder); __instance.GoToRoot(); } + private static TaskFolder CreateFolder(this TaskAdderGame instance, string name, TaskFolder parent, int idx = -1, Color? color = null) + { + var folder = Object.Instantiate(instance.RootFolderPrefab, instance.transform); + folder.gameObject.SetActive(false); + folder.FolderName = name; + folder.SetCustomColor(color ?? new Color(0.937f, 0.811f, 0.592f)); + folder.name = name + "Folder"; + if (idx == -1) + parent.SubFolders.Add(folder); + else if (idx > 0) + parent.SubFolders.Insert(idx, folder); + return folder; + } + + private static void SetCustomColor(this TaskFolder folder, Color color) + { + folder.currentFolderColor = color; + folder.folderSpriteRenderer.color = color; + folder.buttonRolloverHandler.OutColor = color; + folder.buttonRolloverHandler.UnselectedColor = color; + } + private static void AddFileAsChildCustom( this TaskAdderGame instance, TaskAddButton item, @@ -122,40 +158,46 @@ internal static class TaskAdderPatches instance.ActiveItems.Add(item.transform); } + private static bool IsChildOf(this TaskFolder child, TaskFolder parent) => parent.SubFolders + .ToArray() + .Any(x => x.FolderName == child.FolderName); + // yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry: + // true -chip [HarmonyPrefix] [HarmonyPatch(nameof(TaskAdderGame.ShowFolder))] public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder) { - var stringBuilder = new Il2CppSystem.Text.StringBuilder(64); + StringBuilder stringBuilder = new StringBuilder(64); __instance.Hierarchy.Add(taskFolder); - for (var i = 0; i < __instance.Hierarchy.Count; i++) + foreach (var t in __instance.Hierarchy) { - stringBuilder.Append(__instance.Hierarchy.ToArray()[i].FolderName); - stringBuilder.Append("\\"); + stringBuilder.Append(t.FolderName); + stringBuilder.Append('\\'); } - __instance.PathText.text = stringBuilder.ToString(); - for (var j = 0; j < __instance.ActiveItems.Count; j++) + var prettyEnabled = LocalSettingsTabSingleton.Instance.PrettyTaskAdder.Value; + if (prettyEnabled) { - Object.Destroy(__instance.ActiveItems.ToArray()[j].gameObject); + __instance.PathText.fontSizeMin = 3; + __instance.PathText.fontSizeMax = 3; + __instance.transform.FindChild("TitleText_TMP").gameObject.SetActive(false); } + __instance.ActiveItems.ToArray().Do(x => x.gameObject.Destroy()); __instance.ActiveItems.Clear(); - var num = 0f; - var num2 = 0f; - var num3 = 0f; - for (var k = 0; k < taskFolder.SubFolders.Count; k++) - { - var taskFolder2 = Object.Instantiate(taskFolder.SubFolders.ToArray()[k], __instance.TaskParent); - var folderTransform = taskFolder2.transform; + float num = 0f; + float num2 = 0f; + float num3 = 0f; + foreach (var t in taskFolder.SubFolders) + { + TaskFolder taskFolder2 = Object.Instantiate(t, __instance.TaskParent); taskFolder2.gameObject.SetActive(true); taskFolder2.Parent = __instance; - - folderTransform.localPosition = new Vector3(num, num2, 0f); - folderTransform.localScale = Vector3.one; - num3 = Mathf.Max(num3, taskFolder2.Text.bounds.size.y + 1.3f); + taskFolder2.transform.localPosition = new Vector3(num, num2, 0f); + taskFolder2.transform.localScale = Vector3.one; + num3 = Mathf.Max(num3, taskFolder2.Text.bounds.size.y + 1.1f); num += __instance.folderWidth; if (num > __instance.lineWidth) { @@ -164,241 +206,67 @@ internal static class TaskAdderPatches num3 = 0f; } - __instance.ActiveItems.Add(folderTransform); - if (!taskFolder2 || !taskFolder2.Button) + __instance.ActiveItems.Add(taskFolder2.transform); + if (taskFolder2 != null && taskFolder2.Button != null) { - continue; - } - - ControllerManager.Instance.AddSelectableUiElement(taskFolder2.Button); - if (!string.IsNullOrEmpty(__instance.restorePreviousSelectionByFolderName) && - taskFolder2.FolderName.Equals(__instance.restorePreviousSelectionByFolderName, StringComparison.Ordinal)) - { - __instance.restorePreviousSelectionFound = taskFolder2.Button; + ControllerManager.Instance.AddSelectableUiElement(taskFolder2.Button); } } - var flag = false; - var list = taskFolder.Children.ToArray().OrderBy(t => t.TaskType).ToList(); - - for (var l = 0; l < list.Count; l++) + var list = taskFolder.TaskChildren + .ToArray() + .OrderBy(x => x.TaskType) + .ToArray(); + foreach (var task in list) { - var taskAddButton = Object.Instantiate(__instance.TaskPrefab); - taskAddButton.MyTask = list.ToArray()[l]; - switch (taskAddButton.MyTask.TaskType) + TaskAddButton taskAddButton = Object.Instantiate(__instance.TaskPrefab); + taskAddButton.MyTask = task; + switch (task.TaskType) { case TaskTypes.DivertPower: - { - var targetSystem = taskAddButton.MyTask.Cast().TargetSystem; - taskAddButton.Text.text = TranslationController.Instance.GetString( - StringNames.DivertPowerTo, - TranslationController.Instance.GetString(targetSystem)); - break; - } + { + var targetSystem = task.Cast().TargetSystem; + taskAddButton.Text.text = TranslationController.Instance.GetString( + StringNames.DivertPowerTo, + TranslationController.Instance.GetString(targetSystem)); + break; + } case TaskTypes.FixWeatherNode: - { - var nodeId = taskAddButton.MyTask.Cast().NodeId; - taskAddButton.Text.text = - TranslationController.Instance.GetString( - StringNames.FixWeatherNode) + " " + - TranslationController.Instance.GetString( - WeatherSwitchGame.ControlNames[nodeId]); - break; - } + { + var nodeId = task.Cast().NodeId; + taskAddButton.Text.text = + TranslationController.Instance.GetString( + StringNames.FixWeatherNode) + " " + + TranslationController.Instance.GetString( + WeatherSwitchGame.ControlNames[nodeId]); + break; + } default: taskAddButton.Text.text = - TranslationController.Instance.GetString(taskAddButton.MyTask.TaskType); + TranslationController.Instance.GetString(task.TaskType); break; } __instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); - if (taskAddButton.Button == null) + if (taskAddButton != null && taskAddButton.Button != null) { - continue; - } - - ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); - if (__instance.Hierarchy.Count == 1 || flag) - { - continue; - } - - var component = - ControllerManager.Instance.CurrentUiState.CurrentSelection.GetComponent(); - if (component != null) - { - __instance.restorePreviousSelectionByFolderName = component.FolderName; - } - - ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button); - flag = true; - } - - if (taskFolder.FolderName == "Roles") - { - for (var m = 0; m < RoleManager.Instance.AllRoles.Length; m++) - { - var roleBehaviour = RoleManager.Instance.AllRoles[m]; - if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || - CustomRoleManager.CustomRoles.ContainsKey((ushort)roleBehaviour.Role)) - { - continue; - } - - var taskAddButton2 = Object.Instantiate(__instance.RoleButton); - taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; - taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); - taskAddButton2.Role = roleBehaviour; - - if (taskAddButton2.Button == null) - { - continue; - } - - ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); - switch (m) - { - case 0 when __instance.restorePreviousSelectionFound != null: - ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound); - __instance.restorePreviousSelectionByFolderName = string.Empty; - __instance.restorePreviousSelectionFound = null; - break; - case 0: - ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); - break; - } + ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button, false); } } - if (taskFolder) + var roleChildren = taskFolder.RoleChildren + .ToArray() + .Where(x => !(taskFolder.IsChildOf(__instance.Root) && x is ICustomRole)) + .ToArray(); + foreach (var role in roleChildren) { - var split = taskFolder.name.Split(':'); - if (split.Length == 2) - { - var pluginId = split[0]; - var folderName = split[1]; - - if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) - { - if (folderName == "Roles(Clone)") - { - PluginRoles(__instance, plugin, ref num, ref num2, ref num3); - } - else if (folderName == "Modifiers(Clone)") - { - PluginModifiers(__instance, plugin, ref num, ref num2, ref num3); - } - } - } - } - - foreach (var chip in __instance.ActiveItems) - { - chip.GetComponentInChildren().EnableStencilMasking(); - chip.GetComponent().maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - } - - if (_scroller) - { - _scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 4.5f, 1.65f); - _scroller.SetYBoundsMin(0.0f); - _scroller.ScrollToTop(); - } - - if (__instance.Hierarchy.Count == 1) - { - ControllerManager.Instance.SetBackButton(__instance.BackButton); - return false; - } - - ControllerManager.Instance.SetBackButton(__instance.FolderBackButton); - return false; - } - - private static void PluginModifiers( - TaskAdderGame instance, - MiraPluginInfo plugin, - ref float num, - ref float num2, - ref float num3) - { - for (var m = 0; m < plugin.InternalModifiers.Count; m++) - { - var modifier = plugin.InternalModifiers[m]; - if (!modifier.ShowInFreeplay) - { - continue; - } - - var taskAddButton = Object.Instantiate(instance.RoleButton); - taskAddButton.name = modifier.TypeId.ToString(NumberFormatInfo.InvariantInfo); - taskAddButton.role = null; - taskAddButton.MyTask = null; - taskAddButton.SafePositionWorld = instance.SafePositionWorld; - taskAddButton.Text.text = modifier.ModifierName; - taskAddButton.Text.fontSizeMin = 1; - taskAddButton.Text.EnableMasking(); - taskAddButton.FileImage.color = modifier.FreeplayFileColor; - taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor; - taskAddButton.Button.OnClick = new Button.ButtonClickedEvent(); - taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => - { - var id = modifier.TypeId; - if (PlayerControl.LocalPlayer.HasModifier(id)) - { - PlayerControl.LocalPlayer.RemoveModifier(id); - taskAddButton.Overlay.enabled = false; - } - else - { - PlayerControl.LocalPlayer.AddModifier(id); - taskAddButton.Overlay.enabled = true; - } - })); - - if (modifier is TimedModifier timed) - { - taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset(); - taskAddButton.Text.text += $"({timed.Duration}s)"; - } - - instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); - - ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); - switch (m) - { - case 0 when instance.restorePreviousSelectionFound != null: - ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound); - instance.restorePreviousSelectionByFolderName = string.Empty; - instance.restorePreviousSelectionFound = null; - break; - case 0: - ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button); - break; - } - } - } - - private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3) - { - for (var m = 0; m < plugin.InternalRoles.Count; m++) - { - var roleBehaviour = plugin.InternalRoles.ElementAt(m).Value; - if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || - roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false }) - { - continue; - } - - var taskAddButton2 = Object.Instantiate(instance.RoleButton); - taskAddButton2.SafePositionWorld = instance.SafePositionWorld; - taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - taskAddButton2.Text.EnableMasking(); - - instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); - taskAddButton2.Role = roleBehaviour; - if (roleBehaviour is ICustomRole custom) + TaskAddButton roleAddButton = Object.Instantiate(__instance.RoleButton); + roleAddButton.SafePositionWorld = __instance.SafePositionWorld; + roleAddButton.Text.text = prettyEnabled ? role.NiceName : "Be_" + role.NiceName + ".exe"; + roleAddButton.Text.fontSizeMin = 1; + roleAddButton.Role = role; + if (prettyEnabled) roleAddButton.FileImage.sprite = MiraAssets.RoleFile.LoadAsset(); + if (role is ICustomRole custom) { var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray; if (custom.Team is ModdedRoleTeams.Crewmate) @@ -409,26 +277,84 @@ internal static class TaskAdderPatches { customColor = Palette.ImpostorRed; } - taskAddButton2.FileImage.color = customColor; - taskAddButton2.RolloverHandler.OutColor = customColor; + roleAddButton.FileImage.color = customColor; + roleAddButton.RolloverHandler.OutColor = customColor; } - if (taskAddButton2.Button == null) + __instance.AddFileAsChildCustom(roleAddButton, ref num, ref num2, ref num3); + if (roleAddButton != null && roleAddButton.Button != null) { - continue; - } - - ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); - switch (m) - { - case 0 when instance.restorePreviousSelectionFound != null: - ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound); - instance.restorePreviousSelectionByFolderName = string.Empty; - instance.restorePreviousSelectionFound = null; - break; - case 0: - ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); - break; + ControllerManager.Instance.AddSelectableUiElement(roleAddButton.Button); } } + + if (folders.TryGetValue("Modifiers", out var modifiersFolder) && taskFolder.IsChildOf(modifiersFolder)) + { + var plugin = MiraPluginManager.GetPluginByGuid(taskFolder.gameObject.name.Replace("(Clone)", string.Empty)); + if (plugin != null) + { + foreach (var modifier in plugin.InternalModifiers) + { + if (!modifier.ShowInFreeplay) + { + continue; + } + + var taskAddButton = Object.Instantiate(__instance.RoleButton); + taskAddButton.name = modifier.TypeId.ToString(NumberFormatInfo.InvariantInfo); + taskAddButton.role = null; + taskAddButton.MyTask = null; + taskAddButton.SafePositionWorld = __instance.SafePositionWorld; + taskAddButton.Text.text = modifier.ModifierName; + taskAddButton.Text.fontSizeMin = 1; + taskAddButton.Text.EnableMasking(); + taskAddButton.FileImage.color = modifier.FreeplayFileColor; + taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor; + if (modifier is TimedModifier timed) + { + taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset(); + taskAddButton.Text.text += $" ({timed.Duration}s)"; + } + else + { + taskAddButton.FileImage.sprite = MiraAssets.ModifierFile.LoadAsset(); + } + + taskAddButton.Button.OnClick = new Button.ButtonClickedEvent(); + var m = modifier; + taskAddButton.Button.OnClick.AddListener((UnityAction)(() => + { + var id = m.TypeId; + if (PlayerControl.LocalPlayer.HasModifier(id)) + { + PlayerControl.LocalPlayer.RemoveModifier(id); + taskAddButton.Overlay.enabled = false; + } + else + { + PlayerControl.LocalPlayer.AddModifier(id); + taskAddButton.Overlay.enabled = true; + } + })); + + __instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); + + ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); + } + } + } + + if (_scroller) + { + _scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 4.5f, 1.65f); + _scroller.SetYBoundsMin(0.0f); + _scroller.ScrollToTop(); + } + + ControllerManager.Instance.SetBackButton(__instance.Hierarchy.Count == 1 + ? __instance.BackButton + : __instance.FolderBackButton); + + __instance.SetPreviousControllerSelection(taskFolder.FolderName); + return false; } } diff --git a/MiraAPI/Resources/ModifierFile.png b/MiraAPI/Resources/ModifierFile.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5b78d47a57e55be95822216f9bf6c9226f4b75 GIT binary patch literal 4664 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 O`$J1jPqkJV9`!%7oVh~) literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/RoleFile.png b/MiraAPI/Resources/RoleFile.png new file mode 100644 index 0000000000000000000000000000000000000000..534d6b7a91437ae3eec9307eafdac42a23fc8d46 GIT binary patch literal 6070 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 delta 4614 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 diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 5b0158d..3974665 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -32,6 +32,12 @@ public record struct CustomRoleConfiguration TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate; HideSettings = roleBehaviour?.IsDead == true; ShowInFreeplay = roleBehaviour?.IsDead == false; + FreeplayFolder = role.Team switch + { + ModdedRoleTeams.Crewmate => "Crewmate", + ModdedRoleTeams.Impostor => "Impostor", + _ => "Neutral", + }; IntroSound = role.Team is ModdedRoleTeams.Crewmate ? CustomRoleManager.CrewmateIntroSound : CustomRoleManager.ImpostorIntroSound; @@ -112,6 +118,11 @@ public record struct CustomRoleConfiguration /// public bool ShowInFreeplay { get; set; } + /// + /// Gets or sets a freeplay folder this role belongs to. Defaults to team folder. + /// + public string FreeplayFolder { get; set; } + /// /// Gets or sets the outline color for the KillButton if is true. /// diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 1e39c28..19c633b 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -105,6 +105,16 @@ public static class MiraAssets /// public static LoadableResourceAsset KeybindButton { get; } = new("MiraAPI.Resources.KeybindButton.png"); + /// + /// Gets the sprite used for role file in TaskAdderGame. + /// + public static LoadableResourceAsset RoleFile { get; } = new("MiraAPI.Resources.RoleFile.png"); + + /// + /// Gets the sprite used for modifier file in TaskAdderGame. + /// + public static LoadableResourceAsset ModifierFile { get; } = new("MiraAPI.Resources.ModifierFile.png"); + /// /// Gets the sprite used for timed modifier file in TaskAdderGame. /// From 790b37e8d35d2a61e39b0b2174a943f9c767bac5 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Wed, 10 Sep 2025 22:40:09 +0200 Subject: [PATCH 59/86] cool role file icons --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 12 +++++++++--- MiraAPI/Resources/CrewmateFile.png | Bin 0 -> 5123 bytes MiraAPI/Resources/CustomFile.png | Bin 0 -> 5274 bytes MiraAPI/Resources/ImpostorFile.png | Bin 0 -> 6323 bytes MiraAPI/Resources/RoleFile.png | Bin 6070 -> 0 bytes MiraAPI/Utilities/Assets/MiraAssets.cs | 14 ++++++++++++-- 6 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 MiraAPI/Resources/CrewmateFile.png create mode 100644 MiraAPI/Resources/CustomFile.png create mode 100644 MiraAPI/Resources/ImpostorFile.png delete mode 100644 MiraAPI/Resources/RoleFile.png diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index c96c609..77dc106 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -88,7 +88,7 @@ internal static class TaskAdderPatches var folderName = customRole!.Configuration.FreeplayFolder; if (!folders.TryGetValue(folderName, out var teamFolder)) { - teamFolder = __instance.CreateFolder(folderName, __instance.Root, folderIdx++, customRole.RoleColor); + teamFolder = __instance.CreateFolder(folderName, __instance.Root, folderIdx++, customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray); folders.Add(folderName, teamFolder); } @@ -263,11 +263,17 @@ internal static class TaskAdderPatches TaskAddButton roleAddButton = Object.Instantiate(__instance.RoleButton); roleAddButton.SafePositionWorld = __instance.SafePositionWorld; roleAddButton.Text.text = prettyEnabled ? role.NiceName : "Be_" + role.NiceName + ".exe"; - roleAddButton.Text.fontSizeMin = 1; roleAddButton.Role = role; - if (prettyEnabled) roleAddButton.FileImage.sprite = MiraAssets.RoleFile.LoadAsset(); + if (prettyEnabled) + { + roleAddButton.Text.fontSizeMin = 1; + roleAddButton.FileImage.sprite = role.IsImpostor + ? MiraAssets.ImpostorFile.LoadAsset() + : MiraAssets.CrewmateFile.LoadAsset(); + } if (role is ICustomRole custom) { + if (prettyEnabled && custom.Team == ModdedRoleTeams.Custom) roleAddButton.FileImage.sprite = MiraAssets.CustomTeamFile.LoadAsset(); var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray; if (custom.Team is ModdedRoleTeams.Crewmate) { diff --git a/MiraAPI/Resources/CrewmateFile.png b/MiraAPI/Resources/CrewmateFile.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf1e743989ec861d38c58dee91c820b6191981e GIT binary patch literal 5123 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 - /// Gets the sprite used for role file in TaskAdderGame. + /// Gets the sprite used for crewmate role files in TaskAdderGame. /// - public static LoadableResourceAsset RoleFile { get; } = new("MiraAPI.Resources.RoleFile.png"); + public static LoadableResourceAsset CrewmateFile { get; } = new("MiraAPI.Resources.CrewmateFile.png"); + + /// + /// Gets the sprite used for impostor role files in TaskAdderGame. + /// + public static LoadableResourceAsset ImpostorFile { get; } = new("MiraAPI.Resources.ImpostorFile.png"); + + /// + /// Gets the sprite used for custom team role files in TaskAdderGame. + /// + public static LoadableResourceAsset CustomTeamFile { get; } = new("MiraAPI.Resources.CustomFile.png"); /// /// Gets the sprite used for modifier file in TaskAdderGame. From 834d1d878e4f20219cccc21dbfc29d6c415d6017 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Wed, 10 Sep 2025 22:45:48 +0200 Subject: [PATCH 60/86] fix --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 77dc106..59d87dd 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -181,7 +181,7 @@ internal static class TaskAdderPatches { __instance.PathText.fontSizeMin = 3; __instance.PathText.fontSizeMax = 3; - __instance.transform.FindChild("TitleText_TMP").gameObject.SetActive(false); + __instance.transform.FindChild("TitleText_TMP")?.gameObject.DestroyImmediate(); } __instance.ActiveItems.ToArray().Do(x => x.gameObject.Destroy()); From 5fd57633ff96c60227f3ea1f7a3ad3b4db367dc1 Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Thu, 11 Sep 2025 11:01:35 +0100 Subject: [PATCH 61/86] Update RoleSettingMenuPatches.cs --- .../Patches/Options/RoleSettingMenuPatches.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index d99d2e5..d6eb035 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -532,6 +532,18 @@ public static class RoleSettingMenuPatches roleOptionSetting.titleText.color = customRole.OptionsMenuColor.FindAlternateColor(); roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left; + if (customRole.Configuration.Icon != null) + { + var roleIcon = new GameObject("RoleIcon"); + roleIcon.transform.parent = roleOptionSetting.transform; + roleIcon.transform.localScale = new(.25f, .25f, 1); + roleIcon.layer = LayerMask.NameToLayer("UI"); + roleIcon.transform.localPosition = new Vector3(-1.3f, -0.3f, -2f); + var rend = roleIcon.AddComponent(); + rend.sprite = customRole.Configuration.Icon.LoadAsset(); + rend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + } + if (GameSettingMenuPatches.SelectedMod is null || GameSettingMenuPatches.SelectedMod.InternalOptionGroups .Exists(x => x.GroupVisible() && x.OptionableType == role.GetType() && x.Options.Any(y => y.Visible()))) @@ -551,18 +563,6 @@ public static class RoleSettingMenuPatches passiveButton.interactableHoveredColor = Color.white; passiveButton.OnClick.AddListener((UnityAction)(() => { ChangeTab(role, __instance); })); - - if (customRole.Configuration.Icon != null) - { - var roleIcon = new GameObject("RoleIcon"); - roleIcon.transform.parent = roleOptionSetting.transform; - roleIcon.transform.localScale = new(.25f, .25f, 1); - roleIcon.layer = LayerMask.NameToLayer("UI"); - roleIcon.transform.localPosition = new Vector3(-1.3f, -0.3f, -2f); - var rend = roleIcon.AddComponent(); - rend.sprite = customRole.Configuration.Icon.LoadAsset(); - rend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - } } if (customRole.Configuration is { MaxRoleCount: 0 }) From 5492c2b3cc0e56317d05e297595a0bb84b66a0c2 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 12 Sep 2025 22:02:21 +0200 Subject: [PATCH 62/86] fix how to play role tutorial --- MiraAPI/Patches/Roles/HowToPlayPatch.cs | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 MiraAPI/Patches/Roles/HowToPlayPatch.cs diff --git a/MiraAPI/Patches/Roles/HowToPlayPatch.cs b/MiraAPI/Patches/Roles/HowToPlayPatch.cs new file mode 100644 index 0000000..c080ea0 --- /dev/null +++ b/MiraAPI/Patches/Roles/HowToPlayPatch.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; +using HarmonyLib; +using MiraAPI.Roles; +using UnityEngine; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(HowToPlayScene), nameof(HowToPlayScene.OpenRolesSelectionMenu))] +internal static class HowToPlayPatch +{ + // yes i patched the entire method + private static void Prefix(HowToPlayScene __instance) + { + __instance.sceneIndex = 0; + __instance.category = HowToPlayScene.HowToPlayCategory.RolesSelection; + __instance.startPage.SetActive(false); + if (__instance.roleButtonsParent.childCount == 0) + { + using (IEnumerator enumerator = RoleManager.Instance.AllRoles.ToArray().Where(x => !x.IsCustomRole()).GetEnumerator()) + { + while (enumerator.MoveNext()) + { + RoleBehaviour role = enumerator.Current; + if (!role.IsSimpleRole && role.Role != RoleTypes.CrewmateGhost && role.Role != RoleTypes.ImpostorGhost) + { + HowToPlayRoleButton component = Object.Instantiate(__instance.roleButtonPrefab, __instance.roleButtonsParent).GetComponent(); + Sprite roleIcon = __instance.rolesScenes.ToArray().First(r => r.role == role.Role).roleIcon; + component.SetRoleInfo(role, roleIcon); + component.SetButtonAction((Il2CppSystem.Action)(() => + { + __instance.OpenRolePage(role.Role); + })); + __instance.controllerSelectables.Add(component.GetComponent()); + } + } + } + foreach (UiElement uiElement in __instance.controllerSelectables) + { + uiElement.ReceiveMouseOut(); + } + ControllerManager.Instance.NewScene(__instance.name, __instance.closeButton, __instance.defaultButtonSelected, __instance.controllerSelectables, false); + } + __instance.DisableAllScenes(); + __instance.roleSelectionScene.SetActive(true); + ControllerManager.Instance.SetDefaultSelection(__instance.defaultButtonSelected, null); + } +} \ No newline at end of file From 810fd54e10d7d2b65ca01bc16bd1746f9864e3b5 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 12 Sep 2025 22:33:05 +0200 Subject: [PATCH 63/86] i love useless using thingies --- MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 07d37f5..3e4053e 100644 --- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -1,7 +1,5 @@ using HarmonyLib; using MiraAPI.Modifiers; -using MiraAPI.Roles; -using UnityEngine; namespace MiraAPI.Patches.Freeplay; From 81887c34d9e3bb39da0ad3a4f24bbd4aed76865e Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 12 Sep 2025 23:23:10 +0200 Subject: [PATCH 64/86] fix misaligned settings (again) --- MiraAPI/LocalSettings/LocalSettingsTab.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index dee9ae1..ce78c85 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -128,6 +128,7 @@ public abstract class LocalSettingsTab(ConfigFile config) { CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset); contentOrder = 1; + contentIndex = 1; } foreach (var setting in pair.Value) From 1b6a2c4eb3f537c765d76b4c26a2c32fe5fa09f0 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Fri, 12 Sep 2025 23:49:24 +0200 Subject: [PATCH 65/86] part 1 of trying to fix the task adder --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 92 ++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 59d87dd..6ae86cf 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -2,7 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using AmongUs.GameOptions; +using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppSystem; using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; @@ -114,6 +118,94 @@ internal static class TaskAdderPatches __instance.GoToRoot(); } + [HarmonyPrefix] + [HarmonyPatch(nameof(TaskAdderGame.PopulateRoot))] + private static bool PopulateRootPrefix(TaskAdderGame __instance, TaskAdderGame.FolderType folderType, TaskFolder rootFolder, Il2CppSystem.Collections.Generic.Dictionary folders, Il2CppReferenceArray taskList) + { + if (folderType != TaskAdderGame.FolderType.Tasks) + { + if (folderType != TaskAdderGame.FolderType.Roles) + { + return false; + } + + TaskFolder crewFolder = folders["Crewmates"] = + Object.Instantiate(__instance.RootFolderPrefab, __instance.transform); + crewFolder.gameObject.SetActive(false); + crewFolder.SetFolderColor(TaskFolder.FolderColor.Blue); + TaskFolder impFolder = folders["Impostors"] = + 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); + rootFolder.SubFolders.Insert(0, crewFolder); + rootFolder.SubFolders.Insert(0, impFolder); + Il2CppSystem.Collections.Generic.List impRoles = new(); + Il2CppSystem.Collections.Generic.List crewRoles = new(); + foreach (var role in RoleManager.Instance.AllRoles) + { + if (role.Role != RoleTypes.ImpostorGhost && role.Role != RoleTypes.CrewmateGhost && + !role.IsCustomRole()) + { + if (role.TeamType == RoleTeamTypes.Crewmate) + { + crewRoles.Add(role); + } + else + { + impRoles.Add(role); + } + } + } + + crewFolder.RoleChildren = crewRoles; + impFolder.RoleChildren = impRoles; + return false; + } + + if (taskList == null) + { + return false; + } + + foreach (NormalPlayerTask normalPlayerTask in taskList) + { + SystemTypes systemTypes = normalPlayerTask.StartAt; + if (normalPlayerTask is DivertPowerTask task) + { + systemTypes = task.TargetSystem; + } + + if (systemTypes == SystemTypes.LowerEngine) + { + systemTypes = SystemTypes.UpperEngine; + } + + if (!folders.TryGetValue(systemTypes.ToString(), out var taskFolder3)) + { + taskFolder3 = folders[systemTypes.ToString()] = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform); + taskFolder3.SetFolderColor(TaskFolder.FolderColor.Tan); + taskFolder3.gameObject.SetActive(false); + if (systemTypes == SystemTypes.UpperEngine) + { + taskFolder3.FolderName = TranslationController.Instance.GetString(StringNames.Engines); + } + else + { + taskFolder3.FolderName = TranslationController.Instance.GetString(systemTypes); + } + + rootFolder.SubFolders.Add(taskFolder3); + } + + taskFolder3.TaskChildren.Add(normalPlayerTask); + } + + return false; + } + + private static TaskFolder CreateFolder(this TaskAdderGame instance, string name, TaskFolder parent, int idx = -1, Color? color = null) { var folder = Object.Instantiate(instance.RootFolderPrefab, instance.transform); From 9f11874189d57a2fcbcc641876644479c6806015 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:15:06 -0400 Subject: [PATCH 66/86] fix role settings --- MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs index d867139..7f53b0a 100644 --- a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs +++ b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs @@ -9,17 +9,17 @@ namespace MiraAPI.Patches.Roles; /// /// Patches to return the correct role counts. /// -[HarmonyPatch(typeof(RoleOptionsCollectionV09))] +[HarmonyPatch(typeof(RoleOptionsCollectionV10))] public static class RoleOptionsCollectionPatch { /// /// This patch fixes GetNumPerGame being inlined (2025.5.20) in the original code. /// [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV09.AnyRolesEnabled))] - public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV09 __instance, ref bool __result) + [HarmonyPatch(nameof(RoleOptionsCollectionV10.AnyRolesEnabled))] + public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV10 __instance, ref bool __result) { - foreach (KeyValuePair keyValuePair in __instance.roles) + foreach (KeyValuePair keyValuePair in __instance.roles) { if (__instance.GetNumPerGame(keyValuePair.Key) > 0) { @@ -34,7 +34,7 @@ public static class RoleOptionsCollectionPatch /// /// Return false to skip original method, true to not. [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV09.GetChancePerGame))] + [HarmonyPatch(nameof(RoleOptionsCollectionV10.GetChancePerGame))] public static bool GetChancePrefix(RoleTypes role, ref int __result) { if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null) @@ -64,7 +64,7 @@ public static class RoleOptionsCollectionPatch /// /// Return false to skip original method, true to not. [HarmonyPrefix] - [HarmonyPatch(nameof(RoleOptionsCollectionV09.GetNumPerGame))] + [HarmonyPatch(nameof(RoleOptionsCollectionV10.GetNumPerGame))] public static bool GetNumPrefix(RoleTypes role, ref int __result) { if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null) From 790d868759a15da66c38d45dce142ea69aadff5f Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:26:58 -0400 Subject: [PATCH 67/86] fix viper --- MiraAPI/Patches/PlayerControlPatches.cs | 45 +++++++++++++++++++++- MiraAPI/Patches/Roles/KillButtonPatches.cs | 5 +++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index a0b88cd..ac88d94 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Globalization; +using System.Linq; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; @@ -66,6 +67,48 @@ internal static class PlayerControlPatches } } + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerControl.CheckMurder))] + public static void PlayerControlCheckMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool __runOriginal) + { + __runOriginal = false; + + __instance.logger.Debug($"Checking if {__instance.PlayerId} murdered {(target == null ? "null player" : target.PlayerId.ToString(NumberFormatInfo.InvariantInfo))}"); + + __instance.isKilling = false; + if (AmongUsClient.Instance.IsGameOver || !AmongUsClient.Instance.AmHost) + { + return; + } + + if (!target || __instance.Data.IsDead || !__instance.Data.Role.IsImpostor || __instance.Data.Disconnected) + { + int num = target ? target!.PlayerId : -1; + __instance.logger.Warning($"Bad kill from {__instance.PlayerId} to {num}"); + __instance.RpcMurderPlayer(target, false); + return; + } + + NetworkedPlayerInfo data = target!.Data; + if (data == null || data.IsDead || target.inVent || target.MyPhysics.Animations.IsPlayingEnterVentAnimation() || + target.MyPhysics.Animations.IsPlayingAnyLadderAnimation() || target.inMovingPlat) + { + __instance.logger.Warning("Invalid target data for kill"); + __instance.RpcMurderPlayer(target, false); + return; + } + + if (MeetingHud.Instance) + { + __instance.logger.Warning("Tried to kill while a meeting was starting"); + __instance.RpcMurderPlayer(target, false); + return; + } + + __instance.isKilling = true; + __instance.RpcMurderPlayer(target, true); + } + [HarmonyPrefix] [HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))] // ReSharper disable once InconsistentNaming diff --git a/MiraAPI/Patches/Roles/KillButtonPatches.cs b/MiraAPI/Patches/Roles/KillButtonPatches.cs index 2a04e9d..902a9e6 100644 --- a/MiraAPI/Patches/Roles/KillButtonPatches.cs +++ b/MiraAPI/Patches/Roles/KillButtonPatches.cs @@ -58,6 +58,11 @@ public static class KillButtonPatches return false; } + if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole) + { + return true; + } + PlayerControl.LocalPlayer.RpcCustomMurder(__instance.currentTarget); __instance.SetTarget(null); From f3175990d3431c40d2fc97bd3ea9c8e0ab3a0788 Mon Sep 17 00:00:00 2001 From: Pietro420 <77608535+Pietrodjaowjao@users.noreply.github.com> Date: Sun, 14 Sep 2025 20:07:17 -0300 Subject: [PATCH 68/86] ACTUALLY fix Viper --- MiraAPI/Patches/PlayerControlPatches.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index ac88d94..df73774 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -105,20 +105,16 @@ internal static class PlayerControlPatches return; } - __instance.isKilling = true; - __instance.RpcMurderPlayer(target, true); - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))] - // ReSharper disable once InconsistentNaming - // Note: This method is partially inlined in 2025.5.20 but we already use custom murder RPCs. - public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed) - { var beforeMurderEvent = new BeforeMurderEvent(__instance, target); MiraEventManager.InvokeEvent(beforeMurderEvent); - didSucceed = beforeMurderEvent.IsCancelled; + if (beforeMurderEvent.IsCancelled) + { + return; + } + + __instance.isKilling = true; + __instance.RpcMurderPlayer(target, true); } [HarmonyPostfix] From c4cbf0b18d5ba61f2a56bbfa603250ea8667ff2e Mon Sep 17 00:00:00 2001 From: xChipseq Date: Wed, 17 Sep 2025 19:19:47 +0200 Subject: [PATCH 69/86] use the old system instead of role children --- .../SettingTypes/LocalSettingBase.cs | 2 +- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 91 ++++++++++++++----- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index d144ce4..629cd45 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -53,7 +53,7 @@ public abstract class LocalSettingBase : ILocalSetting } /// - /// Gets the value of the config entry, casted to the setting type. + /// Gets the value of the config entry, cast to the setting type. /// /// The value. public virtual T GetValue() diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 6ae86cf..9e89db8 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -3,16 +3,15 @@ using System.Globalization; using System.Linq; using System.Text; using AmongUs.GameOptions; -using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; -using Il2CppSystem; using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities.Assets; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; @@ -99,10 +98,9 @@ internal static class TaskAdderPatches if (!pluginFolders.TryGetValue(folderName, out var pluginFolder)) { pluginFolder = __instance.CreateFolder(plugin.PluginInfo.Metadata.Name, teamFolder); + pluginFolder.name = $"Roles:{folderName}:{plugin.PluginId}"; pluginFolders.Add(folderName, teamFolder); } - - pluginFolder.RoleChildren.Add(role); } if (plugin.InternalModifiers.Exists(x => x.ShowInFreeplay)) @@ -348,7 +346,7 @@ internal static class TaskAdderPatches var roleChildren = taskFolder.RoleChildren .ToArray() - .Where(x => !(taskFolder.IsChildOf(__instance.Root) && x is ICustomRole)) + .Where(x => x is not ICustomRole) .ToArray(); foreach (var role in roleChildren) { @@ -363,21 +361,7 @@ internal static class TaskAdderPatches ? MiraAssets.ImpostorFile.LoadAsset() : MiraAssets.CrewmateFile.LoadAsset(); } - if (role is ICustomRole custom) - { - if (prettyEnabled && custom.Team == ModdedRoleTeams.Custom) roleAddButton.FileImage.sprite = MiraAssets.CustomTeamFile.LoadAsset(); - var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray; - if (custom.Team is ModdedRoleTeams.Crewmate) - { - customColor = Palette.CrewmateBlue; - } - else if (custom.Team is ModdedRoleTeams.Impostor) - { - customColor = Palette.ImpostorRed; - } - roleAddButton.FileImage.color = customColor; - roleAddButton.RolloverHandler.OutColor = customColor; - } + __instance.AddFileAsChildCustom(roleAddButton, ref num, ref num2, ref num3); if (roleAddButton != null && roleAddButton.Button != null) { @@ -385,9 +369,73 @@ internal static class TaskAdderPatches } } + var split = taskFolder + .name + .Replace("(Clone)", string.Empty) + .Split(':'); + if (split is ["Roles", _, _] && folders.Any(x => taskFolder.IsChildOf(x.Value))) + { + var plugin = MiraPluginManager.GetPluginByGuid(split[2]); + if (plugin != null) + { + foreach (var role in plugin.InternalRoles) + { + var roleBehaviour = role.Value; + if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || + roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false }) + { + continue; + } + + var teamFolder = roleBehaviour.IsImpostor ? "Impostor" : "Crewmate"; + if (roleBehaviour is ICustomRole cs) + { + teamFolder = cs.Configuration.FreeplayFolder; + } + if (split[1] != teamFolder) + { + continue; + } + + var roleAddButton = Object.Instantiate(__instance.RoleButton); + roleAddButton.MyTask = null; + roleAddButton.SafePositionWorld = __instance.SafePositionWorld; + roleAddButton.Text.text = prettyEnabled ? roleBehaviour.NiceName : "Be_" + roleBehaviour.NiceName + ".exe"; + roleAddButton.Text.fontSizeMin = 1; + roleAddButton.Text.EnableMasking(); + roleAddButton.role = roleBehaviour; + if (prettyEnabled) + { + roleAddButton.Text.fontSizeMin = 1; + roleAddButton.FileImage.sprite = roleBehaviour.IsImpostor + ? MiraAssets.ImpostorFile.LoadAsset() + : MiraAssets.CrewmateFile.LoadAsset(); + } + if (roleBehaviour is ICustomRole custom) + { + if (prettyEnabled && custom.Team == ModdedRoleTeams.Custom) roleAddButton.FileImage.sprite = MiraAssets.CustomTeamFile.LoadAsset(); + var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray; + if (custom.Team is ModdedRoleTeams.Crewmate) + { + customColor = Palette.CrewmateBlue; + } + else if (custom.Team is ModdedRoleTeams.Impostor) + { + customColor = Palette.ImpostorRed; + } + roleAddButton.FileImage.color = customColor; + roleAddButton.RolloverHandler.OutColor = customColor; + } + + __instance.AddFileAsChildCustom(roleAddButton, ref num, ref num2, ref num3); + ControllerManager.Instance.AddSelectableUiElement(roleAddButton.Button); + } + } + } + if (folders.TryGetValue("Modifiers", out var modifiersFolder) && taskFolder.IsChildOf(modifiersFolder)) { - var plugin = MiraPluginManager.GetPluginByGuid(taskFolder.gameObject.name.Replace("(Clone)", string.Empty)); + var plugin = MiraPluginManager.GetPluginByGuid(taskFolder.name.Replace("(Clone)", string.Empty)); if (plugin != null) { foreach (var modifier in plugin.InternalModifiers) @@ -435,7 +483,6 @@ internal static class TaskAdderPatches })); __instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); - ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); } } From ac66e0d62b15633ac170bda767bf15fd78c3d42c Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:23:40 -0500 Subject: [PATCH 70/86] Clear up warnings --- .../LocalSettings/LocalSettingsTabAppearance.cs | 2 +- .../Patches/LocalSettings/OptionsMenuPatches.cs | 2 +- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 15 ++++++++------- MiraAPI/Utilities/Assets/MiraAssets.cs | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs index 26fd726..d76a2d9 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs @@ -78,7 +78,7 @@ public record struct LocalSettingTabAppearance() /// Gets or sets hover color of enum buttons. /// public Color EnumHoverColor { get; set; } = Palette.AcceptedGreen; - + /// /// Gets or sets color of buttons. /// diff --git a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs index 574070f..7434aae 100644 --- a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -22,7 +22,7 @@ public static class OptionsMenuPatches private static Dictionary> tabButtons = new(); /// - /// Creates the tabs and their content + /// Creates the tabs and their content. /// [HarmonyPostfix] [HarmonyPatch(nameof(OptionsMenuBehaviour.Start))] diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index d6eb035..d7d125b 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -536,13 +536,14 @@ public static class RoleSettingMenuPatches { var roleIcon = new GameObject("RoleIcon"); roleIcon.transform.parent = roleOptionSetting.transform; - roleIcon.transform.localScale = new(.25f, .25f, 1); - roleIcon.layer = LayerMask.NameToLayer("UI"); - roleIcon.transform.localPosition = new Vector3(-1.3f, -0.3f, -2f); - var rend = roleIcon.AddComponent(); - rend.sprite = customRole.Configuration.Icon.LoadAsset(); - rend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - } + roleIcon.transform.localScale = new(.25f, .25f, 1); + roleIcon.layer = LayerMask.NameToLayer("UI"); + roleIcon.transform.localPosition = new Vector3(-1.3f, -0.3f, -2f); + var rend = roleIcon.AddComponent(); + rend.sprite = customRole.Configuration.Icon.LoadAsset(); + + rend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + } if (GameSettingMenuPatches.SelectedMod is null || GameSettingMenuPatches.SelectedMod.InternalOptionGroups diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index b10944f..4368508 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -109,12 +109,12 @@ public static class MiraAssets /// Gets the sprite used for crewmate role files in TaskAdderGame. /// public static LoadableResourceAsset CrewmateFile { get; } = new("MiraAPI.Resources.CrewmateFile.png"); - + /// /// Gets the sprite used for impostor role files in TaskAdderGame. /// public static LoadableResourceAsset ImpostorFile { get; } = new("MiraAPI.Resources.ImpostorFile.png"); - + /// /// Gets the sprite used for custom team role files in TaskAdderGame. /// From c75e03914109d52868f780d771cf507820638ba9 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Wed, 17 Sep 2025 21:04:05 +0200 Subject: [PATCH 71/86] fixes --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 9e89db8..96dbf3a 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -11,7 +11,6 @@ using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities.Assets; -using Reactor.Utilities; using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; @@ -73,9 +72,9 @@ internal static class TaskAdderPatches var modifiersFolder = __instance.CreateFolder("Modifiers", __instance.Root, 0, Color.blue); folders.Clear(); - folders.Add("Crewmate", crewmateFolder); - folders.Add("Impostor", impostorFolder); - folders.Add("Neutral", neutralFolder); + folders.Add(crewmateFolder.FolderName, crewmateFolder); + folders.Add(impostorFolder.FolderName, impostorFolder); + folders.Add("Neutrals", neutralFolder); folders.Add("Modifiers", modifiersFolder); int folderIdx = 3; @@ -143,8 +142,7 @@ internal static class TaskAdderPatches Il2CppSystem.Collections.Generic.List crewRoles = new(); foreach (var role in RoleManager.Instance.AllRoles) { - if (role.Role != RoleTypes.ImpostorGhost && role.Role != RoleTypes.CrewmateGhost && - !role.IsCustomRole()) + if (role.Role != RoleTypes.ImpostorGhost && role.Role != RoleTypes.CrewmateGhost && !role.IsCustomRole()) { if (role.TeamType == RoleTeamTypes.Crewmate) { @@ -369,10 +367,15 @@ internal static class TaskAdderPatches } } - var split = taskFolder - .name - .Replace("(Clone)", string.Empty) - .Split(':'); + string[] split = []; + try + { + split = taskFolder.name.Replace("(Clone)", string.Empty).Split(':'); + } + catch + { + // I hate you + } if (split is ["Roles", _, _] && folders.Any(x => taskFolder.IsChildOf(x.Value))) { var plugin = MiraPluginManager.GetPluginByGuid(split[2]); @@ -401,7 +404,6 @@ internal static class TaskAdderPatches roleAddButton.MyTask = null; roleAddButton.SafePositionWorld = __instance.SafePositionWorld; roleAddButton.Text.text = prettyEnabled ? roleBehaviour.NiceName : "Be_" + roleBehaviour.NiceName + ".exe"; - roleAddButton.Text.fontSizeMin = 1; roleAddButton.Text.EnableMasking(); roleAddButton.role = roleBehaviour; if (prettyEnabled) From ae5f3783b4cb33648aa3c48ba9c17f0564061f96 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:07:05 -0500 Subject: [PATCH 72/86] Add locale to task adder --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 19 ++++++++++++------- MiraAPI/Roles/CustomRoleConfiguration.cs | 4 ++-- MiraAPI/Utilities/Helpers.cs | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 96dbf3a..b1d9012 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -10,6 +10,7 @@ using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; using UnityEngine; @@ -65,16 +66,18 @@ 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 == "Crewmate")!; - var impostorFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == "Impostor")!; - var neutralFolder = __instance.CreateFolder("Neutral", __instance.Root, 2, Color.gray); + var crewmateFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == crewLocale)!; + var impostorFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == impLocale)!; + //var neutralFolder = __instance.CreateFolder("Neutral", __instance.Root, 2, Color.gray); var modifiersFolder = __instance.CreateFolder("Modifiers", __instance.Root, 0, Color.blue); folders.Clear(); folders.Add(crewmateFolder.FolderName, crewmateFolder); folders.Add(impostorFolder.FolderName, impostorFolder); - folders.Add("Neutrals", neutralFolder); + //folders.Add("Neutrals", neutralFolder); folders.Add("Modifiers", modifiersFolder); int folderIdx = 3; @@ -350,7 +353,7 @@ internal static class TaskAdderPatches { TaskAddButton roleAddButton = Object.Instantiate(__instance.RoleButton); roleAddButton.SafePositionWorld = __instance.SafePositionWorld; - roleAddButton.Text.text = prettyEnabled ? role.NiceName : "Be_" + role.NiceName + ".exe"; + roleAddButton.Text.text = prettyEnabled ? role.GetRoleName() : "Be_" + role.GetRoleName() + ".exe"; roleAddButton.Role = role; if (prettyEnabled) { @@ -389,8 +392,10 @@ internal static class TaskAdderPatches { continue; } + var crewLocale = TranslationController.Instance.GetString(StringNames.Crewmate); + var impLocale = TranslationController.Instance.GetString(StringNames.Impostor); - var teamFolder = roleBehaviour.IsImpostor ? "Impostor" : "Crewmate"; + var teamFolder = roleBehaviour.IsImpostor ? impLocale : crewLocale; if (roleBehaviour is ICustomRole cs) { teamFolder = cs.Configuration.FreeplayFolder; @@ -403,7 +408,7 @@ internal static class TaskAdderPatches var roleAddButton = Object.Instantiate(__instance.RoleButton); roleAddButton.MyTask = null; roleAddButton.SafePositionWorld = __instance.SafePositionWorld; - roleAddButton.Text.text = prettyEnabled ? roleBehaviour.NiceName : "Be_" + roleBehaviour.NiceName + ".exe"; + roleAddButton.Text.text = prettyEnabled ? roleBehaviour.GetRoleName() : "Be_" + roleBehaviour.GetRoleName() + ".exe"; roleAddButton.Text.EnableMasking(); roleAddButton.role = roleBehaviour; if (prettyEnabled) diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 3974665..d93fbf2 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -34,8 +34,8 @@ public record struct CustomRoleConfiguration ShowInFreeplay = roleBehaviour?.IsDead == false; FreeplayFolder = role.Team switch { - ModdedRoleTeams.Crewmate => "Crewmate", - ModdedRoleTeams.Impostor => "Impostor", + ModdedRoleTeams.Crewmate => TranslationController.Instance.GetString(StringNames.Crewmate), + ModdedRoleTeams.Impostor => TranslationController.Instance.GetString(StringNames.Impostor), _ => "Neutral", }; IntroSound = role.Team is ModdedRoleTeams.Crewmate diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 416cd28..a845fb4 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Keybinds; +using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; @@ -495,4 +496,18 @@ public static class Helpers (Il2CppSystem.Object[])[value.ToString(formatString, CultureInfo.InvariantCulture)]), }; } + + /// + /// Returns the string of a role. + /// + /// The role to find. + /// The role name. + public static string GetRoleName(this RoleBehaviour role) + { + if (role is ICustomRole custom) + { + return custom.RoleName; + } + return role.NiceName; + } } From 36991f34eec74c9bc70d168a50c9abe3bbb11001 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 19 Sep 2025 20:52:01 -0400 Subject: [PATCH 73/86] fix index of task adder patch --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index b1d9012..78fa445 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -80,7 +80,7 @@ internal static class TaskAdderPatches //folders.Add("Neutrals", neutralFolder); folders.Add("Modifiers", modifiersFolder); - int folderIdx = 3; + int folderIdx = 2; foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins) { var pluginFolders = new Dictionary(); From 0f1962394a68be22e197d9562498de04d45dfc86 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 19 Sep 2025 21:07:22 -0400 Subject: [PATCH 74/86] un-inline rpcendgame --- MiraAPI/Patches/Events/EndGamePatches.cs | 93 +++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Events/EndGamePatches.cs b/MiraAPI/Patches/Events/EndGamePatches.cs index 558c468..13291c6 100644 --- a/MiraAPI/Patches/Events/EndGamePatches.cs +++ b/MiraAPI/Patches/Events/EndGamePatches.cs @@ -1,4 +1,6 @@ -using HarmonyLib; +using System.Linq; +using AmongUs.Data; +using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.GameEnd; @@ -7,6 +9,95 @@ namespace MiraAPI.Patches.Events; internal static class EndGamePatches { + // To un-inline RpcEndGame + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] + public static class LogicGameFlowNormalPatches + { + public static void Prefix(LogicGameFlowNormal __instance, ref bool __runOriginal) + { + __runOriginal = false; + if (!GameData.Instance) + { + return; + } + + if (ShipStatus.Instance.Systems.TryGetValue(SystemTypes.LifeSupp, out var systemType)) + { + var lifeSuppSystemType = systemType.Cast(); + if (lifeSuppSystemType.Countdown < 0f) + { + __instance.EndGameForSabotage(); + lifeSuppSystemType.Countdown = 10000f; + } + } + + foreach (var systemType2 in ShipStatus.Instance.Systems.Values) + { + var criticalSabotage = systemType2.TryCast(); + if (criticalSabotage is { Countdown: < 0f }) + { + __instance.EndGameForSabotage(); + criticalSabotage.ClearSabotage(); + } + } + + var playerCounts = __instance.GetPlayerCounts(); + var item = playerCounts.Item1; + var item2 = playerCounts.Item2; + var item3 = playerCounts.Item3; + if (item2 <= 0 && (!TutorialManager.InstanceExists || item3 > 0)) + { + if (!TutorialManager.InstanceExists) + { + var gameOverReason = GameOverReason.CrewmatesByVote; + __instance.Manager.RpcEndGame(gameOverReason, !DataManager.Player.Ads.HasPurchasedAdRemoval); + return; + } + + if (!Minigame.Instance) + { + HudManager.Instance.ShowPopUp( + TranslationController.Instance.GetString(StringNames.GameOverImpostorDead)); + __instance.Manager.ReviveEveryoneFreeplay(); + } + } + else if (item <= item2) + { + if (!TutorialManager.InstanceExists) + { + var gameOverReason2 = GameData.LastDeathReason switch + { + DeathReason.Exile => GameOverReason.ImpostorsByVote, + DeathReason.Kill => GameOverReason.ImpostorsByKill, + _ => GameOverReason.CrewmateDisconnect, + }; + + __instance.Manager.RpcEndGame(gameOverReason2, !DataManager.Player.Ads.HasPurchasedAdRemoval); + return; + } + + HudManager.Instance.ShowPopUp(TranslationController.Instance.GetString(StringNames.GameOverImpostorKills)); + __instance.Manager.ReviveEveryoneFreeplay(); + } + else + { + if (!TutorialManager.InstanceExists) + { + __instance.Manager.CheckEndGameViaTasks(); + return; + } + + if (PlayerControl.LocalPlayer.myTasks.ToArray().All(t => t.IsComplete)) + { + HudManager.Instance.ShowPopUp( + TranslationController.Instance.GetString( + StringNames.GameOverTaskWin)); + ShipStatus.Instance.Begin(); + } + } + } + } + [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] public static class GameManagerPatches { From af75cc36069c2b8fd9307c6cb7110f11245c32c3 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:16:56 -0400 Subject: [PATCH 75/86] revert un-inline --- MiraAPI/Patches/Events/EndGamePatches.cs | 93 +----------------------- 1 file changed, 1 insertion(+), 92 deletions(-) diff --git a/MiraAPI/Patches/Events/EndGamePatches.cs b/MiraAPI/Patches/Events/EndGamePatches.cs index 13291c6..558c468 100644 --- a/MiraAPI/Patches/Events/EndGamePatches.cs +++ b/MiraAPI/Patches/Events/EndGamePatches.cs @@ -1,6 +1,4 @@ -using System.Linq; -using AmongUs.Data; -using HarmonyLib; +using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.GameEnd; @@ -9,95 +7,6 @@ namespace MiraAPI.Patches.Events; internal static class EndGamePatches { - // To un-inline RpcEndGame - [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] - public static class LogicGameFlowNormalPatches - { - public static void Prefix(LogicGameFlowNormal __instance, ref bool __runOriginal) - { - __runOriginal = false; - if (!GameData.Instance) - { - return; - } - - if (ShipStatus.Instance.Systems.TryGetValue(SystemTypes.LifeSupp, out var systemType)) - { - var lifeSuppSystemType = systemType.Cast(); - if (lifeSuppSystemType.Countdown < 0f) - { - __instance.EndGameForSabotage(); - lifeSuppSystemType.Countdown = 10000f; - } - } - - foreach (var systemType2 in ShipStatus.Instance.Systems.Values) - { - var criticalSabotage = systemType2.TryCast(); - if (criticalSabotage is { Countdown: < 0f }) - { - __instance.EndGameForSabotage(); - criticalSabotage.ClearSabotage(); - } - } - - var playerCounts = __instance.GetPlayerCounts(); - var item = playerCounts.Item1; - var item2 = playerCounts.Item2; - var item3 = playerCounts.Item3; - if (item2 <= 0 && (!TutorialManager.InstanceExists || item3 > 0)) - { - if (!TutorialManager.InstanceExists) - { - var gameOverReason = GameOverReason.CrewmatesByVote; - __instance.Manager.RpcEndGame(gameOverReason, !DataManager.Player.Ads.HasPurchasedAdRemoval); - return; - } - - if (!Minigame.Instance) - { - HudManager.Instance.ShowPopUp( - TranslationController.Instance.GetString(StringNames.GameOverImpostorDead)); - __instance.Manager.ReviveEveryoneFreeplay(); - } - } - else if (item <= item2) - { - if (!TutorialManager.InstanceExists) - { - var gameOverReason2 = GameData.LastDeathReason switch - { - DeathReason.Exile => GameOverReason.ImpostorsByVote, - DeathReason.Kill => GameOverReason.ImpostorsByKill, - _ => GameOverReason.CrewmateDisconnect, - }; - - __instance.Manager.RpcEndGame(gameOverReason2, !DataManager.Player.Ads.HasPurchasedAdRemoval); - return; - } - - HudManager.Instance.ShowPopUp(TranslationController.Instance.GetString(StringNames.GameOverImpostorKills)); - __instance.Manager.ReviveEveryoneFreeplay(); - } - else - { - if (!TutorialManager.InstanceExists) - { - __instance.Manager.CheckEndGameViaTasks(); - return; - } - - if (PlayerControl.LocalPlayer.myTasks.ToArray().All(t => t.IsComplete)) - { - HudManager.Instance.ShowPopUp( - TranslationController.Instance.GetString( - StringNames.GameOverTaskWin)); - ShipStatus.Instance.Begin(); - } - } - } - } - [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] public static class GameManagerPatches { From 8da80be027ade6b89de6c90e30e4cf127053517b Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sat, 20 Sep 2025 21:58:30 +0200 Subject: [PATCH 76/86] add HideIconOnHover for local settings --- MiraAPI/LocalSettings/LocalSettingsTab.cs | 1 + MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs | 7 ++++++- MiraAPI/MiraApiSettings.cs | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index ce78c85..8b9ede8 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -246,6 +246,7 @@ public abstract class LocalSettingsTab(ConfigFile config) tabButtonText.maxVisibleCharacters = int.MaxValue; tabButtonText.text = TabName; tabButtonText.alignment = TextAlignmentOptions.Left; + tabButtonRend?.gameObject.SetActive(!TabAppearance.HideIconOnHover); })); tabButton.OnMouseOut.AddListener((UnityAction)(() => { diff --git a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs index d76a2d9..9692be4 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs @@ -4,7 +4,7 @@ using UnityEngine; namespace MiraAPI.LocalSettings; /// -/// Struct for modyfing tab apparance. +/// Struct for modifying tab appearance. /// Defines how the settings look. /// public record struct LocalSettingTabAppearance() @@ -24,6 +24,11 @@ public record struct LocalSettingTabAppearance() /// public Color TabButtonColor { get; set; } = Color.white; + /// + /// Gets or sets a value indicating whether the icon should be hidden when tab button is hovered. + /// + public bool HideIconOnHover { get; set; } = true; + /// /// Gets or sets hover color of the tab button. /// diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 574212c..59eb6a4 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -18,6 +18,7 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) { TabButtonHoverColor = MiraApiPlugin.MiraColor, TabIcon = MiraAssets.SettingsIcon, + HideIconOnHover = false, }; /// From e166efa6669c082fd82548f596e2921b33a993e5 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sat, 20 Sep 2025 23:47:36 +0200 Subject: [PATCH 77/86] ignore custom roles in vanilla role options --- .../Patches/Roles/RolesSettingsMenuPatch.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 MiraAPI/Patches/Roles/RolesSettingsMenuPatch.cs diff --git a/MiraAPI/Patches/Roles/RolesSettingsMenuPatch.cs b/MiraAPI/Patches/Roles/RolesSettingsMenuPatch.cs new file mode 100644 index 0000000..46a1a5f --- /dev/null +++ b/MiraAPI/Patches/Roles/RolesSettingsMenuPatch.cs @@ -0,0 +1,53 @@ +using System.Linq; +using AmongUs.GameOptions; +using HarmonyLib; +using MiraAPI.Roles; +using UnityEngine; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(RolesSettingsMenu))] +public static class RolesSettingsMenuPatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] + private static bool SetQuotaTabPrefix(RolesSettingsMenu __instance) + { + float num = 0.662f; + float num2 = -1.928f; + __instance.roleTabs = new(); + __instance.roleTabs.Add(__instance.AllButton); + var list = CustomRoleManager.AllRoles.Where(r => r is not ICustomRole && r.TeamType == RoleTeamTypes.Crewmate && r.Role != RoleTypes.Crewmate && r.Role != RoleTypes.CrewmateGhost).ToList(); + var list2 = CustomRoleManager.AllRoles.Where(r => r is not ICustomRole && r.TeamType == RoleTeamTypes.Impostor && r.Role != RoleTypes.Impostor && r.Role != RoleTypes.ImpostorGhost).ToList(); + for (int i = 0; i < list.Count; i++) + { + __instance.AddRoleTab(list[i], ref num2); + } + for (int j = 0; j < list2.Count; j++) + { + __instance.AddRoleTab(list2[j], ref num2); + } + CategoryHeaderEditRole categoryHeaderEditRole = Object.Instantiate(__instance.categoryHeaderEditRoleOrigin, Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform); + categoryHeaderEditRole.SetHeader(StringNames.CrewmateRolesHeader, 20); + categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, num, -2f); + num -= 0.522f; + int num3 = 0; + for (int k = 0; k < list.Count; k++) + { + __instance.CreateQuotaOption(list[k], ref num, num3); + num3++; + } + num -= 0.22f; + CategoryHeaderEditRole categoryHeaderEditRole2 = Object.Instantiate(__instance.categoryHeaderEditRoleOrigin, Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform); + categoryHeaderEditRole2.SetHeader(StringNames.ImpostorRolesHeader, 20); + categoryHeaderEditRole2.transform.localPosition = new Vector3(4.986f, num, -2f); + num -= 0.522f; + for (int l = 0; l < list2.Count; l++) + { + __instance.CreateQuotaOption(list2[l], ref num, num3); + num3++; + } + + return false; + } +} From e98c16b9f5e356b5219f8cc8e0ef731ae602b219 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 20 Sep 2025 22:12:51 -0400 Subject: [PATCH 78/86] fix role settings tab --- .../Patches/Options/RoleSettingMenuPatches.cs | 70 +++++++++++++++++-- .../Patches/Roles/RolesSettingsMenuPatch.cs | 53 -------------- 2 files changed, 65 insertions(+), 58 deletions(-) delete mode 100644 MiraAPI/Patches/Roles/RolesSettingsMenuPatch.cs diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index d7d125b..9d25635 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.GameOptions; @@ -57,6 +58,7 @@ public static class RoleSettingMenuPatches var maskBg = __instance.scrollBar.transform.FindChild("MaskBg"); var hitbox = __instance.scrollBar.transform.FindChild("Hitbox"); + var dividerImage = __instance.transform.FindChild("HeaderButtons/DividerImage"); if (GameSettingMenuPatches.SelectedModIdx == 0) { @@ -67,7 +69,64 @@ public static class RoleSettingMenuPatches maskBg.localScale = new Vector3(6.6811f, 3.3563f, 0.5598f); hitbox.localPosition = new Vector3(0.3297f, -.2333f, 4f); hitbox.localScale = new Vector3(1, 1, 1); - return true; + dividerImage.gameObject.SetActive(true); + + var num = 0.662f; + + var list = CustomRoleManager.AllRoles.Where( + r => !r.IsCustomRole() && 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.Role != RoleTypes.ImpostorGhost).ToList(); + + if (__instance.roleTabs == null || __instance.roleTabs.Count == 0) + { + var num2 = -1.928f; + __instance.roleTabs = new(); + __instance.roleTabs.Add(__instance.AllButton); + foreach (var t in list) + { + __instance.AddRoleTab(t, ref num2); + } + + foreach (var t in list2) + { + __instance.AddRoleTab(t, ref num2); + } + } + + var categoryHeaderEditRole = Object.Instantiate( + __instance.categoryHeaderEditRoleOrigin, + Vector3.zero, + Quaternion.identity, + __instance.RoleChancesSettings.transform); + categoryHeaderEditRole.SetHeader(StringNames.CrewmateRolesHeader, 20); + categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, num, -2f); + num -= 0.522f; + var num3 = 0; + foreach (var t in list) + { + __instance.CreateQuotaOption(t, ref num, num3); + num3++; + } + + num -= 0.22f; + var categoryHeaderEditRole2 = Object.Instantiate( + __instance.categoryHeaderEditRoleOrigin, + Vector3.zero, + Quaternion.identity, + __instance.RoleChancesSettings.transform); + categoryHeaderEditRole2.SetHeader(StringNames.ImpostorRolesHeader, 20); + categoryHeaderEditRole2.transform.localPosition = new Vector3(4.986f, num, -2f); + num -= 0.522f; + foreach (var t in list2) + { + __instance.CreateQuotaOption(t, ref num, num3); + num3++; + } + + return false; } ScrollerNum = 0.522f; @@ -79,15 +138,16 @@ public static class RoleSettingMenuPatches maskBg.localScale = new Vector3(6.6811f, 4.1563f, 0.5598f); hitbox.localPosition = new Vector3(0.3297f, -.6333f, 4f); hitbox.localScale = new Vector3(1, 1.2f, 1); + dividerImage.gameObject.SetActive(false); - var num3 = 0; + var num4 = 0; var roleGroups = GameSettingMenuPatches.SelectedMod?.InternalRoles.Values.OfType() .ToLookup(x => x.RoleOptionsGroup); if (roleGroups is null) { - return true; + return false; } // sort the groups by priority @@ -197,11 +257,11 @@ public static class RoleSettingMenuPatches continue; } - var option = CreateQuotaOption(__instance, roleBehaviour, num3); + var option = CreateQuotaOption(__instance, roleBehaviour, num4); if (option is not null) { RoleOptionSettings.Add(option); - num3++; + num4++; } } } diff --git a/MiraAPI/Patches/Roles/RolesSettingsMenuPatch.cs b/MiraAPI/Patches/Roles/RolesSettingsMenuPatch.cs deleted file mode 100644 index 46a1a5f..0000000 --- a/MiraAPI/Patches/Roles/RolesSettingsMenuPatch.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Linq; -using AmongUs.GameOptions; -using HarmonyLib; -using MiraAPI.Roles; -using UnityEngine; - -namespace MiraAPI.Patches.Roles; - -[HarmonyPatch(typeof(RolesSettingsMenu))] -public static class RolesSettingsMenuPatch -{ - [HarmonyPrefix] - [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] - private static bool SetQuotaTabPrefix(RolesSettingsMenu __instance) - { - float num = 0.662f; - float num2 = -1.928f; - __instance.roleTabs = new(); - __instance.roleTabs.Add(__instance.AllButton); - var list = CustomRoleManager.AllRoles.Where(r => r is not ICustomRole && r.TeamType == RoleTeamTypes.Crewmate && r.Role != RoleTypes.Crewmate && r.Role != RoleTypes.CrewmateGhost).ToList(); - var list2 = CustomRoleManager.AllRoles.Where(r => r is not ICustomRole && r.TeamType == RoleTeamTypes.Impostor && r.Role != RoleTypes.Impostor && r.Role != RoleTypes.ImpostorGhost).ToList(); - for (int i = 0; i < list.Count; i++) - { - __instance.AddRoleTab(list[i], ref num2); - } - for (int j = 0; j < list2.Count; j++) - { - __instance.AddRoleTab(list2[j], ref num2); - } - CategoryHeaderEditRole categoryHeaderEditRole = Object.Instantiate(__instance.categoryHeaderEditRoleOrigin, Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform); - categoryHeaderEditRole.SetHeader(StringNames.CrewmateRolesHeader, 20); - categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, num, -2f); - num -= 0.522f; - int num3 = 0; - for (int k = 0; k < list.Count; k++) - { - __instance.CreateQuotaOption(list[k], ref num, num3); - num3++; - } - num -= 0.22f; - CategoryHeaderEditRole categoryHeaderEditRole2 = Object.Instantiate(__instance.categoryHeaderEditRoleOrigin, Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform); - categoryHeaderEditRole2.SetHeader(StringNames.ImpostorRolesHeader, 20); - categoryHeaderEditRole2.transform.localPosition = new Vector3(4.986f, num, -2f); - num -= 0.522f; - for (int l = 0; l < list2.Count; l++) - { - __instance.CreateQuotaOption(list2[l], ref num, num3); - num3++; - } - - return false; - } -} From b64bf7c9de1eb58c16e7a18cc9bfcc8c769e917f Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 20 Sep 2025 22:14:07 -0400 Subject: [PATCH 79/86] bump version --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index c8f52ba..5460560 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ true - 0.2.1 + 0.2.2 dev All Of Us, XtraCube, Angxl From 18c52e01385a0706a122079f7238d1a52d6a718d Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 20 Sep 2025 22:17:51 -0500 Subject: [PATCH 80/86] Expose certain local option overrides and tabs --- .../LocalSettings/Attributes/LocalEnumSettingAttribute.cs | 2 +- .../Attributes/LocalNumberSettingAttribute.cs | 2 +- MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs | 2 +- .../Attributes/LocalSliderSettingAttribute.cs | 2 +- .../Attributes/LocalToggleSettingAttribute.cs | 2 +- MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs | 5 ++++- MiraAPI/Patches/HudManagerPatches.cs | 8 +++----- MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs | 2 +- MiraAPI/Patches/Roles/HowToPlayPatch.cs | 2 +- 9 files changed, 14 insertions(+), 13 deletions(-) diff --git a/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs index 24ea47f..f114818 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs @@ -20,7 +20,7 @@ public class LocalEnumSettingAttribute( private readonly string? _description = description; /// - internal override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + public override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { return new LocalEnumSetting(tab, configEntryBase, configEntryBase.SettingType, _name, _description, names); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs index 3008755..79d4cc7 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs @@ -29,7 +29,7 @@ public class LocalNumberSettingAttribute( private readonly string? _description = description; /// - internal override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + public override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { return new LocalNumberSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), increment, suffixType, formatString); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs index 86dfbc9..d966cbb 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -20,5 +20,5 @@ public abstract class LocalSettingAttribute( /// Gets the tab where the setting is located. /// Gets the config entry the setting is attached to. /// The created class. - internal abstract ILocalSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase); + public abstract ILocalSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs index 2db56b4..c8e5712 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs @@ -29,7 +29,7 @@ public class LocalSliderSettingAttribute( private readonly string? _description = description; /// - internal override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + public override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { return new LocalSliderSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), displayValue, suffixType, formatString, roundValue); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs index 022fb38..2d2aa14 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs @@ -18,7 +18,7 @@ public class LocalToggleSettingAttribute( private readonly string? _description = description; /// - internal override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + public override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) { return new LocalToggleSetting(tab, configEntryBase, _name, _description); } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index 629cd45..c25f04e 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -20,7 +20,10 @@ public abstract class LocalSettingBase : ILocalSetting /// public ConfigEntryBase ConfigEntry { get; } - internal LocalSettingsTab? Tab => LocalSettingsManager.Tabs.FirstOrDefault(x => x.Settings.Contains(this)); + /// + /// Gets the tab of the local setting. + /// + public LocalSettingsTab? Tab => LocalSettingsManager.Tabs.FirstOrDefault(x => x.Settings.Contains(this)); /// /// Initializes a new instance of the class. diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 54e7fae..e8c13ac 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -182,12 +182,10 @@ public static class HudManagerPatches entry.Invoke(); } } - foreach (var entry in KeybindManager.VanillaKeybinds.Values) + + foreach (var entry in KeybindManager.VanillaKeybinds.Values.Where(x => player.GetButtonDown(x.Id))) { - if (player.GetButtonDown(entry.Id)) - { - entry.Invoke(); - } + entry.Invoke(); } } } diff --git a/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs index 9a8321d..ffbfdea 100644 --- a/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs +++ b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs @@ -8,7 +8,7 @@ public static class ToggleBehaviourPatch { /// /// ResetText was inlined ofc. - /// Skill issue + /// Skill issue. /// [HarmonyPrefix] [HarmonyPatch(nameof(ToggleButtonBehaviour.UpdateText))] diff --git a/MiraAPI/Patches/Roles/HowToPlayPatch.cs b/MiraAPI/Patches/Roles/HowToPlayPatch.cs index c080ea0..9e5ad24 100644 --- a/MiraAPI/Patches/Roles/HowToPlayPatch.cs +++ b/MiraAPI/Patches/Roles/HowToPlayPatch.cs @@ -46,4 +46,4 @@ internal static class HowToPlayPatch __instance.roleSelectionScene.SetActive(true); ControllerManager.Instance.SetDefaultSelection(__instance.defaultButtonSelected, null); } -} \ No newline at end of file +} From c1d2868e775d8375a299e062495408f7248feef7 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 21 Sep 2025 16:57:00 -0400 Subject: [PATCH 81/86] fix server dropdown --- MiraAPI/Patches/ServerDropdownPatch.cs | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Patches/ServerDropdownPatch.cs b/MiraAPI/Patches/ServerDropdownPatch.cs index 36da2d7..6ff7e68 100644 --- a/MiraAPI/Patches/ServerDropdownPatch.cs +++ b/MiraAPI/Patches/ServerDropdownPatch.cs @@ -1,4 +1,6 @@ -using HarmonyLib; +using System.Linq; +using HarmonyLib; +using Reactor.Utilities; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; @@ -8,20 +10,29 @@ namespace MiraAPI.Patches; [HarmonyPatch(typeof(ServerDropdown), nameof(ServerDropdown.FillServerOptions))] public static class ServerDropdownPatch { + public static bool RegionEquals(this IRegionInfo region, IRegionInfo other) + { + return region.Name == other.Name && + region.TranslateName == other.TranslateName && + region.PingServer == other.PingServer && + region.TargetServer == other.TargetServer && + region.Servers.All(s=>other.Servers.Any(x=>x.Equals(s))); + } + public static bool Prefix(ServerDropdown __instance) { var num = 0; __instance.background.size = new Vector2(8.4f, 4.8f); - foreach (var regionInfo in DestroyableSingleton.Instance.AvailableRegions) + foreach (var regionInfo in ServerManager.Instance.AvailableRegions) { var findingGame = SceneManager.GetActiveScene().name is "FindAGame"; - if (DestroyableSingleton.Instance.CurrentRegion.Equals(regionInfo)) + if (ServerManager.Instance.CurrentRegion.RegionEquals(regionInfo)) { __instance.defaultButtonSelected = __instance.firstOption; __instance.firstOption.ChangeButtonText( - DestroyableSingleton.Instance.GetStringWithDefault( + TranslationController.Instance.GetStringWithDefault( regionInfo.TranslateName, regionInfo.Name)); } @@ -34,11 +45,11 @@ public static class ServerDropdownPatch { x += 2; } - var y = -0.55f * (num / 2); + var y = -0.55f * (num / 2f); serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f); serverListButton.transform.localScale = Vector3.one; serverListButton.Text.text = - DestroyableSingleton.Instance.GetStringWithDefault( + TranslationController.Instance.GetStringWithDefault( regionInfo.TranslateName, regionInfo.Name); serverListButton.Text.ForceMeshUpdate(); @@ -47,9 +58,9 @@ public static class ServerDropdownPatch __instance.controllerSelectable.Add(serverListButton.Button); __instance.background.transform.localPosition = new Vector3( findingGame ? 2f : 0f, - __instance.initialYPos + (-0.3f * (num / 2)), + __instance.initialYPos + -0.3f * (num / 2f), 0f); - __instance.background.size = new Vector2(__instance.background.size.x, 1.2f + (0.6f * (num / 2))); + __instance.background.size = new Vector2(__instance.background.size.x, 1.2f + 0.6f * (num / 2f)); num++; } } From 9dfb40d7bf8492ba1329a08771d19942b264be32 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 24 Sep 2025 07:12:34 -0400 Subject: [PATCH 82/86] add new role stubs --- MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs | 73 +++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs index fe04962..356bf68 100644 --- a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs +++ b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using HarmonyLib; +using Hazel; using Il2CppSystem.Text; namespace MiraAPI.Patches.Stubs; @@ -229,4 +230,76 @@ public static class RoleBehaviourStubs { // nothing needed } + + /// + /// Stub method for RoleBehaviour.UseSecondaryAbility. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.UseSecondaryAbility))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void UseSecondaryAbility(RoleBehaviour instance) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.OnRoleSet. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.OnRoleSet))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void OnRoleSet(RoleBehaviour instance) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.InitializeMeetingAbilityButton. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.InitializeMeetingAbilityButton))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void InitializeMeetingAbilityButton(RoleBehaviour instance) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.FindClosestBody. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.FindClosestBody))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static DeadBody FindClosestBody(RoleBehaviour instance) + { + return null!; + } + + /// + /// Stub method for RoleBehaviour.HandleRoleRpc. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.HandleRoleRpc))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void HandleRoleRpc(RoleBehaviour instance, byte callId, MessageReader reader) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.KillAnimSpecialSetup. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.KillAnimSpecialSetup))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void KillAnimSpecialSetup(RoleBehaviour instance, DeadBody deadBody, PlayerControl killer, PlayerControl victim) + { + // nothing needed + } } From 59f30586857c0b7309ee47d1b9835953ac08a988 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 24 Sep 2025 07:21:31 -0400 Subject: [PATCH 83/86] update comment --- MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs index 7f53b0a..25143c9 100644 --- a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs +++ b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs @@ -13,7 +13,7 @@ namespace MiraAPI.Patches.Roles; public static class RoleOptionsCollectionPatch { /// - /// This patch fixes GetNumPerGame being inlined (2025.5.20) in the original code. + /// This patch fixes GetNumPerGame being inlined (2025.9.9) in the original code. /// [HarmonyPrefix] [HarmonyPatch(nameof(RoleOptionsCollectionV10.AnyRolesEnabled))] From 6d387cae96a7cf77bb9aa20724ab22733f6450b4 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 24 Sep 2025 07:35:50 -0400 Subject: [PATCH 84/86] fix round start events for airship --- MiraAPI/Patches/Roles/EjectionPatches.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Roles/EjectionPatches.cs b/MiraAPI/Patches/Roles/EjectionPatches.cs index 009da27..d37b550 100644 --- a/MiraAPI/Patches/Roles/EjectionPatches.cs +++ b/MiraAPI/Patches/Roles/EjectionPatches.cs @@ -33,8 +33,8 @@ internal static class EjectionPatches __instance.completeString = role.GetCustomEjectionMessage(__instance.initData.networkedPlayer); } - [HarmonyPatch(typeof(ExileController), nameof(ExileController.WrapUp))] - public static class WrapUpPatch + [HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))] + public static class ReEnableGameplayPatch { public static void Postfix() { From e5cfd8a2fcfe74fabedcde9b90ba985612b11505 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 24 Sep 2025 08:24:14 -0400 Subject: [PATCH 85/86] fix game never ending --- MiraAPI/Patches/Events/EndGamePatches.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Events/EndGamePatches.cs b/MiraAPI/Patches/Events/EndGamePatches.cs index 558c468..fe5a7bb 100644 --- a/MiraAPI/Patches/Events/EndGamePatches.cs +++ b/MiraAPI/Patches/Events/EndGamePatches.cs @@ -7,8 +7,17 @@ namespace MiraAPI.Patches.Events; internal static class EndGamePatches { + [HarmonyPatch(typeof(GameManager), nameof(GameManager.Awake))] + public static class GameManagerAwakePatch + { + public static void Postfix(GameManager __instance) + { + __instance.enabled = true; + } + } + [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] - public static class GameManagerPatches + public static class GameManagerRpcEndGamePatch { public static bool Prefix(GameOverReason endReason) { From 4dd2554b38666836a4996c999f3b4dae781c0391 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 24 Sep 2025 08:53:09 -0400 Subject: [PATCH 86/86] bump version --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 5460560..ac800b3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ true - 0.2.2 + 0.3.0 dev All Of Us, XtraCube, Angxl