major rework of the keybinds api

This commit is contained in:
xChipseq committed 2025-08-30 21:13:42 +02:00
1 parent a6f7171b71
commit 45972f71a9
19 files changed
+506 -263

No files matched your search

@@ -3,10 +3,10 @@ using MiraAPI.Example.Options.Roles;
using MiraAPI.Example.Roles; using MiraAPI.Example.Roles;
using MiraAPI.GameOptions; using MiraAPI.GameOptions;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Utilities; using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using Rewired;
using UnityEngine; using UnityEngine;
namespace MiraAPI.Example.Buttons.Freezer; namespace MiraAPI.Example.Buttons.Freezer;
@@ -19,8 +19,7 @@ public class FreezeButton : CustomActionButton<PlayerControl>
public override bool PauseTimerInVent => true; public override bool PauseTimerInVent => true;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton; public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.T; public override MiraKeybind? Keybind => MiraGlobalKeybinds.PrimaryAbility;
public override ModifierKey Modifier1 => ModifierKey.Control;
protected override void OnClick() protected override void OnClick()
{ {
+2 -4
View File
@@ -1,8 +1,8 @@
using MiraAPI.Example.Modifiers; using MiraAPI.Example.Modifiers;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using Rewired;
using UnityEngine; using UnityEngine;
namespace MiraAPI.Example.Buttons; namespace MiraAPI.Example.Buttons;
@@ -16,9 +16,7 @@ public class MeetingButton : CustomActionButton
public override int MaxUses => 3; public override int MaxUses => 3;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton; public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.P; public override MiraKeybind? Keybind => MiraGlobalKeybinds.ModifierPrimaryAbility;
public override ModifierKey Modifier1 => ModifierKey.Control;
public override ModifierKey Modifier2 => ModifierKey.Shift;
public override bool Enabled(RoleBehaviour? role) public override bool Enabled(RoleBehaviour? role)
{ {
@@ -2,8 +2,8 @@
using MiraAPI.Example.Roles; using MiraAPI.Example.Roles;
using MiraAPI.GameEnd; using MiraAPI.GameEnd;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using Rewired;
using UnityEngine; using UnityEngine;
namespace MiraAPI.Example.Buttons; namespace MiraAPI.Example.Buttons;
@@ -13,7 +13,8 @@ public class NeutralKillerButton : CustomActionButton
public override string Name => "Win Game"; public override string Name => "Win Game";
public override float Cooldown => 0f; public override float Cooldown => 0f;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton; public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override KeyboardKeyCode? DefaultKeybind => KeyboardKeyCode.K; public override MiraKeybind Keybind => ExampleKeybinds.NeutralWinKeybind;
protected override void OnClick() protected override void OnClick()
{ {
CustomGameOver.Trigger<NeutralKillerGameOver>([PlayerControl.LocalPlayer.Data]); CustomGameOver.Trigger<NeutralKillerGameOver>([PlayerControl.LocalPlayer.Data]);
@@ -3,9 +3,9 @@ using MiraAPI.Example.Options.Roles;
using MiraAPI.Example.Roles; using MiraAPI.Example.Roles;
using MiraAPI.GameOptions; using MiraAPI.GameOptions;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using Reactor.Utilities; using Reactor.Utilities;
using Rewired;
using UnityEngine; using UnityEngine;
namespace MiraAPI.Example.Buttons.Teleporter; namespace MiraAPI.Example.Buttons.Teleporter;
@@ -22,7 +22,8 @@ public class TeleportButton : CustomActionButton
public override LoadableAsset<Sprite> Sprite => ExampleAssets.TeleportButton; public override LoadableAsset<Sprite> Sprite => ExampleAssets.TeleportButton;
public override Color TextOutlineColor => new Color32(221, 176, 152, 255); public override Color TextOutlineColor => new Color32(221, 176, 152, 255);
public static bool IsZoom { get; private set; } 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) public override bool Enabled(RoleBehaviour? role)
{ {
return role is TeleporterRole; return role is TeleporterRole;
+10
View File
@@ -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]);
}
+16 -31
View File
@@ -1,6 +1,5 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq;
using MiraAPI.Events; using MiraAPI.Events;
using MiraAPI.Events.Mira; using MiraAPI.Events.Mira;
using MiraAPI.Keybinds; using MiraAPI.Keybinds;
@@ -78,26 +77,9 @@ public abstract class CustomActionButton
public virtual ButtonUsesMode UsesMode => ButtonUsesMode.PerGame; public virtual ButtonUsesMode UsesMode => ButtonUsesMode.PerGame;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public virtual KeyboardKeyCode? DefaultKeybind { get; } public virtual MiraKeybind? Keybind => null;
/// <summary>
/// Gets the first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key to activate the button.
/// </summary>
public virtual ModifierKey Modifier1 => ModifierKey.None;
/// <summary>
/// Gets the second optional modifier key.
/// Set to <see cref="ModifierKey.None"/> if unused.
/// </summary>
public virtual ModifierKey Modifier2 => ModifierKey.None;
/// <summary>
/// Gets the third optional modifier key.
/// Set to <see cref="ModifierKey.None"/> if unused.
/// </summary>
public virtual ModifierKey Modifier3 => ModifierKey.None;
/// <summary> /// <summary>
/// Gets the button's text outline color. /// Gets the button's text outline color.
@@ -144,11 +126,6 @@ public abstract class CustomActionButton
/// </summary> /// </summary>
public GameObject? KeybindIcon { get; private set; } public GameObject? KeybindIcon { get; private set; }
/// <summary>
/// Gets or sets the keybind entry for the button.
/// </summary>
private KeybindManager.KeybindEntry? KeybindEntry { get; set; }
/// <summary> /// <summary>
/// Gets or sets the keybind icon text. /// Gets or sets the keybind icon text.
/// </summary> /// </summary>
@@ -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 = KeybindIcon =
Helpers.CreateKeybindIcon( Helpers.CreateKeybindIcon(
Button.gameObject, Button.gameObject,
KeybindEntry.Key, Keybind.CurrentKey,
new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f) new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f)
); );
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>(); KeybindText = KeybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>();
@@ -503,9 +487,10 @@ public abstract class CustomActionButton
/// <param name="playerControl">The local PlayerControl.</param> /// <param name="playerControl">The local PlayerControl.</param>
public virtual void FixedUpdateHandler(PlayerControl 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) if (Timer >= 0)
@@ -617,4 +602,4 @@ public abstract class CustomActionButton<T> : CustomActionButton where T : MonoB
Target = null; Target = null;
SetOutline(false); SetOutline(false);
} }
} }
+45 -98
View File
@@ -1,116 +1,63 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Rewired; using Rewired;
namespace MiraAPI.Keybinds namespace MiraAPI.Keybinds;
/// <summary>
/// Lets you register and manage mod keybinds with conflict checks and rebinding support.
/// </summary>
public static class KeybindManager
{ {
/// <summary> /// <summary>
/// Lets you register and manage mod keybinds with conflict checks and rebinding support. /// Gets a list of all registered keybinds.
/// </summary> /// </summary>
public static class KeybindManager public static List<MiraKeybind> Keybinds { get; } = new();
/// <summary>
/// Returns all conflicts where keybinds use the same key.
/// </summary>
/// <returns>A list of pairs of keybinds that conflict.</returns>
public static Dictionary<KeyboardKeyCode, List<MiraKeybind>> GetConflicts()
{ {
/// <summary> var conflicts = new Dictionary<KeyboardKeyCode, List<MiraKeybind>>();
/// Stores all info for a registered keybind. foreach (var keybind in Keybinds)
/// </summary>
public class KeybindEntry
{ {
/// <summary>Unique identifier for this keybind.</summary> var all = GetKeybindsForKey(keybind.CurrentKey, true);
public string Id; if (all.Length > 1)
/// <summary>Text shown in the Among Us rebinding menu to describe this keybind.</summary>
public string Description;
/// <summary>
/// The method that runs when this keybind is pressed.
/// </summary>
public Action Handler;
/// <summary>
/// The key currently assigned to this keybind.
/// </summary>
public KeyboardKeyCode Key;
/// <summary>
/// If true, this keybind will be checked for conflicts with other exclusive keybinds using the same key.
/// </summary>
public bool Exclusive;
/// <summary>
/// The first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key.
/// </summary>
public ModifierKey Modifier1 = ModifierKey.None;
/// <summary>
/// The second optional modifier key, if needed. Set to <c>ModifierKey.None</c> if unused.
/// </summary>
public ModifierKey Modifier2 = ModifierKey.None;
/// <summary>
/// The third optional modifier key, if needed. Set to <c>ModifierKey.None</c> if unused.
/// </summary>
public ModifierKey Modifier3 = ModifierKey.None;
}
private static readonly List<KeybindEntry> Registered = new();
/// <summary>
/// Registers a new mod keybind.
/// </summary>
/// <param name="id">Unique identifier for the keybind.</param>
/// <param name="description">Description to display in UI.</param>
/// <param name="key">Default key to use if no rebind exists.</param>
/// <param name="handler">Method to invoke when the keybind is pressed.</param>
/// <param name="exclusive">If true, conflicts with other exclusive keybinds on the same key.</param>
/// <param name="modifier1">Optional first modifier key (e.g., <c>Control</c>, <c>Shift</c>, <c>Alt</c>) that must be held with the main key.</param>
/// <param name="modifier2">Optional second modifier key. Use <c>ModifierKey.None</c> if unused.</param>
/// <param name="modifier3">Optional third modifier key. Use <c>ModifierKey.None</c> if unused.</param>
/// <returns> Returns the key that was registered for this keybind. </returns>
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
{ {
Id = id, if (!conflicts.TryGetValue(keybind.CurrentKey, out var group))
Description = description,
Key = key,
Handler = handler,
Exclusive = exclusive,
Modifier1 = modifier1,
Modifier2 = modifier2,
Modifier3 = modifier3,
});
return key;
}
/// <summary>
/// Returns all conflicts where exclusive keybinds use the same key.
/// </summary>
/// <returns>A list of pairs of keybinds that conflict.</returns>
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]; conflicts.Add(keybind.CurrentKey, new List<MiraKeybind>());
var b = Registered[j]; group = conflicts[keybind.CurrentKey];
if (a.Key == b.Key && a.Exclusive && b.Exclusive) }
{
conflicts.Add((a, b)); if (!group.Contains(keybind))
} {
group.Add(keybind);
} }
} }
return conflicts;
} }
/// <summary> return conflicts;
/// Returns all currently registered keybind entries. }
/// </summary>
/// <returns>A list of all registered keybind entries.</returns> /// <summary>
public static List<KeybindEntry> GetEntries() /// Returns all keybinds for a specified keycode.
/// </summary>
/// <param name="keyCode">The key to look for.</param>
/// /// <param name="exclusiveCheck">Should only exlusive keybinds be present.</param>
/// <returns>The list of keybinds.</returns>
public static MiraKeybind[] GetKeybindsForKey(KeyboardKeyCode keyCode, bool exclusiveCheck = false)
{
var all = new List<MiraKeybind>();
foreach (var keybind in Keybinds)
{ {
return Registered; if (keybind.CurrentKey == keyCode && (keybind.Exclusive || !exclusiveCheck))
{
all.Add(keybind);
}
} }
return all.ToArray();
} }
} }
+61
View File
@@ -0,0 +1,61 @@
using System;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Collection of keybind-related utility classes.
/// </summary>
public static class KeybindUtils
{
/// <summary>
/// Gets the Rewired <see cref="InputManager_Base"/> instance.
/// </summary>
public static InputManager_Base? RewiredInputManager { get; internal set; }
/// <summary>
/// Returns the currently assigned keycode of a keybind.
/// </summary>
/// <param name="keybind">The keybind to get the keycode from.</param>
/// <returns>The currently assigned keycode.</returns>
public static KeyboardKeyCode? GetKeycodeByKeybind(MiraKeybind keybind)
{
if (keybind.RewiredInputAction == null)
{
return null;
}
return GetKeycodeByActionId(keybind.RewiredInputAction.id);
}
/// <summary>
/// Gets the keycode for an action with ReInput.
/// </summary>
/// <param name="actionId">The action ID.</param>
/// <returns>The keyboard keycode.</returns>
public static KeyboardKeyCode? GetKeycodeByActionId(int actionId)
{
var player = ReInput.players.GetPlayer(0);
return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false)?
.keyboardKeyCode;
}
/// <summary>
/// Finds and returns an unused KeyCode that is not equal to the excluded key.
/// </summary>
/// <param name="exclude">The KeyCode to skip during the search.</param>
/// <returns>
/// The first available KeyCode not currently used by any registered keybind,
/// or KeyCode.None if none are available.
/// </returns>
public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude)
{
foreach (KeyboardKeyCode key in Enum.GetValues<KeyboardKeyCode>())
{
if (key == exclude) continue;
bool used = KeybindManager.Keybinds.Exists(e => e.DefaultKey == key);
if (!used) return key;
}
return KeyboardKeyCode.None;
}
}
+41
View File
@@ -0,0 +1,41 @@
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Class containing all default keybinds.
/// Recommended for cross-mod compatibility.
/// </summary>
[RegisterCustomKeybinds]
public static class MiraGlobalKeybinds
{
/// <summary>
/// Gets the keybind for primary abilities.
/// </summary>
public static MiraKeybind PrimaryAbility { get; } = new("Primary Ability", KeyboardKeyCode.T);
/// <summary>
/// Gets the keybind for secondary abilities.
/// </summary>
public static MiraKeybind SecondaryAbility { get; } = new("Secondary Ability", KeyboardKeyCode.Y);
/// <summary>
/// Gets the keybind for Tetiary abilities.
/// </summary>
public static MiraKeybind TertiaryAbility { get; } = new("Tertiary Ability", KeyboardKeyCode.U);
/// <summary>
/// Gets the keybind for primary modifier abilities.
/// </summary>
public static MiraKeybind ModifierPrimaryAbility { get; } = new("Modifier Primary Ability", KeyboardKeyCode.M);
/// <summary>
/// Gets the keybind for secondary modifier abilities.
/// </summary>
public static MiraKeybind ModifierSecondaryAbility { get; } = new("Modifier Secondary Ability", KeyboardKeyCode.N);
/// <summary>
/// Gets the keybind for Tetiary modifier abilities.
/// </summary>
public static MiraKeybind ModifierTertiaryAbility { get; } = new("Modifier Tertiary Ability", KeyboardKeyCode.T);
}
+111
View File
@@ -0,0 +1,111 @@
using System;
using System.Globalization;
using BepInEx.Unity.IL2CPP;
using Reactor.Utilities;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Represents a registered keybind.
/// </summary>
public class MiraKeybind
{
/// <summary>
/// Gets name of the keybind
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the unique identifier for this keybind. Used in Rewired.
/// </summary>
public string Id { get; }
/// <summary>
/// Gets the default keycode of this keybind.
/// </summary>
public KeyboardKeyCode DefaultKey { get; }
/// <summary>
/// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key.
/// </summary>
public bool Exclusive { get; }
/// <summary>
/// Gets the modifier keys assinged to this keybind.
/// Due to Rewired limitations, there can only be 3 modifier keys.
/// </summary>
public ModifierKey[] ModifierKeys { get; }
/// <summary>
/// Gets the Rewired <see cref="InputAction"/> assinged for this keybind.
/// </summary>
public InputAction? RewiredInputAction { get; internal set; }
/// <summary>
/// Gets the currently assigned keycode.
/// </summary>
public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this) ?? KeyboardKeyCode.None;
/// <summary>
/// Gets or sets the handler of the keybind. Invoked when the keybind is activated.
/// </summary>
private Action Handler { get; set; }
internal BasePlugin? SourcePlugin { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="MiraKeybind"/> class.
/// </summary>
/// <param name="name">The name of the keybind.</param>
/// <param name="defaultKeycode">The default keycode.</param>
/// <param name="modifierKeys">Up to 3 optional modifier keys.</param>
/// <param name="exclusive">Is exclusive.</param>
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<ModifierKey>();
Exclusive = exclusive;
Handler = () => { };
}
/// <inheritdoc/>
public override string ToString()
{
return $"{Name} ({Id})";
}
/// <summary>
/// Triggers the keybind, invoking its handler.
/// </summary>
public void Invoke()
{
Handler.Invoke();
Logger<MiraApiPlugin>.Warning($"Keybind invoked: {Name} ({Id}) - {DefaultKey.ToString()}");
}
/// <summary>
/// Adds an <see cref="Action"/> that will be invoked when the keybind is used.
/// </summary>
/// <param name="action">The <see cref="Action"/> to add.</param>
public void OnActivate(Action action)
{
Handler += action;
}
/// <summary>
/// Removes an <see cref="Action"/> that would be invoked when the keybind is used.
/// Not recommended to use - add checks in the method added in <see cref="OnActivate"/> itself rather than removing it from the keybind.
/// </summary>
/// <param name="action">The <see cref="Action"/> to remove.</param>
public void RemoveOnActivate(Action action)
{
Handler -= action;
}
}
@@ -0,0 +1,9 @@
using System;
namespace MiraAPI.Keybinds;
/// <summary>
/// Used to mark a class for custom keybinds registration.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class RegisterCustomKeybindsAttribute : Attribute;
-36
View File
@@ -1,36 +0,0 @@
using HarmonyLib;
using MiraAPI.Keybinds;
using UnityEngine;
namespace MiraAPI.Patches;
/// <summary>
/// Checks keybind conflicts in the lobby and disables secondary ones.
/// </summary>
[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 += $"• <color=#FF5555>{b.Description}</color> (conflicts with {a.Description})\n";
b.Handler = null;
}
HudManager.Instance.ShowPopUp(message.TrimEnd());
}
}
+18 -15
View File
@@ -1,6 +1,5 @@
using System; using System.Collections.Generic;
using System.Collections; using System.Linq;
using System.Collections.Generic;
using HarmonyLib; using HarmonyLib;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Keybinds; using MiraAPI.Keybinds;
@@ -25,7 +24,7 @@ public static class HudManagerPatches
public static Transform? BottomRight { get; private set; } public static Transform? BottomRight { get; private set; }
public static Transform? Buttons { get; private set; } public static Transform? Buttons { get; private set; }
private static Dictionary<TextMeshPro, int> VanillaKeybindIcons = new(); private static Dictionary<TextMeshPro, int> vanillaKeybindIcons = new();
/* /*
/// <summary> /// <summary>
@@ -117,9 +116,13 @@ public static class HudManagerPatches
var buttonObj = kvp.Key; var buttonObj = kvp.Key;
var actionId = kvp.Value; 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); var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos);
VanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId); vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId);
} }
} }
@@ -155,24 +158,24 @@ public static class HudManagerPatches
[HarmonyPatch(nameof(HudManager.Update))] [HarmonyPatch(nameof(HudManager.Update))]
[HarmonyPostfix] [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 player = ReInput.players.GetPlayer(0);
var keyboard = player.controllers.Keyboard; var keyboard = player.controllers.Keyboard;
if (player.GetButtonDown(entry.Id) && bool modifierKeysPressed = entry.ModifierKeys.All(k => keyboard.GetModifierKey(k)) ||
(entry.Modifier1 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier1)) && entry.ModifierKeys.Length <= 0;
(entry.Modifier2 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier2)) && if (player.GetButtonDown(entry.Id) && modifierKeysPressed)
(entry.Modifier3 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier3)))
{ {
entry.Handler?.Invoke(); entry.Invoke();
} }
} }
} }
@@ -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<InputFieldInfo>();
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<Image>().color = Color.red;
}
}
}
}
+41 -9
View File
@@ -1,20 +1,52 @@
using System.Linq;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib; using HarmonyLib;
using MiraAPI.Keybinds; using MiraAPI.Keybinds;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities; using MiraAPI.Utilities;
using Reactor.Utilities;
using Rewired; 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))] private static bool _registered;
public static class KeybindMenuPatch
[HarmonyPrefix]
private static void AwakePrefix(InputManager_Base __instance)
{ {
[HarmonyPrefix] if (_registered)
private static void StartPrefix(InputManager_Base __instance)
{ {
foreach (var entry in KeybindManager.GetEntries()) return;
{
__instance.userData.RegisterModBind(entry.Id, entry.Description, entry.Key, modifier1: entry.Modifier1, modifier2: entry.Modifier2, modifier3: entry.Modifier3);
}
} }
KeybindUtils.RewiredInputManager = __instance;
foreach (var keybind in KeybindManager.Keybinds)
{
if (__instance.userData.actions.ToArray().Any(x => x.name == keybind.Id))
{
Logger<MiraApiPlugin>.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;
} }
} }
@@ -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<ButtonRolloverHandler>()!;
_remap_background = _remap_rollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
_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;
}
}
+46 -13
View File
@@ -18,8 +18,6 @@ using MiraAPI.Roles;
using MiraAPI.Utilities; using MiraAPI.Utilities;
using Reactor.Networking; using Reactor.Networking;
using Reactor.Utilities; using Reactor.Utilities;
using Rewired;
using UnityEngine;
namespace MiraAPI.PluginLoading; namespace MiraAPI.PluginLoading;
@@ -110,6 +108,7 @@ public sealed class MiraPluginManager
} }
RegisterColorClasses(type); RegisterColorClasses(type);
RegisterKeybindClasses(type, plugin);
} }
info.PluginConfig.Save(); info.PluginConfig.Save();
@@ -135,18 +134,9 @@ public sealed class MiraPluginManager
RegisteredPlugins = [.. _registeredPlugins.Values]; RegisteredPlugins = [.. _registeredPlugins.Values];
ModifierManager.Modifiers = new ReadOnlyCollection<BaseModifier>(ModifierManager.InternalModifiers); ModifierManager.Modifiers = new ReadOnlyCollection<BaseModifier>(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<MiraApiPlugin>.Info($"Registered keybind for button '{button.GetType().Name}' with default key {(KeyboardKeyCode)button.DefaultKeybind}.");
}
}; };
RegisterKeybindClasses(typeof(MiraGlobalKeybinds), PluginSingleton<MiraApiPlugin>.Instance);
} }
/// <summary> /// <summary>
@@ -317,4 +307,47 @@ public sealed class MiraPluginManager
return false; return false;
} }
private static void RegisterKeybindClasses(Type type, BasePlugin source)
{
try
{
if (type.GetCustomAttribute<RegisterCustomKeybindsAttribute>() == null)
{
return;
}
if (!type.IsStatic())
{
Logger<MiraApiPlugin>.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<MiraApiPlugin>.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<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for keybinds.");
}
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register keybind class {type.Name}: {e}");
}
}
} }
+16 -14
View File
@@ -554,24 +554,22 @@ public static class Extensions
/// Registers a new mod keybind as a user-assignable button action in Rewired. /// Registers a new mod keybind as a user-assignable button action in Rewired.
/// </summary> /// </summary>
/// <param name="userData">The Rewired user data to add the action to.</param> /// <param name="userData">The Rewired user data to add the action to.</param>
/// <param name="actionName">The internal name of the action.</param> /// <param name="id">The internal name of the action.</param>
/// <param name="description">Text shown in the rebinding UI.</param> /// <param name="name">Text shown in the rebinding UI.</param>
/// /// <param name="group">Group shown above the label.</param>
/// <param name="key">The default key to assign to this action.</param> /// <param name="key">The default key to assign to this action.</param>
/// <param name="category">Category ID to group actions in Rewired (default is 0).</param> /// <param name="category">Category ID to group actions in Rewired (default is 0).</param>
/// <param name="elementIdentifierId">The element identifier ID (default is -1, meaning none specified).</param> /// <param name="elementIdentifierId">The element identifier ID (default is -1, meaning none specified).</param>
/// <param name="type">The <see cref="InputActionType"/> for this action (default is Button).</param> /// <param name="type">The <see cref="InputActionType"/> for this action (default is Button).</param>
/// <param name="modifier1">The first optional modifier key (e.g., <c>Control</c>, <c>Shift</c>, <c>Alt</c>) that must be held together with the main key.</param> /// <param name="modifiers">Optional modifier keys (e.g., <c>Control</c>, <c>Shift</c>, <c>Alt</c>) that must be held together with the main key.</param>
/// <param name="modifier2">The second optional modifier key. Set to <see cref="ModifierKey.None"/> if not used.</param>
/// <param name="modifier3">The third optional modifier key. Set to <see cref="ModifierKey.None"/> if not used.</param>
/// <returns>The action ID of the newly registered action.</returns> /// <returns>The action ID of the newly registered action.</returns>
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); userData.AddAction(category);
var action = userData.GetAction(userData.actions.Count - 1)!; var action = userData.GetAction(userData.actions.Count - 1)!;
action.name = actionName; action.name = id;
action.descriptiveName = description; action.descriptiveName = $"<b><size=70%>{Palette.CrewmateRoleHeaderDarkBlue.ToTextColor()}{group.ReplaceLineEndings(" ")}</color></size></b>\n{name}";
action.categoryId = category; action.categoryId = category;
action.type = type; action.type = type;
action.userAssignable = true; action.userAssignable = true;
@@ -583,13 +581,17 @@ public static class Extensions
_elementType = ControllerElementType.Button, _elementType = ControllerElementType.Button,
_axisContribution = Pole.Positive, _axisContribution = Pole.Positive,
_keyboardKeyCode = key, _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.keyboardMaps[0].actionElementMaps.Add(map);
userData.joystickMaps[0].actionElementMaps.Add(map); userData.joystickMaps[0].actionElementMaps.Add(map);
return action;
return action.id;
} }
} }
+4 -36
View File
@@ -1,10 +1,7 @@
using System; using System.Collections;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Il2CppSystem.IO;
using MiraAPI.Keybinds;
using MiraAPI.Utilities.Assets; using MiraAPI.Utilities.Assets;
using Rewired; using Rewired;
using TMPro; 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)]; 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) internal static GameObject CreateKeybindIcon(GameObject button, KeyboardKeyCode keyCode, Vector3 localPos)
{ {
var keybindIcon = Object.Instantiate(HudManager.Instance.AbilityButton.usesRemainingSprite.gameObject, button.transform); var keybindIcon = Object.Instantiate(HudManager.Instance.AbilityButton.usesRemainingSprite.gameObject, button.transform);
keybindIcon.GetComponent<SpriteRenderer>().sprite = MiraAssets.KeybindButton.LoadAsset(); keybindIcon.GetComponent<SpriteRenderer>().sprite = MiraAssets.KeybindButton.LoadAsset();
keybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>().text = keyCode.ToString(); keybindIcon.transform.GetComponentInChildren<TextMeshPro>().text = keyCode.ToString();
keybindIcon.name = "KeybindIcon"; keybindIcon.name = "KeybindIcon";
keybindIcon.transform.localPosition = localPos; keybindIcon.transform.localPosition = localPos;
return keybindIcon; return keybindIcon;
} }
/// <summary>
/// Gets the keybind for an action with ReInput.
/// </summary>
/// <param name="actionId">The action ID.</param>
/// <returns>The keyboard key code.</returns>
public static KeyboardKeyCode GetKeybindByActionId(int actionId)
{
var player = ReInput.players.GetPlayer(0);
return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false)
.keyboardKeyCode;
}
/// <summary> /// <summary>
/// Determines whether a given probability check succeeds. /// Determines whether a given probability check succeeds.
/// </summary> /// </summary>
@@ -426,23 +412,5 @@ public static class Helpers
return new string(Enumerable.Repeat(chars, length) return new string(Enumerable.Repeat(chars, length)
.Select(s => s[UnityEngine.Random.RandomRangeInt(0, s.Length)]).ToArray()); .Select(s => s[UnityEngine.Random.RandomRangeInt(0, s.Length)]).ToArray());
} }
/// <summary>
/// Finds and returns an unused KeyCode that is not equal to the excluded key.
/// </summary>
/// <param name="exclude">The KeyCode to skip during the search.</param>
/// <returns>
/// The first available KeyCode not currently used by any registered keybind,
/// or KeyCode.None if none are available.
/// </returns>
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;
}
} }