diff --git a/MiraAPI/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs new file mode 100644 index 0000000..d65e300 --- /dev/null +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using MiraAPI.Networking; +using Reactor.Networking.Rpc; +using Reactor.Utilities; + +namespace MiraAPI.GameEnd; + +/// +/// Custom Game Over. +/// +public abstract class CustomGameOver +{ + /// + /// Gets the current CustomGameOver. + /// + public static CustomGameOver? Instance { get; internal set; } + + /// + /// Run before setting up the game over. + /// + /// The EndGameManager instance. + /// Return True to use the default implementation, return False to skip it. + public virtual bool BeforeSetEverythingUp(EndGameManager endGameManager) + { + return true; + } + + /// + /// Run after setting up the game over. + /// + /// The EndGameManager instance. + public virtual void AfterSetEverythingUp(EndGameManager endGameManager) + { + } + + /// + /// Send a custom game over. + /// + /// A collection of winners. + /// Type of the custom game over. + public static void Send(IEnumerable winners) where T : CustomGameOver + { + if (!AmongUsClient.Instance.AmHost) + { + Logger.Error("Only the host can send a custom game over."); + return; + } + + var reason = GameOverManager.GetGameOverId(); + var data = new GameOverData(reason, [.. winners]); + + Rpc.Instance.Send(PlayerControl.LocalPlayer, data, true); + } + + /// + /// Implicitly convert a CustomGameOver to a GameOverReason. + /// + /// The CustomGameOver instance. + /// The GameOverReason associated with the CustomGameOver. + public static implicit operator GameOverReason(CustomGameOver gameOver) + { + return (GameOverReason)GameOverManager.GetGameOverId(gameOver.GetType()); + } +} diff --git a/MiraAPI/GameEnd/GameOverData.cs b/MiraAPI/GameEnd/GameOverData.cs new file mode 100644 index 0000000..0de0783 --- /dev/null +++ b/MiraAPI/GameEnd/GameOverData.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace MiraAPI.GameEnd; + +/// +/// Data for a game over. +/// +/// Reason for the game over. +/// A collection of winners. +public record struct GameOverData(int Reason, List Winners); diff --git a/MiraAPI/GameEnd/GameOverManager.cs b/MiraAPI/GameEnd/GameOverManager.cs new file mode 100644 index 0000000..147536c --- /dev/null +++ b/MiraAPI/GameEnd/GameOverManager.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; + +namespace MiraAPI.GameEnd; + +/// +/// Manage custom game overs. +/// +public static class GameOverManager +{ + private static readonly Dictionary GameOverIds = []; + private static readonly Dictionary GameOverTypes = []; + + private static int _nextId = Enum.GetNames().Length; + + /// + /// Register a custom game over. + /// + /// Type of the custom game over. + /// Thrown when the type is not a subclass of CustomGameOver, is abstract, or does not have a parameterless constructor. + public static void RegisterGameOver(Type gameOverType) + { + if (!typeof(CustomGameOver).IsAssignableFrom(gameOverType)) + throw new ArgumentException("The type must be a subclass of CustomGameOver."); + + if (gameOverType.IsAbstract) + throw new ArgumentException("The type must not be abstract."); + + if (gameOverType.GetConstructor(Type.EmptyTypes) == null) + throw new ArgumentException("The type must have a parameterless constructor."); + + GameOverIds.Add(gameOverType, _nextId); + GameOverTypes.Add(_nextId, gameOverType); + _nextId++; + } + + /// + /// Create an instance of a custom game over. + /// + /// ID of the custom game over. + /// The created instance of the custom game over. + /// An instance of the custom game over. + public static bool TryGetGameOver(int id, out CustomGameOver? customGameOver) + { + if (GameOverTypes.TryGetValue(id, out var gameOverType)) + { + customGameOver = Activator.CreateInstance(gameOverType) as CustomGameOver; + return customGameOver != null; + } + + customGameOver = null; + return false; + } + + /// + /// Get the ID of a custom game over. + /// + /// Type of the custom game over. + /// The ID of the custom game over. + public static int GetGameOverId() where T : CustomGameOver + { + return GetGameOverId(typeof(T)); + } + + /// + /// Get the ID of a custom game over. + /// + /// Type of the custom game over. + /// The ID of the custom game over. + /// Thrown when the type is not registered. + public static int GetGameOverId(Type gameOverType) + { + if (GameOverIds.TryGetValue(gameOverType, out var id)) + return id; + + throw new ArgumentException($"{gameOverType.FullName} is not a registered custom game over!"); + } +} diff --git a/MiraAPI/Networking/CustomGameOverRpc.cs b/MiraAPI/Networking/CustomGameOverRpc.cs new file mode 100644 index 0000000..0c95d63 --- /dev/null +++ b/MiraAPI/Networking/CustomGameOverRpc.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Hazel; +using InnerNet; +using MiraAPI.GameEnd; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; +using Reactor.Utilities; + +namespace MiraAPI.Networking; + +/// +/// Custom Game Over RPC. +/// +/// +[RegisterCustomRpc((uint)MiraRpc.CustomGameOver)] +public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +{ + /// + public override RpcLocalHandling LocalHandling => RpcLocalHandling.After; + + /// + public override void Write(MessageWriter writer, GameOverData data) + { + writer.WritePacked(data.Reason); + writer.WritePacked(data.Winners.Count); + foreach (var player in data.Winners) + { + writer.WriteNetObject(player); + } + } + + /// + public override GameOverData Read(MessageReader reader) + { + var reason = reader.ReadPackedInt32(); + var count = reader.ReadInt32(); + var data = new List(count); + for (var i = 0; i < count; i++) + { + data.Add(reader.ReadNetObject()); + } + + return new GameOverData(reason, data); + } + + /// + public override void Handle(PlayerControl innerNetObject, GameOverData data) + { + if (GameOverManager.TryGetGameOver(data.Reason, out var gameOver)) + { + CustomGameOver.Instance = gameOver; + + if (AmongUsClient.Instance.AmHost) + { + GameManager.Instance.RpcEndGame(gameOver!, false); + } + } + else + { + CustomGameOver.Instance = null; + Logger.Error($"Unknown game over reason: {data.Reason}"); + } + } +} diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index 5126dea..280fa7e 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -34,4 +34,9 @@ public enum MiraRpc : uint /// Custom Murder RPC. /// CustomMurder, + + /// + /// Custom Game Over RPC. + /// + CustomGameOver, } diff --git a/MiraAPI/Patches/Events/EndGamePatches.cs b/MiraAPI/Patches/Events/EndGamePatches.cs index 4f7118b..3e9b0fa 100644 --- a/MiraAPI/Patches/Events/EndGamePatches.cs +++ b/MiraAPI/Patches/Events/EndGamePatches.cs @@ -1,19 +1,32 @@ using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.GameEnd; namespace MiraAPI.Patches.Events; -/// -/// Patch for trigging the GameEndEvent. -/// -[HarmonyPatch] -public static class EndGamePatches +[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))] +internal static class EndGamePatches { - [HarmonyPostfix] - [HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))] - public static void SetEverythingUpPatch(EndGameManager __instance) + public static bool Prefix(EndGameManager __instance) { + if (CustomGameOver.Instance is not { } gameOver) + { + return true; + } + + var result = gameOver.BeforeSetEverythingUp(__instance); + return result; + } + + public static void Postfix(EndGameManager __instance) + { + if (CustomGameOver.Instance is { } gameOver) + { + gameOver.AfterSetEverythingUp(__instance); + CustomGameOver.Instance = null; + } + var @event = new GameEndEvent(__instance); MiraEventManager.InvokeEvent(@event); }