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);
}