Merge pull request #29 from All-Of-Us-Mods/custom-game-over

Custom game over
This commit is contained in:
XtraCube authored and GitHub committed 2025-02-27 20:20:11 -05:00
commit 3387564f2d
10 files changed
+332 -17

No files matched your search

@@ -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> Sprite => ExampleAssets.ExampleButton;
protected override void OnClick()
{
CustomGameOver.Trigger<NeutralKillerGameOver>([PlayerControl.LocalPlayer.Data]);
}
public override bool Enabled(RoleBehaviour? role)
{
return role is NeutralKillerRole;
}
}
@@ -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);
}
}
+5 -3
View File
@@ -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<NeutralKillerGameOver>();
}
}
+74
View File
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using MiraAPI.Networking;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
namespace MiraAPI.GameEnd;
/// <summary>
/// Custom Game Over.
/// </summary>
public abstract class CustomGameOver
{
/// <summary>
/// Gets the current CustomGameOver.
/// </summary>
public static CustomGameOver? Instance { get; internal set; }
/// <summary>
/// Runs before the base game calls EndGameManager.SetEverythingUp.
/// </summary>
/// <param name="endGameManager">The EndGameManager instance.</param>
/// <returns>Return True to use the run the original method, return False to skip it.</returns>
public virtual bool BeforeEndGameSetup(EndGameManager endGameManager)
{
return true;
}
/// <summary>
/// Runs after the base game calls EndGameManager.SetEverythingUp.
/// </summary>
/// <param name="endGameManager">The EndGameManager instance.</param>
public virtual void AfterEndGameSetup(EndGameManager endGameManager)
{
}
/// <summary>
/// Get the GameOverReason associated with this CustomGameOver.
/// </summary>
/// <typeparam name="T">Type of the custom game over.</typeparam>
/// <returns>The GameOverReason associated with the custom game over.</returns>
public static GameOverReason GameOverReason<T>() where T : CustomGameOver
{
return (GameOverReason)GameOverManager.GetGameOverId<T>();
}
/// <summary>
/// Send a custom game over.
/// </summary>
/// <param name="winners">A collection of winners.</param>
/// <typeparam name="T">Type of the custom game over.</typeparam>
public static void Trigger<T>(IEnumerable<NetworkedPlayerInfo> winners) where T : CustomGameOver
{
if (!AmongUsClient.Instance.AmHost)
{
Logger<MiraApiPlugin>.Error("Only the host can send a custom game over.");
return;
}
var reason = GameOverManager.GetGameOverId<T>();
var data = new GameOverData(reason, [.. winners]);
Rpc<CustomGameOverRpc>.Instance.Send(PlayerControl.LocalPlayer, data, true);
}
/// <summary>
/// Implicitly convert a CustomGameOver to a GameOverReason.
/// </summary>
/// <param name="gameOver">The CustomGameOver instance.</param>
/// <returns>The GameOverReason associated with the CustomGameOver.</returns>
public static implicit operator GameOverReason(CustomGameOver gameOver)
{
return (GameOverReason)GameOverManager.GetGameOverId(gameOver.GetType());
}
}
+10
View File
@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace MiraAPI.GameEnd;
/// <summary>
/// Data for a game over.
/// </summary>
/// <param name="Reason">Reason for the game over.</param>
/// <param name="Winners">A collection of winners.</param>
public record struct GameOverData(int Reason, List<NetworkedPlayerInfo> Winners);
+89
View File
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using Reactor.Utilities;
namespace MiraAPI.GameEnd;
/// <summary>
/// Manage custom game overs.
/// </summary>
public static class GameOverManager
{
private static readonly Dictionary<Type, int> GameOverIds = [];
private static readonly Dictionary<int, Type> GameOverTypes = [];
private static int _nextId = Enum.GetNames<GameOverReason>().Length;
/// <summary>
/// Register a custom game over.
/// </summary>
/// <param name="gameOverType">Type of the custom game over.</param>
/// <exception cref="ArgumentException">Thrown when the type is not a subclass of CustomGameOver, is abstract, or does not have a parameterless constructor.</exception>
/// <returns>>True if the game over was registered successfully, false otherwise.</returns>
public static bool RegisterGameOver(Type gameOverType)
{
if (!typeof(CustomGameOver).IsAssignableFrom(gameOverType))
{
return false;
}
if (gameOverType.IsAbstract)
{
Logger<MiraApiPlugin>.Error("The type must not be abstract.");
return false;
}
if (gameOverType.GetConstructor(Type.EmptyTypes) == null)
{
Logger<MiraApiPlugin>.Error("The type must have a parameterless constructor.");
return false;
}
GameOverIds.Add(gameOverType, _nextId);
GameOverTypes.Add(_nextId, gameOverType);
_nextId++;
return true;
}
/// <summary>
/// Create an instance of a custom game over.
/// </summary>
/// <param name="id">ID of the custom game over.</param>
/// <param name="customGameOver">The created instance of the custom game over.</param>
/// <returns>An instance of the custom game over.</returns>
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;
}
/// <summary>
/// Get the ID of a custom game over.
/// </summary>
/// <typeparam name="T">Type of the custom game over.</typeparam>
/// <returns>The ID of the custom game over.</returns>
public static int GetGameOverId<T>() where T : CustomGameOver
{
return GetGameOverId(typeof(T));
}
/// <summary>
/// Get the ID of a custom game over.
/// </summary>
/// <param name="gameOverType">Type of the custom game over.</param>
/// <returns>The ID of the custom game over.</returns>
/// <exception cref="ArgumentException">Thrown when the type is not registered.</exception>
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!");
}
}
+64
View File
@@ -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;
/// <summary>
/// Custom Game Over RPC.
/// </summary>
/// <inheritdoc />
[RegisterCustomRpc((uint)MiraRpc.CustomGameOver)]
public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, GameOverData>(plugin, id)
{
/// <inheritdoc />
public override RpcLocalHandling LocalHandling => RpcLocalHandling.After;
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
public override GameOverData Read(MessageReader reader)
{
var reason = reader.ReadPackedInt32();
var count = reader.ReadPackedInt32();
var data = new List<NetworkedPlayerInfo>(count);
for (var i = 0; i < count; i++)
{
data.Add(reader.ReadNetObject<NetworkedPlayerInfo>());
}
return new GameOverData(reason, data);
}
/// <inheritdoc />
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<MiraApiPlugin>.Error($"Unknown game over reason: {data.Reason}");
}
}
}
+5
View File
@@ -34,4 +34,9 @@ public enum MiraRpc : uint
/// Custom Murder RPC.
/// </summary>
CustomMurder,
/// <summary>
/// Custom Game Over RPC.
/// </summary>
CustomGameOver,
}
+21 -8
View File
@@ -1,19 +1,32 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.GameEnd;
namespace MiraAPI.Patches.Events;
/// <summary>
/// Patch for trigging the GameEndEvent.
/// </summary>
[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.BeforeEndGameSetup(__instance);
return result;
}
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);
}
+25 -6
View File
@@ -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<MiraApiPlugin>.Error($"Failed to register game over {type.Name}: {e}");
return false;
}
}
private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo)
{
try