From 45972f71a911336492514f519e6ca13ce8bf9ccf Mon Sep 17 00:00:00 2001 From: xChipseq Date: Sat, 30 Aug 2025 21:13:42 +0200 Subject: [PATCH] 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; - } } +