From 372a9e3aa90271db7fe0c39ab2aa6c96c8e76c8c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 11 Jun 2025 21:26:08 -0400 Subject: [PATCH 1/8] suppress RPC warnings --- MiraAPI/Modifiers/ModifierExtensions.cs | 4 ++-- MiraAPI/Voting/VotingUtils.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index c028fa3..309a7d2 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -30,7 +30,7 @@ public static class ModifierExtensions _ = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException( $"Modifier with ID {typeId} is not registered."); - Rpc.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args)); + Rpc.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args), true); } /// @@ -63,7 +63,7 @@ public static class ModifierExtensions /// /// The player to remove the modifier from. /// The unique ID of the modifier. - [MethodRpc((uint)MiraRpc.RemoveModifier)] + [MethodRpc((uint)MiraRpc.RemoveModifier, SendImmediately = true)] public static void RpcRemoveModifier(this PlayerControl target, Guid uniqueId) { target.RemoveModifier(uniqueId); diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index 38fbf16..159b576 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -71,7 +71,7 @@ public static class VotingUtils /// The player who is sending the RPC. Should be the host. /// The player who voted. /// The player who the voter voted for. - [MethodRpc((uint)MiraRpc.RemoveVote)] + [MethodRpc((uint)MiraRpc.RemoveVote, SendImmediately = true)] public static void RpcRemoveVote(PlayerControl source, byte voterId, byte votedFor) { if (!source.IsHost()) return; @@ -107,7 +107,7 @@ public static class VotingUtils /// The player who sent this RPC. /// The id of the player who casted the vote. /// The voted player's id. - [MethodRpc((uint)MiraRpc.CastVote)] + [MethodRpc((uint)MiraRpc.CastVote, SendImmediately = true)] public static void RpcCastVote(PlayerControl source, byte srcPlayerId, byte suspectPlayerId) { CustomCastVote(srcPlayerId, suspectPlayerId); From 3f6ae4cc410684a9d4987540bb4c41027784411d Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:21:37 -0400 Subject: [PATCH 2/8] send gameover immediately --- MiraAPI/GameEnd/CustomGameOver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs index cee076b..70d7b27 100644 --- a/MiraAPI/GameEnd/CustomGameOver.cs +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -63,7 +63,7 @@ public abstract class CustomGameOver var reason = GameOverManager.GetGameOverId(); var data = new GameOverData(reason, [.. winners]); - Rpc.Instance.Send(PlayerControl.LocalPlayer, data); + Rpc.Instance.Send(PlayerControl.LocalPlayer, data, true); } /// From a26ec51c7d3f91de9d041161891137207a6e3bc1 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:24:32 -0400 Subject: [PATCH 3/8] send murder RPC immediately --- MiraAPI/Networking/CustomMurderRpc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 209afcd..4a7bc30 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -29,7 +29,7 @@ public static class CustomMurderRpc /// Should the killer be snapped to the dead player. /// Should the kill animation be shown. /// Should the kill sound be played. - [MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before)] + [MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before, SendImmediately = true)] public static void RpcCustomMurder( this PlayerControl source, PlayerControl target, From 6e35de61856b4adbea9545f66788231fcf86ec05 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:43:56 -0400 Subject: [PATCH 4/8] add BeforeGameEndEvent.cs --- .../Vanilla/Gameplay/BeforeGameEndEvent.cs | 21 ++++++++++ MiraAPI/Patches/Events/EndGamePatches.cs | 41 +++++++++++++------ 2 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 MiraAPI/Events/Vanilla/Gameplay/BeforeGameEndEvent.cs diff --git a/MiraAPI/Events/Vanilla/Gameplay/BeforeGameEndEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/BeforeGameEndEvent.cs new file mode 100644 index 0000000..0a70a1d --- /dev/null +++ b/MiraAPI/Events/Vanilla/Gameplay/BeforeGameEndEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla.Gameplay; + +/// +/// Invoked before RpcEndGame is called, allowing cancellation of the game end entirely. +/// +public class BeforeGameEndEvent : MiraCancelableEvent +{ + /// + /// Gets the reason for the game end. + /// + public GameOverReason Reason { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The reason for the game end. + public BeforeGameEndEvent(GameOverReason reason) + { + Reason = reason; + } +} diff --git a/MiraAPI/Patches/Events/EndGamePatches.cs b/MiraAPI/Patches/Events/EndGamePatches.cs index 2c04d6b..558c468 100644 --- a/MiraAPI/Patches/Events/EndGamePatches.cs +++ b/MiraAPI/Patches/Events/EndGamePatches.cs @@ -5,29 +5,44 @@ using MiraAPI.GameEnd; namespace MiraAPI.Patches.Events; -[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))] internal static class EndGamePatches { - public static bool Prefix(EndGameManager __instance) + [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] + public static class GameManagerPatches { - if (CustomGameOver.Instance is not { } gameOver) + public static bool Prefix(GameOverReason endReason) { - return true; - } + var @event = new BeforeGameEndEvent(endReason); + MiraEventManager.InvokeEvent(@event); - var result = gameOver.BeforeEndGameSetup(__instance); - return result; + return !@event.IsCancelled; + } } - public static void Postfix(EndGameManager __instance) + [HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))] + public static class EndGameManagerPatches { - if (CustomGameOver.Instance is { } gameOver) + public static bool Prefix(EndGameManager __instance) { - gameOver.AfterEndGameSetup(__instance); - CustomGameOver.Instance = null; + if (CustomGameOver.Instance is not { } gameOver) + { + return true; + } + + var result = gameOver.BeforeEndGameSetup(__instance); + return result; } - var @event = new GameEndEvent(__instance); - MiraEventManager.InvokeEvent(@event); + public static void Postfix(EndGameManager __instance) + { + if (CustomGameOver.Instance is { } gameOver) + { + gameOver.AfterEndGameSetup(__instance); + CustomGameOver.Instance = null; + } + + var @event = new GameEndEvent(__instance); + MiraEventManager.InvokeEvent(@event); + } } } From 67604600cd1eddefd84a73075b982e36ccc79bdf Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:52:09 -0400 Subject: [PATCH 5/8] remove obsolete code --- MiraAPI/Utilities/Extensions.cs | 34 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 7a0d9a7..74e5ea2 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -102,6 +102,12 @@ public static class Extensions return component; } + /// + /// Gets the maximum value from a dictionary of integers, returning the key and value. + /// + /// The dictionary to search. + /// Whether there is a tie for the maximum value. + /// The key-value pair with the maximum value. public static KeyValuePair MaxPair(this Dictionary self, out bool tie) { tie = true; @@ -121,6 +127,12 @@ public static class Extensions return result; } + /// + /// Gets the maximum value from a dictionary of floats, returning the key and value. + /// + /// The dictionary to search. + /// Whether there is a tie for the maximum value. + /// The key-value pair with the maximum value. public static KeyValuePair MaxPair(this Dictionary self, out bool tie) { tie = true; @@ -342,18 +354,6 @@ public static class Extensions return new Color(color.r - amount, color.g - amount, color.b - amount); } - /// - /// Gets an alternate color based on the original color. - /// - /// The original color. - /// The amount to darken or lighten the original color by between 0.0 and 1.0. - /// An alternate color that has been darkened or lightened. - [Obsolete("Use FindAlternateColor for WACG compliance.")] - public static Color GetAlternateColor(this Color color, float amount = 0.45f) - { - return color.IsColorDark() ? LightenColor(color, amount) : DarkenColor(color, amount); - } - /// /// Lightens a color by a specified amount. /// @@ -365,16 +365,6 @@ public static class Extensions return new Color(color.r + amount, color.g + amount, color.b + amount); } - /// - /// Checks if a color is dark. - /// - /// The color to check. - /// True if the color is dark, false otherwise. - public static bool IsColorDark(this Color color) - { - return color.r < 0.5f && color is { g: < 0.5f, b: < 0.5f }; - } - /// /// Gets the nearest dead body to a player. /// From b3ff6755bb0e97aa41df2925781e4aba511e538e Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:52:18 -0400 Subject: [PATCH 6/8] suppress naming warnings --- MiraAPI/GameOptions/ModdedOptionsManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index c3e7e22..d602f45 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using BepInEx.Configuration; @@ -175,7 +176,10 @@ public static class ModdedOptionsManager /// /// The original setter method. /// The new object value. +#pragma warning disable CA1707 + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] public static void PropertySetterPatch(MethodBase __originalMethod, object value) +#pragma warning restore CA1707 { var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value; attribute.SetValue(value); @@ -187,7 +191,10 @@ public static class ModdedOptionsManager /// The original getter method. /// The result of the property getter. /// False so the original getter gets skipped. +#pragma warning disable CA1707 + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")] public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result) +#pragma warning restore CA1707 { var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value; __result = attribute.GetValue(); From 13ef3bdd498faf7d2151de3ee3e70095a8d44ced Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 21 Jun 2025 19:32:28 -0400 Subject: [PATCH 7/8] add format string for cooldown timers --- MiraAPI/Hud/CustomActionButton.cs | 10 +++++-- MiraAPI/Utilities/Extensions.cs | 46 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 7e0c0e2..42edf39 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -3,6 +3,7 @@ using System.Globalization; using MiraAPI.Events; using MiraAPI.Events.Mira; using MiraAPI.Patches; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; using UnityEngine.Events; @@ -36,6 +37,11 @@ public abstract class CustomActionButton /// public abstract LoadableAsset Sprite { get; } + /// + /// Gets the format string for the cooldown timer. + /// + public virtual string CooldownTimerFormatString => "0"; + /// /// Gets the button's effect duration in seconds. If the button has no effect, set to 0. /// @@ -446,12 +452,12 @@ public abstract class CustomActionButton { Button.SetFillUp(Timer, EffectDuration); - Button.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo); + Button.cooldownTimerText.text = Timer.ToString(CooldownTimerFormatString, NumberFormatInfo.InvariantInfo); Button.cooldownTimerText.gameObject.SetActive(true); } else { - Button.SetCoolDown(Timer, Cooldown); + Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString); } } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 74e5ea2..86e62e9 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; @@ -41,6 +42,51 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Sets the cooldown of a button with a formatted string. + /// + /// The ActionButton to set the cooldown for. + /// 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") + { + var num = Mathf.Clamp(timer / maxTimer, 0f, 1f); + button.isCoolingDown = num > 0f; + button.SetCooldownFill(num); + if (button.isCoolingDown) + { + button.cooldownTimerText.text = timer.ToString(format, NumberFormatInfo.InvariantInfo); + button.cooldownTimerText.gameObject.SetActive(true); + return; + } + button.cooldownTimerText.gameObject.SetActive(false); + } + + /// + /// Sets the fill-up variant of a cooldown button with a formatted string. + /// + /// The ActionButton to set the cooldown for. + /// 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") + { + var num = Mathf.Clamp(timer / maxTimer, 0f, 1f); + button.isCoolingDown = num > 0f; + if (button.isCoolingDown && timer < 3f) + { + button.graphic.transform.localPosition = button.position + (Vector3)UnityEngine.Random.insideUnitCircle * 0.05f; + button.cooldownTimerText.text = timer.ToString(format, NumberFormatInfo.InvariantInfo); + button.cooldownTimerText.gameObject.SetActive(true); + } + else + { + button.graphic.transform.localPosition = button.position; + } + button.SetCooldownFill(num); + } + /// /// Gets a PlayerControl from their PlayerVoteArea in a meeting. /// From 20e927208c5dca6dd32c56d29c79c0dabca0564c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 21 Jun 2025 20:17:57 -0400 Subject: [PATCH 8/8] make options wrap around in values --- .../GameOptions/OptionTypes/ModdedOption.cs | 2 +- MiraAPI/Patches/Options/OptionsPatches.cs | 109 +++++++++++++++--- .../Patches/Options/RoleSettingMenuPatches.cs | 2 +- 3 files changed, 93 insertions(+), 20 deletions(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 43b6e13..93b5b5f 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -122,7 +122,7 @@ public abstract class ModdedOption : IModdedOption entry.Value = Value; } - Rpc.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()]); + Rpc.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true); } OnValueChanged(newValue); diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index 8560a77..c61613c 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -37,6 +37,22 @@ public static class OptionsPatches return false; } + [HarmonyPrefix] + [HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))] + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))] + public static bool UpdateValuePrefix(OptionBehaviour __instance) + { + return !__instance.IsCustom(); + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.AdjustButtonsActiveState))] + [HarmonyPatch(typeof(StringOption), nameof(StringOption.AdjustButtonsActiveState))] + public static bool AdjustButtonsPrefix(OptionBehaviour __instance) + { + return !__instance.IsCustom(); + } + [HarmonyPrefix] [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Initialize))] public static bool NumberInit(NumberOption __instance) @@ -46,17 +62,84 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = - TranslationController.Instance.GetString(__instance.Title, Array.Empty()); - + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()); return false; } [HarmonyPrefix] - [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))] - public static bool NumberUpdate(NumberOption __instance) + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))] + public static bool NumberIncrease(NumberOption __instance) { - return !__instance.IsCustom(); + if (!__instance.IsCustom()) + { + return true; + } + + __instance.Value += __instance.Increment; + if (__instance.Value > __instance.ValidRange.max) + { + __instance.Value = __instance.ValidRange.min; + } + __instance.OnValueChanged.Invoke(__instance); + __instance.AdjustButtonsActiveState(); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))] + public static bool NumberDecrease(NumberOption __instance) + { + if (!__instance.IsCustom()) + { + return true; + } + + __instance.Value -= __instance.Increment; + if (__instance.Value < __instance.ValidRange.min) + { + __instance.Value = __instance.ValidRange.max; + } + __instance.OnValueChanged.Invoke(__instance); + __instance.AdjustButtonsActiveState(); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(StringOption), nameof(StringOption.Increase))] + public static bool StringOptionIncrease(StringOption __instance) + { + if (!__instance.IsCustom()) + { + return true; + } + + __instance.Value += 1; + if (__instance.Value >= __instance.Values.Length) + { + __instance.Value = 0; + } + __instance.OnValueChanged.Invoke(__instance); + __instance.AdjustButtonsActiveState(); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(StringOption), nameof(StringOption.Decrease))] + public static bool StringOptionDecrease(StringOption __instance) + { + if (!__instance.IsCustom()) + { + return true; + } + + __instance.Value -= 1; + if (__instance.Value < 0) + { + __instance.Value = __instance.Values.Length - 1; + } + __instance.OnValueChanged.Invoke(__instance); + __instance.AdjustButtonsActiveState(); + return false; } [HarmonyPrefix] @@ -68,19 +151,9 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = - TranslationController.Instance.GetString(__instance.Title, Array.Empty()); - __instance.ValueText.text = TranslationController.Instance.GetString( - __instance.Values[__instance.Value], - Array.Empty()); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()); + __instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty()); return false; } - - [HarmonyPrefix] - [HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))] - public static bool StringUpdate(StringOption __instance) - { - return !__instance.IsCustom(); - } } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 617fc0d..a15ce2a 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -300,7 +300,7 @@ public static class RoleSettingMenuPatches if (AmongUsClient.Instance.AmHost) { - Rpc.Instance.Send(PlayerControl.LocalPlayer, [role.GetNetData()]); + Rpc.Instance.Send(PlayerControl.LocalPlayer, [role.GetNetData()], true); } GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions;