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