From d9d0d6bc622244956ae1b0c139feb2bdd682dcfa Mon Sep 17 00:00:00 2001 From: CallOfCreator Date: Tue, 8 Jul 2025 20:41:20 +0000 Subject: [PATCH 1/6] Implement Keybind API --- .../Buttons/Freezer/FreezeButton.cs | 2 + MiraAPI.Example/Buttons/MeetingButton.cs | 2 + .../Buttons/NeutralKillerButton.cs | 2 + .../Buttons/Teleporter/TeleportButton.cs | 3 +- MiraAPI/Hud/CustomActionButton.cs | 6 ++ MiraAPI/Keybinds/KeybindManager.cs | 95 +++++++++++++++++++ MiraAPI/Keybinds/UserDataExtensions.cs | 52 ++++++++++ MiraAPI/Patches/HudManagerPatches.cs | 24 ++++- MiraAPI/Patches/Keybinds/KeybindsPatch.cs | 19 ++++ MiraAPI/PluginLoading/MiraPluginManager.cs | 23 ++++- MiraAPI/Utilities/Helpers.cs | 37 ++++++-- 11 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 MiraAPI/Keybinds/KeybindManager.cs create mode 100644 MiraAPI/Keybinds/UserDataExtensions.cs create mode 100644 MiraAPI/Patches/Keybinds/KeybindsPatch.cs diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 3b7363a..05f4f3d 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -6,6 +6,7 @@ using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using Rewired; using UnityEngine; namespace MiraAPI.Example.Buttons.Freezer; @@ -19,6 +20,7 @@ public class FreezeButton : CustomActionButton public override int MaxUses => (int)OptionGroupSingleton.Instance.FreezeUses; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + public override KeyboardKeyCode CurrentKeybind => KeyboardKeyCode.T; protected override void OnClick() { diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index c4d59e3..0e46a67 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -2,6 +2,7 @@ using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Utilities.Assets; +using Rewired; using UnityEngine; namespace MiraAPI.Example.Buttons; @@ -15,6 +16,7 @@ public class MeetingButton : CustomActionButton public override int MaxUses => 3; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + public override KeyboardKeyCode CurrentKeybind => KeyboardKeyCode.P; public override bool Enabled(RoleBehaviour? role) { diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example/Buttons/NeutralKillerButton.cs index d38253a..46982ff 100644 --- a/MiraAPI.Example/Buttons/NeutralKillerButton.cs +++ b/MiraAPI.Example/Buttons/NeutralKillerButton.cs @@ -3,6 +3,7 @@ using MiraAPI.Example.Roles; using MiraAPI.GameEnd; using MiraAPI.Hud; using MiraAPI.Utilities.Assets; +using Rewired; using UnityEngine; namespace MiraAPI.Example.Buttons; @@ -12,6 +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 CurrentKeybind => 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 76a5b9e..d8ad236 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -5,6 +5,7 @@ using MiraAPI.GameOptions; using MiraAPI.Hud; using MiraAPI.Utilities.Assets; using Reactor.Utilities; +using Rewired; using UnityEngine; namespace MiraAPI.Example.Buttons.Teleporter; @@ -21,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 CurrentKeybind => 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 988aac9..d403110 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -5,6 +5,7 @@ using MiraAPI.Events.Mira; using MiraAPI.Patches; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using Rewired; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -52,6 +53,11 @@ public abstract class CustomActionButton /// public virtual int MaxUses => 0; + /// + /// Gets or sets the current key binding for this button. + /// + public abstract KeyboardKeyCode CurrentKeybind { get; } + /// /// Gets the button's text outline color. /// diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs new file mode 100644 index 0000000..1d61890 --- /dev/null +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using Rewired; + +namespace MiraAPI.Keybinds +{ + /// + /// Lets you register and manage mod keybinds with conflict checks and rebinding support. + /// + public static class KeybindManager + { + /// + /// Stores all info for a registered keybind. + /// + public class KeybindEntry + { + /// 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; + } + + 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. + /// Returns the key that was registered for this keybind. + public static KeyboardKeyCode Register(string id, string description, KeyboardKeyCode key, Action handler, bool exclusive = true) + { + Registered.Add(new KeybindEntry + { + Id = id, + Description = description, + Key = key, + Handler = handler, + Exclusive = exclusive, + }); + + 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++) + { + var a = Registered[i]; + var b = Registered[j]; + if (a.Key == b.Key && a.Exclusive && b.Exclusive) + { + conflicts.Add((a, b)); + } + } + } + return conflicts; + } + + /// + /// Returns all currently registered keybind entries. + /// + /// A list of all registered keybind entries. + public static List GetEntries() + { + return Registered; + } + } +} diff --git a/MiraAPI/Keybinds/UserDataExtensions.cs b/MiraAPI/Keybinds/UserDataExtensions.cs new file mode 100644 index 0000000..cc2c98b --- /dev/null +++ b/MiraAPI/Keybinds/UserDataExtensions.cs @@ -0,0 +1,52 @@ +// Inspired by: https://github.com/eDonnes124/Town-Of-Us-R/blob/master/source/Patches/Keybinds.cs#L29 +using Rewired; +using Rewired.Data; + +namespace MiraAPI.Keybinds +{ + /// + /// Adds helper methods for registering mod keybinds with Rewired. + /// + public static class UserDataExtensions + { + /// + /// 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 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 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) + { + userData.AddAction(category); + + var action = userData.GetAction(userData.actions.Count - 1)!; + + action.name = actionName; + action.descriptiveName = description; + action.categoryId = category; + action.type = type; + action.userAssignable = true; + + var map = new ActionElementMap + { + _elementIdentifierId = elementIdentifierId, + _actionId = action.id, + _elementType = ControllerElementType.Button, + _axisContribution = Pole.Positive, + _keyboardKeyCode = key, + _modifierKey1 = ModifierKey.None, + _modifierKey2 = ModifierKey.None, + _modifierKey3 = ModifierKey.None, + }; + userData.keyboardMaps[0].actionElementMaps.Add(map); + userData.joystickMaps[0].actionElementMaps.Add(map); + + return action.id; + } + } +} diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 14c2444..57d4d32 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,7 +1,11 @@ -using HarmonyLib; +using System; +using System.Collections; +using HarmonyLib; using MiraAPI.Hud; +using MiraAPI.Keybinds; using Reactor.Utilities; using Reactor.Utilities.Extensions; +using Rewired; using UnityEngine; using Object = UnityEngine.Object; @@ -123,4 +127,22 @@ public static class HudManagerPatches } } } + + [HarmonyPatch(nameof(HudManager.Update))] + [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); + + if (player.GetButtonDown(entry.Id)) + { + entry.Handler?.Invoke(); + } + } + } } diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs new file mode 100644 index 0000000..361ee92 --- /dev/null +++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs @@ -0,0 +1,19 @@ +using HarmonyLib; +using MiraAPI.Keybinds; +using Rewired; + +namespace MiraAPI.Patches.Keybinds +{ + [HarmonyPatch(typeof(InputManager_Base), nameof(InputManager_Base.Awake))] + public static class KeybindMenuPatch + { + [HarmonyPrefix] + private static void StartPrefix(InputManager_Base __instance) + { + foreach (var entry in KeybindManager.GetEntries()) + { + __instance.userData.RegisterModBind(entry.Id, entry.Description, entry.Key); + } + } + } +} diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 035b87c..607a774 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -11,12 +11,14 @@ using MiraAPI.GameEnd; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; +using MiraAPI.Keybinds; using MiraAPI.Modifiers; using MiraAPI.Presets; using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; +using UnityEngine; namespace MiraAPI.PluginLoading; @@ -129,9 +131,28 @@ public sealed class MiraPluginManager CustomButtonManager.Buttons = new ReadOnlyCollection(CustomButtonManager.CustomButtons); // Cache all the registered plugins into an array for easy access - RegisteredPlugins = [.._registeredPlugins.Values]; + RegisteredPlugins = [.. _registeredPlugins.Values]; ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers); + + foreach (var button in CustomButtonManager.Buttons) + { + + KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", button.CurrentKeybind, button.ClickHandler); + + Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {button.CurrentKeybind}."); + } + + var conflicts = KeybindManager.GetConflicts(); + foreach (var (a, b) in conflicts) + { + Logger.Warning($"Keybind conflict detected: {a.Id} and {b.Id} share {a.Key}"); + + var newKey = Helpers.FindAvailableKey(a.Key); + b.Key = newKey; + + Logger.Info($"Resolved conflict: '{b.Id}' reassigned to {newKey}."); + } }; } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 7d8ce9b..2bf7b37 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,7 +1,11 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Il2CppSystem.IO; +using MiraAPI.Keybinds; +using Rewired; using TMPro; using UnityEngine; using Object = UnityEngine.Object; @@ -19,7 +23,7 @@ public static class Helpers /// A list of alive players. public static List GetAlivePlayers() { - return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x=>x.Object)]; + return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x => x.Object)]; } /// @@ -36,10 +40,10 @@ public static class Helpers case 100: return true; default: - { - var num = Random.RandomRangeInt(1, 101); - return num <= probability; - } + { + var num = UnityEngine.Random.RandomRangeInt(1, 101); + return num <= probability; + } } } @@ -398,6 +402,25 @@ public static class Helpers public static string RandomString(int length, string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") { return new string(Enumerable.Repeat(chars, length) - .Select(s => s[Random.RandomRangeInt(0, s.Length)]).ToArray()); + .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 4784b1f0adb7a2011a8edb592c416df074c2eef2 Mon Sep 17 00:00:00 2001 From: CallOfCreator Date: Wed, 9 Jul 2025 13:48:38 +0000 Subject: [PATCH 2/6] Rename CurrentKeybind to DefaultKeybind --- 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 | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 05f4f3d..630bc96 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 CurrentKeybind => KeyboardKeyCode.T; + public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.T; protected override void OnClick() { diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index 0e46a67..d97793f 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 CurrentKeybind => KeyboardKeyCode.P; + public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.P; public override bool Enabled(RoleBehaviour? role) { diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example/Buttons/NeutralKillerButton.cs index 46982ff..02fc908 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 CurrentKeybind => 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 d8ad236..c4af2ca 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 CurrentKeybind => 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 d403110..0eebb1b 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 CurrentKeybind { get; } + public abstract KeyboardKeyCode Defaultkeybind { get; } /// /// Gets the button's text outline color. diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 607a774..4896efe 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -138,9 +138,9 @@ public sealed class MiraPluginManager foreach (var button in CustomButtonManager.Buttons) { - KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", button.CurrentKeybind, button.ClickHandler); + KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", button.Defaultkeybind, button.ClickHandler); - Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {button.CurrentKeybind}."); + Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {button.Defaultkeybind}."); } var conflicts = KeybindManager.GetConflicts(); From 2b06633488b3efcc71330ff33f934a8ae0e3bc02 Mon Sep 17 00:00:00 2001 From: CallOfCreator Date: Fri, 11 Jul 2025 18:00:58 +0000 Subject: [PATCH 3/6] Cleanup --- MiraAPI/Keybinds/UserDataExtensions.cs | 52 ----------------------- MiraAPI/Patches/Keybinds/KeybindsPatch.cs | 1 + MiraAPI/Utilities/Extensions.cs | 46 +++++++++++++++++++- 3 files changed, 45 insertions(+), 54 deletions(-) delete mode 100644 MiraAPI/Keybinds/UserDataExtensions.cs diff --git a/MiraAPI/Keybinds/UserDataExtensions.cs b/MiraAPI/Keybinds/UserDataExtensions.cs deleted file mode 100644 index cc2c98b..0000000 --- a/MiraAPI/Keybinds/UserDataExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Inspired by: https://github.com/eDonnes124/Town-Of-Us-R/blob/master/source/Patches/Keybinds.cs#L29 -using Rewired; -using Rewired.Data; - -namespace MiraAPI.Keybinds -{ - /// - /// Adds helper methods for registering mod keybinds with Rewired. - /// - public static class UserDataExtensions - { - /// - /// 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 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 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) - { - userData.AddAction(category); - - var action = userData.GetAction(userData.actions.Count - 1)!; - - action.name = actionName; - action.descriptiveName = description; - action.categoryId = category; - action.type = type; - action.userAssignable = true; - - var map = new ActionElementMap - { - _elementIdentifierId = elementIdentifierId, - _actionId = action.id, - _elementType = ControllerElementType.Button, - _axisContribution = Pole.Positive, - _keyboardKeyCode = key, - _modifierKey1 = ModifierKey.None, - _modifierKey2 = ModifierKey.None, - _modifierKey3 = ModifierKey.None, - }; - userData.keyboardMaps[0].actionElementMaps.Add(map); - userData.joystickMaps[0].actionElementMaps.Add(map); - - return action.id; - } - } -} diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs index 361ee92..c79f9a3 100644 --- a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs +++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs @@ -1,5 +1,6 @@ using HarmonyLib; using MiraAPI.Keybinds; +using MiraAPI.Utilities; using Rewired; namespace MiraAPI.Patches.Keybinds diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 57887d8..93bd49e 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -10,6 +10,8 @@ using MiraAPI.Networking; using MiraAPI.Roles; using MiraAPI.Voting; using Reactor.Utilities; +using Rewired; +using Rewired.Data; using TMPro; using UnityEngine; @@ -92,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; @@ -113,7 +115,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 SetFillUpFormat(this ActionButton button, float timer, float maxTimer, string format="0") + public static void SetFillUpFormat(this ActionButton button, float timer, float maxTimer, string format = "0") { var num = Mathf.Clamp(timer / maxTimer, 0f, 1f); button.isCoolingDown = num > 0f; @@ -544,4 +546,44 @@ public static class Extensions renderer.material.SetColor(ShaderID.OutlineColor, color ?? Color.clear); renderer.material.SetColor(ShaderID.AddColor, color ?? Color.clear); } + + /// + /// 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 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 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) + { + userData.AddAction(category); + + var action = userData.GetAction(userData.actions.Count - 1)!; + + action.name = actionName; + action.descriptiveName = description; + action.categoryId = category; + action.type = type; + action.userAssignable = true; + + var map = new ActionElementMap + { + _elementIdentifierId = elementIdentifierId, + _actionId = action.id, + _elementType = ControllerElementType.Button, + _axisContribution = Pole.Positive, + _keyboardKeyCode = key, + _modifierKey1 = ModifierKey.None, + _modifierKey2 = ModifierKey.None, + _modifierKey3 = ModifierKey.None, + }; + userData.keyboardMaps[0].actionElementMaps.Add(map); + userData.joystickMaps[0].actionElementMaps.Add(map); + + return action.id; + } } From dfe124d67a13d7821f8cb6093794e1a430a2737b Mon Sep 17 00:00:00 2001 From: CallOfCreator Date: Fri, 11 Jul 2025 18:06:06 +0000 Subject: [PATCH 4/6] Add missing credit from previous commit --- MiraAPI/Utilities/Extensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 93bd49e..8a8ee8c 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -547,6 +547,8 @@ public static class Extensions renderer.material.SetColor(ShaderID.AddColor, color ?? Color.clear); } + // Inspired by: https://github.com/eDonnes124/Town-Of-Us-R/blob/master/source/Patches/Keybinds.cs#L29 + /// /// Registers a new mod keybind as a user-assignable button action in Rewired. /// From 355eef509b31fd2ed8c9cb5865f9d1bc33503d19 Mon Sep 17 00:00:00 2001 From: CallOfCreator Date: Wed, 6 Aug 2025 18:03:48 +0000 Subject: [PATCH 5/6] Add support for modifier keys --- .../Buttons/Freezer/FreezeButton.cs | 1 + MiraAPI.Example/Buttons/MeetingButton.cs | 2 ++ MiraAPI/Hud/CustomActionButton.cs | 17 ++++++++++++++ MiraAPI/Keybinds/KeybindManager.cs | 23 ++++++++++++++++++- MiraAPI/Patches/HudManagerPatches.cs | 6 ++++- MiraAPI/Patches/Keybinds/KeybindsPatch.cs | 2 +- MiraAPI/PluginLoading/MiraPluginManager.cs | 3 +-- MiraAPI/Utilities/Extensions.cs | 11 +++++---- 8 files changed, 56 insertions(+), 9 deletions(-) diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 630bc96..4edd4f8 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -21,6 +21,7 @@ public class FreezeButton : CustomActionButton public override LoadableAsset Sprite => ExampleAssets.ExampleButton; 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 d97793f..c0acc20 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -17,6 +17,8 @@ public class MeetingButton : CustomActionButton 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 bool Enabled(RoleBehaviour? role) { diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 0eebb1b..ac48c4a 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -58,6 +58,23 @@ public abstract class CustomActionButton /// public abstract 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. + /// + public virtual ModifierKey Modifier1 => ModifierKey.None; + + /// + /// The second optional modifier key. + /// Set to if unused. + /// + public virtual ModifierKey Modifier2 => ModifierKey.None; + + /// + /// The third optional modifier key. + /// Set to if unused. + /// + public virtual ModifierKey Modifier3 => ModifierKey.None; + /// /// Gets the button's text outline color. /// diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs index 1d61890..3f95f4f 100644 --- a/MiraAPI/Keybinds/KeybindManager.cs +++ b/MiraAPI/Keybinds/KeybindManager.cs @@ -34,6 +34,21 @@ namespace MiraAPI.Keybinds /// 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(); @@ -46,8 +61,11 @@ namespace MiraAPI.Keybinds /// 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) + 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 { @@ -56,6 +74,9 @@ namespace MiraAPI.Keybinds Key = key, Handler = handler, Exclusive = exclusive, + Modifier1 = modifier1, + Modifier2 = modifier2, + Modifier3 = modifier3, }); return key; diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 57d4d32..acb4482 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -138,8 +138,12 @@ public static class HudManagerPatches foreach (var entry in KeybindManager.GetEntries()) { var player = ReInput.players.GetPlayer(0); + var keyboard = player.controllers.Keyboard; - if (player.GetButtonDown(entry.Id)) + 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))) { entry.Handler?.Invoke(); } diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs index c79f9a3..e609086 100644 --- a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs +++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs @@ -13,7 +13,7 @@ namespace MiraAPI.Patches.Keybinds { foreach (var entry in KeybindManager.GetEntries()) { - __instance.userData.RegisterModBind(entry.Id, entry.Description, entry.Key); + __instance.userData.RegisterModBind(entry.Id, entry.Description, entry.Key, modifier1: entry.Modifier1, modifier2: entry.Modifier2, modifier3: entry.Modifier3); } } } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 4896efe..b845dfb 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -137,8 +137,7 @@ public sealed class MiraPluginManager foreach (var button in CustomButtonManager.Buttons) { - - KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", button.Defaultkeybind, button.ClickHandler); + 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}."); } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 8a8ee8c..005057a 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -559,8 +559,11 @@ public static class Extensions /// 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. /// 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) + 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) { userData.AddAction(category); @@ -579,9 +582,9 @@ public static class Extensions _elementType = ControllerElementType.Button, _axisContribution = Pole.Positive, _keyboardKeyCode = key, - _modifierKey1 = ModifierKey.None, - _modifierKey2 = ModifierKey.None, - _modifierKey3 = ModifierKey.None, + _modifierKey1 = modifier1, + _modifierKey2 = modifier2, + _modifierKey3 = modifier3, }; userData.keyboardMaps[0].actionElementMaps.Add(map); userData.joystickMaps[0].actionElementMaps.Add(map); From 01fa1f0915c7c4dd9ea396593ad61a89c6b0f0a8 Mon Sep 17 00:00:00 2001 From: CallOfCreator Date: Sat, 9 Aug 2025 11:41:43 +0000 Subject: [PATCH 6/6] Update conflict system --- MiraAPI/Patches/GameStartManagerPatch.cs | 36 ++++++++++++++++++++++ MiraAPI/PluginLoading/MiraPluginManager.cs | 11 ------- 2 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 MiraAPI/Patches/GameStartManagerPatch.cs diff --git a/MiraAPI/Patches/GameStartManagerPatch.cs b/MiraAPI/Patches/GameStartManagerPatch.cs new file mode 100644 index 0000000..7ebb05b --- /dev/null +++ b/MiraAPI/Patches/GameStartManagerPatch.cs @@ -0,0 +1,36 @@ +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/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index b845dfb..02e9102 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -141,17 +141,6 @@ public sealed class MiraPluginManager Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {button.Defaultkeybind}."); } - - var conflicts = KeybindManager.GetConflicts(); - foreach (var (a, b) in conflicts) - { - Logger.Warning($"Keybind conflict detected: {a.Id} and {b.Id} share {a.Key}"); - - var newKey = Helpers.FindAvailableKey(a.Key); - b.Key = newKey; - - Logger.Info($"Resolved conflict: '{b.Id}' reassigned to {newKey}."); - } }; }