From 9485099ab94510e66b6051a7cb6537d874a964f1 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 2 Feb 2025 16:09:06 -0500 Subject: [PATCH 1/3] RPC add modifier with arguments --- .../Buttons/Freezer/FreezeButton.cs | 1 + MiraAPI.Example/Buttons/MeetingButton.cs | 2 +- .../OptionTypes/ModdedEnumOption.cs | 1 - MiraAPI/Modifiers/ModifierExtensions.cs | 139 ++++++++++++++++++ MiraAPI/Modifiers/ModifierFactory.cs | 83 +++++++++++ .../Patches/Modifiers/EndGameDidWinPatch.cs | 2 +- MiraAPI/Patches/PlayerControlPatches.cs | 2 +- MiraAPI/Patches/VentPatches.cs | 2 +- MiraAPI/Utilities/Extensions.cs | 125 ---------------- 9 files changed, 227 insertions(+), 130 deletions(-) create mode 100644 MiraAPI/Modifiers/ModifierExtensions.cs create mode 100644 MiraAPI/Modifiers/ModifierFactory.cs diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 8db98c7..86feece 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -3,6 +3,7 @@ using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.Modifiers; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index 85b8ff1..742c7e0 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -1,6 +1,6 @@ using MiraAPI.Example.Modifiers; using MiraAPI.Hud; -using MiraAPI.Utilities; +using MiraAPI.Modifiers; using MiraAPI.Utilities.Assets; using UnityEngine; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index e917cdc..80bd497 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Linq; using System.Text; using Il2CppInterop.Runtime.InteropTypes.Arrays; diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs new file mode 100644 index 0000000..83dfd4c --- /dev/null +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using MiraAPI.Networking; +using Reactor.Networking.Attributes; +using Reactor.Utilities; + +namespace MiraAPI.Modifiers; + +/// +/// Extension methods for modifiers. +/// +public static class ModifierExtensions +{ + internal static Dictionary ModifierComponents { get; } = []; + + /// + /// Gets the ModifierComponent for a player. + /// + /// The PlayerControl object. + /// A ModifierComponent if there is one, null otherwise. + public static ModifierComponent? GetModifierComponent(this PlayerControl player) + { + if (ModifierComponents.TryGetValue(player, out var component)) + { + return component; + } + + component = player.GetComponent(); + if (component == null) + { + return null; + } + + ModifierComponents[player] = component; + return component; + } + + /// + /// Gets a modifier by its type, or null if the player doesn't have it. + /// + /// The PlayerControl object. + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + public static T? GetModifier(this PlayerControl? player) where T : BaseModifier + { + return player?.GetModifierComponent()?.ActiveModifiers.Find(x => x is T) as T; + } + + /// + /// Checks if a player has a modifier. + /// + /// The PlayerControl object. + /// The Type of the Modifier. + /// True if the Modifier is present, false otherwise. + public static bool HasModifier(this PlayerControl? player) where T : BaseModifier + { + return player?.GetModifierComponent() != null && + player.GetModifierComponent()!.HasModifier(); + } + + /// + /// Checks if a player has a modifier by its ID. + /// + /// The PlayerControl object. + /// The Modifier ID. + /// True if the Modifier is present, false otherwise. + public static bool HasModifier(this PlayerControl? player, uint id) + { + return player?.GetModifierComponent() != null && + player.GetModifierComponent()!.HasModifier(id); + } + + /// + /// Remote Procedure Call to remove a modifier from a player. + /// + /// The player to remove the modifier from. + /// The ID of the modifier. + [MethodRpc((uint)MiraRpc.RemoveModifier)] + public static void RpcRemoveModifier(this PlayerControl target, uint modifierId) + { + target.GetModifierComponent()?.RemoveModifier(modifierId); + } + + /// + /// Remote Procedure Call to remove a modifier from a player. + /// + /// The player to remove the modifier from. + /// The Type of the Modifier. + public static void RpcRemoveModifier(this PlayerControl player) where T : BaseModifier + { + var id = ModifierManager.GetModifierId(typeof(T)); + + if (id == null) + { + Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); + return; + } + + player.RpcRemoveModifier(id.Value); + } + + /// + /// Remote Procedure Call to add a modifier to a player. + /// + /// The player to add the modifier to. + /// The modifier ID. + [MethodRpc((uint)MiraRpc.AddModifier)] + public static void RpcAddModifier(this PlayerControl target, uint modifierId, params object[] args) + { + var type = ModifierManager.GetModifierType(modifierId); + if (type == null) + { + Logger.Error($"Cannot add modifier with id {modifierId} because it is not registered."); + return; + } + + var modifier = ModifierFactory.CreateInstance(type, args); + target.GetModifierComponent()?.AddModifier(modifier); + } + + /// + /// Remote Procedure Call to add a modifier to a player. + /// + /// The player to add the modifier to. + /// The arguments to initialize the modifier constructor with. + /// The modifier Type. + public static void RpcAddModifier(this PlayerControl player, params object[] args) where T : BaseModifier + { + var id = ModifierManager.GetModifierId(typeof(T)); + if (id == null) + { + Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); + return; + } + + var modifier = ModifierFactory.CreateInstance(args); + player.GetModifierComponent()!.AddModifier(modifier); + } +} diff --git a/MiraAPI/Modifiers/ModifierFactory.cs b/MiraAPI/Modifiers/ModifierFactory.cs new file mode 100644 index 0000000..e1b018c --- /dev/null +++ b/MiraAPI/Modifiers/ModifierFactory.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace MiraAPI.Modifiers; + +public static class ModifierFactory +{ + private static Func? _constructor; + + private static Func CreateConstructor(Type type) + { + var constructorInfo = type.GetConstructors() + .OrderByDescending(c => c.GetParameters().Length) + .FirstOrDefault() ?? throw new InvalidOperationException($"Type {type} does not have a public constructor."); + var parameters = constructorInfo.GetParameters(); + + var argsParam = Expression.Parameter(typeof(object[]), "args"); + var constructorParams = new Expression[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + var paramType = parameters[i].ParameterType; + var paramAccess = Expression.ArrayIndex(argsParam, Expression.Constant(i)); + var convertedParam = Expression.Convert(paramAccess, paramType); + constructorParams[i] = convertedParam; + } + + var newExpr = Expression.New(constructorInfo, constructorParams); + var lambda = Expression.Lambda>(newExpr, argsParam); + + return lambda.Compile(); + } + + public static BaseModifier CreateInstance(Type type, params object[] args) + { + _constructor ??= CreateConstructor(type); + return _constructor(args); + } +} + +/// +/// Factory for creating instances of a modifier. More efficient than using reflection. +/// +/// The modifier type. +public static class ModifierFactory where T : BaseModifier +{ + private static readonly Func Constructor = CreateConstructor(); + + private static Func CreateConstructor() + { + var constructorInfo = typeof(T).GetConstructors() + .OrderByDescending(c => c.GetParameters().Length) + .FirstOrDefault() ?? throw new InvalidOperationException($"Type {typeof(T)} does not have a public constructor."); + var parameters = constructorInfo.GetParameters(); + + var argsParam = Expression.Parameter(typeof(object[]), "args"); + var constructorParams = new Expression[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + var paramType = parameters[i].ParameterType; + var paramAccess = Expression.ArrayIndex(argsParam, Expression.Constant(i)); + var convertedParam = Expression.Convert(paramAccess, paramType); + constructorParams[i] = convertedParam; + } + + var newExpr = Expression.New(constructorInfo, constructorParams); + var lambda = Expression.Lambda>(newExpr, argsParam); + + return lambda.Compile(); + } + + /// + /// Creates an instance of the modifier. + /// + /// Parameters for the constructor. + /// An instance of the modifier. + public static T CreateInstance(params object[] args) + { + return Constructor(args); + } +} diff --git a/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs index d7a99d5..17356db 100644 --- a/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs +++ b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs @@ -1,7 +1,7 @@ using System.Linq; using HarmonyLib; +using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; -using MiraAPI.Utilities; namespace MiraAPI.Patches.Modifiers; diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index bc98a51..e0d647d 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -86,6 +86,6 @@ public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance) [HarmonyPatch(nameof(PlayerControl.OnDestroy))] public static void PlayerControlOnDestroyPrefix(PlayerControl __instance) { - Utilities.Extensions.ModifierComponents.Remove(__instance); + ModifierExtensions.ModifierComponents.Remove(__instance); } } diff --git a/MiraAPI/Patches/VentPatches.cs b/MiraAPI/Patches/VentPatches.cs index 66c9aae..a41c690 100644 --- a/MiraAPI/Patches/VentPatches.cs +++ b/MiraAPI/Patches/VentPatches.cs @@ -1,6 +1,6 @@ using HarmonyLib; +using MiraAPI.Modifiers; using MiraAPI.Roles; -using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Patches; diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 032cff3..d177d60 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -3,10 +3,8 @@ using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; -using MiraAPI.Modifiers; using MiraAPI.Networking; using MiraAPI.Roles; -using Reactor.Networking.Attributes; using Reactor.Utilities; using UnityEngine; @@ -130,30 +128,6 @@ public static bool IsCustom(this OptionBehaviour optionBehaviour) opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour); } - internal static Dictionary ModifierComponents { get; } = []; - - /// - /// Gets the ModifierComponent for a player. - /// - /// The PlayerControl object. - /// A ModifierComponent if there is one, null otherwise. - public static ModifierComponent? GetModifierComponent(this PlayerControl player) - { - if (ModifierComponents.TryGetValue(player, out var component)) - { - return component; - } - - component = player.GetComponent(); - if (component == null) - { - return null; - } - - ModifierComponents[player] = component; - return component; - } - /// /// Randomizes a list. /// @@ -174,105 +148,6 @@ public static List Randomize(this List list) return randomizedList; } - /// - /// Gets a modifier by its type, or null if the player doesn't have it. - /// - /// The PlayerControl object. - /// The Type of the Modifier. - /// The Modifier if it is found, null otherwise. - public static T? GetModifier(this PlayerControl? player) where T : BaseModifier - { - return player?.GetModifierComponent()?.ActiveModifiers.Find(x => x is T) as T; - } - - /// - /// Checks if a player has a modifier. - /// - /// The PlayerControl object. - /// The Type of the Modifier. - /// True if the Modifier is present, false otherwise. - public static bool HasModifier(this PlayerControl? player) where T : BaseModifier - { - return player?.GetModifierComponent() != null && - player.GetModifierComponent()!.HasModifier(); - } - - /// - /// Checks if a player has a modifier by its ID. - /// - /// The PlayerControl object. - /// The Modifier ID. - /// True if the Modifier is present, false otherwise. - public static bool HasModifier(this PlayerControl? player, uint id) - { - return player?.GetModifierComponent() != null && - player.GetModifierComponent()!.HasModifier(id); - } - - /// - /// Remote Procedure Call to remove a modifier from a player. - /// - /// The player to remove the modifier from. - /// The ID of the modifier. - [MethodRpc((uint)MiraRpc.RemoveModifier)] - public static void RpcRemoveModifier(this PlayerControl target, uint modifierId) - { - target.GetModifierComponent()?.RemoveModifier(modifierId); - } - - /// - /// Remote Procedure Call to remove a modifier from a player. - /// - /// The player to remove the modifier from. - /// The Type of the Modifier. - public static void RpcRemoveModifier(this PlayerControl player) where T : BaseModifier - { - var id = ModifierManager.GetModifierId(typeof(T)); - - if (id == null) - { - Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); - return; - } - - player.RpcRemoveModifier(id.Value); - } - - /// - /// Remote Procedure Call to add a modifier to a player. - /// - /// The player to add the modifier to. - /// The modifier ID. - [MethodRpc((uint)MiraRpc.AddModifier)] - public static void RpcAddModifier(this PlayerControl target, uint modifierId) - { - var type = ModifierManager.GetModifierType(modifierId); - if (type == null) - { - Logger.Error($"Cannot add modifier with id {modifierId} because it is not registered."); - return; - } - - target.GetModifierComponent()?.AddModifier(type); - } - - /// - /// Remote Procedure Call to add a modifier to a player. - /// - /// The player to add the modifier to. - /// The modifier Type. - public static void RpcAddModifier(this PlayerControl player) where T : BaseModifier - { - var id = ModifierManager.GetModifierId(typeof(T)); - if (id == null) - { - Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); - return; - } - - player.RpcAddModifier(id.Value); - } - /// /// Darkens a color by a specified amount. /// From ce81d8c1ff8be72e41ce26415f0667364cd63e03 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 2 Feb 2025 20:36:14 -0500 Subject: [PATCH 2/3] it works? --- MiraAPI/Modifiers/ModifierData.cs | 21 +++++++++ MiraAPI/Modifiers/ModifierExtensions.cs | 9 ++-- MiraAPI/Networking/AddModifierRpc.cs | 61 +++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 MiraAPI/Modifiers/ModifierData.cs create mode 100644 MiraAPI/Networking/AddModifierRpc.cs diff --git a/MiraAPI/Modifiers/ModifierData.cs b/MiraAPI/Modifiers/ModifierData.cs new file mode 100644 index 0000000..425e063 --- /dev/null +++ b/MiraAPI/Modifiers/ModifierData.cs @@ -0,0 +1,21 @@ +using System; + +namespace MiraAPI.Modifiers; + +/// +/// Modifier data for networking. +/// +/// The Type of the modifier. +/// Parameters for constructor. +public readonly struct ModifierData(Type type, object[] args) +{ + /// + /// Gets the type of the modifier. + /// + public Type Type { get; } = type; + + /// + /// Gets the parameters for the constructor. + /// + public object[] Args { get; } = args; +} diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index 83dfd4c..d6ce5ff 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using MiraAPI.Networking; using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; using Reactor.Utilities; namespace MiraAPI.Modifiers; @@ -104,7 +105,7 @@ public static void RpcRemoveModifier(this PlayerControl player) where T : Bas /// /// The player to add the modifier to. /// The modifier ID. - [MethodRpc((uint)MiraRpc.AddModifier)] + /// The arguments to initialize the modifier constructor with. public static void RpcAddModifier(this PlayerControl target, uint modifierId, params object[] args) { var type = ModifierManager.GetModifierType(modifierId); @@ -114,8 +115,7 @@ public static void RpcAddModifier(this PlayerControl target, uint modifierId, pa return; } - var modifier = ModifierFactory.CreateInstance(type, args); - target.GetModifierComponent()?.AddModifier(modifier); + Rpc.Instance.Send(target, new ModifierData(type, args)); } /// @@ -133,7 +133,6 @@ public static void RpcAddModifier(this PlayerControl player, params object[] return; } - var modifier = ModifierFactory.CreateInstance(args); - player.GetModifierComponent()!.AddModifier(modifier); + player.RpcAddModifier(id.Value, args); } } diff --git a/MiraAPI/Networking/AddModifierRpc.cs b/MiraAPI/Networking/AddModifierRpc.cs new file mode 100644 index 0000000..3546d4f --- /dev/null +++ b/MiraAPI/Networking/AddModifierRpc.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Hazel; +using MiraAPI.Modifiers; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; +using Reactor.Networking.Serialization; + +namespace MiraAPI.Networking; + +/// +/// Remote procedure call for adding a modifier. +/// +/// Mira plugin. +/// RPC ID. +[RegisterCustomRpc((uint)MiraRpc.AddModifier)] +public class AddModifierRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +{ + private static readonly Dictionary ParameterCache = []; + + /// + public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before; + + /// + public override void Write(MessageWriter writer, ModifierData data) + { + var modId = ModifierManager.GetModifierId(data.Type); + writer.WritePacked((uint)modId!); + MessageSerializer.Serialize(writer, data.Args); + } + + /// + public override ModifierData Read(MessageReader reader) + { + var modId = reader.ReadPackedUInt32(); + var modifier = ModifierManager.GetModifierType(modId)!; + + if (!ParameterCache.TryGetValue(modifier, out var paramTypes)) + { + paramTypes = modifier.GetConstructors().OrderBy(x => x.GetParameters().Length).First().GetParameters(); + ParameterCache[modifier] = paramTypes; + } + + var objects = new object[paramTypes.Length]; + foreach (var paramType in paramTypes) + { + objects[paramType.Position] = reader.Deserialize(paramType.ParameterType); + } + + return new ModifierData(modifier, objects); + } + + /// + public override void Handle(PlayerControl innerNetObject, ModifierData data) + { + var modifier = ModifierFactory.CreateInstance(data.Type, data.Args); + innerNetObject.GetModifierComponent()?.AddModifier(modifier); + } +} From 8e2535ada25339845470623541afe08386bfc326 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 2 Feb 2025 20:42:23 -0500 Subject: [PATCH 3/3] yes it works --- MiraAPI.Example/Buttons/TestButton.cs | 27 +++++++++++++++++++++ MiraAPI.Example/Modifiers/ModifierParams.cs | 16 ++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 MiraAPI.Example/Buttons/TestButton.cs create mode 100644 MiraAPI.Example/Modifiers/ModifierParams.cs diff --git a/MiraAPI.Example/Buttons/TestButton.cs b/MiraAPI.Example/Buttons/TestButton.cs new file mode 100644 index 0000000..8062c81 --- /dev/null +++ b/MiraAPI.Example/Buttons/TestButton.cs @@ -0,0 +1,27 @@ +using MiraAPI.Example.Modifiers; +using MiraAPI.Hud; +using MiraAPI.Modifiers; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Example.Buttons; + +[RegisterButton] +public class TestButton : CustomActionButton +{ + public override string Name => "Test Button"; + public override float Cooldown => 0f; + public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + + protected override void OnClick() + { + var randomPlayer = PlayerControl.AllPlayerControls.ToArray().Random(); + PlayerControl.LocalPlayer.RpcAddModifier("test", 1, randomPlayer); + } + + public override bool Enabled(RoleBehaviour? role) + { + return true; + } +} diff --git a/MiraAPI.Example/Modifiers/ModifierParams.cs b/MiraAPI.Example/Modifiers/ModifierParams.cs new file mode 100644 index 0000000..cde7d3b --- /dev/null +++ b/MiraAPI.Example/Modifiers/ModifierParams.cs @@ -0,0 +1,16 @@ +using MiraAPI.Modifiers; +using Reactor.Utilities; + +namespace MiraAPI.Example.Modifiers; + +[RegisterModifier] +public class ModifierParams : BaseModifier +{ + public override string ModifierName => "ModifierParams"; + + public ModifierParams(string param1, int param2, PlayerControl playerControl) + { + Logger.Error($"Param1: {param1}, Param2: {param2}"); + Logger.Error($"Player: {playerControl.PlayerId}"); + } +}