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/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 94c1659..0adc461 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(); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 919db15..3c98d24 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -113,7 +113,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/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/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); + } } } 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/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 2669d92..64e02f9 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; @@ -83,7 +84,52 @@ public static class Extensions analytics.MinigameClosed(data, taskType, realtimeSinceStartup, isComplete); self.StartCoroutine(self.CoDestroySelf()); } - + + /// + /// 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. /// @@ -145,6 +191,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; @@ -164,6 +216,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; @@ -385,18 +443,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. /// @@ -408,16 +454,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. ///