From 530cb799818b2a7c321880d12727843d611f4b82 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 19:33:14 -0500 Subject: [PATCH 1/2] custom game over --- MiraAPI/GameEnd/CustomGameOver.cs | 64 +++++++++++++++++++ MiraAPI/GameEnd/GameOverData.cs | 10 +++ MiraAPI/GameEnd/GameOverManager.cs | 78 ++++++++++++++++++++++++ MiraAPI/Networking/CustomGameOverRpc.cs | 64 +++++++++++++++++++ MiraAPI/Networking/MiraRpc.cs | 5 ++ MiraAPI/Patches/Events/EndGamePatches.cs | 29 ++++++--- 6 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 MiraAPI/GameEnd/CustomGameOver.cs create mode 100644 MiraAPI/GameEnd/GameOverData.cs create mode 100644 MiraAPI/GameEnd/GameOverManager.cs create mode 100644 MiraAPI/Networking/CustomGameOverRpc.cs 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); } From 90ed9b2a98201a1cee3b9895a5ece37ccf23111e Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:11:24 -0500 Subject: [PATCH 2/2] finalize custom gameover --- .../Buttons/NeutralKillerButton.cs | 24 ++++++++++++++ .../GameOver/NeutralKillerGameOver.cs | 15 +++++++++ MiraAPI.Example/Roles/NeutralKillerRole.cs | 8 +++-- MiraAPI/GameEnd/CustomGameOver.cs | 22 +++++++++---- MiraAPI/GameEnd/GameOverManager.cs | 19 +++++++++--- MiraAPI/Networking/CustomGameOverRpc.cs | 2 +- MiraAPI/Patches/Events/EndGamePatches.cs | 4 +-- MiraAPI/PluginLoading/MiraPluginManager.cs | 31 +++++++++++++++---- 8 files changed, 103 insertions(+), 22 deletions(-) create mode 100644 MiraAPI.Example/Buttons/NeutralKillerButton.cs create mode 100644 MiraAPI.Example/GameOver/NeutralKillerGameOver.cs diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example/Buttons/NeutralKillerButton.cs new file mode 100644 index 0000000..d38253a --- /dev/null +++ b/MiraAPI.Example/Buttons/NeutralKillerButton.cs @@ -0,0 +1,24 @@ +using MiraAPI.Example.GameOver; +using MiraAPI.Example.Roles; +using MiraAPI.GameEnd; +using MiraAPI.Hud; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.Buttons; + +public class NeutralKillerButton : CustomActionButton +{ + public override string Name => "Win Game"; + public override float Cooldown => 0f; + public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + protected override void OnClick() + { + CustomGameOver.Trigger([PlayerControl.LocalPlayer.Data]); + } + + public override bool Enabled(RoleBehaviour? role) + { + return role is NeutralKillerRole; + } +} diff --git a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs new file mode 100644 index 0000000..8d48a64 --- /dev/null +++ b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs @@ -0,0 +1,15 @@ +using MiraAPI.GameEnd; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Example.GameOver; + +public class NeutralKillerGameOver : CustomGameOver +{ + public override void AfterEndGameSetup(EndGameManager endGameManager) + { + endGameManager.WinText.text = "Outcast Killer Wins!"; + endGameManager.WinText.color = Color.magenta; + endGameManager.BackgroundBar.material.SetColor(ShaderID.Color, Color.magenta); + } +} diff --git a/MiraAPI.Example/Roles/NeutralKillerRole.cs b/MiraAPI.Example/Roles/NeutralKillerRole.cs index e9b4344..55fdc51 100644 --- a/MiraAPI.Example/Roles/NeutralKillerRole.cs +++ b/MiraAPI.Example/Roles/NeutralKillerRole.cs @@ -1,4 +1,6 @@ -using MiraAPI.Roles; +using MiraAPI.Example.GameOver; +using MiraAPI.GameEnd; +using MiraAPI.Roles; using UnityEngine; namespace MiraAPI.Example.Roles; @@ -22,7 +24,7 @@ public class NeutralKillerRole : ImpostorRole, ICustomRole public TeamIntroConfiguration? IntroConfiguration { get; } = new( Color.gray, - "NEUTRAL", + "OUTCAST", "You are an Outcast. You do not have a team."); public override void SpawnTaskHeader(PlayerControl playerControl) @@ -32,6 +34,6 @@ public class NeutralKillerRole : ImpostorRole, ICustomRole public override bool DidWin(GameOverReason gameOverReason) { - return GameManager.Instance.DidHumansWin(gameOverReason); + return gameOverReason == CustomGameOver.GameOverReason(); } } diff --git a/MiraAPI/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs index d65e300..e54c750 100644 --- a/MiraAPI/GameEnd/CustomGameOver.cs +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -16,29 +16,39 @@ public abstract class CustomGameOver public static CustomGameOver? Instance { get; internal set; } /// - /// Run before setting up the game over. + /// Runs before the base game calls EndGameManager.SetEverythingUp. /// /// The EndGameManager instance. - /// Return True to use the default implementation, return False to skip it. - public virtual bool BeforeSetEverythingUp(EndGameManager endGameManager) + /// Return True to use the run the original method, return False to skip it. + public virtual bool BeforeEndGameSetup(EndGameManager endGameManager) { return true; } /// - /// Run after setting up the game over. + /// Runs after the base game calls EndGameManager.SetEverythingUp. /// /// The EndGameManager instance. - public virtual void AfterSetEverythingUp(EndGameManager endGameManager) + public virtual void AfterEndGameSetup(EndGameManager endGameManager) { } + /// + /// Get the GameOverReason associated with this CustomGameOver. + /// + /// Type of the custom game over. + /// The GameOverReason associated with the custom game over. + public static GameOverReason GameOverReason() where T : CustomGameOver + { + return (GameOverReason)GameOverManager.GetGameOverId(); + } + /// /// Send a custom game over. /// /// A collection of winners. /// Type of the custom game over. - public static void Send(IEnumerable winners) where T : CustomGameOver + public static void Trigger(IEnumerable winners) where T : CustomGameOver { if (!AmongUsClient.Instance.AmHost) { diff --git a/MiraAPI/GameEnd/GameOverManager.cs b/MiraAPI/GameEnd/GameOverManager.cs index 147536c..c25aa37 100644 --- a/MiraAPI/GameEnd/GameOverManager.cs +++ b/MiraAPI/GameEnd/GameOverManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Reactor.Utilities; namespace MiraAPI.GameEnd; @@ -18,20 +19,30 @@ public static class GameOverManager /// /// 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) + /// >True if the game over was registered successfully, false otherwise. + public static bool RegisterGameOver(Type gameOverType) { if (!typeof(CustomGameOver).IsAssignableFrom(gameOverType)) - throw new ArgumentException("The type must be a subclass of CustomGameOver."); + { + return false; + } if (gameOverType.IsAbstract) - throw new ArgumentException("The type must not be abstract."); + { + Logger.Error("The type must not be abstract."); + return false; + } if (gameOverType.GetConstructor(Type.EmptyTypes) == null) - throw new ArgumentException("The type must have a parameterless constructor."); + { + Logger.Error("The type must have a parameterless constructor."); + return false; + } GameOverIds.Add(gameOverType, _nextId); GameOverTypes.Add(_nextId, gameOverType); _nextId++; + return true; } /// diff --git a/MiraAPI/Networking/CustomGameOverRpc.cs b/MiraAPI/Networking/CustomGameOverRpc.cs index 0c95d63..a2419f0 100644 --- a/MiraAPI/Networking/CustomGameOverRpc.cs +++ b/MiraAPI/Networking/CustomGameOverRpc.cs @@ -33,7 +33,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc< public override GameOverData Read(MessageReader reader) { var reason = reader.ReadPackedInt32(); - var count = reader.ReadInt32(); + var count = reader.ReadPackedInt32(); var data = new List(count); for (var i = 0; i < count; i++) { diff --git a/MiraAPI/Patches/Events/EndGamePatches.cs b/MiraAPI/Patches/Events/EndGamePatches.cs index 3e9b0fa..2c04d6b 100644 --- a/MiraAPI/Patches/Events/EndGamePatches.cs +++ b/MiraAPI/Patches/Events/EndGamePatches.cs @@ -15,7 +15,7 @@ internal static class EndGamePatches return true; } - var result = gameOver.BeforeSetEverythingUp(__instance); + var result = gameOver.BeforeEndGameSetup(__instance); return result; } @@ -23,7 +23,7 @@ internal static class EndGamePatches { if (CustomGameOver.Instance is { } gameOver) { - gameOver.AfterSetEverythingUp(__instance); + gameOver.AfterEndGameSetup(__instance); CustomGameOver.Instance = null; } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index a7ed480..42be8e0 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -1,6 +1,12 @@ -using BepInEx.Unity.IL2CPP; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using BepInEx.Unity.IL2CPP; using MiraAPI.Colors; using MiraAPI.Events; +using MiraAPI.GameEnd; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; @@ -9,11 +15,6 @@ using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; namespace MiraAPI.PluginLoading; @@ -69,6 +70,11 @@ public sealed class MiraPluginManager continue; } + if (RegisterGameOver(type)) + { + continue; + } + RegisterColorClasses(type); } @@ -96,6 +102,19 @@ public sealed class MiraPluginManager return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId); } + private static bool RegisterGameOver(Type type) + { + try + { + return GameOverManager.RegisterGameOver(type); + } + catch (Exception e) + { + Logger.Error($"Failed to register game over {type.Name}: {e}"); + return false; + } + } + private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo) { try