From 2156c8046753c525a28079d76378a8be09f7d42e Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 4 Jul 2025 22:42:47 -0500 Subject: [PATCH 01/19] Cosmetic Tab Count Improvements --- MiraAPI/Patches/Menu/HatsTabPatches.cs | 2 +- MiraAPI/Patches/Menu/NameplatesTabPatches.cs | 2 +- MiraAPI/Patches/Menu/SkinsTabPatches.cs | 2 +- MiraAPI/Patches/Menu/VisorsTabPatches.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Patches/Menu/HatsTabPatches.cs b/MiraAPI/Patches/Menu/HatsTabPatches.cs index f985027..8d36e9c 100644 --- a/MiraAPI/Patches/Menu/HatsTabPatches.cs +++ b/MiraAPI/Patches/Menu/HatsTabPatches.cs @@ -84,7 +84,7 @@ public static class HatsTabPatches text.gameObject.transform.localScale = Vector3.one; text.GetComponent().Destroy(); text.EnableStencilMasking(); - text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages"; + text.text = $"{groupName} ({currentPage + 1}/{sortedHats.Count})\nPress Ctrl or Tab to cycle pages"; text.alignment = TextAlignmentOptions.Center; text.fontSize = 3f; text.fontSizeMax = 3f; diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs index bb5482a..11657a0 100644 --- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs +++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs @@ -73,7 +73,7 @@ public static class NameplatesTabPatches text.gameObject.transform.localScale = Vector3.one; text.GetComponent().Destroy(); text.EnableStencilMasking(); - text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages"; + text.text = $"{groupName} ({currentPage + 1}/{SortedNameplates.Count})\nPress Ctrl or Tab to cycle pages"; text.alignment = TextAlignmentOptions.Center; text.fontSize = 3f; text.fontSizeMax = 3f; diff --git a/MiraAPI/Patches/Menu/SkinsTabPatches.cs b/MiraAPI/Patches/Menu/SkinsTabPatches.cs index 2e9ef2e..9378d8b 100644 --- a/MiraAPI/Patches/Menu/SkinsTabPatches.cs +++ b/MiraAPI/Patches/Menu/SkinsTabPatches.cs @@ -76,7 +76,7 @@ public static class SkinsTabPatches text.gameObject.transform.localScale = Vector3.one; text.GetComponent().Destroy(); text.EnableStencilMasking(); - text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages"; + text.text = $"{groupName} ({currentPage + 1}/{sortedSkins.Count})\nPress Ctrl or Tab to cycle pages"; text.alignment = TextAlignmentOptions.Center; text.fontSize = 3f; text.fontSizeMax = 3f; diff --git a/MiraAPI/Patches/Menu/VisorsTabPatches.cs b/MiraAPI/Patches/Menu/VisorsTabPatches.cs index d88cdab..92d5b8e 100644 --- a/MiraAPI/Patches/Menu/VisorsTabPatches.cs +++ b/MiraAPI/Patches/Menu/VisorsTabPatches.cs @@ -72,7 +72,7 @@ public static class VisorsTabPatches text.gameObject.transform.localScale = Vector3.one; text.GetComponent().Destroy(); text.EnableStencilMasking(); - text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages"; + text.text = $"{groupName} ({currentPage + 1}/{SortedVisors.Count})\nPress Ctrl or Tab to cycle pages"; text.alignment = TextAlignmentOptions.Center; text.fontSize = 3f; text.fontSizeMax = 3f; From 62bee24789c9e2148f6b98def692ccf81701ceb8 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 4 Jul 2025 22:43:13 -0500 Subject: [PATCH 02/19] Fix Server Dropdown for Game Finder --- MiraAPI/Patches/ServerDropdownPatch.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/ServerDropdownPatch.cs b/MiraAPI/Patches/ServerDropdownPatch.cs index 0fb96e3..36da2d7 100644 --- a/MiraAPI/Patches/ServerDropdownPatch.cs +++ b/MiraAPI/Patches/ServerDropdownPatch.cs @@ -1,6 +1,7 @@ using HarmonyLib; using UnityEngine; using UnityEngine.Events; +using UnityEngine.SceneManagement; namespace MiraAPI.Patches; @@ -14,6 +15,8 @@ public static class ServerDropdownPatch foreach (var regionInfo in DestroyableSingleton.Instance.AvailableRegions) { + var findingGame = SceneManager.GetActiveScene().name is "FindAGame"; + if (DestroyableSingleton.Instance.CurrentRegion.Equals(regionInfo)) { __instance.defaultButtonSelected = __instance.firstOption; @@ -27,6 +30,10 @@ public static class ServerDropdownPatch var region = regionInfo; var serverListButton = __instance.ButtonPool.Get(); var x = num % 2 == 0 ? -2 : 2; + if (findingGame) + { + x += 2; + } var y = -0.55f * (num / 2); serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f); serverListButton.transform.localScale = Vector3.one; @@ -39,7 +46,7 @@ public static class ServerDropdownPatch serverListButton.Button.OnClick.AddListener((UnityAction)(() => { __instance.ChooseOption(region); })); __instance.controllerSelectable.Add(serverListButton.Button); __instance.background.transform.localPosition = new Vector3( - 0f, + findingGame ? 2f : 0f, __instance.initialYPos + (-0.3f * (num / 2)), 0f); __instance.background.size = new Vector2(__instance.background.size.x, 1.2f + (0.6f * (num / 2))); From 6b3038f39755963760f1f558ca9f03fa12be1fef Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:47:12 -0400 Subject: [PATCH 03/19] shuffle modifiers before assigning --- MiraAPI/Modifiers/ModifierManager.cs | 2 + MiraAPI/Utilities/EnumerableExtensions.cs | 49 +++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 MiraAPI/Utilities/EnumerableExtensions.cs diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 6aed1ac..f34d11f 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -5,6 +5,7 @@ using System.Runtime.Serialization; using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Utilities; using Reactor.Utilities; using Reactor.Utilities.Extensions; using Random = System.Random; @@ -125,6 +126,7 @@ public static class ModifierManager .Select(x => Activator.CreateInstance(x.Value) as GameModifier) .OfType() .Where(x => x.GetAmountPerGame() > 0 && x.GetAssignmentChance() > 0) + .Shuffle() .OrderByDescending(x => x.Priority()) .ToArray(); diff --git a/MiraAPI/Utilities/EnumerableExtensions.cs b/MiraAPI/Utilities/EnumerableExtensions.cs new file mode 100644 index 0000000..5e0bc35 --- /dev/null +++ b/MiraAPI/Utilities/EnumerableExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiraAPI.Utilities; + +/// +/// Extensions for IEnumerable. +/// +public static class EnumerableExtensions +{ + /// + /// Shuffle the elements of the source enumerable using a new instance of Random. + /// + /// The source enumerable. + /// The type of the elements. + /// >The shuffled enumerable. + public static IEnumerable Shuffle(this IEnumerable source) + { + return source.Shuffle(new Random()); + } + + /// + /// Shuffle the elements of the source enumerable using the provided Random instance. + /// + /// The source enumerable. + /// The Random instance to use for shuffling. + /// The type of the elements. + /// >The shuffled enumerable. + public static IEnumerable Shuffle(this IEnumerable source, Random rng) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(rng); + + return source.ShuffleIterator(rng); + } + + private static IEnumerable ShuffleIterator(this IEnumerable source, Random rng) + { + var buffer = source.ToList(); + for (var i = 0; i < buffer.Count; i++) + { + var j = rng.Next(i, buffer.Count); + yield return buffer[j]; + + buffer[j] = buffer[i]; + } + } +} From d9d0d6bc622244956ae1b0c139feb2bdd682dcfa Mon Sep 17 00:00:00 2001 From: CallOfCreator Date: Tue, 8 Jul 2025 20:41:20 +0000 Subject: [PATCH 04/19] 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 05/19] 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 06/19] 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 07/19] 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 277391ef1c22edb9dd1c070af6b92083c5ce1a15 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 11 Jul 2025 22:30:09 -0500 Subject: [PATCH 08/19] Add freeplay modifier colors --- MiraAPI/Modifiers/BaseModifier.cs | 5 ++++ .../Patches/Freeplay/TaskAddButtonPatches.cs | 11 -------- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 26 ++++++++++++++++--- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 52e30b6..edbd198 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -64,6 +64,11 @@ public abstract class BaseModifier : IOptionable /// public virtual bool ShowInFreeplay => false; + /// + /// Gets a value indicating the color that should be used for the modifier within freeplay + /// + public virtual Color FreeplayFileColor => Color.gray; + /// /// Gets a value indicating whether the modifier is unique. If true, the player can only have one instance of this modifier. /// diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 184bafb..07d37f5 100644 --- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -22,15 +22,4 @@ internal static class TaskAddButtonPatches return true; } - - [HarmonyPostfix] - [HarmonyPatch(nameof(TaskAddButton.Role), MethodType.Setter)] - public static void RoleGetterPatch(TaskAddButton __instance) - { - if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole) - { - __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray; - } - __instance.RolloverHandler.OutColor = __instance.FileImage.color; - } } diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index f90ffba..ae49ed4 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -10,6 +10,7 @@ using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using TMPro; using UnityEngine; +using UnityEngine.Events; using UnityEngine.UI; using Object = UnityEngine.Object; @@ -19,7 +20,6 @@ namespace MiraAPI.Patches.Freeplay; internal static class TaskAdderPatches { private static Scroller _scroller = null!; - [HarmonyPostfix] [HarmonyPatch(nameof(TaskAdderGame.Begin))] public static void AddRolesFolder(TaskAdderGame __instance) @@ -32,6 +32,11 @@ internal static class TaskAdderPatches _scroller.allowY = true; _scroller.Inner = inner.transform; + var scrollerHelper = __instance.TaskParent.gameObject.AddComponent(); + scrollerHelper.scroller = _scroller; + scrollerHelper.verticalAxis = RewiredConstsEnum.Action.TaskRVertical; + scrollerHelper.scrollSpeed = 10f; + GameObject hitbox = new("Hitbox") { layer = 5, @@ -295,7 +300,7 @@ internal static class TaskAdderPatches if (_scroller) { - _scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 5, 1.4f); + _scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 4.5f, 1.65f); _scroller.SetYBoundsMin(0.0f); _scroller.ScrollToTop(); } @@ -332,7 +337,8 @@ internal static class TaskAdderPatches taskAddButton.SafePositionWorld = instance.SafePositionWorld; taskAddButton.Text.text = modifier.ModifierName; taskAddButton.Text.EnableMasking(); - + taskAddButton.FileImage.color = modifier.FreeplayFileColor; + taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor; taskAddButton.Button.OnClick = new Button.ButtonClickedEvent(); taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => { @@ -384,6 +390,20 @@ internal static class TaskAdderPatches instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; + if (roleBehaviour is ICustomRole custom) + { + var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray; + if (custom.Team is ModdedRoleTeams.Crewmate) + { + customColor = Palette.CrewmateBlue; + } + else if (custom.Team is ModdedRoleTeams.Impostor) + { + customColor = Palette.ImpostorRed; + } + taskAddButton2.FileImage.color = customColor; + taskAddButton2.RolloverHandler.OutColor = customColor; + } if (taskAddButton2.Button == null) { continue; From bf7edb30ab720ca3622a6cf0f00acc07b28c4666 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 20 Jul 2025 07:30:38 -0400 Subject: [PATCH 09/19] fix warnings --- MiraAPI/Modifiers/BaseModifier.cs | 2 +- MiraAPI/Patches/ChatControllerPatch.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index edbd198..0e31779 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -65,7 +65,7 @@ public abstract class BaseModifier : IOptionable public virtual bool ShowInFreeplay => false; /// - /// Gets a value indicating the color that should be used for the modifier within freeplay + /// Gets a value indicating the color that should be used for the modifier within freeplay. /// public virtual Color FreeplayFileColor => Color.gray; diff --git a/MiraAPI/Patches/ChatControllerPatch.cs b/MiraAPI/Patches/ChatControllerPatch.cs index a30307c..e65be7f 100644 --- a/MiraAPI/Patches/ChatControllerPatch.cs +++ b/MiraAPI/Patches/ChatControllerPatch.cs @@ -3,10 +3,12 @@ using UnityEngine; namespace MiraAPI.Patches; +#pragma warning disable SA1629 /// /// Allows players to paste text into chat. /// Source: https://github.com/CallOfCreator/NewMod/blob/main/NewMod/Patches/ClipboardPatch.cs /// +#pragma warning restore SA1629 [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] public static class ChatControllerPatch { From f98429b5f9d23d114402dd1d1ad9b31874296bef Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:18:02 +0100 Subject: [PATCH 10/19] Update GetClosestPlayer method --- MiraAPI/Utilities/Extensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 57887d8..5fbc671 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -513,6 +513,7 @@ public static class Extensions /// Whether impostors should be included in the search. /// The radius to search within. /// Whether colliders should be ignored when searching. + /// Determines if Ghosts are included /// Optional predicate to test if the object is valid. /// The closest player if there is one, false otherwise. public static PlayerControl? GetClosestPlayer( @@ -520,16 +521,16 @@ public static class Extensions bool includeImpostors, float distance, bool ignoreColliders = false, + bool includeGhosts = false, Predicate? predicate = null) { var filteredPlayers = Helpers.GetClosestPlayers(playerControl, distance, ignoreColliders) .Where( playerInfo => !playerInfo.Data.Disconnected && playerInfo.PlayerId != playerControl.PlayerId && - !playerInfo.Data.IsDead && + (includeGhosts || !playerInfo.Data.IsDead) && (includeImpostors || !playerInfo.Data.Role.IsImpostor)) .ToList(); - return predicate != null ? filteredPlayers.Find(predicate) : filteredPlayers.FirstOrDefault(); } From afd6fbc3b65a1d804ef5474fc086046c90b4da30 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 2 Aug 2025 14:24:53 -0500 Subject: [PATCH 11/19] Add BeforeRoundStartEvent --- .../Vanilla/Gameplay/BeforeRoundStartEvent.cs | 21 +++++++++++++++++++ .../Vanilla/Gameplay/RoundStartEvent.cs | 2 +- .../Patches/Events/SpawnInMinigamePatch.cs | 7 ++++++- .../Patches/Events/SubmergedSpawnGamePatch.cs | 7 ++++++- MiraAPI/Patches/IntroCutscenePatches.cs | 5 +++++ MiraAPI/Patches/Roles/EjectionPatches.cs | 7 ++++++- MiraAPI/Utilities/Extensions.cs | 2 +- 7 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs diff --git a/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs new file mode 100644 index 0000000..f9d41ee --- /dev/null +++ b/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla.Gameplay; + +/// +/// Event invoked on IntroCutscene.OnDestroy and ExileController.WrapUp to determine if it should run the round event or not. +/// +public class BeforeRoundStartEvent : MiraCancelableEvent +{ + /// + /// Gets a value indicating whether the event was triggered by the IntroCutscene or ExileController. + /// + public bool TriggeredByIntro { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Whether the event was triggered by the intro or not. + public BeforeRoundStartEvent(bool triggeredByIntro) + { + TriggeredByIntro = triggeredByIntro; + } +} diff --git a/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs index a50fde5..470f85a 100644 --- a/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs @@ -1,7 +1,7 @@ namespace MiraAPI.Events.Vanilla.Gameplay; /// -/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp. +/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp if the event isn't cancelled. /// public class RoundStartEvent : MiraEvent { diff --git a/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs b/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs index b726999..65768d4 100644 --- a/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs +++ b/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs @@ -9,7 +9,12 @@ internal static class SpawnInMinigamePatch { public static void Postfix() { - var @event = new RoundStartEvent(false); + var @event = new BeforeRoundStartEvent(false); MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) return; + + var @event2 = new RoundStartEvent(false); + MiraEventManager.InvokeEvent(@event2); } } diff --git a/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs b/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs index d2664b9..07c358a 100644 --- a/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs +++ b/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs @@ -25,7 +25,12 @@ internal static class SubmergedSpawnGamePatch public static void Postfix() { - var @event = new RoundStartEvent(false); + var @event = new BeforeRoundStartEvent(false); MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) return; + + var @event2 = new RoundStartEvent(false); + MiraEventManager.InvokeEvent(@event2); } } diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index d068f26..af7ce8c 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -61,6 +61,11 @@ public static class IntroCutscenePatches public static void GameBeginPatch(IntroCutscene __instance) { MiraEventManager.InvokeEvent(new IntroEndEvent(__instance)); + + var @event = new BeforeRoundStartEvent(true); + MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) return; MiraEventManager.InvokeEvent(new RoundStartEvent(true)); } } diff --git a/MiraAPI/Patches/Roles/EjectionPatches.cs b/MiraAPI/Patches/Roles/EjectionPatches.cs index 85641eb..009da27 100644 --- a/MiraAPI/Patches/Roles/EjectionPatches.cs +++ b/MiraAPI/Patches/Roles/EjectionPatches.cs @@ -38,8 +38,13 @@ internal static class EjectionPatches { public static void Postfix() { - var @event = new RoundStartEvent(false); + var @event = new BeforeRoundStartEvent(false); MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) return; + + var @event2 = new RoundStartEvent(false); + MiraEventManager.InvokeEvent(@event2); } } } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 5fbc671..95a6692 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -513,7 +513,7 @@ public static class Extensions /// Whether impostors should be included in the search. /// The radius to search within. /// Whether colliders should be ignored when searching. - /// Determines if Ghosts are included + /// Determines if Ghosts are included. /// Optional predicate to test if the object is valid. /// The closest player if there is one, false otherwise. public static PlayerControl? GetClosestPlayer( From 8145c8e5992fc1a86e844053028505f37bd0dfad Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 4 Aug 2025 15:33:04 +0200 Subject: [PATCH 12/19] patch --- .../Patches/Events/FreeplayRoundStartPatch.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs diff --git a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs new file mode 100644 index 0000000..12c95c1 --- /dev/null +++ b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Gameplay; + +namespace MiraAPI.Patches.Events; + +[HarmonyPatch(typeof(TutorialManager._RunTutorial_d__3), nameof(TutorialManager._RunTutorial_d__3.MoveNext))] +public static class FreeplayRoundStartPatch +{ + public static void Postfix(TutorialManager._RunTutorial_d__3 __instance, ref bool __result) + { + if (!__result) + { + MiraEventManager.InvokeEvent(new RoundStartEvent(true)); + } + } +} From e7fba386aad787d781b0fea40144539d4b47d88e Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 4 Aug 2025 20:53:50 +0200 Subject: [PATCH 13/19] dummy changes --- .../Vanilla/Meeting/Voting/DummyVoteEvent.cs | 44 ++++++++++++ MiraAPI/Patches/Voting/DummyBehaviourPatch.cs | 67 +++++++++++++++++-- 2 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs new file mode 100644 index 0000000..4b31694 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs @@ -0,0 +1,44 @@ +using System; +using MiraAPI.Utilities; +using MiraAPI.Voting; + +namespace MiraAPI.Events.Vanilla.Meeting.Voting; + +/// +/// Invoked before the dummy selects their vote. If canceled, the dummy won't vote. +/// +public class DummyVoteEvent : MiraCancelableEvent +{ + /// + /// Gets the dummy that needs to vote + /// + public DummyBehaviour Dummy { get; } + + /// + /// Gets the instance of the dummy's vote data. + /// + public PlayerVoteData VoteData { get; } + + /// + /// Gets or sets a value indicating whether the dummy can vote to skip the meeting. + /// + public bool CanSkip { get; set; } + + /// + /// Gets or sets a predicate for validating whether the dummy can vote on a player. + /// By default, excludes dead and disconnected players. + /// + public Predicate PlayerIsValid { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The voting dummy. + public DummyVoteEvent(DummyBehaviour dummy) + { + Dummy = dummy; + VoteData = dummy.myPlayer.GetVoteData(); + CanSkip = true; + PlayerIsValid = x => !x.Data.IsDead && !x.Data.Disconnected; + } +} diff --git a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs index a276946..3ebaf06 100644 --- a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs +++ b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs @@ -1,16 +1,71 @@ -using HarmonyLib; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.Utilities; +using MiraAPI.Voting; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using UnityEngine; namespace MiraAPI.Patches.Voting; -[HarmonyPatch(typeof(DummyBehaviour), nameof(DummyBehaviour.Update))] -internal static class DummyBehaviourPatch +[HarmonyPatch(typeof(DummyBehaviour))] +internal static class DummyBehaviourPatches { - public static void Postfix(DummyBehaviour __instance) + [HarmonyPrefix] + [HarmonyPatch(nameof(DummyBehaviour.Update))] + public static bool DummyUpdatePatch(DummyBehaviour __instance) { - if (MeetingHud.Instance && __instance.myPlayer.GetVoteData()) + NetworkedPlayerInfo data = __instance.myPlayer.Data; + if (data == null || data.IsDead) return false; + + if (MeetingHud.Instance && !__instance.DidVote()) { - __instance.voted = __instance.myPlayer.GetVoteData().VotesRemaining == 0; + Coroutines.Start(CoDoVote(__instance)); + } + + return false; + } + + private static IEnumerator CoDoVote(DummyBehaviour dummy) + { + dummy.voted = true; + yield return new WaitForSeconds(dummy.voteTime.Next()); + if (DidVote(dummy)) yield break; + + var dummyVoteEvent = new DummyVoteEvent(dummy); + MiraEventManager.InvokeEvent(dummyVoteEvent); + + if (dummyVoteEvent.IsCancelled) + { + dummy.voted = false; + yield break; + } + + List potentialSuspects = new(); + potentialSuspects.AddRange(PlayerControl.AllPlayerControls + .ToArray() + .Where(p => p != dummy.myPlayer) + .Where(p => dummyVoteEvent.PlayerIsValid(p)) + .Select(p => p.PlayerId)); + + if (dummyVoteEvent.CanSkip) + { + potentialSuspects.Add(253); + } + + if (dummy.myPlayer.GetVoteData().VotesRemaining > 0) + { + VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, dummy.myPlayer.PlayerId, potentialSuspects.Random()); } } + + private static bool DidVote(this DummyBehaviour dummy) + { + var dummyVoteData = dummy.myPlayer.GetVoteData(); + return dummyVoteData.Votes.Count > 0; + } } From c0c8fa08ff5a9a78344f3b3604dc19e2322a126a Mon Sep 17 00:00:00 2001 From: xChipseq Date: Mon, 4 Aug 2025 20:58:24 +0200 Subject: [PATCH 14/19] changed DidVote to check VotesRemaining instead of Votes --- MiraAPI/Patches/Voting/DummyBehaviourPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs index 3ebaf06..04f3461 100644 --- a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs +++ b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs @@ -66,6 +66,6 @@ internal static class DummyBehaviourPatches private static bool DidVote(this DummyBehaviour dummy) { var dummyVoteData = dummy.myPlayer.GetVoteData(); - return dummyVoteData.Votes.Count > 0; + return dummyVoteData.VotesRemaining > 0; } } From c343ab5e9fbc64e1e1744f6574927cf3f9c5cf7a Mon Sep 17 00:00:00 2001 From: xChipseq Date: Tue, 5 Aug 2025 22:34:00 +0200 Subject: [PATCH 15/19] dummy fix --- MiraAPI/Patches/Voting/DummyBehaviourPatch.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs index 04f3461..7652d4f 100644 --- a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs +++ b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Meeting.Voting; @@ -22,9 +23,16 @@ internal static class DummyBehaviourPatches NetworkedPlayerInfo data = __instance.myPlayer.Data; if (data == null || data.IsDead) return false; - if (MeetingHud.Instance && !__instance.DidVote()) + if (MeetingHud.Instance) { - Coroutines.Start(CoDoVote(__instance)); + if (!__instance.voted) + { + __instance.StartCoroutine(CoDoVote(__instance).WrapToIl2Cpp()); + } + } + else + { + __instance.voted = false; } return false; @@ -32,9 +40,9 @@ internal static class DummyBehaviourPatches private static IEnumerator CoDoVote(DummyBehaviour dummy) { + Logger.Warning(dummy.voted); dummy.voted = true; yield return new WaitForSeconds(dummy.voteTime.Next()); - if (DidVote(dummy)) yield break; var dummyVoteEvent = new DummyVoteEvent(dummy); MiraEventManager.InvokeEvent(dummyVoteEvent); @@ -57,13 +65,13 @@ internal static class DummyBehaviourPatches potentialSuspects.Add(253); } - if (dummy.myPlayer.GetVoteData().VotesRemaining > 0) + if (CanVote(dummy)) { VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, dummy.myPlayer.PlayerId, potentialSuspects.Random()); } } - private static bool DidVote(this DummyBehaviour dummy) + private static bool CanVote(this DummyBehaviour dummy) { var dummyVoteData = dummy.myPlayer.GetVoteData(); return dummyVoteData.VotesRemaining > 0; From 68e421bf91de9603cfe2249a47196bd2ea4e7c64 Mon Sep 17 00:00:00 2001 From: xChipseq Date: Tue, 5 Aug 2025 22:35:39 +0200 Subject: [PATCH 16/19] literally just a dot --- MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs index 4b31694..2f5cba0 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs @@ -10,7 +10,7 @@ namespace MiraAPI.Events.Vanilla.Meeting.Voting; public class DummyVoteEvent : MiraCancelableEvent { /// - /// Gets the dummy that needs to vote + /// Gets the dummy that needs to vote. /// public DummyBehaviour Dummy { get; } From 355eef509b31fd2ed8c9cb5865f9d1bc33503d19 Mon Sep 17 00:00:00 2001 From: CallOfCreator Date: Wed, 6 Aug 2025 18:03:48 +0000 Subject: [PATCH 17/19] 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 18/19] 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}."); - } }; } From 2ee8730ba483af6ab978626fc2f4dca0ea679ad9 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Thu, 14 Aug 2025 00:56:29 -0400 Subject: [PATCH 19/19] WIP Left side Modifiers HUD option --- MiraAPI/MiraApiConfig.cs | 23 ++++++++++++ MiraAPI/MiraApiPlugin.cs | 5 +++ MiraAPI/Modifiers/ModifierComponent.cs | 14 +++++++- .../ModifierDisplayComponent.cs | 35 +++++++++++++------ MiraAPI/Patches/Roles/HudManagerPatches.cs | 16 ++++----- 5 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 MiraAPI/MiraApiConfig.cs diff --git a/MiraAPI/MiraApiConfig.cs b/MiraAPI/MiraApiConfig.cs new file mode 100644 index 0000000..10c9776 --- /dev/null +++ b/MiraAPI/MiraApiConfig.cs @@ -0,0 +1,23 @@ +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using HarmonyLib; +using MiraAPI.PluginLoading; +using Reactor; +using Reactor.Networking; +using Reactor.Networking.Attributes; +using Reactor.Utilities; +using UnityEngine; + +namespace MiraAPI; + +/// +/// Mira API Config File Handler +/// +public class MiraApiConfig(ConfigFile config) +{ + /// + /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. + /// + public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false); +} diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 8654cf6..aa7f3c6 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -30,6 +30,10 @@ public partial class MiraApiPlugin : BasePlugin /// public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255); + /// + /// Gets the Mira Api Config Handler. + /// + public MiraApiConfig? MiraConfig { get; private set; } private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); @@ -40,6 +44,7 @@ public partial class MiraApiPlugin : BasePlugin ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow); + MiraConfig = new MiraApiConfig(Config); PluginManager = new MiraPluginManager(); PluginManager.Initialize(); } diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 5c29dc2..4f556f5 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -6,6 +6,7 @@ using System.Linq; using Il2CppInterop.Runtime.Attributes; using MiraAPI.Modifiers.ModifierDisplay; using MiraAPI.Modifiers.Types; +using MiraAPI.Patches.Roles; using Reactor.Utilities; using Reactor.Utilities.Attributes; using UnityEngine; @@ -126,7 +127,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists) { - ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled); + var taskPanelOpen = HudManager.Instance.TaskPanel.open; + var roleTabOpen = HudManagerPatches.RoleTab != null && HudManagerPatches.RoleTab.open; + var leftSideHud = PluginSingleton.Instance.MiraConfig!.ModifiersHudLeftSide.Value; + + if (leftSideHud && (taskPanelOpen || roleTabOpen)) + { + ModifierDisplay!.gameObject.SetActive(false); + } + else + { + ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled); + } } } diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs index f1a614e..0836935 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -1,9 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Patches.Roles; using MiraAPI.Utilities.Assets; +using Reactor.Utilities; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using TMPro; @@ -94,6 +97,16 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) IsOpen = false; _children.gameObject.SetActive(false); + + if (PluginSingleton.Instance.MiraConfig!.ModifiersHudLeftSide.Value) + { + var aspect = GetComponent(); + _toggleButton.transform.localPosition = new Vector3(-1f, 2.7f, 0f); + _children.transform.localPosition = new Vector3(-0.2f, -0.05f, 0f); + aspect.Alignment = AspectPosition.EdgeAlignments.LeftTop; + aspect.DistanceFromEdge = new Vector3(1.8f, 2.55f, -20f); + aspect.AdjustPosition(); + } } /// @@ -109,7 +122,16 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) /// public void ToggleTab() { - IsOpen = !IsOpen; + SetOpened(!IsOpen); + } + + /// + /// Use this to set the visibility of the modifiers tab. + /// + /// Whether it should be visible or not. + public void SetOpened(bool val) + { + IsOpen = val; _children.gameObject.SetActive(IsOpen); RefreshModifiers(); } @@ -118,7 +140,6 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) { _modifiers.Remove(component.Modifier!); component.gameObject.DestroyImmediate(); - RefreshModifiers(); } @@ -194,13 +215,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) _toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})"; - if (filteredModifiers.Count == 0) - { - _toggleButton.gameObject.SetActive(false); - return; - } - - _toggleButton.gameObject.SetActive(true); + _toggleButton.gameObject.SetActive(filteredModifiers.Count != 0); foreach (var mod in modifiersToAdd) { diff --git a/MiraAPI/Patches/Roles/HudManagerPatches.cs b/MiraAPI/Patches/Roles/HudManagerPatches.cs index 29affce..348f739 100644 --- a/MiraAPI/Patches/Roles/HudManagerPatches.cs +++ b/MiraAPI/Patches/Roles/HudManagerPatches.cs @@ -12,7 +12,7 @@ namespace MiraAPI.Patches.Roles; public static class HudManagerPatches { // Custom role tab. - private static TaskPanelBehaviour? _roleTab; + public static TaskPanelBehaviour? RoleTab; /// /// Fixes Kill Button not showing for Neutral killing role. @@ -38,9 +38,9 @@ public static class HudManagerPatches __instance.SabotageButton.gameObject.SetActive(isActive && customRole.Configuration.CanUseSabotage); } - if (_roleTab) + if (RoleTab) { - _roleTab?.gameObject.SetActive(isActive); + RoleTab?.gameObject.SetActive(isActive); } } @@ -62,16 +62,16 @@ public static class HudManagerPatches if (role is ICustomRole { Configuration.RoleHintType: RoleHintType.RoleTab } customRole) { - if (_roleTab == null) + if (RoleTab == null) { - _roleTab = CustomRoleManager.CreateRoleTab(customRole); + RoleTab = CustomRoleManager.CreateRoleTab(customRole); } - CustomRoleManager.UpdateRoleTab(_roleTab, customRole); + CustomRoleManager.UpdateRoleTab(RoleTab, customRole); } - else if (_roleTab) + else if (RoleTab) { - _roleTab?.gameObject.Destroy(); + RoleTab?.gameObject.Destroy(); } } }