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 01/71] 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.Options.Roles; 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 class PlayerControlPatches [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.Collections.Generic; 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 class Extensions 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 class Extensions 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 02/71] 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 class ModifierExtensions /// /// 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 class ModifierExtensions 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 class ModifierExtensions 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 03/71] 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}"); + } +} From 5e16429acf88a187eafc052f342239c24a59c82d Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:15:00 -0500 Subject: [PATCH 04/71] fix more scrolling issues --- MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 2 -- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index a44b35c..0cdca29 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -157,8 +157,6 @@ public static class GameSettingMenuPatches roles.AdvancedRolesSettings.gameObject.SetActive(false); roles.RoleChancesSettings.gameObject.SetActive(true); roles.SetQuotaTab(); - - roles.scrollBar.CalculateAndSetYBounds(roles.roleChances.Count + 5, 1f, 6f, 0.43f); roles.scrollBar.ScrollToTop(); } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 518268c..915f2bc 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -32,6 +32,10 @@ public static class RoleSettingMenuPatches [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] public static bool PatchStart(RolesSettingsMenu __instance) { + CategoryHeaderEditRoles.ForEach(Object.Destroy); + CategoryHeaderEditRoles.Clear(); + RoleOptionSettings.ForEach(Object.Destroy); + RoleOptionSettings.Clear(); __instance.roleChances = new Il2CppSystem.Collections.Generic.List(); __instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List(); From ccde63068ba18da058f31cbdb12089c8b9ff0dd8 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:49:54 -0500 Subject: [PATCH 05/71] fix lobby view pane issues --- .../Patches/Options/LobbyViewPanePatches.cs | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index ee26822..e9d3c42 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -6,6 +6,7 @@ using MiraAPI.Utilities.Assets; using System; using System.Collections.Generic; using System.Linq; +using Reactor.Localization.Utilities; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -211,34 +212,43 @@ public static class LobbyViewPanePatches var list = new List(); - foreach (var team in Enum.GetValues()) - { - var filteredRoles = SelectedMod.CustomRoles.Values - .OfType() - .Where(x => !x.Configuration.HideSettings && x.Team == team).ToList(); + var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType() + .ToLookup(x => x.RoleOptionsGroup); - if (filteredRoles.Count == 0) + if (roleGroups is null) + { + return; + } + + // sort the groups by priority + var sortedRoleGroups = roleGroups + .OrderBy(x => x.Key.Priority) + .ThenBy(x => x.Key.Name); + + foreach (var grouping in sortedRoleGroups) + { + if (!grouping.Any()) { continue; } - var categoryHeaderRoleVariant = - Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true); + var group = grouping.Key; - switch (team) + var name = group.Name switch { - case ModdedRoleTeams.Crewmate: - categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61); - break; - case ModdedRoleTeams.Impostor: - categoryHeaderRoleVariant.SetHeader(StringNames.ImpostorRolesHeader, 61); - break; - default: - categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61); - categoryHeaderRoleVariant.Title.text = team + " Roles"; - categoryHeaderRoleVariant.Background.color = Color.gray; - categoryHeaderRoleVariant.Title.color = Color.white; - break; + "Crewmate" => StringNames.CrewmateRolesHeader, + "Impostor" => StringNames.ImpostorRolesHeader, + _ => CustomStringName.CreateAndRegister(group.Name), + }; + + var categoryHeaderRoleVariant = Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true); + categoryHeaderRoleVariant.SetHeader(name, 61); + + if (name is not (StringNames.CrewmateRolesHeader or StringNames.ImpostorRolesHeader)) + { + var veryDarkColor = group.Color.DarkenColor(.35f); + categoryHeaderRoleVariant.Title.color = veryDarkColor; + categoryHeaderRoleVariant.Background.color = group.Color; } categoryHeaderRoleVariant.transform.localScale = Vector3.one; @@ -246,7 +256,7 @@ public static class LobbyViewPanePatches instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject); num -= 0.696f; - foreach (var customRole in filteredRoles) + foreach (var customRole in grouping) { var roleBehaviour = customRole as RoleBehaviour; if (roleBehaviour == null) @@ -259,8 +269,6 @@ public static class LobbyViewPanePatches var numPerGame = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); - var flag = numPerGame == 0; - var viewSettingsInfoPanelRoleVariant = Object.Instantiate( instance.infoPanelRoleOrigin, @@ -273,7 +281,7 @@ public static class LobbyViewPanePatches .Where(x => x.AdvancedRole == customRole.GetType()) .ToList(); - if (!flag && advancedRoleOptions.Count > 0) + if (numPerGame > 0 && advancedRoleOptions.Count > 0) { list.Add(customRole.GetType()); } @@ -286,6 +294,8 @@ public static class LobbyViewPanePatches customRole.RoleColor, customRole.Configuration.Icon.LoadAsset(), true); + viewSettingsInfoPanelRoleVariant.iconSprite.transform.localScale = new Vector3(0.365f, 0.365f, 1f); + viewSettingsInfoPanelRoleVariant.iconSprite.transform.localPosition = new Vector3(0.7144f, -0.028f, -2); viewSettingsInfoPanelRoleVariant.titleText.color = viewSettingsInfoPanelRoleVariant.chanceTitle.color = @@ -370,6 +380,7 @@ public static class LobbyViewPanePatches maskLayer, role.TeamType == RoleTeamTypes.Crewmate, customRole.Configuration.Icon.LoadAsset()); + viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f); viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer); var num = viewPanel.yPosStart; From 4b6ffc65df4da8376e631bdf33079e5f6bed5cd4 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:26:53 -0500 Subject: [PATCH 06/71] fix after murder event --- MiraAPI/Networking/CustomMurderRpc.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 7f1e60f..1a46865 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -263,6 +263,9 @@ public static class CustomMurderRpc deadBody.enabled = true; } + var afterMurderEvent = new AfterMurderEvent(source, target); + MiraEventManager.InvokeEvent(afterMurderEvent); + if (!isParticipant) { yield break; @@ -275,8 +278,5 @@ public static class CustomMurderRpc PlayerControl.LocalPlayer.isKilling = false; source.isKilling = false; - - var afterMurderEvent = new AfterMurderEvent(source, target); - MiraEventManager.InvokeEvent(afterMurderEvent); } } From 9504862bea025c0a7b54fa593e5127cff60c974b Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:01:28 -0500 Subject: [PATCH 07/71] add format string option to number options --- MiraAPI.Example/Options/ExampleOptions.cs | 2 +- .../Attributes/ModdedNumberOptionAttribute.cs | 3 ++- MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs | 10 +++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index 9e9fa59..53bc9eb 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -16,7 +16,7 @@ public class ExampleOptions : AbstractOptionGroup [ModdedToggleOption("Toggle Opt 2")] public bool ToggleOpt2 { get; set; } = true; - [ModdedNumberOption("Number Opt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Percent)] + [ModdedNumberOption("Number Opt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)] public float NumberOpt { get; set; } = 4f; [ModdedEnumOption("Best API", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])] diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index 2bd4438..b7b85c2 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -15,13 +15,14 @@ public class ModdedNumberOptionAttribute( float max, float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null, bool zeroInfinity = false, Type? roleType = null) : ModdedOptionAttribute(title, roleType) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, zeroInfinity, RoleType); + return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity, RoleType); } /// diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index 24cd346..deb8052 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -46,6 +46,7 @@ public class ModdedNumberOption : ModdedOption /// The maximum value. /// The increment. /// The suffix type. + /// Optional format string for the option screen. /// Whether zero is infinity or not. /// An optional role type. public ModdedNumberOption( @@ -55,6 +56,7 @@ public class ModdedNumberOption : ModdedOption float max, float increment, MiraNumberSuffixes suffixType, + string? formatString = null, bool zeroInfinity = false, Type? roleType = null) : base(title, defaultValue, roleType) { @@ -74,7 +76,13 @@ public class ModdedNumberOption : ModdedOption data.Value = Value; data.Increment = Increment; data.ValidRange = new FloatRange(Min, Max); - data.FormatString = Increment % 1 == 0 && Value % 1 == 0 && Min % 1 == 0 && Max % 1 == 0 ? "0" : "0.0"; // not sure if the == checks will work since this is floats but we'll find out + data.FormatString = formatString ?? + (!Mathf.Approximately(defaultValue, 0) || + !Mathf.Approximately(Increment % 1, 0) || + !Mathf.Approximately(Value % 1, 0) || + !Mathf.Approximately(Min % 1, 0) || + !Mathf.Approximately(Max % 1, 0) ? "0.0" : "0"); + data.ZeroIsInfinity = ZeroInfinity; data.SuffixType = (NumberSuffixes)SuffixType; data.OptionName = FloatOptionNames.Invalid; From b6dda5f9704d8e3b6a8ce6daf0a10515f74484d9 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:54:20 -0500 Subject: [PATCH 08/71] freeplay modifiers --- MiraAPI.Example/Modifiers/CaptainModifier.cs | 3 +- .../Modifiers/Freezer/FreezeModifier.cs | 1 - .../Modifiers/HighPriorityModifier.cs | 3 +- .../OptionTypes/ModdedEnumOption.cs | 1 - MiraAPI/Modifiers/BaseModifier.cs | 5 + MiraAPI/Modifiers/ModifierManager.cs | 7 +- MiraAPI/Modifiers/Types/GameModifier.cs | 3 + MiraAPI/Patches/Roles/TaskAddButtonPatches.cs | 43 ++++++ MiraAPI/Patches/Roles/TaskAdderPatch.cs | 141 +++++++++++++----- MiraAPI/PluginLoading/MiraPluginInfo.cs | 3 + MiraAPI/PluginLoading/MiraPluginManager.cs | 10 +- MiraAPI/Roles/ICustomRole.cs | 3 +- 12 files changed, 170 insertions(+), 53 deletions(-) create mode 100644 MiraAPI/Patches/Roles/TaskAddButtonPatches.cs diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 60691a8..63b7c41 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -1,5 +1,4 @@ -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index 642e941..12a9587 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -1,6 +1,5 @@ using Il2CppSystem; using MiraAPI.Example.Roles; -using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using UnityEngine; diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index b94c4d0..c640ed8 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -1,5 +1,4 @@ -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; 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/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 47cd491..78a5cd0 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -30,6 +30,11 @@ public abstract class BaseModifier /// public virtual bool HideOnUi => false; + /// + /// Gets a value indicating whether the modifier is shown in the freeplay menu. + /// + public virtual bool ShowInFreeplay => false; + /// /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true. /// diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index ddb3c87..d4d4004 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using MiraAPI.Modifiers.Types; using MiraAPI.Networking; +using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking.Rpc; @@ -49,7 +51,7 @@ public static class ModifierManager return TypeToIdModifierMap.GetValueOrDefault(type); } - internal static bool RegisterModifier(Type modifierType) + internal static bool RegisterModifier(Type modifierType, MiraPluginInfo info) { if (!typeof(BaseModifier).IsAssignableFrom(modifierType)) { @@ -58,6 +60,9 @@ public static class ModifierManager IdToTypeModifierMap.Add(GetNextId(), modifierType); TypeToIdModifierMap.Add(modifierType, _nextId); + var bm = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); // this probably isn't a great idea + bm.ModifierId = _nextId; + info.Modifiers.Add(bm); if (!typeof(GameModifier).IsAssignableFrom(modifierType)) { diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index a7b1fe1..abcd1f7 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -5,6 +5,9 @@ /// public abstract class GameModifier : BaseModifier { + /// + public override bool ShowInFreeplay => true; + /// /// Gets the chance of the modifier being assigned to a player. /// diff --git a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs b/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs new file mode 100644 index 0000000..c9e3e8b --- /dev/null +++ b/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs @@ -0,0 +1,43 @@ +using HarmonyLib; +using MiraAPI.Modifiers; +using MiraAPI.Utilities; +using UnityEngine.UI; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(TaskAddButton))] +public static class TaskAddButtonPatches +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(TaskAddButton.Start))] + public static bool StartPrefix(TaskAddButton __instance) + { + if (uint.TryParse(__instance.name, out var result)) + { + __instance.Overlay.enabled = false; + if (PlayerControl.LocalPlayer.HasModifier(result)) + { + __instance.Overlay.enabled = true; + __instance.Overlay.sprite = __instance.CheckImage; + } + + __instance.Button.OnClick = new Button.ButtonClickedEvent(); + __instance.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => + { + if (PlayerControl.LocalPlayer.HasModifier(result)) + { + PlayerControl.LocalPlayer.GetModifierComponent()!.RemoveModifier(result); + __instance.Overlay.enabled = false; + } + else + { + PlayerControl.LocalPlayer.GetModifierComponent()!.AddModifier(ModifierManager.GetModifierType(result)!); + __instance.Overlay.enabled = true; + } + })); + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index 98e93bc..b6516bd 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -4,7 +4,6 @@ using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using System; -using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; @@ -18,7 +17,6 @@ namespace MiraAPI.Patches.Roles; [HarmonyPatch] public static class TaskAdderPatch { - private static readonly Dictionary ModsFolders = []; private static Scroller? _scroller; /// @@ -67,25 +65,34 @@ public static class TaskAdderPatch rolesFolder.FolderName = "Roles"; rolesFolder.name = "RolesFolder"; + var modifiersFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + modifiersFolder.gameObject.SetActive(false); + modifiersFolder.FolderName = "Modifiers"; + modifiersFolder.name = "ModifiersFolder"; + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) { - if (plugin.CustomRoles.Count == 0) + if (plugin.CustomRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true })) { - continue; + var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + newFolder.name = plugin.PluginId+":Roles"; + newFolder.FolderName = plugin.PluginInfo.Metadata.Name; + newFolder.gameObject.SetActive(false); + rolesFolder.SubFolders.Add(newFolder); } - var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); - newFolder.FolderName = newFolder.name = plugin.PluginInfo.Metadata.Name; - newFolder.gameObject.SetActive(false); - rolesFolder.SubFolders.Add(newFolder); - - if (!ModsFolders.ContainsKey(plugin.PluginInfo.Metadata.Name)) + if (plugin.Modifiers.Exists(x => x.ShowInFreeplay)) { - ModsFolders.Add(plugin.PluginInfo.Metadata.Name, plugin.PluginId); + var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + newFolder.name = plugin.PluginId+":Modifiers"; + newFolder.FolderName = plugin.PluginInfo.Metadata.Name; + newFolder.gameObject.SetActive(false); + modifiersFolder.SubFolders.Add(newFolder); } } __instance.Root.SubFolders.Add(rolesFolder); + __instance.Root.SubFolders.Add(modifiersFolder); __instance.GoToRoot(); } @@ -277,39 +284,21 @@ public static class TaskAdderPatch } } - if (ModsFolders.TryGetValue(taskFolder.FolderName, out var guid)) + var split = taskFolder.name.Split(':'); + if (split.Length == 2) { - var plugin = MiraPluginManager.GetPluginByGuid(guid); - for (var m = 0; m < plugin.CustomRoles.Count; m++) + var pluginId = split[0]; + var folderName = split[1]; + + if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) { - var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value; - if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || - (roleBehaviour is ICustomRole customRole && !customRole.Configuration.ShowInFreeplay)) + if (folderName == "Roles(Clone)") { - continue; + PluginRoles(__instance, plugin, ref num, ref num2, ref num3); } - - var taskAddButton2 = Object.Instantiate(__instance.RoleButton); - taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; - taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); - taskAddButton2.Role = roleBehaviour; - if (taskAddButton2.Button == null) + else if (folderName == "Modifiers(Clone)") { - continue; - } - - ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); - switch (m) - { - case 0 when __instance.restorePreviousSelectionFound != null: - ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound); - __instance.restorePreviousSelectionByFolderName = string.Empty; - __instance.restorePreviousSelectionFound = null; - break; - case 0: - ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); - break; + PluginModifiers(__instance, plugin, ref num, ref num2, ref num3); } } } @@ -336,4 +325,78 @@ public static class TaskAdderPatch ControllerManager.Instance.SetBackButton(__instance.FolderBackButton); return false; } + + private static void PluginModifiers( + TaskAdderGame instance, + MiraPluginInfo plugin, + ref float num, + ref float num2, + ref float num3) + { + for (var m = 0; m < plugin.Modifiers.Count; m++) + { + var modifier = plugin.Modifiers[m]; + if (!modifier.ShowInFreeplay) + { + continue; + } + + var taskAddButton = Object.Instantiate(instance.RoleButton); + taskAddButton.name = modifier.ModifierId.ToString(); + taskAddButton.role = null; + taskAddButton.MyTask = null; + taskAddButton.SafePositionWorld = instance.SafePositionWorld; + taskAddButton.Text.text = modifier.ModifierName; + instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); + + ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); + switch (m) + { + case 0 when instance.restorePreviousSelectionFound != null: + ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound); + instance.restorePreviousSelectionByFolderName = string.Empty; + instance.restorePreviousSelectionFound = null; + break; + case 0: + ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button); + break; + } + } + } + + private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3) + { + for (var m = 0; m < plugin.CustomRoles.Count; m++) + { + var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value; + if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || + roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false }) + { + continue; + } + + var taskAddButton2 = Object.Instantiate(instance.RoleButton); + taskAddButton2.SafePositionWorld = instance.SafePositionWorld; + taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; + instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); + taskAddButton2.Role = roleBehaviour; + if (taskAddButton2.Button == null) + { + continue; + } + + ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); + switch (m) + { + case 0 when instance.restorePreviousSelectionFound != null: + ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound); + instance.restorePreviousSelectionByFolderName = string.Empty; + instance.restorePreviousSelectionFound = null; + break; + case 0: + ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); + break; + } + } + } } diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 25a4dca..160980e 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -5,6 +5,7 @@ using MiraAPI.GameOptions; using System.Collections.Generic; using System.Collections.ObjectModel; using MiraAPI.Hud; +using MiraAPI.Modifiers; namespace MiraAPI.PluginLoading; @@ -61,6 +62,8 @@ public class MiraPluginInfo internal List Options { get; } = []; + internal List Modifiers { get; } = []; + internal Dictionary CustomRoles { get; } = []; internal Dictionary GameModes { get; } = []; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 678dee4..a7ed480 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -48,7 +48,7 @@ public sealed class MiraPluginManager continue; } - if (RegisterModifier(type)) + if (RegisterModifier(type, info)) { continue; } @@ -91,9 +91,9 @@ public sealed class MiraPluginManager /// /// The plugin GUID. /// A MiraPluginInfo. - public static MiraPluginInfo GetPluginByGuid(string pluginId) + public static MiraPluginInfo? GetPluginByGuid(string pluginId) { - return Instance._registeredPlugins.Values.First(plugin => plugin.PluginId == pluginId); + return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId); } private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo) @@ -199,11 +199,11 @@ public sealed class MiraPluginManager } } - private static bool RegisterModifier(Type type) + private static bool RegisterModifier(Type type, MiraPluginInfo info) { try { - return ModifierManager.RegisterModifier(type); + return ModifierManager.RegisterModifier(type, info); } catch (Exception e) { diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 9afc407..a452d1e 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,5 +1,4 @@ -using System; -using BepInEx.Configuration; +using BepInEx.Configuration; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; From 52ff4bfbfc3b99f649f44eb3db8137aab8a71804 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:59:19 -0500 Subject: [PATCH 09/71] . --- MiraAPI/PluginLoading/MiraPluginInfo.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 160980e..81412e8 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -22,6 +22,15 @@ public class MiraPluginInfo PluginId = info.Metadata.GUID; } + /// + /// Get a read only collection of this plugin's modifiers. This is probably not safe because they are uninitialized. + /// + /// Readonly collection of modifiers. + public IReadOnlyCollection GetModifiers() + { + return Modifiers.AsReadOnly(); + } + /// /// Get a read only collection of this plugin's Option Groups. /// From ba92d9608cdef40b70b7257d3466a235068497dd Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 22 Feb 2025 21:12:54 -0500 Subject: [PATCH 10/71] Updated role settings appearance + role options --- .../Patches/Options/RoleSettingMenuPatches.cs | 184 +++++++++++++----- MiraAPI/Roles/CustomRoleConfiguration.cs | 3 +- MiraAPI/Roles/CustomRoleManager.cs | 2 +- 3 files changed, 139 insertions(+), 50 deletions(-) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 915f2bc..f8dd48d 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; +using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Networking; using MiraAPI.Roles; @@ -11,6 +8,9 @@ using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; using Reactor.Utilities; using Reactor.Utilities.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; using TMPro; using UnityEngine; using UnityEngine.Events; @@ -22,9 +22,9 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(RolesSettingsMenu))] public static class RoleSettingMenuPatches { - private static Dictionary RoleGroupHidden { get; } = []; - private static List CategoryHeaderEditRoles { get; } = []; - private static List RoleOptionSettings { get; } = []; + private static Dictionary RoleGroupHidden { get; set; } = new(); + private static List Headers { get; set; } = new(); + private static List RoleOptionSettings { get; set; } = new(); private static float ScrollerNum { get; set; } = 0.522f; @@ -32,10 +32,11 @@ public static class RoleSettingMenuPatches [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] public static bool PatchStart(RolesSettingsMenu __instance) { - CategoryHeaderEditRoles.ForEach(Object.Destroy); - CategoryHeaderEditRoles.Clear(); + Headers.ForEach(Object.Destroy); RoleOptionSettings.ForEach(Object.Destroy); + Headers.Clear(); RoleOptionSettings.Clear(); + __instance.roleChances = new Il2CppSystem.Collections.Generic.List(); __instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List(); @@ -79,6 +80,10 @@ public static class RoleSettingMenuPatches .OrderBy(x => x.Key.Priority) .ThenBy(x => x.Key.Name); + var quotaThing = __instance.categoryHeaderEditRoleOrigin.transform.FindChild("QuotaHeader"); + var template = __instance.transform.parent.parent.GetComponent().GameSettingsTab.categoryHeaderOrigin; + + foreach (var grouping in sortedRoleGroups) { if (!grouping.Any()) @@ -97,39 +102,73 @@ public static class RoleSettingMenuPatches _ => CustomStringName.CreateAndRegister(group.Name), }; - var categoryHeaderEditRole = Object.Instantiate( - __instance.categoryHeaderEditRoleOrigin, + var categoryHeaderMasked = Object.Instantiate( + template, Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform); - categoryHeaderEditRole.SetHeader(name, 20); - if (name is not (StringNames.CrewmateRolesHeader or StringNames.ImpostorRolesHeader)) + + categoryHeaderMasked.SetHeader(name, 20); + + var quotaInst = Object.Instantiate(quotaThing, categoryHeaderMasked.transform); + quotaInst.transform.localScale = new Vector3(1.3f, 1.3f, 1.3f); + quotaInst.transform.localPosition = new Vector3(0.7f, -0.82f, 0); + + var chanceText = quotaInst.transform.FindChild("Chance Text"); + chanceText.transform.localPosition = new Vector3(4.3f, 0.0993f, 0); + + var countText = quotaInst.transform.FindChild("# Text"); + countText.transform.localPosition = new Vector3(1.9f, 0.0993f, 0f); + + var blankLabel = quotaInst.transform.FindChild("BlankLabel").gameObject; + var chanceLabel = quotaInst.transform.FindChild("Chance Label").gameObject; + var countLabel = quotaInst.transform.FindChild("# Label").gameObject; + blankLabel.Destroy(); + chanceLabel.Destroy(); + countLabel.Destroy(); + + if (name is StringNames.CrewmateRolesHeader) { - var darkColor = group.Color.DarkenColor(.25f); - var veryDarkColor = group.Color.DarkenColor(.35f); - categoryHeaderEditRole.Title.color = veryDarkColor; - categoryHeaderEditRole.blankLabel.color = veryDarkColor; - categoryHeaderEditRole.Background.color = group.Color; - categoryHeaderEditRole.chanceLabel.color = darkColor; - categoryHeaderEditRole.countLabel.color = darkColor; + categoryHeaderMasked.Title.color = Palette.CrewmateRoleHeaderTextBlue; + categoryHeaderMasked.Background.color = Palette.CrewmateRoleHeaderBlue; + } + else if (name is StringNames.ImpostorRolesHeader) + { + categoryHeaderMasked.Title.color = Palette.ImpostorRoleHeaderTextRed; + categoryHeaderMasked.Background.color = Palette.ImpostorRoleHeaderRed; + } + else + { + categoryHeaderMasked.Title.color = group.Color; + categoryHeaderMasked.Divider.color = group.Color; + categoryHeaderMasked.Background.color = group.Color; } - categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, ScrollerNum, -2f); - categoryHeaderEditRole.transform.Find("LabelSprite").transform.localScale = new Vector3(1.3f, 1, 0.5529f); - categoryHeaderEditRole.transform.Find("QuotaHeader").gameObject.SetActive(!RoleGroupHidden[group]); + categoryHeaderMasked.Title.color = categoryHeaderMasked.Title.color.IsColorDark() ? + categoryHeaderMasked.Title.color.LightenColor(0.3f) : categoryHeaderMasked.Title.color.LightenColor(0.1f); - CategoryHeaderEditRoles.Add(categoryHeaderEditRole); + categoryHeaderMasked.Title.fontStyle = __instance.categoryHeaderEditRoleOrigin.Title.fontStyle; + categoryHeaderMasked.Title.font = __instance.categoryHeaderEditRoleOrigin.Title.font; + categoryHeaderMasked.Title.fontMaterial = __instance.categoryHeaderEditRoleOrigin.Title.fontMaterial; - ScrollerNum -= 0.522f; + categoryHeaderMasked.Divider.color = categoryHeaderMasked.Background.color; + categoryHeaderMasked.Background.size = new Vector2(categoryHeaderMasked.Background.size.x + 1.5f, categoryHeaderMasked.Background.size.y); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.44f, ScrollerNum, -2f); + categoryHeaderMasked.gameObject.SetActive(true); + quotaInst.gameObject.SetActive(!RoleGroupHidden[group]); var label = RoleGroupHidden[group] ? "(Click to open)" : "(Click to close)"; - var newText = Object.Instantiate(categoryHeaderEditRole.Title, categoryHeaderEditRole.transform); - newText.text = $"{label}"; - newText.transform.localPosition = new Vector3(-3.3425f, -0.1706f, -1); + var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); + newText.text = $"{label}"; + newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); newText.gameObject.GetComponent().Destroy(); + Headers.Add(categoryHeaderMasked.gameObject); + ScrollerNum -= 0.422f; + if (!RoleGroupHidden[group]) { foreach (var role in grouping) @@ -148,11 +187,11 @@ public static class RoleSettingMenuPatches } } - var boxCol = categoryHeaderEditRole.gameObject.AddComponent(); - boxCol.size = new Vector2(3, 0.25f); - boxCol.offset = new Vector2(-2.05f, -.175f); + var boxCol = categoryHeaderMasked.gameObject.AddComponent(); + boxCol.size = new Vector2(7, 0.7f); + boxCol.offset = new Vector2(1.5f, -0.3f); - var headerBtn = categoryHeaderEditRole.gameObject.AddComponent(); + var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); headerBtn.ClickSound = __instance.BackButton.GetComponent().ClickSound; headerBtn.OnMouseOver = new UnityEvent(); headerBtn.OnMouseOut = new UnityEvent(); @@ -163,11 +202,11 @@ public static class RoleSettingMenuPatches { RoleGroupHidden[group] = !value; } - foreach (var header in CategoryHeaderEditRoles) + foreach (var header in Headers) { - header.gameObject.Destroy(); + header.Destroy(); } - CategoryHeaderEditRoles.Clear(); + Headers.Clear(); foreach (var option in RoleOptionSettings) { option.gameObject.Destroy(); @@ -177,7 +216,7 @@ public static class RoleSettingMenuPatches })); headerBtn.SetButtonEnableState(true); - if (!RoleGroupHidden[group]) + if (RoleGroupHidden.TryGetValue(group, out var value) && !value) { ScrollerNum -= 0.4f; } @@ -188,7 +227,7 @@ public static class RoleSettingMenuPatches private static void SetScrollBounds(this Scroller scroller) { - scroller.CalculateAndSetYBounds(1 + 1.5f * CategoryHeaderEditRoles.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f); + scroller.CalculateAndSetYBounds(1 + 1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f); } [HarmonyPrefix] @@ -255,14 +294,15 @@ public static class RoleSettingMenuPatches private static void CreateAdvancedSettings(RolesSettingsMenu __instance, RoleBehaviour role) { - foreach (var option in __instance.advancedSettingChildren) + foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren()) { - Object.Destroy(option.gameObject); + optBehaviour.gameObject.DestroyImmediate(); } __instance.advancedSettingChildren.Clear(); - var num = -0.872f; + var hasImage = role.RoleScreenshot != null; + var num = hasImage ? -0.872f : -1; var filteredOptions = GameSettingMenuPatches.SelectedMod?.Options.Where(x => x.AdvancedRole == role.GetType()) ?? []; @@ -279,7 +319,7 @@ public static class RoleSettingMenuPatches __instance.stringOptionOrigin, __instance.AdvancedRolesSettings.transform); - newOpt.transform.localPosition = new Vector3(2.17f, num, -2f); + newOpt.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f); newOpt.SetClickMask(__instance.ButtonClickMask); SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); @@ -317,15 +357,63 @@ public static class RoleSettingMenuPatches __instance.roleTitleText.text = TranslationController.Instance.GetString( role.StringName, new Il2CppReferenceArray(0)); - __instance.roleScreenshot.sprite = Sprite.Create( - customRole.Configuration.OptionsScreenshot.LoadAsset().texture, - new Rect(0, 0, 370, 230), - Vector2.one / 2, - 100); - __instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced; + + var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); + var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground"); + if (role.RoleScreenshot == null) + { + imgBg.gameObject.SetActive(false); + __instance.roleScreenshot.gameObject.SetActive(false); + __instance.roleDescriptionText.transform.parent.localPosition = new Vector3(1.5f, -0.2731f, -1); + __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.09f, 0.2f, 0.5687f); + labelBg.localPosition = new Vector3(-0.7f, 0.1054f, -2.5f); + } + else + { + imgBg.gameObject.SetActive(true); + __instance.roleScreenshot.gameObject.SetActive(true); + __instance.roleDescriptionText.transform.parent.localPosition = new Vector3(2.5176f, -0.2731f, -1f); + __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.0675f, 0.1494f, 0.5687f); + labelBg.transform.localPosition = new Vector3(1.082f, 0.1054f, -2.5f); + + __instance.roleScreenshot.sprite = Sprite.Create( + role.RoleScreenshot.texture, + new Rect(0, 0, 370, 230), + Vector2.one / 2, + 100); + __instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced; + } + __instance.roleHeaderSprite.color = customRole.RoleColor; __instance.roleHeaderText.color = customRole.RoleColor.GetAlternateColor(); + var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); + + if (categoryHeaderMasked.Title.gameObject.TryGetComponent(out var comp)) + { + comp.Destroy(); + } + + categoryHeaderMasked.Title.text = "RETURN TO ROLE SETTINGS"; + + if (!categoryHeaderMasked.gameObject.TryGetComponent(out _)) + { + var boxCol = categoryHeaderMasked.gameObject.AddComponent(); + boxCol.size = new Vector2(7, 0.7f); + boxCol.offset = new Vector2(1.5f, -0.3f); + + var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); + headerBtn.ClickSound = __instance.BackButton.GetComponent().ClickSound; + headerBtn.OnMouseOver = new UnityEvent(); + headerBtn.OnMouseOut = new UnityEvent(); + headerBtn.OnClick.AddListener( + (UnityAction)(() => + { + __instance.OpenChancesTab(); + })); + headerBtn.SetButtonEnableState(true); + } + var bg = __instance.AdvancedRolesSettings.transform.Find("Background"); bg.localPosition = new Vector3(1.4041f, -7.08f, 0); bg.GetComponent().size = new Vector2(89.4628f, 100); @@ -369,7 +457,7 @@ public static class RoleSettingMenuPatches Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform); - roleOptionSetting.transform.localPosition = new Vector3(-0.15f, ScrollerNum, -2f); + roleOptionSetting.transform.localPosition = new Vector3(-0.1f, ScrollerNum, -2f); roleOptionSetting.SetRole(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions, role, 20); roleOptionSetting.labelSprite.color = customRole.RoleColor; diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 4500331..efaf444 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -32,6 +32,7 @@ public record struct CustomRoleConfiguration TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate; HideSettings = roleBehaviour?.IsDead == true; ShowInFreeplay = roleBehaviour?.IsDead == false; + OptionsScreenshot = null; } /// @@ -57,7 +58,7 @@ public record struct CustomRoleConfiguration /// /// Gets or sets the Sprite used for the Role Options menu screenshot. /// - public LoadableAsset OptionsScreenshot { get; set; } = MiraAssets.Empty; + public LoadableAsset? OptionsScreenshot { get; set; } /// /// Gets or sets the Sprite used for the Role Icon. diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index cc79c02..c31e922 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -91,7 +91,7 @@ public static class CustomRoleManager roleBehaviour.CanVent = customRole.Configuration.CanUseVent; roleBehaviour.DefaultGhostRole = customRole.Configuration.GhostRole; roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount; - roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot.LoadAsset(); + roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot?.LoadAsset(); if (roleBehaviour.IsDead) { From dda46e590320a2d489ff9b8155b55e9327354446 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 15:45:31 -0500 Subject: [PATCH 11/71] i dont know why this is even a bug --- MiraAPI/Patches/Roles/TaskAdderPatch.cs | 29 ++++++++++++++----------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index b6516bd..6de6649 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -138,7 +138,7 @@ public static class TaskAdderPatch // yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry: [HarmonyPrefix] [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))] - public static bool ShowPatch(TaskAdderGame __instance, [HarmonyArgument(0)] TaskFolder taskFolder) + public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder) { var stringBuilder = new Il2CppSystem.Text.StringBuilder(64); __instance.Hierarchy.Add(taskFolder); @@ -284,21 +284,24 @@ public static class TaskAdderPatch } } - var split = taskFolder.name.Split(':'); - if (split.Length == 2) + if (taskFolder) { - var pluginId = split[0]; - var folderName = split[1]; - - if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) + var split = taskFolder.name.Split(':'); + if (split.Length == 2) { - if (folderName == "Roles(Clone)") + var pluginId = split[0]; + var folderName = split[1]; + + if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) { - PluginRoles(__instance, plugin, ref num, ref num2, ref num3); - } - else if (folderName == "Modifiers(Clone)") - { - PluginModifiers(__instance, plugin, ref num, ref num2, ref num3); + if (folderName == "Roles(Clone)") + { + PluginRoles(__instance, plugin, ref num, ref num2, ref num3); + } + else if (folderName == "Modifiers(Clone)") + { + PluginModifiers(__instance, plugin, ref num, ref num2, ref num3); + } } } } From 2af89a678ff89f9206bc85c51fc6364d355d5508 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:34:59 -0500 Subject: [PATCH 12/71] add masking to task adder roles --- MiraAPI/Patches/Roles/TaskAdderPatch.cs | 5 +++++ MiraAPI/Utilities/Extensions.cs | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index 6de6649..b15e9cf 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -5,6 +5,7 @@ using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using System; using System.Linq; +using MiraAPI.Utilities; using TMPro; using UnityEngine; using Object = UnityEngine.Object; @@ -261,6 +262,7 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(__instance.RoleButton); taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; + taskAddButton2.Text.fontMaterial.EnableMasking(); __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; @@ -350,6 +352,7 @@ public static class TaskAdderPatch taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; taskAddButton.Text.text = modifier.ModifierName; + taskAddButton.Text.fontMaterial.EnableMasking(); instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); @@ -381,6 +384,8 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(instance.RoleButton); taskAddButton2.SafePositionWorld = instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; + taskAddButton2.Text.fontMaterial.EnableMasking(); + instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; if (taskAddButton2.Button == null) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index dc3bba1..97bd8c9 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -39,6 +39,16 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Sets masking on a TMP MATERIAL! Do not use this on other materials. + /// + /// The TMP material. + public static void EnableMasking(this Material material) + { + material.SetFloat(ShaderID.Get("_Stencil"), 1); + material.SetFloat(ShaderID.Get("_StencilComp"), 4); + } + /// /// Checks if a type is static. /// From 6775294a1995ff85a311fa305b037f16ea30b273 Mon Sep 17 00:00:00 2001 From: Pietrodjaowjao Date: Sun, 23 Feb 2025 20:22:11 -0300 Subject: [PATCH 13/71] Change util to TMP_Text (all textmeshpro render components) --- MiraAPI/Patches/Roles/TaskAdderPatch.cs | 6 +++--- MiraAPI/Utilities/Extensions.cs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index b15e9cf..2135dc5 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -262,7 +262,7 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(__instance.RoleButton); taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - taskAddButton2.Text.fontMaterial.EnableMasking(); + taskAddButton2.Text.EnableMasking(); __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; @@ -352,7 +352,7 @@ public static class TaskAdderPatch taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; taskAddButton.Text.text = modifier.ModifierName; - taskAddButton.Text.fontMaterial.EnableMasking(); + taskAddButton.Text.EnableMasking(); instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); @@ -384,7 +384,7 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(instance.RoleButton); taskAddButton2.SafePositionWorld = instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - taskAddButton2.Text.fontMaterial.EnableMasking(); + taskAddButton2.Text.EnableMasking(); instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 97bd8c9..a89bfd3 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -8,6 +8,7 @@ using Reactor.Utilities; using System; using System.Collections.Generic; using System.Linq; +using TMPro; using UnityEngine; namespace MiraAPI.Utilities; @@ -42,11 +43,11 @@ public static class Extensions /// /// Sets masking on a TMP MATERIAL! Do not use this on other materials. /// - /// The TMP material. - public static void EnableMasking(this Material material) + /// The TMP text. + public static void EnableMasking(this TMP_Text text) { - material.SetFloat(ShaderID.Get("_Stencil"), 1); - material.SetFloat(ShaderID.Get("_StencilComp"), 4); + text.fontMaterial.SetFloat(ShaderID.Get("_Stencil"), 1); + text.fontMaterial.SetFloat(ShaderID.Get("_StencilComp"), 4); } /// From 975e8cb9b77500578765f0216df733a262babe35 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:24:56 -0500 Subject: [PATCH 14/71] allow stackable modifiers --- MiraAPI/Modifiers/BaseModifier.cs | 5 +++++ MiraAPI/Modifiers/ModifierComponent.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 47cd491..dcea493 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -30,6 +30,11 @@ public abstract class BaseModifier /// public virtual bool HideOnUi => false; + /// + /// Gets a value indicating whether the modifier can be added multiple times. + /// + public virtual bool Stackable => false; + /// /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true. /// diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 7db81f5..faffcc0 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -228,7 +228,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return null; } - if (Modifiers.Find(x => x.ModifierId == modifierId) != null) + if (Modifiers.Find(x => x.ModifierId == modifierId && !x.Stackable) != null) { Logger.Error($"Player already has modifier with id {modifierId}!"); return null; From 87187fe205f981312f3791fdd5d1ebbe539d9a63 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:25:25 -0500 Subject: [PATCH 15/71] improve modifier extensions and add more method for dealing with modifiers --- MiraAPI/Modifiers/ModifierComponent.cs | 44 ++++- MiraAPI/Patches/PlayerControlPatches.cs | 2 +- MiraAPI/Utilities/Extensions.cs | 134 +------------ MiraAPI/Utilities/ModifierExtensions.cs | 252 ++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 135 deletions(-) create mode 100644 MiraAPI/Utilities/ModifierExtensions.cs diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index faffcc0..9918eab 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -122,6 +122,38 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } } + /// + /// Gets a collection of modifiers by their type, or null if the player doesn't have one. + /// + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + public IEnumerable GetModifiersByType() where T : BaseModifier + { + return ActiveModifiers.OfType(); + } + + /// + /// Gets a modifier by its type, or null if the player doesn't have it. + /// + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + public T? GetModifier() where T : BaseModifier + { + return GetModifiersByType().FirstOrDefault(); + } + + /// + /// Tries to get a modifier by its type. + /// + /// The modifier or null. + /// The Type of the Modifier. + /// True if the modifier was found, false otherwise. + public bool TryGetModifier(out T? modifier) where T : BaseModifier + { + modifier = GetModifier(); + return modifier != null; + } + /// /// Removes a modifier from the player. /// @@ -260,19 +292,23 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// Checks if a player has an active or queued modifier by its ID. /// /// The Modifier ID. + /// The predicate to check the modifier. /// True if the Modifier is present, false otherwise. - public bool HasModifier(uint id) + public bool HasModifier(uint id, Func? predicate=null) { - return ActiveModifiers.Exists(x => x.ModifierId == id) || _toAdd.Exists(x => x.ModifierId == id); + return ActiveModifiers.Exists(x => x.ModifierId == id && (predicate == null || predicate(x))) || + _toAdd.Exists(x => x.ModifierId == id && (predicate == null || predicate(x))); } /// /// Checks if a player has an active or queued modifier by its type. /// + /// The predicate to check the modifier. /// The Type of the Modifier. /// True if the Modifier is present, false otherwise. - public bool HasModifier() + public bool HasModifier(Func? predicate=null) where T : BaseModifier { - return ActiveModifiers.Exists(x => x is T) || _toAdd.Exists(x => x is T); + return ModifierManager.GetModifierId(typeof(T)) is { } id && + HasModifier(id, (Func?)predicate); } } diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index c7918fe..fbabe1a 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -126,6 +126,6 @@ public static class PlayerControlPatches [HarmonyPatch(nameof(PlayerControl.OnDestroy))] public static void PlayerControlOnDestroyPrefix(PlayerControl __instance) { - Utilities.Extensions.ModifierComponents.Remove(__instance); + Utilities.ModifierExtensions.ModifierComponents.Remove(__instance); } } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index dc3bba1..f738558 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -130,30 +130,6 @@ public static class Extensions 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 +150,6 @@ public static class Extensions 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. /// @@ -328,6 +205,7 @@ public static class Extensions .GetNearestDeadBodies(playerControl.GetTruePosition(), radius, Helpers.CreateFilter(Constants.NotShipMask)) .Find(component => component && !component.Reported); } + /// /// Finds the nearest object of a specified type to a player. It will only work if the object has a collider. /// @@ -376,10 +254,6 @@ public static class Extensions return predicate != null ? filteredPlayers.Find(predicate) : filteredPlayers.FirstOrDefault(); } - private static readonly int _outline = Shader.PropertyToID("_Outline"); - private static readonly int _outlineColor = Shader.PropertyToID("_OutlineColor"); - private static readonly int _addColor = Shader.PropertyToID("_AddColor"); - /// /// Fixed version of Reactor's SetOutline. /// @@ -387,8 +261,8 @@ public static class Extensions /// The outline color. public static void UpdateOutline(this Renderer renderer, Color? color) { - renderer.material.SetFloat(_outline, color.HasValue ? 1 : 0); - renderer.material.SetColor(_outlineColor, color.HasValue ? color.Value : Color.clear); - renderer.material.SetColor(_addColor, color.HasValue ? color.Value : Color.clear); + renderer.material.SetFloat(ShaderID.Outline, color.HasValue ? 1 : 0); + renderer.material.SetColor(ShaderID.OutlineColor, color ?? Color.clear); + renderer.material.SetColor(ShaderID.AddColor, color ?? Color.clear); } } diff --git a/MiraAPI/Utilities/ModifierExtensions.cs b/MiraAPI/Utilities/ModifierExtensions.cs new file mode 100644 index 0000000..dadb62c --- /dev/null +++ b/MiraAPI/Utilities/ModifierExtensions.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using MiraAPI.Modifiers; +using MiraAPI.Networking; +using Reactor.Networking.Attributes; +using Reactor.Utilities; + +namespace MiraAPI.Utilities; + +/// +/// Extensions for modifiers. +/// +public static class ModifierExtensions +{ + /// + /// Gets the dictionary to cache ModifierComponents. + /// + public static Dictionary ModifierComponents { get; } = []; + + /// + /// 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.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); + } + + /// + /// 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.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); + } + + /// + /// 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) + { + return null; + } + + ModifierComponents[player] = component; + return component; + } + + /// + /// Gets the ModifierComponent for a player. + /// + /// The PlayerControl object. + /// The ModifierComponent if it is found. + /// A ModifierComponent if there is one, null otherwise. + public static bool TryGetModifierComponent(this PlayerControl player, out ModifierComponent? component) + { + if (ModifierComponents.TryGetValue(player, out component)) + { + return component; + } + + component = player.GetComponent(); + return component; + } + + /// + /// Gets a collection of modifiers by their type, or null if the player doesn't have one. + /// + /// The PlayerControl object. + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + public static IEnumerable GetModifiersByType(this PlayerControl player) where T : BaseModifier + { + return player.GetModifierComponent()!.GetModifiersByType(); + } + + /// + /// 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()!.GetModifier(); + } + + /// + /// Tries to get a modifier by its type. + /// + /// The PlayerControl object. + /// The modifier or null. + /// The Type of the Modifier. + /// True if the modifier was found, false otherwise. + public static bool TryGetModifier(this PlayerControl player, out T? modifier) where T : BaseModifier + { + return player.GetModifierComponent()!.TryGetModifier(out modifier); + } + + /// + /// Removes a modifier from the player. + /// + /// The PlayerControl object. + /// The modifier type. + public static void RemoveModifier(this PlayerControl player, Type type) + { + player.GetModifierComponent()!.RemoveModifier(type); + } + + /// + /// Removes a modifier from the player. + /// + /// The PlayerControl object. + /// The modifier type. + public static void RemoveModifier(this PlayerControl player) where T : BaseModifier + { + player.GetModifierComponent()!.RemoveModifier(); + } + + /// + /// Removes a modifier from the player. + /// + /// The PlayerControl object. + /// The modifier ID. + public static void RemoveModifier(this PlayerControl player, uint modifierId) + { + player.GetModifierComponent()!.RemoveModifier(modifierId); + } + + /// + /// Removes a modifier from the player. + /// + /// The PlayerControl object. + /// The modifier object. + public static void RemoveModifier(this PlayerControl player, BaseModifier modifier) + { + player.GetModifierComponent()!.RemoveModifier(modifier); + } + + /// + /// Adds a modifier to the player. + /// + /// The PlayerControl object. + /// The modifier to add. + /// The modifier that was added. + public static BaseModifier? AddModifier(this PlayerControl player, BaseModifier modifier) + { + return player.GetModifierComponent()!.AddModifier(modifier); + } + + /// + /// Adds a modifier to the player. + /// + /// The PlayerControl object. + /// The modifier type. + /// The modifier that was added. + public static BaseModifier? AddModifier(this PlayerControl player, Type type) + { + return player.GetModifierComponent()!.AddModifier(type); + } + + /// + /// Adds a modifier to the player. + /// + /// The PlayerControl object. + /// The Type of the modifier. + /// The new modifier. + public static T? AddModifier(this PlayerControl player) where T : BaseModifier + { + return player.GetModifierComponent()!.AddModifier(); + } + + /// + /// Checks if a player has an active or queued modifier by its ID. + /// + /// The PlayerControl object. + /// The Modifier ID. + /// The predicate to check the modifier. + /// True if the Modifier is present, false otherwise. + public static bool HasModifier(this PlayerControl player, uint id, Func? predicate=null) + { + return player.GetModifierComponent()!.HasModifier(id, predicate); + } + + /// + /// Checks if a player has an active or queued modifier by its type. + /// + /// The PlayerControl object. + /// The predicate to check the modifier. + /// The Type of the Modifier. + /// True if the Modifier is present, false otherwise. + public static bool HasModifier(this PlayerControl player, Func? predicate=null) where T : BaseModifier + { + return player.GetModifierComponent()!.HasModifier(predicate); + } +} From 055b097c2165ad1dac9b1de77cfb1590e196eece Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:32:23 -0500 Subject: [PATCH 16/71] better summary comment --- MiraAPI/Utilities/Extensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index a89bfd3..9d8367c 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -41,7 +41,7 @@ public static class Extensions } /// - /// Sets masking on a TMP MATERIAL! Do not use this on other materials. + /// Enables Among Us style masking on a TMP text object. /// /// The TMP text. public static void EnableMasking(this TMP_Text text) From 9cc9a137cccdfe55364eb94fe5be46b35ea3623c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:54:51 -0500 Subject: [PATCH 17/71] fix pietros mistake and cleanup things --- MiraAPI/Patches/Roles/TaskAddButtonPatches.cs | 40 +++++------- ...{TaskAdderPatch.cs => TaskAdderPatches.cs} | 63 +++++++++---------- MiraAPI/Utilities/Extensions.cs | 2 +- 3 files changed, 46 insertions(+), 59 deletions(-) rename MiraAPI/Patches/Roles/{TaskAdderPatch.cs => TaskAdderPatches.cs} (91%) diff --git a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs b/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs index c9e3e8b..510d5e9 100644 --- a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs @@ -1,43 +1,35 @@ using HarmonyLib; -using MiraAPI.Modifiers; +using MiraAPI.Roles; using MiraAPI.Utilities; -using UnityEngine.UI; +using UnityEngine; namespace MiraAPI.Patches.Roles; [HarmonyPatch(typeof(TaskAddButton))] -public static class TaskAddButtonPatches +internal static class TaskAddButtonPatches { [HarmonyPrefix] [HarmonyPatch(nameof(TaskAddButton.Start))] public static bool StartPrefix(TaskAddButton __instance) { + // if this becomes problematic in the future, find a new method. if (uint.TryParse(__instance.name, out var result)) { - __instance.Overlay.enabled = false; - if (PlayerControl.LocalPlayer.HasModifier(result)) - { - __instance.Overlay.enabled = true; - __instance.Overlay.sprite = __instance.CheckImage; - } - - __instance.Button.OnClick = new Button.ButtonClickedEvent(); - __instance.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => - { - if (PlayerControl.LocalPlayer.HasModifier(result)) - { - PlayerControl.LocalPlayer.GetModifierComponent()!.RemoveModifier(result); - __instance.Overlay.enabled = false; - } - else - { - PlayerControl.LocalPlayer.GetModifierComponent()!.AddModifier(ModifierManager.GetModifierType(result)!); - __instance.Overlay.enabled = true; - } - })); + __instance.Overlay.sprite = __instance.CheckImage; + __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifier(result); return false; } return true; } + + [HarmonyPostfix] + [HarmonyPatch(nameof(TaskAddButton.Role), MethodType.Setter)] + public static void RoleGetterPatch(TaskAddButton __instance) + { + if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole) + { + __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray; + } + } } diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatches.cs similarity index 91% rename from MiraAPI/Patches/Roles/TaskAdderPatch.cs rename to MiraAPI/Patches/Roles/TaskAdderPatches.cs index 2135dc5..37f9744 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatches.cs @@ -1,31 +1,27 @@ -using AmongUs.GameOptions; +using System; +using System.Globalization; +using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; +using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Roles; -using MiraAPI.Utilities.Assets; -using System; -using System.Linq; using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; using TMPro; using UnityEngine; +using UnityEngine.UI; using Object = UnityEngine.Object; namespace MiraAPI.Patches.Roles; -/// -/// Patch TaskAdder for organizing Roles and adding scroller. -/// -[HarmonyPatch] -public static class TaskAdderPatch +[HarmonyPatch(typeof(TaskAdderGame))] +internal static class TaskAdderPatches { private static Scroller? _scroller; - /// - /// Adds custom roles to the TaskAdder. - /// - /// TaskAdder instance. [HarmonyPostfix] - [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))] + [HarmonyPatch(nameof(TaskAdderGame.Begin))] public static void AddRolesFolder(TaskAdderGame __instance) { GameObject inner = new("Inner"); @@ -98,22 +94,6 @@ public static class TaskAdderPatch __instance.GoToRoot(); } - /// - /// Sets the color of the role button based on the role. - /// - /// TaskAddButton instance. - [HarmonyPostfix] - [HarmonyPatch(typeof(TaskAddButton), nameof(TaskAddButton.Role), MethodType.Setter)] - public static void RoleGetterPatch(TaskAddButton __instance) - { - if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole) - { - __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray; - } - - __instance.RolloverHandler.OutColor = __instance.FileImage.color; - } - private static void AddFileAsChildCustom( this TaskAdderGame instance, TaskAddButton item, @@ -138,7 +118,7 @@ public static class TaskAdderPatch // yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry: [HarmonyPrefix] - [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))] + [HarmonyPatch(nameof(TaskAdderGame.ShowFolder))] public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder) { var stringBuilder = new Il2CppSystem.Text.StringBuilder(64); @@ -262,7 +242,6 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(__instance.RoleButton); taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - taskAddButton2.Text.EnableMasking(); __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; @@ -310,7 +289,7 @@ public static class TaskAdderPatch foreach (var chip in __instance.ActiveItems) { - chip.GetComponentInChildren().EnableMasking(); + chip.GetComponentInChildren().EnableStencilMasking(); chip.GetComponent().maskInteraction = SpriteMaskInteraction.VisibleInsideMask; } @@ -347,12 +326,28 @@ public static class TaskAdderPatch } var taskAddButton = Object.Instantiate(instance.RoleButton); - taskAddButton.name = modifier.ModifierId.ToString(); + taskAddButton.name = modifier.ModifierId.ToString(CultureInfo.InvariantCulture); taskAddButton.role = null; taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; taskAddButton.Text.text = modifier.ModifierName; taskAddButton.Text.EnableMasking(); + + taskAddButton.Button.OnClick = new Button.ButtonClickedEvent(); + taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => + { + if (PlayerControl.LocalPlayer.HasModifier(modifier.ModifierId)) + { + PlayerControl.LocalPlayer.GetModifierComponent()!.RemoveModifier(modifier.ModifierId); + taskAddButton.Overlay.enabled = false; + } + else + { + PlayerControl.LocalPlayer.GetModifierComponent()!.AddModifier(ModifierManager.GetModifierType(modifier.ModifierId)!); + taskAddButton.Overlay.enabled = true; + } + })); + instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 9d8367c..89fc17b 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -44,7 +44,7 @@ public static class Extensions /// Enables Among Us style masking on a TMP text object. /// /// The TMP text. - public static void EnableMasking(this TMP_Text text) + public static void EnableStencilMasking(this TMP_Text text) { text.fontMaterial.SetFloat(ShaderID.Get("_Stencil"), 1); text.fontMaterial.SetFloat(ShaderID.Get("_StencilComp"), 4); From 4849b52b47e34b60e5e363824746294dd45735e7 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:59:14 -0500 Subject: [PATCH 18/71] organize task add patches --- MiraAPI/Patches/{Roles => Freeplay}/TaskAddButtonPatches.cs | 2 +- MiraAPI/Patches/{Roles => Freeplay}/TaskAdderPatches.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename MiraAPI/Patches/{Roles => Freeplay}/TaskAddButtonPatches.cs (96%) rename MiraAPI/Patches/{Roles => Freeplay}/TaskAdderPatches.cs (99%) diff --git a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs similarity index 96% rename from MiraAPI/Patches/Roles/TaskAddButtonPatches.cs rename to MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 510d5e9..07b7f75 100644 --- a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -3,7 +3,7 @@ using MiraAPI.Roles; using MiraAPI.Utilities; using UnityEngine; -namespace MiraAPI.Patches.Roles; +namespace MiraAPI.Patches.Freeplay; [HarmonyPatch(typeof(TaskAddButton))] internal static class TaskAddButtonPatches diff --git a/MiraAPI/Patches/Roles/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs similarity index 99% rename from MiraAPI/Patches/Roles/TaskAdderPatches.cs rename to MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 37f9744..e36ac61 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -13,7 +13,7 @@ using UnityEngine; using UnityEngine.UI; using Object = UnityEngine.Object; -namespace MiraAPI.Patches.Roles; +namespace MiraAPI.Patches.Freeplay; [HarmonyPatch(typeof(TaskAdderGame))] internal static class TaskAdderPatches From 8e0a8f0585a0f3fc29f26c853341cc742c97ba0c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 19:02:22 -0500 Subject: [PATCH 19/71] fix something i almost broke --- MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 07b7f75..fb57d70 100644 --- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -31,5 +31,6 @@ internal static class TaskAddButtonPatches { __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray; } + __instance.RolloverHandler.OutColor = __instance.FileImage.color; } } From fb5ff4b53e997fd1f89bcdb10b96f0c48fd00ee4 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:54:20 -0500 Subject: [PATCH 20/71] freeplay modifiers --- MiraAPI.Example/Modifiers/CaptainModifier.cs | 3 +- .../Modifiers/Freezer/FreezeModifier.cs | 1 - .../Modifiers/HighPriorityModifier.cs | 3 +- .../OptionTypes/ModdedEnumOption.cs | 1 - MiraAPI/Modifiers/BaseModifier.cs | 5 + MiraAPI/Modifiers/ModifierManager.cs | 7 +- MiraAPI/Modifiers/Types/GameModifier.cs | 3 + MiraAPI/Patches/Roles/TaskAddButtonPatches.cs | 43 ++++++ MiraAPI/Patches/Roles/TaskAdderPatch.cs | 141 +++++++++++++----- MiraAPI/PluginLoading/MiraPluginInfo.cs | 3 + MiraAPI/PluginLoading/MiraPluginManager.cs | 10 +- MiraAPI/Roles/ICustomRole.cs | 3 +- 12 files changed, 170 insertions(+), 53 deletions(-) create mode 100644 MiraAPI/Patches/Roles/TaskAddButtonPatches.cs diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 60691a8..63b7c41 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -1,5 +1,4 @@ -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index 642e941..12a9587 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -1,6 +1,5 @@ using Il2CppSystem; using MiraAPI.Example.Roles; -using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using UnityEngine; diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index b94c4d0..c640ed8 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -1,5 +1,4 @@ -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; 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/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index dcea493..67f7410 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -30,6 +30,11 @@ public abstract class BaseModifier /// public virtual bool HideOnUi => false; + /// + /// Gets a value indicating whether the modifier is shown in the freeplay menu. + /// + public virtual bool ShowInFreeplay => false; + /// /// Gets a value indicating whether the modifier can be added multiple times. /// diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index ddb3c87..d4d4004 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using MiraAPI.Modifiers.Types; using MiraAPI.Networking; +using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking.Rpc; @@ -49,7 +51,7 @@ public static class ModifierManager return TypeToIdModifierMap.GetValueOrDefault(type); } - internal static bool RegisterModifier(Type modifierType) + internal static bool RegisterModifier(Type modifierType, MiraPluginInfo info) { if (!typeof(BaseModifier).IsAssignableFrom(modifierType)) { @@ -58,6 +60,9 @@ public static class ModifierManager IdToTypeModifierMap.Add(GetNextId(), modifierType); TypeToIdModifierMap.Add(modifierType, _nextId); + var bm = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); // this probably isn't a great idea + bm.ModifierId = _nextId; + info.Modifiers.Add(bm); if (!typeof(GameModifier).IsAssignableFrom(modifierType)) { diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index a7b1fe1..abcd1f7 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -5,6 +5,9 @@ /// public abstract class GameModifier : BaseModifier { + /// + public override bool ShowInFreeplay => true; + /// /// Gets the chance of the modifier being assigned to a player. /// diff --git a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs b/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs new file mode 100644 index 0000000..c9e3e8b --- /dev/null +++ b/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs @@ -0,0 +1,43 @@ +using HarmonyLib; +using MiraAPI.Modifiers; +using MiraAPI.Utilities; +using UnityEngine.UI; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(TaskAddButton))] +public static class TaskAddButtonPatches +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(TaskAddButton.Start))] + public static bool StartPrefix(TaskAddButton __instance) + { + if (uint.TryParse(__instance.name, out var result)) + { + __instance.Overlay.enabled = false; + if (PlayerControl.LocalPlayer.HasModifier(result)) + { + __instance.Overlay.enabled = true; + __instance.Overlay.sprite = __instance.CheckImage; + } + + __instance.Button.OnClick = new Button.ButtonClickedEvent(); + __instance.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => + { + if (PlayerControl.LocalPlayer.HasModifier(result)) + { + PlayerControl.LocalPlayer.GetModifierComponent()!.RemoveModifier(result); + __instance.Overlay.enabled = false; + } + else + { + PlayerControl.LocalPlayer.GetModifierComponent()!.AddModifier(ModifierManager.GetModifierType(result)!); + __instance.Overlay.enabled = true; + } + })); + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index 98e93bc..b6516bd 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -4,7 +4,6 @@ using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using System; -using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; @@ -18,7 +17,6 @@ namespace MiraAPI.Patches.Roles; [HarmonyPatch] public static class TaskAdderPatch { - private static readonly Dictionary ModsFolders = []; private static Scroller? _scroller; /// @@ -67,25 +65,34 @@ public static class TaskAdderPatch rolesFolder.FolderName = "Roles"; rolesFolder.name = "RolesFolder"; + var modifiersFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + modifiersFolder.gameObject.SetActive(false); + modifiersFolder.FolderName = "Modifiers"; + modifiersFolder.name = "ModifiersFolder"; + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) { - if (plugin.CustomRoles.Count == 0) + if (plugin.CustomRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true })) { - continue; + var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + newFolder.name = plugin.PluginId+":Roles"; + newFolder.FolderName = plugin.PluginInfo.Metadata.Name; + newFolder.gameObject.SetActive(false); + rolesFolder.SubFolders.Add(newFolder); } - var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); - newFolder.FolderName = newFolder.name = plugin.PluginInfo.Metadata.Name; - newFolder.gameObject.SetActive(false); - rolesFolder.SubFolders.Add(newFolder); - - if (!ModsFolders.ContainsKey(plugin.PluginInfo.Metadata.Name)) + if (plugin.Modifiers.Exists(x => x.ShowInFreeplay)) { - ModsFolders.Add(plugin.PluginInfo.Metadata.Name, plugin.PluginId); + var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + newFolder.name = plugin.PluginId+":Modifiers"; + newFolder.FolderName = plugin.PluginInfo.Metadata.Name; + newFolder.gameObject.SetActive(false); + modifiersFolder.SubFolders.Add(newFolder); } } __instance.Root.SubFolders.Add(rolesFolder); + __instance.Root.SubFolders.Add(modifiersFolder); __instance.GoToRoot(); } @@ -277,39 +284,21 @@ public static class TaskAdderPatch } } - if (ModsFolders.TryGetValue(taskFolder.FolderName, out var guid)) + var split = taskFolder.name.Split(':'); + if (split.Length == 2) { - var plugin = MiraPluginManager.GetPluginByGuid(guid); - for (var m = 0; m < plugin.CustomRoles.Count; m++) + var pluginId = split[0]; + var folderName = split[1]; + + if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) { - var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value; - if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || - (roleBehaviour is ICustomRole customRole && !customRole.Configuration.ShowInFreeplay)) + if (folderName == "Roles(Clone)") { - continue; + PluginRoles(__instance, plugin, ref num, ref num2, ref num3); } - - var taskAddButton2 = Object.Instantiate(__instance.RoleButton); - taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; - taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); - taskAddButton2.Role = roleBehaviour; - if (taskAddButton2.Button == null) + else if (folderName == "Modifiers(Clone)") { - continue; - } - - ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); - switch (m) - { - case 0 when __instance.restorePreviousSelectionFound != null: - ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound); - __instance.restorePreviousSelectionByFolderName = string.Empty; - __instance.restorePreviousSelectionFound = null; - break; - case 0: - ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); - break; + PluginModifiers(__instance, plugin, ref num, ref num2, ref num3); } } } @@ -336,4 +325,78 @@ public static class TaskAdderPatch ControllerManager.Instance.SetBackButton(__instance.FolderBackButton); return false; } + + private static void PluginModifiers( + TaskAdderGame instance, + MiraPluginInfo plugin, + ref float num, + ref float num2, + ref float num3) + { + for (var m = 0; m < plugin.Modifiers.Count; m++) + { + var modifier = plugin.Modifiers[m]; + if (!modifier.ShowInFreeplay) + { + continue; + } + + var taskAddButton = Object.Instantiate(instance.RoleButton); + taskAddButton.name = modifier.ModifierId.ToString(); + taskAddButton.role = null; + taskAddButton.MyTask = null; + taskAddButton.SafePositionWorld = instance.SafePositionWorld; + taskAddButton.Text.text = modifier.ModifierName; + instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); + + ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); + switch (m) + { + case 0 when instance.restorePreviousSelectionFound != null: + ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound); + instance.restorePreviousSelectionByFolderName = string.Empty; + instance.restorePreviousSelectionFound = null; + break; + case 0: + ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button); + break; + } + } + } + + private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3) + { + for (var m = 0; m < plugin.CustomRoles.Count; m++) + { + var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value; + if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || + roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false }) + { + continue; + } + + var taskAddButton2 = Object.Instantiate(instance.RoleButton); + taskAddButton2.SafePositionWorld = instance.SafePositionWorld; + taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; + instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); + taskAddButton2.Role = roleBehaviour; + if (taskAddButton2.Button == null) + { + continue; + } + + ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); + switch (m) + { + case 0 when instance.restorePreviousSelectionFound != null: + ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound); + instance.restorePreviousSelectionByFolderName = string.Empty; + instance.restorePreviousSelectionFound = null; + break; + case 0: + ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); + break; + } + } + } } diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 25a4dca..160980e 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -5,6 +5,7 @@ using MiraAPI.GameOptions; using System.Collections.Generic; using System.Collections.ObjectModel; using MiraAPI.Hud; +using MiraAPI.Modifiers; namespace MiraAPI.PluginLoading; @@ -61,6 +62,8 @@ public class MiraPluginInfo internal List Options { get; } = []; + internal List Modifiers { get; } = []; + internal Dictionary CustomRoles { get; } = []; internal Dictionary GameModes { get; } = []; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 678dee4..a7ed480 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -48,7 +48,7 @@ public sealed class MiraPluginManager continue; } - if (RegisterModifier(type)) + if (RegisterModifier(type, info)) { continue; } @@ -91,9 +91,9 @@ public sealed class MiraPluginManager /// /// The plugin GUID. /// A MiraPluginInfo. - public static MiraPluginInfo GetPluginByGuid(string pluginId) + public static MiraPluginInfo? GetPluginByGuid(string pluginId) { - return Instance._registeredPlugins.Values.First(plugin => plugin.PluginId == pluginId); + return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId); } private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo) @@ -199,11 +199,11 @@ public sealed class MiraPluginManager } } - private static bool RegisterModifier(Type type) + private static bool RegisterModifier(Type type, MiraPluginInfo info) { try { - return ModifierManager.RegisterModifier(type); + return ModifierManager.RegisterModifier(type, info); } catch (Exception e) { diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 9afc407..a452d1e 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,5 +1,4 @@ -using System; -using BepInEx.Configuration; +using BepInEx.Configuration; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; From 3905a3b39464c1561e8178f9f0ba33b07107a185 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:59:19 -0500 Subject: [PATCH 21/71] . --- MiraAPI/PluginLoading/MiraPluginInfo.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 160980e..81412e8 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -22,6 +22,15 @@ public class MiraPluginInfo PluginId = info.Metadata.GUID; } + /// + /// Get a read only collection of this plugin's modifiers. This is probably not safe because they are uninitialized. + /// + /// Readonly collection of modifiers. + public IReadOnlyCollection GetModifiers() + { + return Modifiers.AsReadOnly(); + } + /// /// Get a read only collection of this plugin's Option Groups. /// From 84c8f743f8066f6165bcb6de71cc07007708a527 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 15:45:31 -0500 Subject: [PATCH 22/71] i dont know why this is even a bug --- MiraAPI/Patches/Roles/TaskAdderPatch.cs | 29 ++++++++++++++----------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index b6516bd..6de6649 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -138,7 +138,7 @@ public static class TaskAdderPatch // yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry: [HarmonyPrefix] [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))] - public static bool ShowPatch(TaskAdderGame __instance, [HarmonyArgument(0)] TaskFolder taskFolder) + public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder) { var stringBuilder = new Il2CppSystem.Text.StringBuilder(64); __instance.Hierarchy.Add(taskFolder); @@ -284,21 +284,24 @@ public static class TaskAdderPatch } } - var split = taskFolder.name.Split(':'); - if (split.Length == 2) + if (taskFolder) { - var pluginId = split[0]; - var folderName = split[1]; - - if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) + var split = taskFolder.name.Split(':'); + if (split.Length == 2) { - if (folderName == "Roles(Clone)") + var pluginId = split[0]; + var folderName = split[1]; + + if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) { - PluginRoles(__instance, plugin, ref num, ref num2, ref num3); - } - else if (folderName == "Modifiers(Clone)") - { - PluginModifiers(__instance, plugin, ref num, ref num2, ref num3); + if (folderName == "Roles(Clone)") + { + PluginRoles(__instance, plugin, ref num, ref num2, ref num3); + } + else if (folderName == "Modifiers(Clone)") + { + PluginModifiers(__instance, plugin, ref num, ref num2, ref num3); + } } } } From 5dfbd5dc8f64a17d327e8a103ff8cee1796ff668 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:34:59 -0500 Subject: [PATCH 23/71] add masking to task adder roles --- MiraAPI/Patches/Roles/TaskAdderPatch.cs | 5 +++++ MiraAPI/Utilities/Extensions.cs | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index 6de6649..b15e9cf 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -5,6 +5,7 @@ using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using System; using System.Linq; +using MiraAPI.Utilities; using TMPro; using UnityEngine; using Object = UnityEngine.Object; @@ -261,6 +262,7 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(__instance.RoleButton); taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; + taskAddButton2.Text.fontMaterial.EnableMasking(); __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; @@ -350,6 +352,7 @@ public static class TaskAdderPatch taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; taskAddButton.Text.text = modifier.ModifierName; + taskAddButton.Text.fontMaterial.EnableMasking(); instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); @@ -381,6 +384,8 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(instance.RoleButton); taskAddButton2.SafePositionWorld = instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; + taskAddButton2.Text.fontMaterial.EnableMasking(); + instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; if (taskAddButton2.Button == null) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index f738558..4461ba6 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -39,6 +39,16 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Sets masking on a TMP MATERIAL! Do not use this on other materials. + /// + /// The TMP material. + public static void EnableMasking(this Material material) + { + material.SetFloat(ShaderID.Get("_Stencil"), 1); + material.SetFloat(ShaderID.Get("_StencilComp"), 4); + } + /// /// Checks if a type is static. /// From bc12c5a5f78aee91f836f2671b019b22a6655b78 Mon Sep 17 00:00:00 2001 From: Pietrodjaowjao Date: Sun, 23 Feb 2025 20:22:11 -0300 Subject: [PATCH 24/71] Change util to TMP_Text (all textmeshpro render components) --- MiraAPI/Patches/Roles/TaskAdderPatch.cs | 6 +++--- MiraAPI/Utilities/Extensions.cs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index b15e9cf..2135dc5 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -262,7 +262,7 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(__instance.RoleButton); taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - taskAddButton2.Text.fontMaterial.EnableMasking(); + taskAddButton2.Text.EnableMasking(); __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; @@ -352,7 +352,7 @@ public static class TaskAdderPatch taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; taskAddButton.Text.text = modifier.ModifierName; - taskAddButton.Text.fontMaterial.EnableMasking(); + taskAddButton.Text.EnableMasking(); instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); @@ -384,7 +384,7 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(instance.RoleButton); taskAddButton2.SafePositionWorld = instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - taskAddButton2.Text.fontMaterial.EnableMasking(); + taskAddButton2.Text.EnableMasking(); instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 4461ba6..f9a7974 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -8,6 +8,7 @@ using Reactor.Utilities; using System; using System.Collections.Generic; using System.Linq; +using TMPro; using UnityEngine; namespace MiraAPI.Utilities; @@ -42,11 +43,11 @@ public static class Extensions /// /// Sets masking on a TMP MATERIAL! Do not use this on other materials. /// - /// The TMP material. - public static void EnableMasking(this Material material) + /// The TMP text. + public static void EnableMasking(this TMP_Text text) { - material.SetFloat(ShaderID.Get("_Stencil"), 1); - material.SetFloat(ShaderID.Get("_StencilComp"), 4); + text.fontMaterial.SetFloat(ShaderID.Get("_Stencil"), 1); + text.fontMaterial.SetFloat(ShaderID.Get("_StencilComp"), 4); } /// From 9ccf7104cbbde1763fa0ebdacc3289b6afe547c7 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:32:23 -0500 Subject: [PATCH 25/71] better summary comment --- MiraAPI/Utilities/Extensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index f9a7974..3879029 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -41,7 +41,7 @@ public static class Extensions } /// - /// Sets masking on a TMP MATERIAL! Do not use this on other materials. + /// Enables Among Us style masking on a TMP text object. /// /// The TMP text. public static void EnableMasking(this TMP_Text text) From d8131d1f5d49b6f998d4472869f0db1228ea4ecd Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:54:51 -0500 Subject: [PATCH 26/71] fix pietros mistake and cleanup things --- MiraAPI/Patches/Roles/TaskAddButtonPatches.cs | 40 +++++------- ...{TaskAdderPatch.cs => TaskAdderPatches.cs} | 63 +++++++++---------- MiraAPI/Utilities/Extensions.cs | 2 +- 3 files changed, 46 insertions(+), 59 deletions(-) rename MiraAPI/Patches/Roles/{TaskAdderPatch.cs => TaskAdderPatches.cs} (91%) diff --git a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs b/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs index c9e3e8b..510d5e9 100644 --- a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs @@ -1,43 +1,35 @@ using HarmonyLib; -using MiraAPI.Modifiers; +using MiraAPI.Roles; using MiraAPI.Utilities; -using UnityEngine.UI; +using UnityEngine; namespace MiraAPI.Patches.Roles; [HarmonyPatch(typeof(TaskAddButton))] -public static class TaskAddButtonPatches +internal static class TaskAddButtonPatches { [HarmonyPrefix] [HarmonyPatch(nameof(TaskAddButton.Start))] public static bool StartPrefix(TaskAddButton __instance) { + // if this becomes problematic in the future, find a new method. if (uint.TryParse(__instance.name, out var result)) { - __instance.Overlay.enabled = false; - if (PlayerControl.LocalPlayer.HasModifier(result)) - { - __instance.Overlay.enabled = true; - __instance.Overlay.sprite = __instance.CheckImage; - } - - __instance.Button.OnClick = new Button.ButtonClickedEvent(); - __instance.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => - { - if (PlayerControl.LocalPlayer.HasModifier(result)) - { - PlayerControl.LocalPlayer.GetModifierComponent()!.RemoveModifier(result); - __instance.Overlay.enabled = false; - } - else - { - PlayerControl.LocalPlayer.GetModifierComponent()!.AddModifier(ModifierManager.GetModifierType(result)!); - __instance.Overlay.enabled = true; - } - })); + __instance.Overlay.sprite = __instance.CheckImage; + __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifier(result); return false; } return true; } + + [HarmonyPostfix] + [HarmonyPatch(nameof(TaskAddButton.Role), MethodType.Setter)] + public static void RoleGetterPatch(TaskAddButton __instance) + { + if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole) + { + __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray; + } + } } diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatches.cs similarity index 91% rename from MiraAPI/Patches/Roles/TaskAdderPatch.cs rename to MiraAPI/Patches/Roles/TaskAdderPatches.cs index 2135dc5..37f9744 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatches.cs @@ -1,31 +1,27 @@ -using AmongUs.GameOptions; +using System; +using System.Globalization; +using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; +using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Roles; -using MiraAPI.Utilities.Assets; -using System; -using System.Linq; using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; using TMPro; using UnityEngine; +using UnityEngine.UI; using Object = UnityEngine.Object; namespace MiraAPI.Patches.Roles; -/// -/// Patch TaskAdder for organizing Roles and adding scroller. -/// -[HarmonyPatch] -public static class TaskAdderPatch +[HarmonyPatch(typeof(TaskAdderGame))] +internal static class TaskAdderPatches { private static Scroller? _scroller; - /// - /// Adds custom roles to the TaskAdder. - /// - /// TaskAdder instance. [HarmonyPostfix] - [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))] + [HarmonyPatch(nameof(TaskAdderGame.Begin))] public static void AddRolesFolder(TaskAdderGame __instance) { GameObject inner = new("Inner"); @@ -98,22 +94,6 @@ public static class TaskAdderPatch __instance.GoToRoot(); } - /// - /// Sets the color of the role button based on the role. - /// - /// TaskAddButton instance. - [HarmonyPostfix] - [HarmonyPatch(typeof(TaskAddButton), nameof(TaskAddButton.Role), MethodType.Setter)] - public static void RoleGetterPatch(TaskAddButton __instance) - { - if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole) - { - __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray; - } - - __instance.RolloverHandler.OutColor = __instance.FileImage.color; - } - private static void AddFileAsChildCustom( this TaskAdderGame instance, TaskAddButton item, @@ -138,7 +118,7 @@ public static class TaskAdderPatch // yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry: [HarmonyPrefix] - [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))] + [HarmonyPatch(nameof(TaskAdderGame.ShowFolder))] public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder) { var stringBuilder = new Il2CppSystem.Text.StringBuilder(64); @@ -262,7 +242,6 @@ public static class TaskAdderPatch var taskAddButton2 = Object.Instantiate(__instance.RoleButton); taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - taskAddButton2.Text.EnableMasking(); __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); taskAddButton2.Role = roleBehaviour; @@ -310,7 +289,7 @@ public static class TaskAdderPatch foreach (var chip in __instance.ActiveItems) { - chip.GetComponentInChildren().EnableMasking(); + chip.GetComponentInChildren().EnableStencilMasking(); chip.GetComponent().maskInteraction = SpriteMaskInteraction.VisibleInsideMask; } @@ -347,12 +326,28 @@ public static class TaskAdderPatch } var taskAddButton = Object.Instantiate(instance.RoleButton); - taskAddButton.name = modifier.ModifierId.ToString(); + taskAddButton.name = modifier.ModifierId.ToString(CultureInfo.InvariantCulture); taskAddButton.role = null; taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; taskAddButton.Text.text = modifier.ModifierName; taskAddButton.Text.EnableMasking(); + + taskAddButton.Button.OnClick = new Button.ButtonClickedEvent(); + taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => + { + if (PlayerControl.LocalPlayer.HasModifier(modifier.ModifierId)) + { + PlayerControl.LocalPlayer.GetModifierComponent()!.RemoveModifier(modifier.ModifierId); + taskAddButton.Overlay.enabled = false; + } + else + { + PlayerControl.LocalPlayer.GetModifierComponent()!.AddModifier(ModifierManager.GetModifierType(modifier.ModifierId)!); + taskAddButton.Overlay.enabled = true; + } + })); + instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 3879029..6d8cbf6 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -44,7 +44,7 @@ public static class Extensions /// Enables Among Us style masking on a TMP text object. /// /// The TMP text. - public static void EnableMasking(this TMP_Text text) + public static void EnableStencilMasking(this TMP_Text text) { text.fontMaterial.SetFloat(ShaderID.Get("_Stencil"), 1); text.fontMaterial.SetFloat(ShaderID.Get("_StencilComp"), 4); From 758b1d3183f067abaacc7da5752b6c23d652e617 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:59:14 -0500 Subject: [PATCH 27/71] organize task add patches --- MiraAPI/Patches/{Roles => Freeplay}/TaskAddButtonPatches.cs | 2 +- MiraAPI/Patches/{Roles => Freeplay}/TaskAdderPatches.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename MiraAPI/Patches/{Roles => Freeplay}/TaskAddButtonPatches.cs (96%) rename MiraAPI/Patches/{Roles => Freeplay}/TaskAdderPatches.cs (99%) diff --git a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs similarity index 96% rename from MiraAPI/Patches/Roles/TaskAddButtonPatches.cs rename to MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 510d5e9..07b7f75 100644 --- a/MiraAPI/Patches/Roles/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -3,7 +3,7 @@ using MiraAPI.Roles; using MiraAPI.Utilities; using UnityEngine; -namespace MiraAPI.Patches.Roles; +namespace MiraAPI.Patches.Freeplay; [HarmonyPatch(typeof(TaskAddButton))] internal static class TaskAddButtonPatches diff --git a/MiraAPI/Patches/Roles/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs similarity index 99% rename from MiraAPI/Patches/Roles/TaskAdderPatches.cs rename to MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 37f9744..e36ac61 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -13,7 +13,7 @@ using UnityEngine; using UnityEngine.UI; using Object = UnityEngine.Object; -namespace MiraAPI.Patches.Roles; +namespace MiraAPI.Patches.Freeplay; [HarmonyPatch(typeof(TaskAdderGame))] internal static class TaskAdderPatches From bbec3c1958643f5dbf4534d55bf0cc3515ccbfaa Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 19:02:22 -0500 Subject: [PATCH 28/71] fix something i almost broke --- MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 07b7f75..fb57d70 100644 --- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -31,5 +31,6 @@ internal static class TaskAddButtonPatches { __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray; } + __instance.RolloverHandler.OutColor = __instance.FileImage.color; } } From 8b8f9ea5db8d433bf31b6ab1e4512d28fdd5f453 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 19:06:59 -0500 Subject: [PATCH 29/71] utilize new modifier extension stuff --- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index e36ac61..d1c6cb9 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -338,12 +338,12 @@ internal static class TaskAdderPatches { if (PlayerControl.LocalPlayer.HasModifier(modifier.ModifierId)) { - PlayerControl.LocalPlayer.GetModifierComponent()!.RemoveModifier(modifier.ModifierId); + PlayerControl.LocalPlayer.RemoveModifier(modifier.ModifierId); taskAddButton.Overlay.enabled = false; } else { - PlayerControl.LocalPlayer.GetModifierComponent()!.AddModifier(ModifierManager.GetModifierType(modifier.ModifierId)!); + PlayerControl.LocalPlayer.AddModifier(ModifierManager.GetModifierType(modifier.ModifierId)!); taskAddButton.Overlay.enabled = true; } })); From 8fb8d18be9a20dbd7fc1cea3e65d9c896f1b90ca Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 19:10:57 -0500 Subject: [PATCH 30/71] fix name --- MiraAPI/Utilities/Extensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 6d8cbf6..6912ac8 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -41,7 +41,7 @@ public static class Extensions } /// - /// Enables Among Us style masking on a TMP text object. + /// Enables stencil masking on a TMP text object. /// /// The TMP text. public static void EnableStencilMasking(this TMP_Text text) From fb19b4bc041fdd481aece090f7d6b30c11da4ee2 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:13:12 -0500 Subject: [PATCH 31/71] better naming for "stackable" modifier --- MiraAPI/Modifiers/BaseModifier.cs | 4 ++-- MiraAPI/Modifiers/ModifierComponent.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 67f7410..9f6aa93 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -36,9 +36,9 @@ public abstract class BaseModifier public virtual bool ShowInFreeplay => false; /// - /// Gets a value indicating whether the modifier can be added multiple times. + /// Gets a value indicating whether the modifier is unique. If true, the player can only have one instance of this modifier. /// - public virtual bool Stackable => false; + public virtual bool Unique => true; /// /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true. diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 9918eab..2b00652 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -260,7 +260,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return null; } - if (Modifiers.Find(x => x.ModifierId == modifierId && !x.Stackable) != null) + if (Modifiers.Find(x => x.ModifierId == modifierId && x.Unique) != null) { Logger.Error($"Player already has modifier with id {modifierId}!"); return null; From 0ebec6d0f1ba220950db3932ee93b3d7a1df42d0 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:28:24 -0500 Subject: [PATCH 32/71] fix testing thing --- MiraAPI.Example/Buttons/TestButton.cs | 1 - MiraAPI.Example/Modifiers/ModifierParams.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/MiraAPI.Example/Buttons/TestButton.cs b/MiraAPI.Example/Buttons/TestButton.cs index 8062c81..feb7992 100644 --- a/MiraAPI.Example/Buttons/TestButton.cs +++ b/MiraAPI.Example/Buttons/TestButton.cs @@ -7,7 +7,6 @@ using UnityEngine; namespace MiraAPI.Example.Buttons; -[RegisterButton] public class TestButton : CustomActionButton { public override string Name => "Test Button"; diff --git a/MiraAPI.Example/Modifiers/ModifierParams.cs b/MiraAPI.Example/Modifiers/ModifierParams.cs index cde7d3b..e92bbda 100644 --- a/MiraAPI.Example/Modifiers/ModifierParams.cs +++ b/MiraAPI.Example/Modifiers/ModifierParams.cs @@ -3,7 +3,6 @@ using Reactor.Utilities; namespace MiraAPI.Example.Modifiers; -[RegisterModifier] public class ModifierParams : BaseModifier { public override string ModifierName => "ModifierParams"; From 1c6fa9a84dae0b057b32fcbeca83d7b1bdd9aff8 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:51:02 -0500 Subject: [PATCH 33/71] major modifier overhaul --- MiraAPI/Modifiers/BaseModifier.cs | 23 +- MiraAPI/Modifiers/ModifierComponent.cs | 270 ++++++++++----- MiraAPI/Modifiers/ModifierData.cs | 10 +- MiraAPI/Modifiers/ModifierExtensions.cs | 313 ++++++++++++++---- MiraAPI/Modifiers/ModifierFactory.cs | 43 ++- MiraAPI/Modifiers/ModifierManager.cs | 8 +- MiraAPI/Modifiers/Types/TimedModifier.cs | 2 +- MiraAPI/Networking/AddModifierRpc.cs | 42 ++- .../Patches/Freeplay/TaskAddButtonPatches.cs | 2 +- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 9 +- MiraAPI/Patches/PlayerControlPatches.cs | 2 +- MiraAPI/Patches/Roles/TaskPanelPatch.cs | 2 +- MiraAPI/Utilities/ModifierExtensions.cs | 252 -------------- 13 files changed, 531 insertions(+), 447 deletions(-) delete mode 100644 MiraAPI/Utilities/ModifierExtensions.cs diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 9f6aa93..8834454 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,4 +1,6 @@ -namespace MiraAPI.Modifiers; +using System; + +namespace MiraAPI.Modifiers; /// /// Base class for all modifiers. @@ -15,11 +17,6 @@ public abstract class BaseModifier /// public ModifierComponent? ModifierComponent { get; internal set; } - /// - /// Gets the modifier id. - /// - public uint ModifierId { get; internal set; } - /// /// Gets the modifier name. /// @@ -40,6 +37,20 @@ public abstract class BaseModifier /// public virtual bool Unique => true; + /// + /// Gets a value indicating whether the modifier has been initialized. + /// + public bool Initialized { get; internal set; } + + /// + /// Get the ID of the modifier. + /// + /// The ID of the modifier. + public uint GetId() + { + return ModifierManager.GetModifierId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered."); + } + /// /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true. /// diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 2b00652..6cd3d5e 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -67,6 +67,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) foreach (var modifier in _toAdd) { Modifiers.Add(modifier); + modifier.Initialized = true; modifier.OnActivate(); if (modifier is TimedModifier { AutoStart: true } timer) @@ -123,46 +124,153 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } /// - /// Gets a collection of modifiers by their type, or null if the player doesn't have one. + /// Gets a collection of modifiers by type. /// + /// The predicate to check the modifier by. /// The Type of the Modifier. /// The Modifier if it is found, null otherwise. - public IEnumerable GetModifiersByType() where T : BaseModifier + [HideFromIl2Cpp] + public IEnumerable GetModifiersByType(Func? predicate=null) where T : BaseModifier { - return ActiveModifiers.OfType(); + return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x)); } /// - /// Gets a modifier by its type, or null if the player doesn't have it. + /// Gets a collection of modifiers by type. /// - /// The Type of the Modifier. + /// The modifier type. + /// The predicate to check the modifier by. /// The Modifier if it is found, null otherwise. - public T? GetModifier() where T : BaseModifier + [HideFromIl2Cpp] + public IEnumerable GetModifiersByType(Type type, Func? predicate=null) { - return GetModifiersByType().FirstOrDefault(); + return ActiveModifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))); + } + + /// + /// Gets a collection of modifiers by ID. + /// + /// The Modifier ID. + /// The predicate to check the modifier by. + /// The Modifier if it is found, null otherwise. + [HideFromIl2Cpp] + public IEnumerable GetModifiersByType(uint id, Func? predicate=null) + { + var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( + $"Cannot get modifier with id {id} because it is not registered."); + + return GetModifiersByType(type, predicate); } /// /// Tries to get a modifier by its type. /// /// The modifier or null. + /// The predicate to check the modifier by. /// The Type of the Modifier. /// True if the modifier was found, false otherwise. - public bool TryGetModifier(out T? modifier) where T : BaseModifier + [HideFromIl2Cpp] + public bool TryGetModifier(out T? modifier, Func? predicate = null) where T : BaseModifier { - modifier = GetModifier(); + modifier = GetModifier(predicate); return modifier != null; } + /// + /// Tries to get a modifier by its type. + /// + /// The modifier type. + /// The modifier or null. + /// The predicate to check the modifier by. + /// True if the modifier was found, false otherwise. + [HideFromIl2Cpp] + public bool TryGetModifier(Type type, out BaseModifier? modifier, Func? predicate = null) + { + modifier = GetModifier(type, predicate); + return modifier != null; + } + + /// + /// Tries to get a modifier by its ID. + /// + /// The modifier ID. + /// The modifier or null. + /// The predicate to check the modifier by. + /// True if the modifier was found, false otherwise. + [HideFromIl2Cpp] + public bool TryGetModifier(uint id, out BaseModifier? modifier, Func? predicate = null) + { + var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( + $"Cannot get modifier with id {id} because it is not registered."); + + return TryGetModifier(type, out modifier, predicate); + } + + /// + /// Gets a modifier by its type, or null if the player doesn't have it. + /// + /// The predicate to check the modifier by. + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + [HideFromIl2Cpp] + public T? GetModifier(Func? predicate = null) where T : BaseModifier + { + return GetModifiersByType(predicate).FirstOrDefault(); + } + + /// + /// Gets a modifier by its type, or null if the player doesn't have it. + /// + /// The modifier type. + /// The predicate to check the modifier by. + /// The Modifier if it is found, null otherwise. + [HideFromIl2Cpp] + public BaseModifier? GetModifier(Type type, Func? predicate = null) + { + return GetModifiersByType(type).FirstOrDefault(predicate ?? (_ => true)); + } + + /// + /// Gets a modifier by its type, or null if the player doesn't have it. + /// + /// The modifier ID. + /// The predicate to check the modifier by. + /// The Modifier if it is found, null otherwise. + [HideFromIl2Cpp] + public BaseModifier? GetModifier(uint id, Func? predicate = null) + { + var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( + $"Cannot get modifier with id {id} because it is not registered."); + + return GetModifier(type, predicate); + } + + /// + /// Removes a modifier from the player. + /// + /// The modifier type. + /// The predicate to check the modifier by. + [HideFromIl2Cpp] + public void RemoveModifier(Func? predicate = null) where T : BaseModifier + { + RemoveModifier(typeof(T), x => predicate == null || predicate((T)x)); + } + /// /// Removes a modifier from the player. /// /// The modifier type. + /// The predicate to check the modifier by. [HideFromIl2Cpp] - public void RemoveModifier(Type type) + public void RemoveModifier(Type type, Func? predicate = null) { - var modifier = Modifiers.Find(x => x.GetType() == type); + var modifiers = Modifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))).ToList(); + if (modifiers.Count > 1) + { + throw new InvalidOperationException($"Cannot remove modifier {type.Name} because there are multiple instances of that modifier."); + } + var modifier = modifiers.FirstOrDefault(); if (modifier is null) { Logger.Error($"Cannot remove modifier {type.Name} because it is not active."); @@ -172,32 +280,17 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) RemoveModifier(modifier); } - /// - /// Removes a modifier from the player. - /// - /// The modifier type. - [HideFromIl2Cpp] - public void RemoveModifier() where T : BaseModifier - { - RemoveModifier(typeof(T)); - } - /// /// Removes a modifier from the player. /// /// The modifier ID. + /// The predicate to check the modifier by. [HideFromIl2Cpp] - public void RemoveModifier(uint modifierId) + public void RemoveModifier(uint modifierId, Func? predicate = null) { - var modifier = Modifiers.Find(x => x.ModifierId == modifierId); - - if (modifier is null) - { - Logger.Error($"Cannot remove modifier with id {modifierId} because it is not active."); - return; - } - - RemoveModifier(modifier); + var type = ModifierManager.GetModifierType(modifierId) ?? throw new InvalidOperationException( + $"Cannot remove modifier with id {modifierId} because it is not registered."); + RemoveModifier(type, predicate); } /// @@ -216,6 +309,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) _toRemove.Add(modifier); } + /// + /// Adds a modifier to the player. + /// + /// The arguments to initialize the modifier constructor with. + /// The Type of the modifier. + /// The new modifier. + [HideFromIl2Cpp] + public T? AddModifier(params object[] args) where T : BaseModifier + { + return AddModifier(typeof(T), args) as T; + } + /// /// Adds a modifier to the player. /// @@ -224,24 +329,22 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? AddModifier(BaseModifier modifier) { - if (Modifiers.Contains(modifier)) + var id = modifier.GetId(); + if (modifier.Unique && Modifiers.Find(x => x.GetId() == id) != null) { - Logger.Error($"Player already has modifier with id {modifier.ModifierId}!"); + Logger.Error($"Player already has modifier with id {id}!"); return null; } - var modifierId = ModifierManager.GetModifierId(modifier.GetType()); - - if (modifierId == null) + if (Modifiers.Contains(modifier)) { - Logger.Error($"Cannot add modifier {modifier.GetType().Name} because it has no ID!"); + Logger.Error($"Player already has this modifier!"); return null; } _toAdd.Add(modifier); modifier.Player = _player; modifier.ModifierComponent = this; - modifier.ModifierId = modifierId.Value; return modifier; } @@ -249,66 +352,79 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// Adds a modifier to the player. /// /// The modifier type. + /// The arguments to initialize the modifier constructor with. /// The modifier that was added. [HideFromIl2Cpp] - public BaseModifier? AddModifier(Type type) + public BaseModifier? AddModifier(Type type, params object[] args) { - var modifierId = ModifierManager.GetModifierId(type); - if (modifierId == null) + BaseModifier? modifier; + if (args.Length > 0) { - Logger.Error($"Cannot add modifier {type.Name} because it is not registered."); - return null; + modifier = ModifierFactory.CreateInstance(type, args); + } + else + { + modifier = Activator.CreateInstance(type) as BaseModifier; + if (modifier == null) + { + throw new InvalidOperationException($"Cannot add modifier {type.Name} because it is not a valid modifier."); + } } - if (Modifiers.Find(x => x.ModifierId == modifierId && x.Unique) != null) - { - Logger.Error($"Player already has modifier with id {modifierId}!"); - return null; - } - - if (Activator.CreateInstance(type) is not BaseModifier modifier) - { - Logger.Error($"Cannot add modifier {type.Name} because it is null."); - return null; - } - - AddModifier(modifier); - - return modifier; + return AddModifier(modifier); } /// /// Adds a modifier to the player. /// - /// The Type of the modifier. - /// The new modifier. + /// The ID of the modifier. + /// The arguments to initialize the modifier constructor with. + /// The modifier if it was created, or null if it failed. [HideFromIl2Cpp] - public T? AddModifier() where T : BaseModifier + public BaseModifier? AddModifier(uint id, params object[] args) { - return AddModifier(typeof(T)) as T; + var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( + $"Cannot add modifier with id {id} because it is not registered."); + + return AddModifier(type, args); } /// - /// Checks if a player has an active or queued modifier by its ID. - /// - /// The Modifier ID. - /// The predicate to check the modifier. - /// True if the Modifier is present, false otherwise. - public bool HasModifier(uint id, Func? predicate=null) - { - return ActiveModifiers.Exists(x => x.ModifierId == id && (predicate == null || predicate(x))) || - _toAdd.Exists(x => x.ModifierId == id && (predicate == null || predicate(x))); - } - - /// - /// Checks if a player has an active or queued modifier by its type. + /// Checks if a player has an active modifier by its type. /// /// The predicate to check the modifier. /// The Type of the Modifier. /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] public bool HasModifier(Func? predicate=null) where T : BaseModifier { - return ModifierManager.GetModifierId(typeof(T)) is { } id && - HasModifier(id, (Func?)predicate); + return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); + } + + /// + /// Checks if a player has an active modifier by its type. + /// + /// The modifier type. + /// The predicate to check the modifier. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifier(Type type, Func? predicate=null) + { + return ActiveModifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x))); + } + + /// + /// Checks if a player has an active modifier by its ID. + /// + /// The Modifier ID. + /// The predicate to check the modifier. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifier(uint id, Func? predicate=null) + { + var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( + $"Cannot get modifier with id {id} because it is not registered."); + + return HasModifier(type, predicate); } } diff --git a/MiraAPI/Modifiers/ModifierData.cs b/MiraAPI/Modifiers/ModifierData.cs index 425e063..7260834 100644 --- a/MiraAPI/Modifiers/ModifierData.cs +++ b/MiraAPI/Modifiers/ModifierData.cs @@ -1,18 +1,16 @@ -using System; - -namespace MiraAPI.Modifiers; +namespace MiraAPI.Modifiers; /// /// Modifier data for networking. /// -/// The Type of the modifier. +/// The ID of the modifier. /// Parameters for constructor. -public readonly struct ModifierData(Type type, object[] args) +public readonly struct ModifierData(uint id, object[] args) { /// /// Gets the type of the modifier. /// - public Type Type { get; } = type; + public uint Id { get; } = id; /// /// Gets the parameters for the constructor. diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index d6ce5ff..226b16b 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -8,11 +8,81 @@ using Reactor.Utilities; namespace MiraAPI.Modifiers; /// -/// Extension methods for modifiers. +/// Extensions for modifiers. /// public static class ModifierExtensions { - internal static Dictionary ModifierComponents { get; } = []; + /// + /// Gets the dictionary to cache ModifierComponents. + /// + public static Dictionary ModifierComponents { get; } = []; + + /// + /// Remote Procedure Call to add a modifier to a player. + /// + /// The player to add the modifier to. + /// The modifier ID. + /// The arguments to initialize the modifier constructor with. + public static void RpcAddModifier(this PlayerControl target, uint modifierId, params object[] args) + { + _ = ModifierManager.GetModifierType(modifierId) ?? throw new InvalidOperationException( + $"Modifier with ID {modifierId} is not registered."); + + Rpc.Instance.Send(target, new ModifierData(modifierId, args)); + } + + /// + /// Remote Procedure Call to add a modifier to a player. + /// + /// The player to add the modifier to. + /// The type of the modifier. + /// The arguments to initialize the modifier constructor with. + public static void RpcAddModifier(this PlayerControl player, Type type, params object[] args) + { + var id = ModifierManager.GetModifierId(type) ?? throw new InvalidOperationException( + $"Modifier {type.Name} is not registered."); + + player.RpcAddModifier(id, args); + } + + /// + /// 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 + { + player.RpcAddModifier(typeof(T), args); + } + + /// + /// 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.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); + } /// /// Gets the ModifierComponent for a player. @@ -37,102 +107,217 @@ public static class ModifierExtensions } /// - /// Gets a modifier by its type, or null if the player doesn't have it. + /// Checks if the player has a specific modifier. /// - /// 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 + /// The type of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// True if the player has the modifier, false otherwise. + public static bool HasModifier(this PlayerControl player, Func? predicate = null) where T : BaseModifier { - return player?.GetModifierComponent()?.ActiveModifiers.Find(x => x is T) as T; + return player.GetModifierComponent()!.HasModifier(predicate); } /// - /// Checks if a player has a modifier. + /// Checks if the player has a specific modifier by type. /// - /// 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 + /// The PlayerControl instance. + /// The type of the modifier. + /// Optional predicate to filter the modifiers. + /// True if the player has the modifier, false otherwise. + public static bool HasModifier(this PlayerControl player, Type type, Func? predicate = null) { - return player?.GetModifierComponent() != null && - player.GetModifierComponent()!.HasModifier(); + return player.GetModifierComponent()!.HasModifier(type, predicate); } /// - /// Checks if a player has a modifier by its ID. + /// Checks if the player has a specific modifier by ID. /// - /// The PlayerControl object. - /// The Modifier ID. - /// True if the Modifier is present, false otherwise. - public static bool HasModifier(this PlayerControl? player, uint id) + /// The PlayerControl instance. + /// The ID of the modifier. + /// Optional predicate to filter the modifiers. + /// True if the player has the modifier, false otherwise. + public static bool HasModifier(this PlayerControl player, uint id, Func? predicate = null) { - return player?.GetModifierComponent() != null && - player.GetModifierComponent()!.HasModifier(id); + return player.GetModifierComponent()!.HasModifier(id, predicate); } /// - /// Remote Procedure Call to remove a modifier from a player. + /// Gets a specific modifier from the player. /// - /// The player to remove the modifier from. + /// The type of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// The modifier if found, null otherwise. + public static T? GetModifier(this PlayerControl player, Func? predicate = null) where T : BaseModifier + { + return player.GetModifierComponent()!.GetModifier(predicate); + } + + /// + /// Gets a specific modifier from the player by type. + /// + /// The PlayerControl instance. + /// The type of the modifier. + /// Optional predicate to filter the modifiers. + /// The modifier if found, null otherwise. + public static BaseModifier? GetModifier( + this PlayerControl player, + Type type, + Func? predicate = null) + { + return player.GetModifierComponent()!.GetModifier(type, predicate); + } + + /// + /// Gets a specific modifier from the player by ID. + /// + /// The PlayerControl instance. + /// The ID of the modifier. + /// Optional predicate to filter the modifiers. + /// The modifier if found, null otherwise. + public static BaseModifier? GetModifier( + this PlayerControl player, + uint id, + Func? predicate = null) + { + return player.GetModifierComponent()!.GetModifier(id, predicate); + } + + /// + /// Gets all modifiers of a specific type from the player. + /// + /// The type of the modifiers. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// An enumerable of modifiers. + public static IEnumerable GetModifiersByType(this PlayerControl player, Func? predicate = null) + where T : BaseModifier + { + return player.GetModifierComponent()!.GetModifiersByType(predicate); + } + + /// + /// Gets all modifiers of a specific type from the player. + /// + /// The PlayerControl instance. + /// The type of the modifiers. + /// Optional predicate to filter the modifiers. + /// An enumerable of modifiers. + public static IEnumerable GetModifiersByType( + this PlayerControl player, + Type type, + Func? predicate = null) + { + return player.GetModifierComponent()!.GetModifiersByType(type, predicate); + } + + /// + /// Gets all modifiers of a specific type from the player by ID. + /// + /// The PlayerControl instance. + /// The ID of the modifiers. + /// Optional predicate to filter the modifiers. + /// An enumerable of modifiers. + public static IEnumerable GetModifiersByType( + this PlayerControl player, + uint id, + Func? predicate = null) + { + return player.GetModifierComponent()!.GetModifiersByType(id, predicate); + } + + /// + /// Removes a specific modifier from the player. + /// + /// The type of the modifier. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + public static void RemoveModifier(this PlayerControl player, Func? predicate = null) + where T : BaseModifier + { + player.GetModifierComponent()!.RemoveModifier(predicate); + } + + /// + /// Removes a specific modifier from the player by type. + /// + /// The PlayerControl instance. + /// The type of the modifier. + /// Optional predicate to filter the modifiers. + public static void RemoveModifier(this PlayerControl player, Type type, Func? predicate = null) + { + player.GetModifierComponent()!.RemoveModifier(type, predicate); + } + + /// + /// Removes a specific modifier from the player by ID. + /// + /// The PlayerControl instance. /// The ID of the modifier. - [MethodRpc((uint)MiraRpc.RemoveModifier)] - public static void RpcRemoveModifier(this PlayerControl target, uint modifierId) + /// Optional predicate to filter the modifiers. + public static void RemoveModifier( + this PlayerControl player, + uint modifierId, + Func? predicate = null) { - target.GetModifierComponent()?.RemoveModifier(modifierId); + player.GetModifierComponent()!.RemoveModifier(modifierId, predicate); } /// - /// Remote Procedure Call to remove a modifier from a player. + /// Removes a specific modifier from the player. /// - /// The player to remove the modifier from. - /// The Type of the Modifier. - public static void RpcRemoveModifier(this PlayerControl player) where T : BaseModifier + /// The PlayerControl instance. + /// The modifier to remove. + public static void RemoveModifier(this PlayerControl player, BaseModifier modifier) { - 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); + player.GetModifierComponent()!.RemoveModifier(modifier); } /// - /// Remote Procedure Call to add a modifier to a player. + /// Adds a specific modifier to the player. /// - /// The player to add the modifier to. - /// The modifier ID. + /// The type of the modifier. + /// The PlayerControl instance. /// The arguments to initialize the modifier constructor with. - public static void RpcAddModifier(this PlayerControl target, uint modifierId, params object[] args) + /// The added modifier. + public static T? AddModifier(this PlayerControl player, params object[] args) where T : BaseModifier { - var type = ModifierManager.GetModifierType(modifierId); - if (type == null) - { - Logger.Error($"Cannot add modifier with id {modifierId} because it is not registered."); - return; - } - - Rpc.Instance.Send(target, new ModifierData(type, args)); + return player.GetModifierComponent()!.AddModifier(args); } /// - /// Remote Procedure Call to add a modifier to a player. + /// Adds a specific modifier to the 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 + /// The PlayerControl instance. + /// The modifier to add. + /// The added modifier. + public static BaseModifier? AddModifier(this PlayerControl player, BaseModifier modifier) { - var id = ModifierManager.GetModifierId(typeof(T)); - if (id == null) - { - Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); - return; - } + return player.GetModifierComponent()!.AddModifier(modifier); + } - player.RpcAddModifier(id.Value, args); + /// + /// Adds a specific modifier to the player by type. + /// + /// The PlayerControl instance. + /// The type of the modifier. + /// The arguments to initialize the modifier constructor with. + /// The added modifier. + public static BaseModifier? AddModifier(this PlayerControl player, Type type, params object[] args) + { + return player.GetModifierComponent()!.AddModifier(type, args); + } + + /// + /// Adds a specific modifier to the player by ID. + /// + /// The PlayerControl instance. + /// The ID of the modifier. + /// The arguments to initialize the modifier constructor with. + /// The added modifier. + public static BaseModifier? AddModifier(this PlayerControl player, uint id, params object[] args) + { + return player.GetModifierComponent()!.AddModifier(id, args); } } diff --git a/MiraAPI/Modifiers/ModifierFactory.cs b/MiraAPI/Modifiers/ModifierFactory.cs index e1b018c..d145b82 100644 --- a/MiraAPI/Modifiers/ModifierFactory.cs +++ b/MiraAPI/Modifiers/ModifierFactory.cs @@ -1,18 +1,28 @@ using System; -using System.Linq; using System.Linq.Expressions; namespace MiraAPI.Modifiers; +/// +/// Factory for creating instances of a modifier. More efficient than using reflection. +/// public static class ModifierFactory { private static Func? _constructor; - private static Func CreateConstructor(Type type) + private static Func CreateConstructor(Type type, params object[] args) { - var constructorInfo = type.GetConstructors() - .OrderByDescending(c => c.GetParameters().Length) - .FirstOrDefault() ?? throw new InvalidOperationException($"Type {type} does not have a public constructor."); + var constructorInfo = Array.Find( + type.GetConstructors(), + x => + { + var parameters = x.GetParameters(); + return parameters.Length == args.Length && Array.TrueForAll( + parameters, + t => t.ParameterType.IsInstanceOfType(args[t.Position])); + }) ?? throw new InvalidOperationException( + $"Could not find a constructor for type {type} with the specified arguments."); + var parameters = constructorInfo.GetParameters(); var argsParam = Expression.Parameter(typeof(object[]), "args"); @@ -32,9 +42,15 @@ public static class ModifierFactory return lambda.Compile(); } + /// + /// Creates a modifier with the specified type and arguments. + /// + /// Modifier type. + /// Arguments. + /// An instance of the modifier. public static BaseModifier CreateInstance(Type type, params object[] args) { - _constructor ??= CreateConstructor(type); + _constructor ??= CreateConstructor(type, args); return _constructor(args); } } @@ -47,11 +63,18 @@ public static class ModifierFactory where T : BaseModifier { private static readonly Func Constructor = CreateConstructor(); - private static Func CreateConstructor() + private static Func CreateConstructor(params object[] args) { - var constructorInfo = typeof(T).GetConstructors() - .OrderByDescending(c => c.GetParameters().Length) - .FirstOrDefault() ?? throw new InvalidOperationException($"Type {typeof(T)} does not have a public constructor."); + var constructorInfo = Array.Find( + typeof(T).GetConstructors(), + x => + { + var parameters = x.GetParameters(); + return parameters.Length == args.Length && Array.TrueForAll( + parameters, + t => t.ParameterType.IsInstanceOfType(args[t.Position])); + }) ?? throw new InvalidOperationException($"Could not find a constructor for type {typeof(T)} with the specified arguments."); + var parameters = constructorInfo.GetParameters(); var argsParam = Expression.Parameter(typeof(object[]), "args"); diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index d4d4004..65341a0 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -48,7 +48,12 @@ public static class ModifierManager /// The ID of the modifier. public static uint? GetModifierId(Type type) { - return TypeToIdModifierMap.GetValueOrDefault(type); + if (!TypeToIdModifierMap.TryGetValue(type, out var id)) + { + return null; + } + + return id; } internal static bool RegisterModifier(Type modifierType, MiraPluginInfo info) @@ -61,7 +66,6 @@ public static class ModifierManager IdToTypeModifierMap.Add(GetNextId(), modifierType); TypeToIdModifierMap.Add(modifierType, _nextId); var bm = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); // this probably isn't a great idea - bm.ModifierId = _nextId; info.Modifiers.Add(bm); if (!typeof(GameModifier).IsAssignableFrom(modifierType)) diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 286b80a..038fa6d 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -102,7 +102,7 @@ public abstract class TimedModifier : BaseModifier if (RemoveOnComplete) { - ModifierComponent?.RemoveModifier(ModifierId); + ModifierComponent?.RemoveModifier(this); } } diff --git a/MiraAPI/Networking/AddModifierRpc.cs b/MiraAPI/Networking/AddModifierRpc.cs index 3546d4f..fdc37a9 100644 --- a/MiraAPI/Networking/AddModifierRpc.cs +++ b/MiraAPI/Networking/AddModifierRpc.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using Hazel; using MiraAPI.Modifiers; using Reactor.Networking.Attributes; @@ -18,44 +15,45 @@ namespace MiraAPI.Networking; [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); + writer.WritePacked(data.Id); + writer.WritePacked(data.Args.Length); + foreach (var arg in data.Args) + { + writer.Write(arg.GetType().AssemblyQualifiedName); + writer.Serialize(arg); + } } /// public override ModifierData Read(MessageReader reader) { var modId = reader.ReadPackedUInt32(); - var modifier = ModifierManager.GetModifierType(modId)!; + var argCount = reader.ReadPackedUInt32(); + var objects = new object[argCount]; - if (!ParameterCache.TryGetValue(modifier, out var paramTypes)) + if (argCount > 0) { - paramTypes = modifier.GetConstructors().OrderBy(x => x.GetParameters().Length).First().GetParameters(); - ParameterCache[modifier] = paramTypes; + var types = new Type[argCount]; + for (var i = 0; i < argCount; i++) + { + var name = reader.ReadString(); + types[i] = Type.GetType(name) ?? throw new InvalidOperationException($"Type not found: {name}"); + objects[i] = reader.Deserialize(types[i]); + } } - var objects = new object[paramTypes.Length]; - foreach (var paramType in paramTypes) - { - objects[paramType.Position] = reader.Deserialize(paramType.ParameterType); - } - - return new ModifierData(modifier, objects); + return new ModifierData(modId, objects); } /// - public override void Handle(PlayerControl innerNetObject, ModifierData data) + public override void Handle(PlayerControl player, ModifierData data) { - var modifier = ModifierFactory.CreateInstance(data.Type, data.Args); - innerNetObject.GetModifierComponent()?.AddModifier(modifier); + player.AddModifier(data.Id, data.Args); } } diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index fb57d70..184bafb 100644 --- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -1,6 +1,6 @@ using HarmonyLib; +using MiraAPI.Modifiers; using MiraAPI.Roles; -using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Patches.Freeplay; diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index d1c6cb9..b634c91 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -326,7 +326,7 @@ internal static class TaskAdderPatches } var taskAddButton = Object.Instantiate(instance.RoleButton); - taskAddButton.name = modifier.ModifierId.ToString(CultureInfo.InvariantCulture); + taskAddButton.name = modifier.GetId().ToString(NumberFormatInfo.InvariantInfo); taskAddButton.role = null; taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; @@ -336,14 +336,15 @@ internal static class TaskAdderPatches taskAddButton.Button.OnClick = new Button.ButtonClickedEvent(); taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => { - if (PlayerControl.LocalPlayer.HasModifier(modifier.ModifierId)) + var id = modifier.GetId(); + if (PlayerControl.LocalPlayer.HasModifier(id)) { - PlayerControl.LocalPlayer.RemoveModifier(modifier.ModifierId); + PlayerControl.LocalPlayer.RemoveModifier(id); taskAddButton.Overlay.enabled = false; } else { - PlayerControl.LocalPlayer.AddModifier(ModifierManager.GetModifierType(modifier.ModifierId)!); + PlayerControl.LocalPlayer.AddModifier(id); taskAddButton.Overlay.enabled = true; } })); diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index fbabe1a..31fdbb8 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -126,6 +126,6 @@ public static class PlayerControlPatches [HarmonyPatch(nameof(PlayerControl.OnDestroy))] public static void PlayerControlOnDestroyPrefix(PlayerControl __instance) { - Utilities.ModifierExtensions.ModifierComponents.Remove(__instance); + ModifierExtensions.ModifierComponents.Remove(__instance); } } diff --git a/MiraAPI/Patches/Roles/TaskPanelPatch.cs b/MiraAPI/Patches/Roles/TaskPanelPatch.cs index 431d47b..a08445a 100644 --- a/MiraAPI/Patches/Roles/TaskPanelPatch.cs +++ b/MiraAPI/Patches/Roles/TaskPanelPatch.cs @@ -4,7 +4,7 @@ using UnityEngine; namespace MiraAPI.Patches.Roles; [HarmonyPatch(typeof(TaskPanelBehaviour))] -public static class TaskPanelPatch +internal static class TaskPanelPatch { /// /// This patch is to override the automatic updating of the y position on the tab (which is in base game) diff --git a/MiraAPI/Utilities/ModifierExtensions.cs b/MiraAPI/Utilities/ModifierExtensions.cs deleted file mode 100644 index dadb62c..0000000 --- a/MiraAPI/Utilities/ModifierExtensions.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using MiraAPI.Modifiers; -using MiraAPI.Networking; -using Reactor.Networking.Attributes; -using Reactor.Utilities; - -namespace MiraAPI.Utilities; - -/// -/// Extensions for modifiers. -/// -public static class ModifierExtensions -{ - /// - /// Gets the dictionary to cache ModifierComponents. - /// - public static Dictionary ModifierComponents { get; } = []; - - /// - /// 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.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); - } - - /// - /// 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.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); - } - - /// - /// 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) - { - return null; - } - - ModifierComponents[player] = component; - return component; - } - - /// - /// Gets the ModifierComponent for a player. - /// - /// The PlayerControl object. - /// The ModifierComponent if it is found. - /// A ModifierComponent if there is one, null otherwise. - public static bool TryGetModifierComponent(this PlayerControl player, out ModifierComponent? component) - { - if (ModifierComponents.TryGetValue(player, out component)) - { - return component; - } - - component = player.GetComponent(); - return component; - } - - /// - /// Gets a collection of modifiers by their type, or null if the player doesn't have one. - /// - /// The PlayerControl object. - /// The Type of the Modifier. - /// The Modifier if it is found, null otherwise. - public static IEnumerable GetModifiersByType(this PlayerControl player) where T : BaseModifier - { - return player.GetModifierComponent()!.GetModifiersByType(); - } - - /// - /// 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()!.GetModifier(); - } - - /// - /// Tries to get a modifier by its type. - /// - /// The PlayerControl object. - /// The modifier or null. - /// The Type of the Modifier. - /// True if the modifier was found, false otherwise. - public static bool TryGetModifier(this PlayerControl player, out T? modifier) where T : BaseModifier - { - return player.GetModifierComponent()!.TryGetModifier(out modifier); - } - - /// - /// Removes a modifier from the player. - /// - /// The PlayerControl object. - /// The modifier type. - public static void RemoveModifier(this PlayerControl player, Type type) - { - player.GetModifierComponent()!.RemoveModifier(type); - } - - /// - /// Removes a modifier from the player. - /// - /// The PlayerControl object. - /// The modifier type. - public static void RemoveModifier(this PlayerControl player) where T : BaseModifier - { - player.GetModifierComponent()!.RemoveModifier(); - } - - /// - /// Removes a modifier from the player. - /// - /// The PlayerControl object. - /// The modifier ID. - public static void RemoveModifier(this PlayerControl player, uint modifierId) - { - player.GetModifierComponent()!.RemoveModifier(modifierId); - } - - /// - /// Removes a modifier from the player. - /// - /// The PlayerControl object. - /// The modifier object. - public static void RemoveModifier(this PlayerControl player, BaseModifier modifier) - { - player.GetModifierComponent()!.RemoveModifier(modifier); - } - - /// - /// Adds a modifier to the player. - /// - /// The PlayerControl object. - /// The modifier to add. - /// The modifier that was added. - public static BaseModifier? AddModifier(this PlayerControl player, BaseModifier modifier) - { - return player.GetModifierComponent()!.AddModifier(modifier); - } - - /// - /// Adds a modifier to the player. - /// - /// The PlayerControl object. - /// The modifier type. - /// The modifier that was added. - public static BaseModifier? AddModifier(this PlayerControl player, Type type) - { - return player.GetModifierComponent()!.AddModifier(type); - } - - /// - /// Adds a modifier to the player. - /// - /// The PlayerControl object. - /// The Type of the modifier. - /// The new modifier. - public static T? AddModifier(this PlayerControl player) where T : BaseModifier - { - return player.GetModifierComponent()!.AddModifier(); - } - - /// - /// Checks if a player has an active or queued modifier by its ID. - /// - /// The PlayerControl object. - /// The Modifier ID. - /// The predicate to check the modifier. - /// True if the Modifier is present, false otherwise. - public static bool HasModifier(this PlayerControl player, uint id, Func? predicate=null) - { - return player.GetModifierComponent()!.HasModifier(id, predicate); - } - - /// - /// Checks if a player has an active or queued modifier by its type. - /// - /// The PlayerControl object. - /// The predicate to check the modifier. - /// The Type of the Modifier. - /// True if the Modifier is present, false otherwise. - public static bool HasModifier(this PlayerControl player, Func? predicate=null) where T : BaseModifier - { - return player.GetModifierComponent()!.HasModifier(predicate); - } -} From 9ae9094a85e2443e98725e8cff0603b2f114f88a Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:52:06 -0500 Subject: [PATCH 34/71] oops fix that --- MiraAPI/Patches/VentPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/VentPatches.cs b/MiraAPI/Patches/VentPatches.cs index 621abbc..8aa174b 100644 --- a/MiraAPI/Patches/VentPatches.cs +++ b/MiraAPI/Patches/VentPatches.cs @@ -1,8 +1,8 @@ using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Usables; +using MiraAPI.Modifiers; using MiraAPI.Roles; -using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Patches; From 66375e884f70d2f0a439df8c2536d374eccf3561 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:52:53 -0500 Subject: [PATCH 35/71] use invariant info to shut up warnings --- MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs | 13 +++++++------ MiraAPI/Hud/CustomActionButton.cs | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 80bd497..f25fda6 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Text; using Il2CppInterop.Runtime.InteropTypes.Arrays; @@ -146,7 +147,7 @@ public class ModdedEnumOption : ModdedOption where T : Enum // SetUpFromData method doesnt work correctly so we must set the values manually stringOption.Title = StringName; stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray(0); - stringOption.Value = Convert.ToInt32(Value); + stringOption.Value = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo); OptionBehaviour = stringOption; @@ -156,13 +157,13 @@ public class ModdedEnumOption : ModdedOption where T : Enum /// public override float GetFloatData() { - return Convert.ToSingle(Value); + return Convert.ToSingle(Value, NumberFormatInfo.InvariantInfo); } /// public override NetData GetNetData() { - return new NetData(Id, Encoding.Unicode.GetBytes(Convert.ToString(Value))); + return new NetData(Id, Encoding.Unicode.GetBytes(Convert.ToString(Value, NumberFormatInfo.InvariantInfo)!)); } /// @@ -174,13 +175,13 @@ public class ModdedEnumOption : ModdedOption where T : Enum /// public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) { - return (T)Enum.Parse(typeof(T), optionBehaviour.GetInt().ToString()); + return (T)Enum.Parse(typeof(T), optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo)); } /// protected override void OnValueChanged(T newValue) { - HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data?.GetValueString(Convert.ToInt32(newValue)), false); + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo)), false); if (!OptionBehaviour) { return; @@ -188,7 +189,7 @@ public class ModdedEnumOption : ModdedOption where T : Enum if (OptionBehaviour is StringOption opt) { - opt.Value = Convert.ToInt32(newValue); + opt.Value = Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo); } } } diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 7049a8e..6b8b8cc 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -3,6 +3,7 @@ using MiraAPI.Events.Mira; using MiraAPI.Patches; using MiraAPI.Utilities.Assets; using System; +using System.Globalization; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -421,7 +422,7 @@ public abstract class CustomActionButton { Button?.SetFillUp(Timer, EffectDuration); - Button!.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(); + Button!.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo); Button!.cooldownTimerText.gameObject.SetActive(true); } else From 4a8fb38380402e2745c080103bed37155de1aa15 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:17:40 -0500 Subject: [PATCH 36/71] improvements to timed modifier --- MiraAPI/Modifiers/Types/TimedModifier.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 038fa6d..ebb0457 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -17,7 +17,7 @@ public abstract class TimedModifier : BaseModifier /// /// Gets a value indicating whether the timer should start automatically when added. /// - public virtual bool AutoStart => false; + public virtual bool AutoStart => true; /// /// Gets a value indicating whether the modifier should be removed when the timer completes. @@ -27,7 +27,9 @@ public abstract class TimedModifier : BaseModifier /// /// Called when the timer completes. /// - public abstract void OnTimerComplete(); + public virtual void OnTimerComplete() + { + } /// /// Gets or sets a value indicating whether the timer is active. From caa27642db35486f326ac6e81a8f2e4ab539faed Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:22:00 -0500 Subject: [PATCH 37/71] further modifier improvements (massive changes) --- MiraAPI.Example/Buttons/TestButton.cs | 6 + .../Events/Vanilla/Usables/PlayerUseEvent.cs | 2 +- MiraAPI/Modifiers/BaseModifier.cs | 13 +- MiraAPI/Modifiers/ModifierComponent.cs | 155 +++++++++----- MiraAPI/Modifiers/ModifierExtensions.cs | 191 ++++++++++++------ MiraAPI/Modifiers/ModifierManager.cs | 24 +-- .../{ => Modifiers}/AddModifierRpc.cs | 2 +- .../{ => Modifiers}/SyncModifiersRpc.cs | 2 +- .../Patches/Events/KillAnimationPatches.cs | 2 +- MiraAPI/Patches/Events/UsePatches.cs | 2 +- .../Patches/Freeplay/TaskAddButtonPatches.cs | 2 +- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 8 +- .../Patches/Modifiers/EndGameDidWinPatch.cs | 2 +- MiraAPI/Patches/VentPatches.cs | 4 +- 14 files changed, 276 insertions(+), 139 deletions(-) rename MiraAPI/Networking/{ => Modifiers}/AddModifierRpc.cs (97%) rename MiraAPI/Networking/{ => Modifiers}/SyncModifiersRpc.cs (97%) diff --git a/MiraAPI.Example/Buttons/TestButton.cs b/MiraAPI.Example/Buttons/TestButton.cs index feb7992..776dc40 100644 --- a/MiraAPI.Example/Buttons/TestButton.cs +++ b/MiraAPI.Example/Buttons/TestButton.cs @@ -15,6 +15,12 @@ public class TestButton : CustomActionButton protected override void OnClick() { + if (PlayerControl.LocalPlayer.HasModifier()) + { + PlayerControl.LocalPlayer.RpcRemoveModifier(); + return; + } + var randomPlayer = PlayerControl.AllPlayerControls.ToArray().Random(); PlayerControl.LocalPlayer.RpcAddModifier("test", 1, randomPlayer); } diff --git a/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs b/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs index 655a46c..5f05676 100644 --- a/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs +++ b/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs @@ -1,4 +1,4 @@ -namespace MiraAPI.Events.Vanilla; +namespace MiraAPI.Events.Vanilla.Usables; /// /// Event for using a from Vanilla Among Us. Will always be ran locally. diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 8834454..21ce064 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -43,13 +43,14 @@ public abstract class BaseModifier public bool Initialized { get; internal set; } /// - /// Get the ID of the modifier. + /// Gets the unique ID of the modifier. /// - /// The ID of the modifier. - public uint GetId() - { - return ModifierManager.GetModifierId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered."); - } + public uint UniqueId { get; internal set; } + + /// + /// Gets the type ID of the modifier. + /// + public uint TypeId => ModifierManager.GetModifierTypeId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered."); /// /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true. diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 6cd3d5e..94ab3db 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -37,6 +37,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) private readonly List _toAdd = []; + private uint _nextUniqueId; + + private uint GetNextUniqueId() + { + _nextUniqueId++; + return _nextUniqueId; + } + internal void ClearModifiers() { _toRemove.AddRange(Modifiers); @@ -104,14 +112,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return; } - var filteredModifiers = Modifiers.Where(mod => !mod.HideOnUi); + var filteredModifiers = Modifiers.Where(mod => !mod.HideOnUi).ToArray(); - var baseModifiers = filteredModifiers as BaseModifier[] ?? filteredModifiers.ToArray(); - - if (baseModifiers.Length != 0 && !MeetingHud.Instance) + if (filteredModifiers.Length != 0 && !MeetingHud.Instance) { var stringBuild = new StringBuilder(); - foreach (var mod in baseModifiers) + foreach (var mod in filteredModifiers) { stringBuild.Append(CultureInfo.InvariantCulture, $"\n{mod.GetHudString()}"); } @@ -128,11 +134,11 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// /// The predicate to check the modifier by. /// The Type of the Modifier. - /// The Modifier if it is found, null otherwise. + /// A collection of modifiers. [HideFromIl2Cpp] - public IEnumerable GetModifiersByType(Func? predicate=null) where T : BaseModifier + public IEnumerable GetModifiers(Func? predicate=null) where T : BaseModifier { - return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x)); + return Modifiers.OfType().Where(x => predicate == null || predicate(x)); } /// @@ -140,26 +146,26 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// /// The modifier type. /// The predicate to check the modifier by. - /// The Modifier if it is found, null otherwise. + /// A collection of modifiers. [HideFromIl2Cpp] - public IEnumerable GetModifiersByType(Type type, Func? predicate=null) + public IEnumerable GetModifiers(Type type, Func? predicate=null) { - return ActiveModifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))); + return Modifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))); } /// - /// Gets a collection of modifiers by ID. + /// Gets a collection of modifiers by type ID. /// - /// The Modifier ID. + /// The modifier's type ID. /// The predicate to check the modifier by. - /// The Modifier if it is found, null otherwise. + /// A collection of modifiers. [HideFromIl2Cpp] - public IEnumerable GetModifiersByType(uint id, Func? predicate=null) + public IEnumerable GetModifiers(uint id, Func? predicate=null) { var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( $"Cannot get modifier with id {id} because it is not registered."); - return GetModifiersByType(type, predicate); + return GetModifiers(type, predicate); } /// @@ -191,23 +197,34 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } /// - /// Tries to get a modifier by its ID. + /// Tries to get a modifier by its type ID. /// - /// The modifier ID. + /// The modifier type ID. /// The modifier or null. /// The predicate to check the modifier by. /// True if the modifier was found, false otherwise. [HideFromIl2Cpp] - public bool TryGetModifier(uint id, out BaseModifier? modifier, Func? predicate = null) + public bool TryGetModifierByTypeId(uint id, out BaseModifier? modifier, Func? predicate = null) { - var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( - $"Cannot get modifier with id {id} because it is not registered."); - - return TryGetModifier(type, out modifier, predicate); + modifier = GetModifierByTypeId(id, predicate); + return modifier != null; } /// - /// Gets a modifier by its type, or null if the player doesn't have it. + /// Tries to get a modifier by its unique ID. + /// + /// The modifier unique ID. + /// The modifier or null. + /// True if the modifier was found, false otherwise. + [HideFromIl2Cpp] + public bool TryGetModifierByUniqueId(uint id, out BaseModifier? modifier) + { + modifier = GetModifierByUniqueId(id); + return modifier != null; + } + + /// + /// Gets a modifier by its type. /// /// The predicate to check the modifier by. /// The Type of the Modifier. @@ -215,11 +232,11 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public T? GetModifier(Func? predicate = null) where T : BaseModifier { - return GetModifiersByType(predicate).FirstOrDefault(); + return GetModifiers(predicate).FirstOrDefault(); } /// - /// Gets a modifier by its type, or null if the player doesn't have it. + /// Gets a modifier by its type. /// /// The modifier type. /// The predicate to check the modifier by. @@ -227,17 +244,17 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? GetModifier(Type type, Func? predicate = null) { - return GetModifiersByType(type).FirstOrDefault(predicate ?? (_ => true)); + return GetModifiers(type).FirstOrDefault(predicate ?? (_ => true)); } /// - /// Gets a modifier by its type, or null if the player doesn't have it. + /// Gets a modifier by its type ID. /// /// The modifier ID. /// The predicate to check the modifier by. /// The Modifier if it is found, null otherwise. [HideFromIl2Cpp] - public BaseModifier? GetModifier(uint id, Func? predicate = null) + public BaseModifier? GetModifierByTypeId(uint id, Func? predicate = null) { var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( $"Cannot get modifier with id {id} because it is not registered."); @@ -245,6 +262,17 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return GetModifier(type, predicate); } + /// + /// Gets a modifier by unique ID. + /// + /// The modifier's unique ID. + /// The modifier if it is found, or null. + [HideFromIl2Cpp] + public BaseModifier? GetModifierByUniqueId(uint id) + { + return Modifiers.Find(x => x.UniqueId == id); + } + /// /// Removes a modifier from the player. /// @@ -280,19 +308,6 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) RemoveModifier(modifier); } - /// - /// Removes a modifier from the player. - /// - /// The modifier ID. - /// The predicate to check the modifier by. - [HideFromIl2Cpp] - public void RemoveModifier(uint modifierId, Func? predicate = null) - { - var type = ModifierManager.GetModifierType(modifierId) ?? throw new InvalidOperationException( - $"Cannot remove modifier with id {modifierId} because it is not registered."); - RemoveModifier(type, predicate); - } - /// /// Removes a modifier from the player. /// @@ -309,6 +324,36 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) _toRemove.Add(modifier); } + /// + /// Removes a modifier from the player. + /// + /// The modifier's type ID. + /// The predicate to check the modifier by. + [HideFromIl2Cpp] + public void RemoveModifierByTypeId(uint typeId, Func? predicate = null) + { + var type = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException( + $"Cannot remove modifier with id {typeId} because it is not registered."); + RemoveModifier(type, predicate); + } + + /// + /// Removes a modifier from the player. + /// + /// The modifier's unique ID. + [HideFromIl2Cpp] + public void RemoveModifierByUniqueId(uint uniqueId) + { + var modifier = GetModifierByUniqueId(uniqueId); + if (modifier == null) + { + Logger.Error($"Cannot remove modifier with unique id {uniqueId} because it is not active."); + return; + } + + RemoveModifier(modifier); + } + /// /// Adds a modifier to the player. /// @@ -329,8 +374,8 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? AddModifier(BaseModifier modifier) { - var id = modifier.GetId(); - if (modifier.Unique && Modifiers.Find(x => x.GetId() == id) != null) + var id = modifier.TypeId; + if (modifier.Unique && Modifiers.Find(x => x.TypeId == id) != null) { Logger.Error($"Player already has modifier with id {id}!"); return null; @@ -345,6 +390,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) _toAdd.Add(modifier); modifier.Player = _player; modifier.ModifierComponent = this; + modifier.UniqueId = GetNextUniqueId(); return modifier; } @@ -398,7 +444,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public bool HasModifier(Func? predicate=null) where T : BaseModifier { - return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); + return Modifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); } /// @@ -410,21 +456,32 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public bool HasModifier(Type type, Func? predicate=null) { - return ActiveModifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x))); + return Modifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x))); } /// - /// Checks if a player has an active modifier by its ID. + /// Checks if a player has an active modifier by its type ID /// - /// The Modifier ID. + /// The Modifier type ID. /// The predicate to check the modifier. /// True if the Modifier is present, false otherwise. [HideFromIl2Cpp] - public bool HasModifier(uint id, Func? predicate=null) + public bool HasModifierByTypeId(uint id, Func? predicate=null) { var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( $"Cannot get modifier with id {id} because it is not registered."); return HasModifier(type, predicate); } + + /// + /// Checks if a player has an active modifier by its unique ID + /// + /// The Modifier type ID. + /// True if the Modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifierByUniqueId(uint id) + { + return Modifiers.Exists(x => x.UniqueId == id); + } } diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index 226b16b..5b42c6b 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using MiraAPI.Networking; +using MiraAPI.Networking.Modifiers; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; using Reactor.Utilities; @@ -21,14 +22,14 @@ public static class ModifierExtensions /// Remote Procedure Call to add a modifier to a player. /// /// The player to add the modifier to. - /// The modifier ID. + /// The modifier type ID. /// The arguments to initialize the modifier constructor with. - public static void RpcAddModifier(this PlayerControl target, uint modifierId, params object[] args) + public static void RpcAddModifier(this PlayerControl target, uint typeId, params object[] args) { - _ = ModifierManager.GetModifierType(modifierId) ?? throw new InvalidOperationException( - $"Modifier with ID {modifierId} is not registered."); + _ = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException( + $"Modifier with ID {typeId} is not registered."); - Rpc.Instance.Send(target, new ModifierData(modifierId, args)); + Rpc.Instance.Send(target, new ModifierData(typeId, args)); } /// @@ -39,7 +40,7 @@ public static class ModifierExtensions /// The arguments to initialize the modifier constructor with. public static void RpcAddModifier(this PlayerControl player, Type type, params object[] args) { - var id = ModifierManager.GetModifierId(type) ?? throw new InvalidOperationException( + var id = ModifierManager.GetModifierTypeId(type) ?? throw new InvalidOperationException( $"Modifier {type.Name} is not registered."); player.RpcAddModifier(id, args); @@ -60,28 +61,61 @@ public static class ModifierExtensions /// Remote Procedure Call to remove a modifier from a player. /// /// The player to remove the modifier from. - /// The ID of the modifier. + /// The unique ID of the modifier. [MethodRpc((uint)MiraRpc.RemoveModifier)] - public static void RpcRemoveModifier(this PlayerControl target, uint modifierId) + public static void RpcRemoveModifierByUniqueId(this PlayerControl target, uint uniqueId) { - target.RemoveModifier(modifierId); + target.RemoveModifierByUniqueId(uniqueId); + } + + /// + /// Remote Procedure Call to remove a modifier from a player by type ID. + /// + /// The player to remove the modifier from. + /// The type ID of the modifier. + /// Optional predicate to filter the modifiers. + public static void RpcRemoveModifierByTypeId( + this PlayerControl target, + uint typeId, + Func? predicate = null) + { + var modifier = target.GetModifierByTypeId(typeId, predicate); + if (modifier is null) + { + Logger.Error($"Player {target.PlayerId} does not have modifier with type ID {typeId}."); + return; + } + + target.RpcRemoveModifierByUniqueId(modifier.UniqueId); } /// /// 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 + /// The type of the modifier. + /// Optional predicate to filter the modifiers. + public static void RpcRemoveModifier( + this PlayerControl player, + Type type, + Func? predicate = null) { - var id = ModifierManager.GetModifierId(typeof(T)); - if (id == null) - { - Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); - return; - } + var id = ModifierManager.GetModifierTypeId(type) ?? throw new InvalidOperationException( + $"Modifier {type.Name} is not registered."); - player.RpcRemoveModifier(id.Value); + player.RpcRemoveModifierByTypeId(id, predicate); + } + + /// + /// Remote Procedure Call to remove a modifier from a player. + /// + /// The player to remove the modifier from. + /// Optional predicate to filter the modifiers. + /// The Type of the Modifier. + public static void RpcRemoveModifier(this PlayerControl player, Func? predicate = null) + where T : BaseModifier + { + player.RpcRemoveModifier(typeof(T), predicate); } /// @@ -89,7 +123,7 @@ public static class ModifierExtensions /// /// The PlayerControl object. /// A ModifierComponent if there is one, null otherwise. - public static ModifierComponent? GetModifierComponent(this PlayerControl player) + public static ModifierComponent GetModifierComponent(this PlayerControl player) { if (ModifierComponents.TryGetValue(player, out var component)) { @@ -97,9 +131,9 @@ public static class ModifierExtensions } component = player.GetComponent(); - if (component == null) + if (!component) { - return null; + throw new InvalidOperationException("ModifierComponent is not attached to the player."); } ModifierComponents[player] = component; @@ -115,7 +149,7 @@ public static class ModifierExtensions /// True if the player has the modifier, false otherwise. public static bool HasModifier(this PlayerControl player, Func? predicate = null) where T : BaseModifier { - return player.GetModifierComponent()!.HasModifier(predicate); + return player.GetModifierComponent().HasModifier(predicate); } /// @@ -127,19 +161,33 @@ public static class ModifierExtensions /// True if the player has the modifier, false otherwise. public static bool HasModifier(this PlayerControl player, Type type, Func? predicate = null) { - return player.GetModifierComponent()!.HasModifier(type, predicate); + return player.GetModifierComponent().HasModifier(type, predicate); } /// - /// Checks if the player has a specific modifier by ID. + /// Checks if the player has a specific modifier by type ID. /// /// The PlayerControl instance. - /// The ID of the modifier. + /// The type ID of the modifier. /// Optional predicate to filter the modifiers. /// True if the player has the modifier, false otherwise. - public static bool HasModifier(this PlayerControl player, uint id, Func? predicate = null) + public static bool HasModifierByTypeId( + this PlayerControl player, + uint id, + Func? predicate = null) { - return player.GetModifierComponent()!.HasModifier(id, predicate); + return player.GetModifierComponent().HasModifierByTypeId(id, predicate); + } + + /// + /// Checks if the player has a specific modifier by unique ID. + /// + /// The PlayerControl instance. + /// The unique ID of the modifier. + /// True if the player has the modifier, false otherwise. + public static bool HasModifierByUniqueId(this PlayerControl player, uint id) + { + return player.GetModifierComponent().HasModifierByUniqueId(id); } /// @@ -151,7 +199,7 @@ public static class ModifierExtensions /// The modifier if found, null otherwise. public static T? GetModifier(this PlayerControl player, Func? predicate = null) where T : BaseModifier { - return player.GetModifierComponent()!.GetModifier(predicate); + return player.GetModifierComponent().GetModifier(predicate); } /// @@ -166,7 +214,22 @@ public static class ModifierExtensions Type type, Func? predicate = null) { - return player.GetModifierComponent()!.GetModifier(type, predicate); + return player.GetModifierComponent().GetModifier(type, predicate); + } + + /// + /// Gets a specific modifier from the player by type ID. + /// + /// The PlayerControl instance. + /// The type ID of the modifier. + /// Optional predicate to filter the modifiers. + /// The modifier if found, null otherwise. + public static BaseModifier? GetModifierByTypeId( + this PlayerControl player, + uint id, + Func? predicate = null) + { + return player.GetModifierComponent().GetModifierByTypeId(id, predicate); } /// @@ -174,14 +237,12 @@ public static class ModifierExtensions /// /// The PlayerControl instance. /// The ID of the modifier. - /// Optional predicate to filter the modifiers. /// The modifier if found, null otherwise. - public static BaseModifier? GetModifier( + public static BaseModifier? GetModifierByUniqueId( this PlayerControl player, - uint id, - Func? predicate = null) + uint id) { - return player.GetModifierComponent()!.GetModifier(id, predicate); + return player.GetModifierComponent().GetModifierByUniqueId(id); } /// @@ -191,10 +252,10 @@ public static class ModifierExtensions /// The PlayerControl instance. /// Optional predicate to filter the modifiers. /// An enumerable of modifiers. - public static IEnumerable GetModifiersByType(this PlayerControl player, Func? predicate = null) + public static IEnumerable GetModifiers(this PlayerControl player, Func? predicate = null) where T : BaseModifier { - return player.GetModifierComponent()!.GetModifiersByType(predicate); + return player.GetModifierComponent().GetModifiers(predicate); } /// @@ -204,27 +265,37 @@ public static class ModifierExtensions /// The type of the modifiers. /// Optional predicate to filter the modifiers. /// An enumerable of modifiers. - public static IEnumerable GetModifiersByType( + public static IEnumerable GetModifiers( this PlayerControl player, Type type, Func? predicate = null) { - return player.GetModifierComponent()!.GetModifiersByType(type, predicate); + return player.GetModifierComponent().GetModifiers(type, predicate); } /// - /// Gets all modifiers of a specific type from the player by ID. + /// Gets all modifiers of a specific type from the player by type ID. /// /// The PlayerControl instance. - /// The ID of the modifiers. + /// The type ID of the modifiers. /// Optional predicate to filter the modifiers. /// An enumerable of modifiers. - public static IEnumerable GetModifiersByType( + public static IEnumerable GetModifiers( this PlayerControl player, uint id, Func? predicate = null) { - return player.GetModifierComponent()!.GetModifiersByType(id, predicate); + return player.GetModifierComponent().GetModifiers(id, predicate); + } + + /// + /// Removes a specific modifier from the player. + /// + /// The PlayerControl instance. + /// The modifier to remove. + public static void RemoveModifier(this PlayerControl player, BaseModifier modifier) + { + player.GetModifierComponent().RemoveModifier(modifier); } /// @@ -236,7 +307,7 @@ public static class ModifierExtensions public static void RemoveModifier(this PlayerControl player, Func? predicate = null) where T : BaseModifier { - player.GetModifierComponent()!.RemoveModifier(predicate); + player.GetModifierComponent().RemoveModifier(predicate); } /// @@ -247,31 +318,33 @@ public static class ModifierExtensions /// Optional predicate to filter the modifiers. public static void RemoveModifier(this PlayerControl player, Type type, Func? predicate = null) { - player.GetModifierComponent()!.RemoveModifier(type, predicate); + player.GetModifierComponent().RemoveModifier(type, predicate); } /// /// Removes a specific modifier from the player by ID. /// /// The PlayerControl instance. - /// The ID of the modifier. + /// The ID of the modifier. /// Optional predicate to filter the modifiers. - public static void RemoveModifier( + public static void RemoveModifierByTypeId( this PlayerControl player, - uint modifierId, + uint typeId, Func? predicate = null) { - player.GetModifierComponent()!.RemoveModifier(modifierId, predicate); + player.GetModifierComponent().RemoveModifierByTypeId(typeId, predicate); } /// - /// Removes a specific modifier from the player. + /// Removes a specific modifier from the player by ID. /// /// The PlayerControl instance. - /// The modifier to remove. - public static void RemoveModifier(this PlayerControl player, BaseModifier modifier) + /// The ID of the modifier. + public static void RemoveModifierByUniqueId( + this PlayerControl player, + uint uniqueId) { - player.GetModifierComponent()!.RemoveModifier(modifier); + player.GetModifierComponent().RemoveModifierByUniqueId(uniqueId); } /// @@ -283,7 +356,7 @@ public static class ModifierExtensions /// The added modifier. public static T? AddModifier(this PlayerControl player, params object[] args) where T : BaseModifier { - return player.GetModifierComponent()!.AddModifier(args); + return player.GetModifierComponent().AddModifier(args); } /// @@ -294,7 +367,7 @@ public static class ModifierExtensions /// The added modifier. public static BaseModifier? AddModifier(this PlayerControl player, BaseModifier modifier) { - return player.GetModifierComponent()!.AddModifier(modifier); + return player.GetModifierComponent().AddModifier(modifier); } /// @@ -306,18 +379,18 @@ public static class ModifierExtensions /// The added modifier. public static BaseModifier? AddModifier(this PlayerControl player, Type type, params object[] args) { - return player.GetModifierComponent()!.AddModifier(type, args); + return player.GetModifierComponent().AddModifier(type, args); } /// - /// Adds a specific modifier to the player by ID. + /// Adds a specific modifier to the player by type ID. /// /// The PlayerControl instance. - /// The ID of the modifier. + /// The type ID of the modifier. /// The arguments to initialize the modifier constructor with. /// The added modifier. - public static BaseModifier? AddModifier(this PlayerControl player, uint id, params object[] args) + public static BaseModifier? AddModifier(this PlayerControl player, uint typeId, params object[] args) { - return player.GetModifierComponent()!.AddModifier(id, args); + return player.GetModifierComponent().AddModifier(typeId, args); } } diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 65341a0..209423e 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.Serialization; using MiraAPI.Modifiers.Types; using MiraAPI.Networking; +using MiraAPI.Networking.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -23,16 +24,16 @@ public static class ModifierManager private static readonly Dictionary TypeToIdModifierMap = []; private static readonly Dictionary> PrioritiesToIdsMap = []; - private static uint _nextId; + private static uint _nextTypeId; - private static uint GetNextId() + private static uint GetNextTypeId() { - _nextId++; - return _nextId; + _nextTypeId++; + return _nextTypeId; } /// - /// Gets the modifier type from the id. + /// Gets the modifier type from the type id. /// /// The ID. /// The Type of the modifier. @@ -46,7 +47,7 @@ public static class ModifierManager /// /// The Type. /// The ID of the modifier. - public static uint? GetModifierId(Type type) + public static uint? GetModifierTypeId(Type type) { if (!TypeToIdModifierMap.TryGetValue(type, out var id)) { @@ -63,8 +64,8 @@ public static class ModifierManager return false; } - IdToTypeModifierMap.Add(GetNextId(), modifierType); - TypeToIdModifierMap.Add(modifierType, _nextId); + IdToTypeModifierMap.Add(GetNextTypeId(), modifierType); + TypeToIdModifierMap.Add(modifierType, _nextTypeId); var bm = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); // this probably isn't a great idea info.Modifiers.Add(bm); @@ -81,7 +82,7 @@ public static class ModifierManager PrioritiesToIdsMap[priority] = list = []; } - list.Add(_nextId); + list.Add(_nextTypeId); return true; } @@ -143,7 +144,7 @@ public static class ModifierManager var mod = Activator.CreateInstance(IdToTypeModifierMap[id]) as GameModifier; var plr = plrs.Where(x => IsGameModifierValid(x, mod!, id)).Random(); - if (plr == null) + if (!plr || plr == null) { Logger.Warning($"Somehow valid players for modifier {mod!.ModifierName} disappeared"); } @@ -158,8 +159,7 @@ public static class ModifierManager private static bool IsGameModifierValid(PlayerControl player, GameModifier modifier, uint modifierId) { return (player.Data.Role is not ICustomRole role || role.IsModifierApplicable(modifier)) && - modifier.IsModifierValidOn(player.Data.Role) && - !player.HasModifier(modifierId); + modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifierByTypeId(modifierId); } internal static void SyncAllModifiers(int targetId = -1) diff --git a/MiraAPI/Networking/AddModifierRpc.cs b/MiraAPI/Networking/Modifiers/AddModifierRpc.cs similarity index 97% rename from MiraAPI/Networking/AddModifierRpc.cs rename to MiraAPI/Networking/Modifiers/AddModifierRpc.cs index fdc37a9..5727ba5 100644 --- a/MiraAPI/Networking/AddModifierRpc.cs +++ b/MiraAPI/Networking/Modifiers/AddModifierRpc.cs @@ -5,7 +5,7 @@ using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; using Reactor.Networking.Serialization; -namespace MiraAPI.Networking; +namespace MiraAPI.Networking.Modifiers; /// /// Remote procedure call for adding a modifier. diff --git a/MiraAPI/Networking/SyncModifiersRpc.cs b/MiraAPI/Networking/Modifiers/SyncModifiersRpc.cs similarity index 97% rename from MiraAPI/Networking/SyncModifiersRpc.cs rename to MiraAPI/Networking/Modifiers/SyncModifiersRpc.cs index 288e2c1..0571198 100644 --- a/MiraAPI/Networking/SyncModifiersRpc.cs +++ b/MiraAPI/Networking/Modifiers/SyncModifiersRpc.cs @@ -3,7 +3,7 @@ using MiraAPI.Modifiers; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; -namespace MiraAPI.Networking; +namespace MiraAPI.Networking.Modifiers; [RegisterCustomRpc((uint)MiraRpc.SyncModifiers)] internal sealed class SyncModifiersRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) diff --git a/MiraAPI/Patches/Events/KillAnimationPatches.cs b/MiraAPI/Patches/Events/KillAnimationPatches.cs index 13b4cff..3572ff8 100644 --- a/MiraAPI/Patches/Events/KillAnimationPatches.cs +++ b/MiraAPI/Patches/Events/KillAnimationPatches.cs @@ -2,7 +2,7 @@ using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; -namespace MiraAPI.Patches; +namespace MiraAPI.Patches.Events; /// /// General patches for the KillAnimation class. diff --git a/MiraAPI/Patches/Events/UsePatches.cs b/MiraAPI/Patches/Events/UsePatches.cs index beb1f69..0571cd6 100644 --- a/MiraAPI/Patches/Events/UsePatches.cs +++ b/MiraAPI/Patches/Events/UsePatches.cs @@ -1,8 +1,8 @@ using HarmonyLib; using MiraAPI.Events; -using MiraAPI.Events.Vanilla; using System.Collections.Generic; using System.Reflection; +using MiraAPI.Events.Vanilla.Usables; namespace MiraAPI.Patches.Events; diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 184bafb..0ee6da4 100644 --- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -16,7 +16,7 @@ internal static class TaskAddButtonPatches if (uint.TryParse(__instance.name, out var result)) { __instance.Overlay.sprite = __instance.CheckImage; - __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifier(result); + __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifierByTypeId(result); return false; } diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index b634c91..0c35670 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -326,7 +326,7 @@ internal static class TaskAdderPatches } var taskAddButton = Object.Instantiate(instance.RoleButton); - taskAddButton.name = modifier.GetId().ToString(NumberFormatInfo.InvariantInfo); + taskAddButton.name = modifier.TypeId.ToString(NumberFormatInfo.InvariantInfo); taskAddButton.role = null; taskAddButton.MyTask = null; taskAddButton.SafePositionWorld = instance.SafePositionWorld; @@ -336,10 +336,10 @@ internal static class TaskAdderPatches taskAddButton.Button.OnClick = new Button.ButtonClickedEvent(); taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => { - var id = modifier.GetId(); - if (PlayerControl.LocalPlayer.HasModifier(id)) + var id = modifier.TypeId; + if (PlayerControl.LocalPlayer.HasModifierByTypeId(id)) { - PlayerControl.LocalPlayer.RemoveModifier(id); + PlayerControl.LocalPlayer.RemoveModifierByTypeId(id); taskAddButton.Overlay.enabled = false; } else diff --git a/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs index 17356db..a86383d 100644 --- a/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs +++ b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs @@ -24,7 +24,7 @@ public static class EndGameDidWinPatch var didWin = networkedPlayerInfo.Role.DidWin(gameOverReason); - var modifierWin = networkedPlayerInfo.Object?.GetModifierComponent()?.ActiveModifiers.OfType() + var modifierWin = networkedPlayerInfo.Object?.GetModifierComponent().GetModifiers() .FirstOrDefault(x => x.DidWin(gameOverReason) != null)?.DidWin(gameOverReason); if (modifierWin != null) { diff --git a/MiraAPI/Patches/VentPatches.cs b/MiraAPI/Patches/VentPatches.cs index 8aa174b..2f8991d 100644 --- a/MiraAPI/Patches/VentPatches.cs +++ b/MiraAPI/Patches/VentPatches.cs @@ -35,8 +35,8 @@ public static class VentPatches var canVent = role is ICustomRole customRole ? customRole.Configuration.CanUseVent : role.CanVent; couldUse = canVent; - var modifiers = @object.GetModifierComponent()?.ActiveModifiers; - if (modifiers is { Count: > 0 }) + var modifiers = @object.GetModifierComponent().ActiveModifiers; + if (modifiers.Count > 0) { switch (canVent) { From 7e4a71739c0ba89fdf2312e89c668d4751052619 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:57:38 -0500 Subject: [PATCH 38/71] better modifier unique IDs --- MiraAPI/Modifiers/BaseModifier.cs | 2 +- MiraAPI/Modifiers/ModifierComponent.cs | 53 ++++++------ MiraAPI/Modifiers/ModifierData.cs | 19 ----- MiraAPI/Modifiers/ModifierExtensions.cs | 80 +++++++++---------- MiraAPI/Modifiers/ModifierManager.cs | 69 +--------------- MiraAPI/Networking/GuidConverter.cs | 32 ++++++++ .../Networking/Modifiers/AddModifierRpc.cs | 23 +++++- MiraAPI/Networking/Modifiers/ModifierData.cs | 11 +++ .../Networking/Modifiers/SyncModifiersRpc.cs | 52 ------------ MiraAPI/Patches/AmongUsClientSyncPatch.cs | 6 +- .../Patches/Freeplay/TaskAddButtonPatches.cs | 2 +- MiraAPI/Patches/Freeplay/TaskAdderPatches.cs | 4 +- 12 files changed, 134 insertions(+), 219 deletions(-) delete mode 100644 MiraAPI/Modifiers/ModifierData.cs create mode 100644 MiraAPI/Networking/GuidConverter.cs create mode 100644 MiraAPI/Networking/Modifiers/ModifierData.cs delete mode 100644 MiraAPI/Networking/Modifiers/SyncModifiersRpc.cs diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 21ce064..52a2bb0 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -45,7 +45,7 @@ public abstract class BaseModifier /// /// Gets the unique ID of the modifier. /// - public uint UniqueId { get; internal set; } + public Guid UniqueId { get; internal set; } = Guid.Empty; /// /// Gets the type ID of the modifier. diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 94ab3db..57abd8c 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -37,14 +37,6 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) private readonly List _toAdd = []; - private uint _nextUniqueId; - - private uint GetNextUniqueId() - { - _nextUniqueId++; - return _nextUniqueId; - } - internal void ClearModifiers() { _toRemove.AddRange(Modifiers); @@ -204,22 +196,22 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the modifier by. /// True if the modifier was found, false otherwise. [HideFromIl2Cpp] - public bool TryGetModifierByTypeId(uint id, out BaseModifier? modifier, Func? predicate = null) + public bool TryGetModifier(uint id, out BaseModifier? modifier, Func? predicate = null) { - modifier = GetModifierByTypeId(id, predicate); + modifier = GetModifier(id, predicate); return modifier != null; } /// /// Tries to get a modifier by its unique ID. /// - /// The modifier unique ID. + /// The modifier unique ID. /// The modifier or null. /// True if the modifier was found, false otherwise. [HideFromIl2Cpp] - public bool TryGetModifierByUniqueId(uint id, out BaseModifier? modifier) + public bool TryGetModifier(Guid modifierGuid, out BaseModifier? modifier) { - modifier = GetModifierByUniqueId(id); + modifier = GetModifier(modifierGuid); return modifier != null; } @@ -254,7 +246,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the modifier by. /// The Modifier if it is found, null otherwise. [HideFromIl2Cpp] - public BaseModifier? GetModifierByTypeId(uint id, Func? predicate = null) + public BaseModifier? GetModifier(uint id, Func? predicate = null) { var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( $"Cannot get modifier with id {id} because it is not registered."); @@ -265,12 +257,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// /// Gets a modifier by unique ID. /// - /// The modifier's unique ID. + /// The modifier's unique ID. /// The modifier if it is found, or null. [HideFromIl2Cpp] - public BaseModifier? GetModifierByUniqueId(uint id) + public BaseModifier? GetModifier(Guid modifierGuid) { - return Modifiers.Find(x => x.UniqueId == id); + return Modifiers.Find(x => x.UniqueId == modifierGuid); } /// @@ -330,7 +322,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The modifier's type ID. /// The predicate to check the modifier by. [HideFromIl2Cpp] - public void RemoveModifierByTypeId(uint typeId, Func? predicate = null) + public void RemoveModifier(uint typeId, Func? predicate = null) { var type = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException( $"Cannot remove modifier with id {typeId} because it is not registered."); @@ -342,9 +334,9 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// /// The modifier's unique ID. [HideFromIl2Cpp] - public void RemoveModifierByUniqueId(uint uniqueId) + public void RemoveModifier(Guid uniqueId) { - var modifier = GetModifierByUniqueId(uniqueId); + var modifier = GetModifier(uniqueId); if (modifier == null) { Logger.Error($"Cannot remove modifier with unique id {uniqueId} because it is not active."); @@ -390,7 +382,10 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) _toAdd.Add(modifier); modifier.Player = _player; modifier.ModifierComponent = this; - modifier.UniqueId = GetNextUniqueId(); + if (modifier.UniqueId == Guid.Empty) + { + modifier.UniqueId = Guid.NewGuid(); + } return modifier; } @@ -460,13 +455,13 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } /// - /// Checks if a player has an active modifier by its type ID + /// Checks if a player has an active modifier by its type ID. /// - /// The Modifier type ID. + /// The modifier's type ID. /// The predicate to check the modifier. - /// True if the Modifier is present, false otherwise. + /// True if the modifier is present, false otherwise. [HideFromIl2Cpp] - public bool HasModifierByTypeId(uint id, Func? predicate=null) + public bool HasModifier(uint id, Func? predicate=null) { var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( $"Cannot get modifier with id {id} because it is not registered."); @@ -475,12 +470,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } /// - /// Checks if a player has an active modifier by its unique ID + /// Checks if a player has an active modifier by its unique ID. /// - /// The Modifier type ID. - /// True if the Modifier is present, false otherwise. + /// The modifier's guid. + /// True if the modifier is present, false otherwise. [HideFromIl2Cpp] - public bool HasModifierByUniqueId(uint id) + public bool HasModifier(Guid id) { return Modifiers.Exists(x => x.UniqueId == id); } diff --git a/MiraAPI/Modifiers/ModifierData.cs b/MiraAPI/Modifiers/ModifierData.cs deleted file mode 100644 index 7260834..0000000 --- a/MiraAPI/Modifiers/ModifierData.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MiraAPI.Modifiers; - -/// -/// Modifier data for networking. -/// -/// The ID of the modifier. -/// Parameters for constructor. -public readonly struct ModifierData(uint id, object[] args) -{ - /// - /// Gets the type of the modifier. - /// - public uint Id { get; } = id; - - /// - /// Gets the parameters for the constructor. - /// - public object[] Args { get; } = args; -} diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index 5b42c6b..5d05d69 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -29,7 +29,7 @@ public static class ModifierExtensions _ = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException( $"Modifier with ID {typeId} is not registered."); - Rpc.Instance.Send(target, new ModifierData(typeId, args)); + Rpc.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args)); } /// @@ -63,9 +63,9 @@ public static class ModifierExtensions /// The player to remove the modifier from. /// The unique ID of the modifier. [MethodRpc((uint)MiraRpc.RemoveModifier)] - public static void RpcRemoveModifierByUniqueId(this PlayerControl target, uint uniqueId) + public static void RpcRemoveModifier(this PlayerControl target, Guid uniqueId) { - target.RemoveModifierByUniqueId(uniqueId); + target.RemoveModifier(uniqueId); } /// @@ -74,19 +74,19 @@ public static class ModifierExtensions /// The player to remove the modifier from. /// The type ID of the modifier. /// Optional predicate to filter the modifiers. - public static void RpcRemoveModifierByTypeId( + public static void RpcRemoveModifier( this PlayerControl target, uint typeId, Func? predicate = null) { - var modifier = target.GetModifierByTypeId(typeId, predicate); + var modifier = target.GetModifier(typeId, predicate); if (modifier is null) { Logger.Error($"Player {target.PlayerId} does not have modifier with type ID {typeId}."); return; } - target.RpcRemoveModifierByUniqueId(modifier.UniqueId); + target.RpcRemoveModifier(modifier.UniqueId); } /// @@ -103,7 +103,7 @@ public static class ModifierExtensions var id = ModifierManager.GetModifierTypeId(type) ?? throw new InvalidOperationException( $"Modifier {type.Name} is not registered."); - player.RpcRemoveModifierByTypeId(id, predicate); + player.RpcRemoveModifier(id, predicate); } /// @@ -168,26 +168,26 @@ public static class ModifierExtensions /// Checks if the player has a specific modifier by type ID. /// /// The PlayerControl instance. - /// The type ID of the modifier. + /// The type ID of the modifier. /// Optional predicate to filter the modifiers. /// True if the player has the modifier, false otherwise. - public static bool HasModifierByTypeId( + public static bool HasModifier( this PlayerControl player, - uint id, + uint typeId, Func? predicate = null) { - return player.GetModifierComponent().HasModifierByTypeId(id, predicate); + return player.GetModifierComponent().HasModifier(typeId, predicate); } /// - /// Checks if the player has a specific modifier by unique ID. + /// Checks if the player has a specific modifier by its GUID. /// /// The PlayerControl instance. - /// The unique ID of the modifier. + /// The unique ID of the modifier. /// True if the player has the modifier, false otherwise. - public static bool HasModifierByUniqueId(this PlayerControl player, uint id) + public static bool HasModifier(this PlayerControl player, Guid uniqueId) { - return player.GetModifierComponent().HasModifierByUniqueId(id); + return player.GetModifierComponent().HasModifier(uniqueId); } /// @@ -218,31 +218,31 @@ public static class ModifierExtensions } /// - /// Gets a specific modifier from the player by type ID. + /// Gets a specific modifier from the player by its type ID. /// /// The PlayerControl instance. - /// The type ID of the modifier. + /// The type ID of the modifier. /// Optional predicate to filter the modifiers. /// The modifier if found, null otherwise. - public static BaseModifier? GetModifierByTypeId( + public static BaseModifier? GetModifier( this PlayerControl player, - uint id, + uint typeId, Func? predicate = null) { - return player.GetModifierComponent().GetModifierByTypeId(id, predicate); + return player.GetModifierComponent().GetModifier(typeId, predicate); } /// - /// Gets a specific modifier from the player by ID. + /// Gets a specific modifier from the player by its GUID. /// /// The PlayerControl instance. - /// The ID of the modifier. + /// The GUID of the modifier. /// The modifier if found, null otherwise. - public static BaseModifier? GetModifierByUniqueId( + public static BaseModifier? GetModifier( this PlayerControl player, - uint id) + Guid uniqueId) { - return player.GetModifierComponent().GetModifierByUniqueId(id); + return player.GetModifierComponent().GetModifier(uniqueId); } /// @@ -251,7 +251,7 @@ public static class ModifierExtensions /// The type of the modifiers. /// The PlayerControl instance. /// Optional predicate to filter the modifiers. - /// An enumerable of modifiers. + /// A collection of modifiers. public static IEnumerable GetModifiers(this PlayerControl player, Func? predicate = null) where T : BaseModifier { @@ -264,7 +264,7 @@ public static class ModifierExtensions /// The PlayerControl instance. /// The type of the modifiers. /// Optional predicate to filter the modifiers. - /// An enumerable of modifiers. + /// A collection of modifiers. public static IEnumerable GetModifiers( this PlayerControl player, Type type, @@ -277,15 +277,15 @@ public static class ModifierExtensions /// Gets all modifiers of a specific type from the player by type ID. /// /// The PlayerControl instance. - /// The type ID of the modifiers. + /// The type ID of the modifiers. /// Optional predicate to filter the modifiers. - /// An enumerable of modifiers. + /// A collection of modifiers. public static IEnumerable GetModifiers( this PlayerControl player, - uint id, + uint typeId, Func? predicate = null) { - return player.GetModifierComponent().GetModifiers(id, predicate); + return player.GetModifierComponent().GetModifiers(typeId, predicate); } /// @@ -322,29 +322,29 @@ public static class ModifierExtensions } /// - /// Removes a specific modifier from the player by ID. + /// Removes a specific modifier from the player by its type ID. /// /// The PlayerControl instance. - /// The ID of the modifier. + /// The type ID of the modifier. /// Optional predicate to filter the modifiers. - public static void RemoveModifierByTypeId( + public static void RemoveModifier( this PlayerControl player, uint typeId, Func? predicate = null) { - player.GetModifierComponent().RemoveModifierByTypeId(typeId, predicate); + player.GetModifierComponent().RemoveModifier(typeId, predicate); } /// - /// Removes a specific modifier from the player by ID. + /// Removes a specific modifier from the player by its GUID. /// /// The PlayerControl instance. - /// The ID of the modifier. - public static void RemoveModifierByUniqueId( + /// The GUID of the modifier. + public static void RemoveModifier( this PlayerControl player, - uint uniqueId) + Guid uniqueId) { - player.GetModifierComponent().RemoveModifierByUniqueId(uniqueId); + player.GetModifierComponent().RemoveModifier(uniqueId); } /// diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 209423e..4285a73 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -3,12 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using MiraAPI.Modifiers.Types; -using MiraAPI.Networking; -using MiraAPI.Networking.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; -using Reactor.Networking.Rpc; using Reactor.Utilities; using Reactor.Utilities.Extensions; using Random = System.Random; @@ -159,70 +156,6 @@ public static class ModifierManager private static bool IsGameModifierValid(PlayerControl player, GameModifier modifier, uint modifierId) { return (player.Data.Role is not ICustomRole role || role.IsModifierApplicable(modifier)) && - modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifierByTypeId(modifierId); - } - - internal static void SyncAllModifiers(int targetId = -1) - { - var data = new List(); - - foreach (var player in PlayerControl.AllPlayerControls) - { - data.Add(GetPlayerModifiers(player)); - } - - Rpc.Instance.SendTo(PlayerControl.LocalPlayer, targetId, [.. data]); - } - - internal static void HandleSyncModifiers(NetData[] data) - { - foreach (var netData in data) - { - var ids = new uint[netData.Data.Length / 4]; - Buffer.BlockCopy(netData.Data, 0, ids, 0, netData.Data.Length); - - var plr = GameData.Instance.GetPlayerById((byte)netData.Id).Object; - var modifierComponent = plr.GetComponent(); - - if (!modifierComponent) - { - continue; - } - - modifierComponent.ClearModifiers(); - - foreach (var id in ids) - { - if (!IdToTypeModifierMap.TryGetValue(id, out var type)) - { - Logger.Error($"Cannot add modifier with id {id} because it is not registered."); - continue; - } - - modifierComponent.AddModifier(type); - } - } - } - - private static NetData GetPlayerModifiers(PlayerControl? player) - { - if (player == null || !player) - { - return new NetData(0, []); - } - - List bytes = []; - var modifierComponent = player.GetComponent(); - if (modifierComponent == null || !modifierComponent) - { - return new NetData(player.PlayerId, []); - } - - foreach (var modifier in modifierComponent.ActiveModifiers) - { - bytes.AddRange(BitConverter.GetBytes(TypeToIdModifierMap[modifier.GetType()])); - } - - return new NetData(player.PlayerId, [.. bytes]); + modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifier(modifierId); } } diff --git a/MiraAPI/Networking/GuidConverter.cs b/MiraAPI/Networking/GuidConverter.cs new file mode 100644 index 0000000..c00d759 --- /dev/null +++ b/MiraAPI/Networking/GuidConverter.cs @@ -0,0 +1,32 @@ +using System; +using Hazel; +using Reactor.Networking.Attributes; +using Reactor.Networking.Serialization; + +namespace MiraAPI.Networking; + +/// +/// Converter for . +/// +[MessageConverter] +public class GuidConverter : MessageConverter +{ + /// + /// Writes a to the writer. + /// + /// + public override void Write(MessageWriter writer, Guid value) + { + writer.WriteBytesAndSize(value.ToByteArray()); + } + + /// + /// Reads a from the reader. + /// + /// + /// A GUID object. + public override Guid Read(MessageReader reader, Type objectType) + { + return new Guid(reader.ReadBytesAndSize()); + } +} diff --git a/MiraAPI/Networking/Modifiers/AddModifierRpc.cs b/MiraAPI/Networking/Modifiers/AddModifierRpc.cs index 5727ba5..2e7b19e 100644 --- a/MiraAPI/Networking/Modifiers/AddModifierRpc.cs +++ b/MiraAPI/Networking/Modifiers/AddModifierRpc.cs @@ -21,7 +21,8 @@ public class AddModifierRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc public override void Write(MessageWriter writer, ModifierData data) { - writer.WritePacked(data.Id); + writer.WritePacked(data.TypeId); + writer.WriteBytesAndSize(data.UniqueId.ToByteArray()); writer.WritePacked(data.Args.Length); foreach (var arg in data.Args) { @@ -34,6 +35,7 @@ public class AddModifierRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc public override void Handle(PlayerControl player, ModifierData data) { - player.AddModifier(data.Id, data.Args); + var type = ModifierManager.GetModifierType(data.TypeId) ?? throw new InvalidOperationException($"Modifier type not found for ID {data.TypeId}."); + BaseModifier? modifier; + if (data.Args.Length > 0) + { + modifier = ModifierFactory.CreateInstance(type, data.Args); + } + else + { + modifier = Activator.CreateInstance(type) as BaseModifier; + if (modifier == null) + { + throw new InvalidOperationException($"Cannot add modifier {type.Name} because it is not a valid modifier."); + } + } + modifier.UniqueId = data.UniqueId; + player.AddModifier(modifier); } } diff --git a/MiraAPI/Networking/Modifiers/ModifierData.cs b/MiraAPI/Networking/Modifiers/ModifierData.cs new file mode 100644 index 0000000..3d3f158 --- /dev/null +++ b/MiraAPI/Networking/Modifiers/ModifierData.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiraAPI.Networking.Modifiers; + +/// +/// Modifier data for networking. +/// +/// The ID of the modifier. +/// The unique ID of the modifier. +/// Parameters for constructor. +public record struct ModifierData(uint TypeId, Guid UniqueId, object[] Args); diff --git a/MiraAPI/Networking/Modifiers/SyncModifiersRpc.cs b/MiraAPI/Networking/Modifiers/SyncModifiersRpc.cs deleted file mode 100644 index 0571198..0000000 --- a/MiraAPI/Networking/Modifiers/SyncModifiersRpc.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Hazel; -using MiraAPI.Modifiers; -using Reactor.Networking.Attributes; -using Reactor.Networking.Rpc; - -namespace MiraAPI.Networking.Modifiers; - -[RegisterCustomRpc((uint)MiraRpc.SyncModifiers)] -internal sealed class SyncModifiersRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) -{ - public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; - - public override void Write(MessageWriter writer, NetData[]? data) - { - if (data == null) - { - writer.WritePacked(0U); - return; - } - - writer.WritePacked((uint)data.Length); - foreach (var netData in data) - { - writer.WritePacked(netData.Id); - writer.WriteBytesAndSize(netData.Data); - } - } - - public override NetData[] Read(MessageReader reader) - { - var length = reader.ReadPackedUInt32(); - var data = new NetData[length]; - for (var i = 0; i < length; i++) - { - var dataId = reader.ReadPackedUInt32(); - var bytes = reader.ReadBytesAndSize(); - data[i] = new NetData(dataId, bytes); - } - - return data; - } - - public override void Handle(PlayerControl playerControl, NetData[]? data) - { - if (AmongUsClient.Instance.HostId != playerControl.OwnerId) - { - return; - } - - ModifierManager.HandleSyncModifiers(data ?? []); - } -} diff --git a/MiraAPI/Patches/AmongUsClientSyncPatch.cs b/MiraAPI/Patches/AmongUsClientSyncPatch.cs index 4a76283..6af91c1 100644 --- a/MiraAPI/Patches/AmongUsClientSyncPatch.cs +++ b/MiraAPI/Patches/AmongUsClientSyncPatch.cs @@ -3,16 +3,15 @@ using InnerNet; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Player; using MiraAPI.GameOptions; -using MiraAPI.Modifiers; using MiraAPI.Roles; namespace MiraAPI.Patches; /// -/// Sync all options, role settings, and modifiers to the player when they join the game. +/// Sync all settings to the player when they join the game. /// [HarmonyPatch(typeof(AmongUsClient))] -public static class AmongUsClientSyncPatch +internal static class AmongUsClientSyncPatch { [HarmonyPostfix] [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CreatePlayer))] @@ -33,7 +32,6 @@ public static class AmongUsClientSyncPatch ModdedOptionsManager.SyncAllOptions(clientData.Id); CustomRoleManager.SyncAllRoleSettings(clientData.Id); - ModifierManager.SyncAllModifiers(clientData.Id); } [HarmonyPostfix] diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs index 0ee6da4..184bafb 100644 --- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -16,7 +16,7 @@ internal static class TaskAddButtonPatches if (uint.TryParse(__instance.name, out var result)) { __instance.Overlay.sprite = __instance.CheckImage; - __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifierByTypeId(result); + __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifier(result); return false; } diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 0c35670..666a1ea 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -337,9 +337,9 @@ internal static class TaskAdderPatches taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => { var id = modifier.TypeId; - if (PlayerControl.LocalPlayer.HasModifierByTypeId(id)) + if (PlayerControl.LocalPlayer.HasModifier(id)) { - PlayerControl.LocalPlayer.RemoveModifierByTypeId(id); + PlayerControl.LocalPlayer.RemoveModifier(id); taskAddButton.Overlay.enabled = false; } else From 0e2eeea0691b9f89ff7ea41c600c8a5f310ffa1f Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:01:52 -0500 Subject: [PATCH 39/71] add check to prevent adding modifiers before a game has started --- MiraAPI/Modifiers/ModifierComponent.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 57abd8c..43f3a94 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -366,6 +366,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? AddModifier(BaseModifier modifier) { + if (!AmongUsClient.Instance.IsGameStarted) + { + Logger.Error("Cannot add modifier because the game has not started yet."); + return null; + } + var id = modifier.TypeId; if (modifier.Unique && Modifiers.Find(x => x.TypeId == id) != null) { From 3015c6dd6a8499e03bcd38ec6cfac96951e32cca Mon Sep 17 00:00:00 2001 From: Zeo Date: Wed, 26 Feb 2025 18:26:07 +0000 Subject: [PATCH 40/71] Added method to override the intro team screen to be based on custom role --- MiraAPI/Patches/IntroCutscenePatches.cs | 42 +++---------------------- MiraAPI/Roles/ICustomRole.cs | 17 ++++++++++ 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index b98c58e..c3a8ead 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -32,7 +32,8 @@ public static class IntroCutscenePatches [HarmonyPrefix] [HarmonyPatch(nameof(IntroCutscene.BeginImpostor))] - public static void BeginImpostorPatch(IntroCutscene __instance) + [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] + public static void BeginImpostorPatch(IntroCutscene __instance, [HarmonyArgument(0)] ref Il2CppSystem.Collections.Generic.List yourTeam) { if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole) { @@ -45,46 +46,11 @@ public static class IntroCutscenePatches __instance.TeamTitle.color = introConfig.IntroTeamColor; __instance.TeamTitle.text = introConfig.IntroTeamTitle; __instance.ImpostorText.text = introConfig.IntroTeamDescription; + + customRole.SetupIntroTeam(__instance, ref yourTeam); } } - [HarmonyPrefix] - [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] - public static bool BeginCrewmatePatch(IntroCutscene __instance) - { - if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole) - { - return true; - } - - if (customRole.IntroConfiguration is { } introConfig) - { - __instance.BackgroundBar.material.SetColor(ShaderID.Color, introConfig.IntroTeamColor); - __instance.TeamTitle.color = introConfig.IntroTeamColor; - __instance.TeamTitle.text = introConfig.IntroTeamTitle; - __instance.ImpostorText.text = introConfig.IntroTeamDescription; - } - - if (customRole.Team is not ModdedRoleTeams.Custom) - { - return true; - } - - var barTransform = __instance.BackgroundBar.transform; - var position = barTransform.position; - position.y -= 0.25f; - barTransform.position = position; - - __instance.impostorScale = 1f; - - __instance.ourCrewmate = __instance.CreatePlayer( - 0, - Mathf.CeilToInt(7.5f), - PlayerControl.LocalPlayer.Data, - false); - return false; - } - /* [HarmonyPostfix] [HarmonyPatch(nameof(IntroCutscene.OnDestroy))] diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index a452d1e..f316bf3 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -63,6 +63,23 @@ public interface ICustomRole _ => null, }; + /// + /// Allows the role to specify who is shown on the intro team screen. + /// + /// The intro cutscene instance. + /// The reference to the list of player in the team. + void SetupIntroTeam(IntroCutscene instance, ref Il2CppSystem.Collections.Generic.List yourTeam) + { + if (Team == ModdedRoleTeams.Custom) + { + var team = new Il2CppSystem.Collections.Generic.List(); + + team.Add(PlayerControl.LocalPlayer); + + yourTeam = team; + } + } + /// /// Gets the parent mod of this role. /// From 81834a7f1aeebde6e255001a2f081d620c7f1429 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:38:25 -0500 Subject: [PATCH 41/71] allow modifier in lobby with error --- MiraAPI/Modifiers/ModifierComponent.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 43f3a94..57850b4 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -366,10 +366,9 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? AddModifier(BaseModifier modifier) { - if (!AmongUsClient.Instance.IsGameStarted) + if (LobbyBehaviour.Instance) { - Logger.Error("Cannot add modifier because the game has not started yet."); - return null; + Logger.Warning($"Modifiers added in the lobby won't sync to new players!"); } var id = modifier.TypeId; From a619a975d7382681ba09978417946e7a43050dd7 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:24:05 -0500 Subject: [PATCH 42/71] fix modifier factory issue --- MiraAPI/Modifiers/ModifierFactory.cs | 38 ++++++++---------- MiraAPI/Utilities/Extensions.cs | 59 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 21 deletions(-) diff --git a/MiraAPI/Modifiers/ModifierFactory.cs b/MiraAPI/Modifiers/ModifierFactory.cs index d145b82..8f8d005 100644 --- a/MiraAPI/Modifiers/ModifierFactory.cs +++ b/MiraAPI/Modifiers/ModifierFactory.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; +using MiraAPI.Utilities; namespace MiraAPI.Modifiers; @@ -8,19 +11,11 @@ namespace MiraAPI.Modifiers; /// public static class ModifierFactory { - private static Func? _constructor; + private static readonly Dictionary<(Type, Type[]), Func> _constructorCache = []; private static Func CreateConstructor(Type type, params object[] args) { - var constructorInfo = Array.Find( - type.GetConstructors(), - x => - { - var parameters = x.GetParameters(); - return parameters.Length == args.Length && Array.TrueForAll( - parameters, - t => t.ParameterType.IsInstanceOfType(args[t.Position])); - }) ?? throw new InvalidOperationException( + var constructorInfo = type.GetBestConstructor(args) ?? throw new InvalidOperationException( $"Could not find a constructor for type {type} with the specified arguments."); var parameters = constructorInfo.GetParameters(); @@ -50,8 +45,16 @@ public static class ModifierFactory /// An instance of the modifier. public static BaseModifier CreateInstance(Type type, params object[] args) { - _constructor ??= CreateConstructor(type, args); - return _constructor(args); + var argTypes = args.Select(arg => arg?.GetType() ?? typeof(object)).ToArray(); + var key = (type, argTypes); + + if (!_constructorCache.TryGetValue(key, out var constructor)) + { + constructor = CreateConstructor(type, args); + _constructorCache[key] = constructor; + } + + return constructor(args); } } @@ -65,15 +68,8 @@ public static class ModifierFactory where T : BaseModifier private static Func CreateConstructor(params object[] args) { - var constructorInfo = Array.Find( - typeof(T).GetConstructors(), - x => - { - var parameters = x.GetParameters(); - return parameters.Length == args.Length && Array.TrueForAll( - parameters, - t => t.ParameterType.IsInstanceOfType(args[t.Position])); - }) ?? throw new InvalidOperationException($"Could not find a constructor for type {typeof(T)} with the specified arguments."); + var constructorInfo = typeof(T).GetBestConstructor(args) ?? throw new InvalidOperationException( + $"Could not find a constructor for type {typeof(T)} with the specified arguments."); var parameters = constructorInfo.GetParameters(); diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 8d95294..1faeb94 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -6,6 +6,7 @@ using Reactor.Utilities; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using TMPro; using UnityEngine; @@ -38,6 +39,64 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Gets the best constructor for a type based on the specified arguments. + /// + /// The type to get the constructor from. + /// The arguments to pass into the constructor. + /// The best constructor. + public static ConstructorInfo? GetBestConstructor(this Type type, params object[] args) + { + return type.GetValidConstructors(args) + .OrderBy( + ctor => ctor.GetParameters() + .Select((p, i) => GetInheritanceDistance(args[i].GetType(), p.ParameterType)) + .Sum()) + .FirstOrDefault(); + } + + /// + /// Gets the constructors of a type that match the specified arguments. + /// + /// The type to get constructors from. + /// The arguments to pass into the constructor. + /// A collection of valid constructors. + public static IEnumerable GetValidConstructors(this Type type, params object[] args) + { + return type.GetConstructors().Where( + x => + { + var parameters = x.GetParameters(); + return parameters.Length == args.Length && Array.TrueForAll( + parameters, + t => t.ParameterType.IsInstanceOfType(args[t.Position])); + }); + } + + /// + /// Calculates the inheritance distance from the given type to its target base type. + /// Lower values mean the type is a closer match. + /// + /// The derived type. + /// The base type. + /// The distance between the types. + public static int GetInheritanceDistance(Type from, Type to) + { + if (!from.IsAssignableFrom(to)) + { + return int.MaxValue; + } + + var type = from; + var distance = 0; + while (type != null && type != to) + { + type = type.BaseType; + distance++; + } + return type == to ? distance : int.MaxValue; + } + /// /// Enables stencil masking on a TMP text object. /// From 6443b5932b0fc8936c1e39079eb8883b307ce2b9 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:24:41 -0500 Subject: [PATCH 43/71] modifier converter + cleanup --- MiraAPI.Example/Buttons/TestButton.cs | 32 --------------- MiraAPI.Example/Modifiers/ModifierParams.cs | 15 ------- MiraAPI/Modifiers/ModifierComponent.cs | 1 + .../Networking/Modifiers/ModifierConverter.cs | 40 +++++++++++++++++++ 4 files changed, 41 insertions(+), 47 deletions(-) delete mode 100644 MiraAPI.Example/Buttons/TestButton.cs delete mode 100644 MiraAPI.Example/Modifiers/ModifierParams.cs create mode 100644 MiraAPI/Networking/Modifiers/ModifierConverter.cs diff --git a/MiraAPI.Example/Buttons/TestButton.cs b/MiraAPI.Example/Buttons/TestButton.cs deleted file mode 100644 index 776dc40..0000000 --- a/MiraAPI.Example/Buttons/TestButton.cs +++ /dev/null @@ -1,32 +0,0 @@ -using MiraAPI.Example.Modifiers; -using MiraAPI.Hud; -using MiraAPI.Modifiers; -using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; -using UnityEngine; - -namespace MiraAPI.Example.Buttons; - -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() - { - if (PlayerControl.LocalPlayer.HasModifier()) - { - PlayerControl.LocalPlayer.RpcRemoveModifier(); - return; - } - - 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 deleted file mode 100644 index e92bbda..0000000 --- a/MiraAPI.Example/Modifiers/ModifierParams.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MiraAPI.Modifiers; -using Reactor.Utilities; - -namespace MiraAPI.Example.Modifiers; - -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}"); - } -} diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 57850b4..e7c6991 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -366,6 +366,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? AddModifier(BaseModifier modifier) { + // TODO: Make a proper synchronization system. if (LobbyBehaviour.Instance) { Logger.Warning($"Modifiers added in the lobby won't sync to new players!"); diff --git a/MiraAPI/Networking/Modifiers/ModifierConverter.cs b/MiraAPI/Networking/Modifiers/ModifierConverter.cs new file mode 100644 index 0000000..e8afa1b --- /dev/null +++ b/MiraAPI/Networking/Modifiers/ModifierConverter.cs @@ -0,0 +1,40 @@ +using System; +using Hazel; +using InnerNet; +using MiraAPI.Modifiers; +using Reactor.Networking.Attributes; +using Reactor.Networking.Serialization; + +namespace MiraAPI.Networking.Modifiers; + +/// +/// Converter for serializing and deserializing objects. +/// +[MessageConverter] +public class ModifierConverter : MessageConverter +{ + /// + /// Writes a to the writer. + /// + /// The writer to write to. + /// The to write. + public override void Write(MessageWriter writer, BaseModifier value) + { + writer.WriteNetObject(value.Player); + writer.WriteBytesAndSize(value.UniqueId.ToByteArray()); + } + + /// + /// Reads a from the reader. + /// + /// The reader to read from. + /// The type of the object to read. + /// The that was read. + /// Thrown if the modifier is not found. + public override BaseModifier Read(MessageReader reader, Type objectType) + { + var player = reader.ReadNetObject(); + var guid = new Guid(reader.ReadBytesAndSize()); + return player.GetModifier(guid) ?? throw new InvalidOperationException($"Modifier with GUID {guid} not found for player {player.NetId}."); + } +} From 26b94b01278ac58f1adc1d26689d495adc0a19b9 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:29:46 -0500 Subject: [PATCH 44/71] fix ordering of properties in BaseModifier.cs --- MiraAPI/Modifiers/BaseModifier.cs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 52a2bb0..473c78f 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -17,6 +17,21 @@ public abstract class BaseModifier /// public ModifierComponent? ModifierComponent { get; internal set; } + /// + /// Gets a value indicating whether the modifier has been initialized. + /// + public bool Initialized { get; internal set; } + + /// + /// Gets the unique ID of the modifier. + /// + public Guid UniqueId { get; internal set; } = Guid.Empty; + + /// + /// Gets the type ID of the modifier. + /// + public uint TypeId => ModifierManager.GetModifierTypeId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered."); + /// /// Gets the modifier name. /// @@ -37,21 +52,6 @@ public abstract class BaseModifier /// public virtual bool Unique => true; - /// - /// Gets a value indicating whether the modifier has been initialized. - /// - public bool Initialized { get; internal set; } - - /// - /// Gets the unique ID of the modifier. - /// - public Guid UniqueId { get; internal set; } = Guid.Empty; - - /// - /// Gets the type ID of the modifier. - /// - public uint TypeId => ModifierManager.GetModifierTypeId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered."); - /// /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true. /// From 65a81d0fb4e63ee29ebc5f4372a085f9f61b5284 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:34:31 -0500 Subject: [PATCH 45/71] fix broken wav audio resource loader --- .../Assets/LoadableAudioResourceAsset.cs | 120 ++++++++++++++---- 1 file changed, 95 insertions(+), 25 deletions(-) diff --git a/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs b/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs index 381e9bc..a9b8eb1 100644 --- a/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs @@ -1,9 +1,8 @@ using System; using System.IO; using System.Reflection; +using System.Resources; using System.Text; -using Cpp2IL.Core.Extensions; -using Reactor.Utilities.Extensions; using UnityEngine; namespace MiraAPI.Utilities.Assets; @@ -21,41 +20,112 @@ public class LoadableAudioResourceAsset(string path) : LoadableAsset /// /// The asset to load. /// Attempted to load an Audio file in non WAV format. - /// Stream failed to load. Check if the name of your asset was correct. + /// Stream failed to load. Check if the name of your asset was correct. public override AudioClip LoadAsset() { - var assetStream = _assembly.GetManifestResourceStream(path) ?? - throw new FileNotFoundException( - "Stream failed to load. Check if the name of your asset was correct."); - var audioBytes = assetStream.ReadFully(); + using var assetStream = _assembly.GetManifestResourceStream(path) + ?? throw new MissingManifestResourceException($"Stream failed to load. Check if the asset name is correct: {path}"); - var riffHeader = Encoding.ASCII.GetString(audioBytes.SubArray(0, 4)); - var waveHeader = Encoding.ASCII.GetString(audioBytes.SubArray(8, 4)); + using var reader = new BinaryReader(assetStream); - if (riffHeader != "RIFF" || waveHeader != "WAVE") + // Read RIFF header + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF") { - throw new NotSupportedException($"Attempted to load an Audio file in non WAV format '{path}'."); + throw new NotSupportedException("Invalid WAV file (missing RIFF header)."); } - int channels = BitConverter.ToInt16(audioBytes, 22); - var sampleRate = BitConverter.ToInt32(audioBytes, 24); - var dataSize = BitConverter.ToInt32(audioBytes, 40); + reader.ReadInt32(); // File size (ignore) - var audioData = new float[dataSize / 2]; - for (var i = 0; i < audioData.Length; i++) + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE") { - audioData[i] = BitConverter.ToInt16(audioBytes, 44 + i * 2) / 32768.0f; + throw new NotSupportedException("Invalid WAV file (missing WAVE header)."); } - var audioClip = AudioClip.Create( - path, - audioData.Length, - channels, - sampleRate, - false - ); - audioClip.SetData(audioData, 0); + // Read chunks + int channels = 0, sampleRate = 0, bitsPerSample = 0; + byte[]? audioData = null; + + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4)); + var chunkSize = reader.ReadInt32(); + + // Format chunk + if (chunkId == "fmt ") + { + int audioFormat = reader.ReadInt16(); + channels = reader.ReadInt16(); + sampleRate = reader.ReadInt32(); + reader.ReadInt32(); // Byte rate (ignore) + reader.ReadInt16(); // Block align (ignore) + bitsPerSample = reader.ReadInt16(); + + if (audioFormat != 1) // Only PCM supported + throw new NotSupportedException("Only PCM WAV files are supported."); + } + // Data chunk + else if (chunkId == "data") + { + audioData = reader.ReadBytes(chunkSize); + break; // No need to read further + } + else + { + reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); // Skip unknown chunks + } + } + + if (audioData == null) + { + throw new InvalidOperationException("WAV file does not contain audio data."); + } + + if (channels == 0 || sampleRate == 0 || bitsPerSample == 0) + { + throw new InvalidOperationException("WAV file does not contain valid format information."); + } + + // Convert PCM data to float array + var samples = ConvertPcmToFloat(audioData, bitsPerSample); + + // Create the AudioClip + var audioClip = AudioClip.Create(path, samples.Length / channels, channels, sampleRate, false); + audioClip.SetData(samples, 0); return audioClip; } + + // This is somewhat magic to me, I just copied it from elsewhere. + private static float[] ConvertPcmToFloat(byte[] pcmData, int bitsPerSample) + { + var sampleCount = pcmData.Length / (bitsPerSample / 8); + var floatData = new float[sampleCount]; + + for (var i = 0; i < sampleCount; i++) + { + var byteIndex = i * (bitsPerSample / 8); + + switch (bitsPerSample) + { + case 8: + floatData[i] = (pcmData[byteIndex] - 128) / 128f; // 8-bit PCM is unsigned + break; + case 16: + floatData[i] = BitConverter.ToInt16(pcmData, byteIndex) / 32768f; + break; + case 24: + var sample24 = pcmData[byteIndex] | (pcmData[byteIndex + 1] << 8) | (pcmData[byteIndex + 2] << 16); + if (sample24 > 0x7FFFFF) sample24 -= 0x1000000; // Convert from unsigned to signed + floatData[i] = sample24 / 8388608f; + break; + case 32: + floatData[i] = BitConverter.ToInt32(pcmData, byteIndex) / 2147483648f; + break; + default: + throw new NotSupportedException($"Unsupported bit depth: {bitsPerSample}"); + } + } + + return floatData; + } } From d1116b3fce0284316c641e00a91c6fdc5ccc97cf Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:15:21 -0500 Subject: [PATCH 46/71] comment on miraapiplugin --- MiraAPI/MiraApiPlugin.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index cfe314d..3380820 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -10,17 +10,20 @@ using UnityEngine; namespace MiraAPI; +/// +/// The main plugin class for Mira API. +/// [BepInAutoPlugin("mira.api", "MiraAPI")] [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] [ReactorModFlags(ModFlags.RequireOnAllClients)] public partial class MiraApiPlugin : BasePlugin { - private static MiraPluginManager? PluginManager { get; set; } - - public Harmony Harmony { get; } = new(Id); public static Color MiraColor { get; } = new Color32(238, 154, 112, 255); + private static MiraPluginManager? PluginManager { get; set; } + internal Harmony Harmony { get; } = new(Id); + /// public override void Load() { Harmony.PatchAll(); From c9ff839f12bb67b4206364c5736d99c42496c3db Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:21:04 -0500 Subject: [PATCH 47/71] improve flexibility of setup team intro --- MiraAPI/Patches/IntroCutscenePatches.cs | 8 +++--- MiraAPI/Roles/ICustomRole.cs | 37 +++++++++++++------------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index c3a8ead..415e9d6 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -33,11 +33,11 @@ public static class IntroCutscenePatches [HarmonyPrefix] [HarmonyPatch(nameof(IntroCutscene.BeginImpostor))] [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] - public static void BeginImpostorPatch(IntroCutscene __instance, [HarmonyArgument(0)] ref Il2CppSystem.Collections.Generic.List yourTeam) + public static bool BeginImpostorPatch(IntroCutscene __instance, [HarmonyArgument(0)] ref Il2CppSystem.Collections.Generic.List yourTeam) { if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole) { - return; + return true; } if (customRole.IntroConfiguration is { } introConfig) @@ -46,9 +46,9 @@ public static class IntroCutscenePatches __instance.TeamTitle.color = introConfig.IntroTeamColor; __instance.TeamTitle.text = introConfig.IntroTeamTitle; __instance.ImpostorText.text = introConfig.IntroTeamDescription; - - customRole.SetupIntroTeam(__instance, ref yourTeam); } + + return customRole.SetupIntroTeam(__instance, ref yourTeam); } /* diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index f316bf3..0adc386 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -63,23 +63,6 @@ public interface ICustomRole _ => null, }; - /// - /// Allows the role to specify who is shown on the intro team screen. - /// - /// The intro cutscene instance. - /// The reference to the list of player in the team. - void SetupIntroTeam(IntroCutscene instance, ref Il2CppSystem.Collections.Generic.List yourTeam) - { - if (Team == ModdedRoleTeams.Custom) - { - var team = new Il2CppSystem.Collections.Generic.List(); - - team.Add(PlayerControl.LocalPlayer); - - yourTeam = team; - } - } - /// /// Gets the parent mod of this role. /// @@ -175,6 +158,26 @@ public interface ICustomRole return (PlayerControl.LocalPlayer.Data.Role.IsImpostor && player.Data.Role.IsImpostor) || PlayerControl.LocalPlayer.Data.IsDead; } + /// + /// Allows the role to specify who is shown on the intro team screen. + /// + /// The intro cutscene instance. + /// The reference to the list of player in the team. + /// True to use the original team intro code, false to skip. + public virtual bool SetupIntroTeam(IntroCutscene instance, ref Il2CppSystem.Collections.Generic.List yourTeam) + { + if (Team == ModdedRoleTeams.Custom) + { + var team = new Il2CppSystem.Collections.Generic.List(); + + team.Add(PlayerControl.LocalPlayer); + + yourTeam = team; + } + + return true; + } + /// /// This method runs on the HudManager.Update method ONLY when the LOCAL player has this role. /// From 9ec270bd6851b8c156152b25be21bf34b7a46c45 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:22:42 -0500 Subject: [PATCH 48/71] use precomputed shader ID --- MiraAPI/Utilities/Extensions.cs | 4 ++-- MiraAPI/Utilities/ShaderID.cs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 6912ac8..9316ba8 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -46,8 +46,8 @@ public static class Extensions /// The TMP text. public static void EnableStencilMasking(this TMP_Text text) { - text.fontMaterial.SetFloat(ShaderID.Get("_Stencil"), 1); - text.fontMaterial.SetFloat(ShaderID.Get("_StencilComp"), 4); + text.fontMaterial.SetFloat(ShaderID.Stencil, 1); + text.fontMaterial.SetFloat(ShaderID.StencilComp, 4); } /// diff --git a/MiraAPI/Utilities/ShaderID.cs b/MiraAPI/Utilities/ShaderID.cs index d250336..3ef92e6 100644 --- a/MiraAPI/Utilities/ShaderID.cs +++ b/MiraAPI/Utilities/ShaderID.cs @@ -1,8 +1,12 @@ using System.Collections.Generic; using UnityEngine; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace MiraAPI.Utilities; +/// +/// Utility class for caching shader property IDs. +/// public static class ShaderID { private static readonly Dictionary Cache = []; 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 49/71] 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 50/71] 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 From 9e2787b78146c40036a52de0275fe3eee1842c60 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:19:34 -0500 Subject: [PATCH 51/71] fix intro cutscene bug --- MiraAPI/Patches/IntroCutscenePatches.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index 415e9d6..df15627 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -33,11 +33,19 @@ public static class IntroCutscenePatches [HarmonyPrefix] [HarmonyPatch(nameof(IntroCutscene.BeginImpostor))] [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] - public static bool BeginImpostorPatch(IntroCutscene __instance, [HarmonyArgument(0)] ref Il2CppSystem.Collections.Generic.List yourTeam) + public static bool BeginPrefix(IntroCutscene __instance, [HarmonyArgument(0)] ref Il2CppSystem.Collections.Generic.List yourTeam) + { + return PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole || customRole.SetupIntroTeam(__instance, ref yourTeam); + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(IntroCutscene.BeginImpostor))] + [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] + public static void BeginPostfix(IntroCutscene __instance) { if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole) { - return true; + return; } if (customRole.IntroConfiguration is { } introConfig) @@ -47,8 +55,6 @@ public static class IntroCutscenePatches __instance.TeamTitle.text = introConfig.IntroTeamTitle; __instance.ImpostorText.text = introConfig.IntroTeamDescription; } - - return customRole.SetupIntroTeam(__instance, ref yourTeam); } /* From 3d91c8de25e74e1cbdf57d36217938a6abfd28a8 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:34:27 -0500 Subject: [PATCH 52/71] allow any player to trigger gameover, but add verification --- MiraAPI/GameEnd/CustomGameOver.cs | 15 +++++++++------ MiraAPI/Networking/CustomGameOverRpc.cs | 8 +++++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/MiraAPI/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs index e54c750..7f3d43a 100644 --- a/MiraAPI/GameEnd/CustomGameOver.cs +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -15,6 +15,15 @@ public abstract class CustomGameOver /// public static CustomGameOver? Instance { get; internal set; } + /// + /// Verifies if the condition for this CustomGameOver is met. + /// + /// True if the condition is met, otherwise false. + public virtual bool VerifyCondition() + { + return true; + } + /// /// Runs before the base game calls EndGameManager.SetEverythingUp. /// @@ -50,12 +59,6 @@ public abstract class CustomGameOver /// Type of the custom game over. public static void Trigger(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]); diff --git a/MiraAPI/Networking/CustomGameOverRpc.cs b/MiraAPI/Networking/CustomGameOverRpc.cs index a2419f0..61d1f63 100644 --- a/MiraAPI/Networking/CustomGameOverRpc.cs +++ b/MiraAPI/Networking/CustomGameOverRpc.cs @@ -48,11 +48,17 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc< { if (GameOverManager.TryGetGameOver(data.Reason, out var gameOver)) { + if (!gameOver!.VerifyCondition()) + { + Logger.Info($"Game over condition not met for {gameOver.GetType().Name}"); + return; + } + CustomGameOver.Instance = gameOver; if (AmongUsClient.Instance.AmHost) { - GameManager.Instance.RpcEndGame(gameOver!, false); + GameManager.Instance.RpcEndGame(gameOver, false); } } else From e3cf884975bc03e0b89d9389f3c3ad1e02211681 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:40:57 -0500 Subject: [PATCH 53/71] add player to verify condition --- MiraAPI.Example/GameOver/NeutralKillerGameOver.cs | 8 +++++++- MiraAPI/GameEnd/CustomGameOver.cs | 3 ++- MiraAPI/Networking/CustomGameOverRpc.cs | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs index 8d48a64..f513e8d 100644 --- a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs +++ b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs @@ -1,4 +1,5 @@ -using MiraAPI.GameEnd; +using MiraAPI.Example.Roles; +using MiraAPI.GameEnd; using MiraAPI.Utilities; using UnityEngine; @@ -6,6 +7,11 @@ namespace MiraAPI.Example.GameOver; public class NeutralKillerGameOver : CustomGameOver { + public override bool VerifyCondition(PlayerControl playerControl) + { + return playerControl.Data.Role is NeutralKillerRole; + } + public override void AfterEndGameSetup(EndGameManager endGameManager) { endGameManager.WinText.text = "Outcast Killer Wins!"; diff --git a/MiraAPI/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs index 7f3d43a..bfb048f 100644 --- a/MiraAPI/GameEnd/CustomGameOver.cs +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -18,8 +18,9 @@ public abstract class CustomGameOver /// /// Verifies if the condition for this CustomGameOver is met. /// + /// The PlayerControl that requested the Game Over. /// True if the condition is met, otherwise false. - public virtual bool VerifyCondition() + public virtual bool VerifyCondition(PlayerControl playerControl) { return true; } diff --git a/MiraAPI/Networking/CustomGameOverRpc.cs b/MiraAPI/Networking/CustomGameOverRpc.cs index 61d1f63..c269483 100644 --- a/MiraAPI/Networking/CustomGameOverRpc.cs +++ b/MiraAPI/Networking/CustomGameOverRpc.cs @@ -48,7 +48,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc< { if (GameOverManager.TryGetGameOver(data.Reason, out var gameOver)) { - if (!gameOver!.VerifyCondition()) + if (!gameOver!.VerifyCondition(innerNetObject)) { Logger.Info($"Game over condition not met for {gameOver.GetType().Name}"); return; From f77b694e789f16fd6c56b796a34bdce64cca00c6 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:41:45 -0500 Subject: [PATCH 54/71] cleanup code --- MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs | 4 ++-- MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs | 4 ++-- MiraAPI/GameEnd/CustomGameOver.cs | 1 - MiraAPI/GameModes/CustomGameModeManager.cs | 4 ++-- .../Attributes/ModdedEnumOptionAttribute.cs | 4 ++-- .../Attributes/ModdedNumberOptionAttribute.cs | 6 +++--- .../Attributes/ModdedToggleOptionAttribute.cs | 4 ++-- .../GameOptions/OptionTypes/ModdedNumberOption.cs | 4 ++-- MiraAPI/GameOptions/OptionTypes/ModdedOption.cs | 4 ++-- .../GameOptions/OptionTypes/ModdedToggleOption.cs | 4 ++-- MiraAPI/Hud/CustomActionButton.cs | 6 +++--- MiraAPI/Hud/CustomButtonManager.cs | 6 +++--- MiraAPI/Hud/CustomPlayerMenu.cs | 8 ++++---- MiraAPI/Modifiers/ModifierComponent.cs | 12 ++++++------ MiraAPI/Networking/CustomMurderRpc.cs | 6 +++--- MiraAPI/Patches/Events/CanUsePatches.cs | 6 +++--- MiraAPI/Patches/Events/UsePatches.cs | 6 +++--- MiraAPI/Patches/Events/VentEventPatches.cs | 4 ++-- MiraAPI/Patches/IntroCutscenePatches.cs | 1 - MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 4 ++-- MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 6 +++--- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 8 ++++---- MiraAPI/Patches/Options/NotificationPopperPatch.cs | 4 ++-- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 8 ++++---- MiraAPI/Patches/PlayerControlPatches.cs | 4 ++-- MiraAPI/Patches/Roles/RoleManagerPatches.cs | 4 ++-- MiraAPI/PluginLoading/MiraPluginInfo.cs | 6 +++--- MiraAPI/Roles/CustomRoleManager.cs | 8 ++++---- MiraAPI/Roles/ICustomRole.cs | 4 ++-- MiraAPI/Utilities/Extensions.cs | 8 ++++---- MiraAPI/Utilities/Helpers.cs | 4 ++-- 31 files changed, 80 insertions(+), 82 deletions(-) diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index ce20ddb..208ad6a 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -1,10 +1,10 @@ -using MiraAPI.Example.Options.Roles; +using System.Collections; +using MiraAPI.Example.Options.Roles; using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.Hud; using MiraAPI.Utilities.Assets; using Reactor.Utilities; -using System.Collections; using UnityEngine; namespace MiraAPI.Example.Buttons.Teleporter; diff --git a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs index 0e26665..a99c9e0 100644 --- a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs +++ b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs @@ -1,7 +1,7 @@ -using MiraAPI.Example.Roles; +using System; +using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; -using System; using MiraAPI.Utilities; namespace MiraAPI.Example.Options.Roles; diff --git a/MiraAPI/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs index bfb048f..44c225a 100644 --- a/MiraAPI/GameEnd/CustomGameOver.cs +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using MiraAPI.Networking; using Reactor.Networking.Rpc; -using Reactor.Utilities; namespace MiraAPI.GameEnd; diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index 388eb5d..e4305f5 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,7 +1,7 @@ -using Reactor.Utilities; -using System; +using System; using System.Collections.Generic; using System.Linq; +using Reactor.Utilities; namespace MiraAPI.GameModes; diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index db5594f..da6094b 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -1,6 +1,6 @@ -using MiraAPI.GameOptions.OptionTypes; -using System; +using System; using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; namespace MiraAPI.GameOptions.Attributes; diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index b7b85c2..d679051 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -1,7 +1,7 @@ -using MiraAPI.GameOptions.OptionTypes; -using MiraAPI.Utilities; -using System; +using System; using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; namespace MiraAPI.GameOptions.Attributes; diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs index c389292..d44a7c0 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs @@ -1,6 +1,6 @@ -using MiraAPI.GameOptions.OptionTypes; -using System; +using System; using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; namespace MiraAPI.GameOptions.Attributes; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index deb8052..f075662 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -1,7 +1,7 @@ -using AmongUs.GameOptions; +using System; +using AmongUs.GameOptions; using MiraAPI.Networking; using MiraAPI.Utilities; -using System; using UnityEngine; using Object = UnityEngine.Object; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index c199c12..9d42be0 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -1,10 +1,10 @@ -using BepInEx.Configuration; +using System; +using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; using MiraAPI.Roles; using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; -using System; using UnityEngine; namespace MiraAPI.GameOptions.OptionTypes; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 12f8f5f..1773512 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -1,5 +1,5 @@ -using MiraAPI.Networking; -using System; +using System; +using MiraAPI.Networking; using UnityEngine; using Object = UnityEngine.Object; diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 6b8b8cc..4981e15 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -1,9 +1,9 @@ -using MiraAPI.Events; +using System; +using System.Globalization; +using MiraAPI.Events; using MiraAPI.Events.Mira; using MiraAPI.Patches; using MiraAPI.Utilities.Assets; -using System; -using System.Globalization; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs index 2bdb9bf..dd10f6c 100644 --- a/MiraAPI/Hud/CustomButtonManager.cs +++ b/MiraAPI/Hud/CustomButtonManager.cs @@ -1,9 +1,9 @@ -using MiraAPI.Events.Mira; -using MiraAPI.PluginLoading; -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reflection; +using MiraAPI.Events.Mira; +using MiraAPI.PluginLoading; namespace MiraAPI.Hud; diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index a188596..27f52ac 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -1,11 +1,11 @@ -using AmongUs.GameOptions; +using System; +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; using MiraAPI.Patches.Stubs; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; using UnityEngine; using UnityEngine.Events; diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index e7c6991..1930c2a 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -1,14 +1,14 @@ -using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Modifiers.Types; -using MiraAPI.Utilities; -using Reactor.Utilities; -using Reactor.Utilities.Attributes; -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Modifiers.Types; +using MiraAPI.Utilities; +using Reactor.Utilities; +using Reactor.Utilities.Attributes; using TMPro; using UnityEngine; diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 1a46865..351157c 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -1,4 +1,6 @@ -using AmongUs.GameOptions; +using System.Collections; +using System.Linq; +using AmongUs.GameOptions; using Assets.CoreScripts; using BepInEx.Unity.IL2CPP.Utils; using MiraAPI.Events; @@ -6,8 +8,6 @@ using MiraAPI.Events.Vanilla.Gameplay; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; using Reactor.Utilities.Extensions; -using System.Collections; -using System.Linq; using UnityEngine; namespace MiraAPI.Networking; diff --git a/MiraAPI/Patches/Events/CanUsePatches.cs b/MiraAPI/Patches/Events/CanUsePatches.cs index aa80fd7..143f176 100644 --- a/MiraAPI/Patches/Events/CanUsePatches.cs +++ b/MiraAPI/Patches/Events/CanUsePatches.cs @@ -1,8 +1,8 @@ -using HarmonyLib; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Usables; -using System.Collections.Generic; -using System.Reflection; namespace MiraAPI.Patches.Events; diff --git a/MiraAPI/Patches/Events/UsePatches.cs b/MiraAPI/Patches/Events/UsePatches.cs index 0571cd6..a6c4b1d 100644 --- a/MiraAPI/Patches/Events/UsePatches.cs +++ b/MiraAPI/Patches/Events/UsePatches.cs @@ -1,7 +1,7 @@ -using HarmonyLib; -using MiraAPI.Events; -using System.Collections.Generic; +using System.Collections.Generic; using System.Reflection; +using HarmonyLib; +using MiraAPI.Events; using MiraAPI.Events.Vanilla.Usables; namespace MiraAPI.Patches.Events; diff --git a/MiraAPI/Patches/Events/VentEventPatches.cs b/MiraAPI/Patches/Events/VentEventPatches.cs index 0b1b1cd..d1e0476 100644 --- a/MiraAPI/Patches/Events/VentEventPatches.cs +++ b/MiraAPI/Patches/Events/VentEventPatches.cs @@ -1,7 +1,7 @@ -using HarmonyLib; +using System.Linq; +using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Usables; -using System.Linq; namespace MiraAPI.Patches.Events; diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index df15627..fb6144a 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -3,7 +3,6 @@ using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Roles; using MiraAPI.Utilities; -using UnityEngine; namespace MiraAPI.Patches; diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 92cd925..c641e88 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,9 +1,9 @@ -using HarmonyLib; +using System.Linq; +using HarmonyLib; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; using Reactor.Utilities.Extensions; -using System.Linq; using TMPro; using UnityEngine; using UnityEngine.Events; diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 0cdca29..3c65092 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,9 +1,9 @@ -using HarmonyLib; +using System; +using System.Linq; +using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; -using System; -using System.Linq; using TMPro; using UnityEngine; using UnityEngine.Events; diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index e9d3c42..4f8c060 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -1,11 +1,11 @@ -using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; -using System; -using System.Collections.Generic; -using System.Linq; using Reactor.Localization.Utilities; using UnityEngine; using UnityEngine.Events; diff --git a/MiraAPI/Patches/Options/NotificationPopperPatch.cs b/MiraAPI/Patches/Options/NotificationPopperPatch.cs index 54cbed0..02efe2b 100644 --- a/MiraAPI/Patches/Options/NotificationPopperPatch.cs +++ b/MiraAPI/Patches/Options/NotificationPopperPatch.cs @@ -1,7 +1,7 @@ -using HarmonyLib; +using System.Linq; +using HarmonyLib; using Il2CppSystem; using MiraAPI.Roles; -using System.Linq; namespace MiraAPI.Patches.Options; diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index f8dd48d..33061d4 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -1,4 +1,7 @@ -using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Networking; using MiraAPI.Roles; @@ -8,9 +11,6 @@ using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; using Reactor.Utilities; using Reactor.Utilities.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; using TMPro; using UnityEngine; using UnityEngine.Events; diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index 31fdbb8..b6fd6b8 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using System.Linq; +using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Events.Vanilla.Player; @@ -6,7 +7,6 @@ using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Roles; using Reactor.Utilities.Extensions; -using System.Linq; namespace MiraAPI.Patches; diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index ed1c5e2..f326239 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -1,10 +1,10 @@ -using AmongUs.GameOptions; +using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Modifiers; using MiraAPI.Roles; -using System.Linq; namespace MiraAPI.Patches.Roles; diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 81412e8..68d3ff0 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -1,9 +1,9 @@ -using BepInEx; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using BepInEx; using BepInEx.Configuration; using MiraAPI.GameModes; using MiraAPI.GameOptions; -using System.Collections.Generic; -using System.Collections.ObjectModel; using MiraAPI.Hud; using MiraAPI.Modifiers; diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index c31e922..a778a09 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -1,4 +1,7 @@ -using AmongUs.GameOptions; +using System; +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.Injection; using MiraAPI.Networking; @@ -8,9 +11,6 @@ using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; using Reactor.Utilities; using Reactor.Utilities.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; using TMPro; using UnityEngine; using Object = UnityEngine.Object; diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 0adc386..3c52fb8 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,9 +1,9 @@ -using BepInEx.Configuration; +using System.Text; +using BepInEx.Configuration; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; using Reactor.Utilities; -using System.Text; using UnityEngine; namespace MiraAPI.Roles; diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 7ebaecc..0a4fee3 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -1,12 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using HarmonyLib; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; using Reactor.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using TMPro; using UnityEngine; diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 29ae10e..6b3b79f 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,9 +1,9 @@ -using MiraAPI.Roles; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using MiraAPI.Roles; using TMPro; using UnityEngine; using Object = UnityEngine.Object; From 49d09bb44fb63a59b3f4b3f58cf2e5789fd2909c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 28 Feb 2025 06:24:26 -0500 Subject: [PATCH 55/71] use NotNullWhen attribute --- MiraAPI/GameEnd/GameOverManager.cs | 3 +- MiraAPI/Modifiers/ModifierComponent.cs | 9 +++-- MiraAPI/Modifiers/ModifierExtensions.cs | 52 +++++++++++++++++++++++++ MiraAPI/Networking/CustomGameOverRpc.cs | 2 +- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/MiraAPI/GameEnd/GameOverManager.cs b/MiraAPI/GameEnd/GameOverManager.cs index c25aa37..6144d8a 100644 --- a/MiraAPI/GameEnd/GameOverManager.cs +++ b/MiraAPI/GameEnd/GameOverManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Reactor.Utilities; namespace MiraAPI.GameEnd; @@ -51,7 +52,7 @@ public static class GameOverManager /// 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) + public static bool TryGetGameOver(int id, [NotNullWhen(true)] out CustomGameOver? customGameOver) { if (GameOverTypes.TryGetValue(id, out var gameOverType)) { diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 1930c2a..8c7f6f2 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; @@ -168,7 +169,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The Type of the Modifier. /// True if the modifier was found, false otherwise. [HideFromIl2Cpp] - public bool TryGetModifier(out T? modifier, Func? predicate = null) where T : BaseModifier + public bool TryGetModifier([NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : BaseModifier { modifier = GetModifier(predicate); return modifier != null; @@ -182,7 +183,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the modifier by. /// True if the modifier was found, false otherwise. [HideFromIl2Cpp] - public bool TryGetModifier(Type type, out BaseModifier? modifier, Func? predicate = null) + public bool TryGetModifier(Type type, [NotNullWhen(true)] out BaseModifier? modifier, Func? predicate = null) { modifier = GetModifier(type, predicate); return modifier != null; @@ -196,7 +197,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The predicate to check the modifier by. /// True if the modifier was found, false otherwise. [HideFromIl2Cpp] - public bool TryGetModifier(uint id, out BaseModifier? modifier, Func? predicate = null) + public bool TryGetModifier(uint id, [NotNullWhen(true)] out BaseModifier? modifier, Func? predicate = null) { modifier = GetModifier(id, predicate); return modifier != null; @@ -209,7 +210,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The modifier or null. /// True if the modifier was found, false otherwise. [HideFromIl2Cpp] - public bool TryGetModifier(Guid modifierGuid, out BaseModifier? modifier) + public bool TryGetModifier(Guid modifierGuid, [NotNullWhen(true)] out BaseModifier? modifier) { modifier = GetModifier(modifierGuid); return modifier != null; diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index 5d05d69..06d9f06 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using MiraAPI.Networking; using MiraAPI.Networking.Modifiers; using Reactor.Networking.Attributes; @@ -190,6 +191,57 @@ public static class ModifierExtensions return player.GetModifierComponent().HasModifier(uniqueId); } + /// + /// Tries to get a modifier by its type. + /// + /// The PlayerControl instance. + /// The modifier or null. + /// The predicate to check the modifier by. + /// The Type of the Modifier. + /// True if the modifier was found, false otherwise. + public static bool TryGetModifier(this PlayerControl player, [NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : BaseModifier + { + return player.GetModifierComponent().TryGetModifier(out modifier, predicate); + } + + /// + /// Tries to get a modifier by its type. + /// + /// The PlayerControl instance. + /// The modifier type. + /// The modifier or null. + /// The predicate to check the modifier by. + /// True if the modifier was found, false otherwise. + public static bool TryGetModifier(this PlayerControl player, Type type, [NotNullWhen(true)] out BaseModifier? modifier, Func? predicate = null) + { + return player.GetModifierComponent().TryGetModifier(type, out modifier, predicate); + } + + /// + /// Tries to get a modifier by its type ID. + /// + /// The PlayerControl instance. + /// The modifier type ID. + /// The modifier or null. + /// The predicate to check the modifier by. + /// True if the modifier was found, false otherwise. + public static bool TryGetModifier(this PlayerControl player, uint id, [NotNullWhen(true)] out BaseModifier? modifier, Func? predicate = null) + { + return player.GetModifierComponent().TryGetModifier(id, out modifier, predicate); + } + + /// + /// Tries to get a modifier by its unique ID. + /// + /// The PlayerControl instance. + /// The modifier unique ID. + /// The modifier or null. + /// True if the modifier was found, false otherwise. + public static bool TryGetModifier(this PlayerControl player, Guid modifierGuid, [NotNullWhen(true)] out BaseModifier? modifier) + { + return player.GetModifierComponent().TryGetModifier(modifierGuid, out modifier); + } + /// /// Gets a specific modifier from the player. /// diff --git a/MiraAPI/Networking/CustomGameOverRpc.cs b/MiraAPI/Networking/CustomGameOverRpc.cs index c269483..978b992 100644 --- a/MiraAPI/Networking/CustomGameOverRpc.cs +++ b/MiraAPI/Networking/CustomGameOverRpc.cs @@ -48,7 +48,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc< { if (GameOverManager.TryGetGameOver(data.Reason, out var gameOver)) { - if (!gameOver!.VerifyCondition(innerNetObject)) + if (!gameOver.VerifyCondition(innerNetObject)) { Logger.Info($"Game over condition not met for {gameOver.GetType().Name}"); return; From 309094bdf82903ef03f8c09640fa284e18db679d Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 28 Feb 2025 06:25:53 -0500 Subject: [PATCH 56/71] better error message --- MiraAPI/Networking/Modifiers/ModifierConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Networking/Modifiers/ModifierConverter.cs b/MiraAPI/Networking/Modifiers/ModifierConverter.cs index e8afa1b..34fd714 100644 --- a/MiraAPI/Networking/Modifiers/ModifierConverter.cs +++ b/MiraAPI/Networking/Modifiers/ModifierConverter.cs @@ -35,6 +35,6 @@ public class ModifierConverter : MessageConverter { var player = reader.ReadNetObject(); var guid = new Guid(reader.ReadBytesAndSize()); - return player.GetModifier(guid) ?? throw new InvalidOperationException($"Modifier with GUID {guid} not found for player {player.NetId}."); + return player.GetModifier(guid) ?? throw new InvalidOperationException($"Modifier with GUID {guid} not found for player with ID: {player.PlayerId}"); } } From 669d5765e6cb5fd844496dfbd0469902c3332597 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:14:24 -0500 Subject: [PATCH 57/71] add winner info to verify condition in gameover --- MiraAPI/GameEnd/CustomGameOver.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs index 44c225a..70d7b27 100644 --- a/MiraAPI/GameEnd/CustomGameOver.cs +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -18,8 +18,9 @@ public abstract class CustomGameOver /// Verifies if the condition for this CustomGameOver is met. /// /// The PlayerControl that requested the Game Over. + /// The collection of winners. /// True if the condition is met, otherwise false. - public virtual bool VerifyCondition(PlayerControl playerControl) + public virtual bool VerifyCondition(PlayerControl playerControl, NetworkedPlayerInfo[] winners) { return true; } From 2e0ecd95a6398e61f6b670cd9bc3ea61bc749810 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:17:39 -0500 Subject: [PATCH 58/71] fix things i forgot to commit --- MiraAPI.Example/GameOver/NeutralKillerGameOver.cs | 4 ++-- MiraAPI/Networking/CustomGameOverRpc.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs index f513e8d..351d593 100644 --- a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs +++ b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs @@ -7,9 +7,9 @@ namespace MiraAPI.Example.GameOver; public class NeutralKillerGameOver : CustomGameOver { - public override bool VerifyCondition(PlayerControl playerControl) + public override bool VerifyCondition(PlayerControl playerControl, NetworkedPlayerInfo[] winners) { - return playerControl.Data.Role is NeutralKillerRole; + return winners is [{ Role: NeutralKillerRole }]; } public override void AfterEndGameSetup(EndGameManager endGameManager) diff --git a/MiraAPI/Networking/CustomGameOverRpc.cs b/MiraAPI/Networking/CustomGameOverRpc.cs index 978b992..34dbe6a 100644 --- a/MiraAPI/Networking/CustomGameOverRpc.cs +++ b/MiraAPI/Networking/CustomGameOverRpc.cs @@ -48,7 +48,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc< { if (GameOverManager.TryGetGameOver(data.Reason, out var gameOver)) { - if (!gameOver.VerifyCondition(innerNetObject)) + if (!gameOver.VerifyCondition(innerNetObject, [.. data.Winners])) { Logger.Info($"Game over condition not met for {gameOver.GetType().Name}"); return; From 26afd47d2a32e84820f5c1fd54d07ba00eda765c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 28 Feb 2025 15:41:18 -0500 Subject: [PATCH 59/71] attribute event registration --- MiraAPI.Example/ExampleEventHandlers.cs | 10 ++++----- MiraAPI/Events/MiraEventManager.cs | 26 ++++++++++++++++++++++ MiraAPI/Events/RegisterEventAttribute.cs | 24 ++++++++++++++++++++ MiraAPI/PluginLoading/MiraPluginManager.cs | 18 +++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 MiraAPI/Events/RegisterEventAttribute.cs diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs index e196b28..e2368f7 100644 --- a/MiraAPI.Example/ExampleEventHandlers.cs +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -12,11 +12,7 @@ public static class ExampleEventHandlers { public static void Initialize() { - // Register event handlers here - MiraEventManager.RegisterEventHandler>(FreezeButtonClickHandler, 1); - MiraEventManager.RegisterEventHandler>(FreezeButtonCancelledHandler); - MiraEventManager.RegisterEventHandler(UpdateSystemEventHandler); - + // You can register event handlers with the MiraEventManager class. MiraEventManager.RegisterEventHandler(@event => { Logger.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}"); @@ -33,12 +29,15 @@ public static class ExampleEventHandlers }); } + // Events can be registered using an attribute as well. + [RegisterEvent] public static void UpdateSystemEventHandler(UpdateSystemEvent @event) { Logger.Error(@event.SystemType.ToString()); } // Example event handler + [RegisterEvent(1)] public static void FreezeButtonClickHandler(MiraButtonClickEvent @event) { Logger.Warning("Freeze button clicked!"); @@ -51,6 +50,7 @@ public static class ExampleEventHandlers } // Example event handler + [RegisterEvent] public static void FreezeButtonCancelledHandler(MiraButtonCancelledEvent @event) { Logger.Warning("Freeze button cancelled!"); diff --git a/MiraAPI/Events/MiraEventManager.cs b/MiraAPI/Events/MiraEventManager.cs index eff933d..b375504 100644 --- a/MiraAPI/Events/MiraEventManager.cs +++ b/MiraAPI/Events/MiraEventManager.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Reflection; +using Reactor.Utilities; namespace MiraAPI.Events; @@ -54,6 +56,30 @@ public static class MiraEventManager return true; } + /// + /// Register an event. + /// + /// The type of event. + /// The MethodInfo of the event handler. + /// The priority of the event handler. Higher values are called first. + public static void RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0) + { + if (!type.IsSubclassOf(typeof(MiraEvent))) + { + Logger.Error($"Type must be a subclass of MiraEvent: {nameof(type)}"); + return; + } + + if (!EventWrappers.TryGetValue(type, out var handlers)) + { + handlers = []; + EventWrappers.Add(type, handlers); + } + + var @delegate = Delegate.CreateDelegate(typeof(Action<>).MakeGenericType(type), methodInfo); + handlers.Add(new MiraEventWrapper(@delegate, priority)); + } + /// /// Register an event. /// diff --git a/MiraAPI/Events/RegisterEventAttribute.cs b/MiraAPI/Events/RegisterEventAttribute.cs new file mode 100644 index 0000000..6988d79 --- /dev/null +++ b/MiraAPI/Events/RegisterEventAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace MiraAPI.Events; + +/// +/// Register an event. +/// +[AttributeUsage(AttributeTargets.Method)] +public class RegisterEventAttribute : Attribute +{ + /// + /// Gets the priority of the event. + /// + public int Priority { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The priority of the event. + public RegisterEventAttribute(int priority = 0) + { + Priority = priority; + } +} diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 42be8e0..1fc6be7 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -49,6 +49,24 @@ public sealed class MiraPluginManager continue; } + foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)) + { + var eventAttribute = method.GetCustomAttribute(); + if (eventAttribute != null) + { + Logger.Message("Registering event"); + var parameters = method.GetParameters(); + if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) + { + Logger.Error($"Invalid event registration method {method.Name} in {type.Name}"); + continue; + } + + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + } + } + if (RegisterModifier(type, info)) { continue; From b67fdf5ab1b3a1efc3f83a4ad55de5c200978c8e Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:11:31 -0500 Subject: [PATCH 60/71] optionable v1 --- .../Options/Roles/FreezerRoleSettings.cs | 4 +--- .../Options/Roles/TeleporterOptions.cs | 4 +--- MiraAPI/GameOptions/AbstractOptionGroup.cs | 17 ++++++++++++----- .../Attributes/ModdedEnumOptionAttribute.cs | 6 +++--- .../Attributes/ModdedNumberOptionAttribute.cs | 7 +++---- .../Attributes/ModdedToggleOptionAttribute.cs | 5 ++--- MiraAPI/GameOptions/IModdedOption.cs | 1 - MiraAPI/GameOptions/IOptionable.cs | 6 ++++++ MiraAPI/GameOptions/IOptionableGroup.cs | 14 ++++++++++++++ MiraAPI/GameOptions/ModdedOptionsManager.cs | 3 --- .../GameOptions/OptionTypes/ModdedEnumOption.cs | 8 ++------ .../OptionTypes/ModdedNumberOption.cs | 6 ++---- MiraAPI/GameOptions/OptionTypes/ModdedOption.cs | 13 +------------ .../OptionTypes/ModdedToggleOption.cs | 3 +-- MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 9 ++++++--- .../Patches/Options/GameSettingMenuPatches.cs | 4 +++- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 14 +++++++++----- .../Patches/Options/RoleSettingMenuPatches.cs | 16 ++++++++-------- MiraAPI/Roles/ICustomRole.cs | 3 ++- 19 files changed, 76 insertions(+), 67 deletions(-) create mode 100644 MiraAPI/GameOptions/IOptionable.cs create mode 100644 MiraAPI/GameOptions/IOptionableGroup.cs diff --git a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs index a99c9e0..6113408 100644 --- a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs +++ b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs @@ -6,12 +6,10 @@ using MiraAPI.Utilities; namespace MiraAPI.Example.Options.Roles; -public class FreezerRoleSettings : AbstractOptionGroup +public class FreezerRoleSettings : AbstractOptionGroup { public override string GroupName => "Custom Role"; - public override Type AdvancedRole => typeof(FreezerRole); - [ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)] public float FreezeDuration { get; set; } = 5; diff --git a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs index 34ccb94..1b0c1da 100644 --- a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs +++ b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs @@ -7,12 +7,10 @@ using MiraAPI.Utilities; namespace MiraAPI.Example.Options.Roles; -public class TeleporterOptions : AbstractOptionGroup +public class TeleporterOptions : AbstractOptionGroup { public override string GroupName => "Teleporter"; - public override Type AdvancedRole => typeof(TeleporterRole); - public ModdedNumberOption TeleportCooldown { get; set; } = new("Teleport Cooldown", 10, 5, 60, 2.5f, MiraNumberSuffixes.Seconds); [ModdedNumberOption("Teleport Duration", 5, 25, 1, MiraNumberSuffixes.Seconds)] diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 418f25b..96dcdd7 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -32,12 +32,19 @@ public abstract class AbstractOptionGroup /// public virtual uint GroupPriority => uint.MaxValue; - /// - /// Gets the role the group is associated with. This is used for the advanced role options menu. - /// - public virtual Type? AdvancedRole => null; - internal bool AllOptionsHidden { get; set; } internal CategoryHeaderMasked? Header { get; set; } } + +/// +/// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. +/// +/// The type of the optionable that this group contains. +public abstract class AbstractOptionGroup : AbstractOptionGroup, IOptionableGroup where T : IOptionable +{ + /// + /// Gets the Optionable type of the group. + /// + public Type OptionableType => typeof(T); +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index da6094b..54680f2 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -8,12 +8,12 @@ namespace MiraAPI.GameOptions.Attributes; /// Attribute for creating an enum option. /// [AttributeUsage(AttributeTargets.Property)] -public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null, Type? roleType = null) - : ModdedOptionAttribute(title, roleType) +public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null) + : ModdedOptionAttribute(title) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values, RoleType); + var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values); return opt; } diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index d679051..07b5cfc 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -16,13 +16,12 @@ public class ModdedNumberOptionAttribute( float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, string? formatString = null, - bool zeroInfinity = false, - Type? roleType = null) - : ModdedOptionAttribute(title, roleType) + bool zeroInfinity = false) + : ModdedOptionAttribute(title) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity, RoleType); + return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity); } /// diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs index d44a7c0..0ad64da 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs @@ -8,13 +8,12 @@ namespace MiraAPI.GameOptions.Attributes; /// Attribute for a toggle option. /// /// The option title. -/// An optional role type. [AttributeUsage(AttributeTargets.Property)] -public class ModdedToggleOptionAttribute(string title, Type? roleType = null) : ModdedOptionAttribute(title, roleType) +public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(title) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), RoleType); + var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false)); return toggleOpt; } diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 1aee3c4..f40f3cf 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -13,7 +13,6 @@ public interface IModdedOption StringNames StringName { get; } IMiraPlugin? ParentMod { get; set; } BaseGameSetting Data { get; } - Type? AdvancedRole { get; set; } OptionBehaviour? OptionBehaviour { get; } Func Visible { get; set; } ConfigDefinition? ConfigDefinition { get; set; } diff --git a/MiraAPI/GameOptions/IOptionable.cs b/MiraAPI/GameOptions/IOptionable.cs new file mode 100644 index 0000000..adc28f6 --- /dev/null +++ b/MiraAPI/GameOptions/IOptionable.cs @@ -0,0 +1,6 @@ +namespace MiraAPI.GameOptions; + +/// +/// Interface for classes that can be used for option groups. +/// +public interface IOptionable; diff --git a/MiraAPI/GameOptions/IOptionableGroup.cs b/MiraAPI/GameOptions/IOptionableGroup.cs new file mode 100644 index 0000000..e476548 --- /dev/null +++ b/MiraAPI/GameOptions/IOptionableGroup.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiraAPI.GameOptions; + +/// +/// Interface for groups that have some type of "Optionable" class. +/// +public interface IOptionableGroup +{ + /// + /// Gets the type of the Optionable class. + /// + Type OptionableType { get; } +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index a6dd1c9..cbb6459 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -119,10 +119,7 @@ public static class ModdedOptionsManager option.ConfigDefinition = new ConfigDefinition(groupName, propertyName); option.ParentMod = pluginInfo.MiraPlugin; - option.AdvancedRole = group.AdvancedRole; - pluginInfo.Options.Add(option); - ModdedOptions.Add(option.Id, option); group.Options.Add(option); } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index f25fda6..ff422f5 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -27,8 +27,7 @@ public class ModdedEnumOption : ModdedOption /// The default value as an int. /// The Enum type. /// An option list of string values to use in place of the enum name. - /// An optional role type to specify for a specific role. - public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType) + public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null) : base(title, defaultValue) { Values = values ?? Enum.GetNames(enumType); Data = ScriptableObject.CreateInstance(); @@ -101,7 +100,6 @@ public class ModdedEnumOption : ModdedOption } } - /// /// An option for selecting an enum value. /// @@ -118,10 +116,8 @@ public class ModdedEnumOption : ModdedOption where T : Enum /// /// The title of the option. /// The default value as an int. - /// The Enum type. /// An option list of string values to use in place of the enum name. - /// An optional role type to specify for a specific role. - public ModdedEnumOption(string title, T defaultValue, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType) + public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue) { Values = values ?? Enum.GetNames(typeof(T)); Data = ScriptableObject.CreateInstance(); diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index f075662..0174326 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -48,7 +48,6 @@ public class ModdedNumberOption : ModdedOption /// The suffix type. /// Optional format string for the option screen. /// Whether zero is infinity or not. - /// An optional role type. public ModdedNumberOption( string title, float defaultValue, @@ -57,8 +56,7 @@ public class ModdedNumberOption : ModdedOption float increment, MiraNumberSuffixes suffixType, string? formatString = null, - bool zeroInfinity = false, - Type? roleType = null) : base(title, defaultValue, roleType) + bool zeroInfinity = false) : base(title, defaultValue) { Min = min; Max = max; @@ -144,7 +142,7 @@ public class ModdedNumberOption : ModdedOption Value = Mathf.Clamp(newValue, Min, Max); HudManager.Instance.Notifier.AddSettingsChangeMessage( StringName, - Data?.GetValueString(Value), + Data.GetValueString(Value), false); if (OptionBehaviour is NumberOption opt) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 9d42be0..0cfefa8 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -72,11 +72,6 @@ public abstract class ModdedOption : IModdedOption /// public Func Visible { get; set; } - /// - /// Gets or sets the advanced role of the option. - /// - public Type? AdvancedRole { get; set; } - /// /// Gets or sets the option behaviour of the option. /// @@ -102,8 +97,7 @@ public abstract class ModdedOption : IModdedOption /// /// The option title. /// The default value. - /// The Role Type or null if it doesn't belong to a role. - protected ModdedOption(string title, T defaultValue, Type? roleType) + protected ModdedOption(string title, T defaultValue) { Id = ModdedOptionsManager.NextId; Title = title; @@ -111,11 +105,6 @@ public abstract class ModdedOption : IModdedOption Value = defaultValue; StringName = CustomStringName.CreateAndRegister(Title); Visible = () => true; - - if (roleType is not null && roleType.IsAssignableTo(typeof(ICustomRole))) - { - AdvancedRole = roleType; - } } internal void ValueChanged(OptionBehaviour optionBehaviour) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 1773512..3495e86 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -15,8 +15,7 @@ public class ModdedToggleOption : ModdedOption /// /// The option title. /// The default value. - /// An optional role type. - public ModdedToggleOption(string title, bool defaultValue, Type? roleType = null) : base(title, defaultValue, roleType) + public ModdedToggleOption(string title, bool defaultValue) : base(title, defaultValue) { Data = ScriptableObject.CreateInstance(); diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index c641e88..c0aef14 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,5 +1,7 @@ using System.Linq; using HarmonyLib; +using MiraAPI.GameOptions; +using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; @@ -31,8 +33,8 @@ public static class GameOptionsMenuPatch var num = 2.1f; var filteredGroups = - GameSettingMenuPatches.SelectedMod?.OptionGroups.Where( - x => x.AdvancedRole is null) ?? []; + GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x=> x is not IOptionableGroup) ?? []; foreach (var group in filteredGroups) { @@ -99,7 +101,8 @@ public static class GameOptionsMenuPatch __instance.MapPicker.gameObject.SetActive(false); - var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(x => x.AdvancedRole is null) ?? []; + var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x is not IOptionableGroup) ?? []; foreach (var group in filteredGroups) { diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 3c65092..4f87780 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -113,6 +113,8 @@ public static class GameSettingMenuPatches menu.GameSettingsButton.gameObject.SetActive(true); menu.RoleSettingsButton.gameObject.SetActive(true); + // TODO: reimplement this + /* if (SelectedModIdx != 0) { if (SelectedMod?.Options.Where(x=>x.AdvancedRole==null).ToList().Count == 0) @@ -129,7 +131,7 @@ public static class GameSettingMenuPatches { menu.RoleSettingsButton.transform.localPosition = menu.GameSettingsButton.transform.localPosition; } - } + }*/ if (roles.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0) { diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 4f8c060..bda1c2b 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; +using MiraAPI.GameOptions; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -125,7 +126,8 @@ public static class LobbyViewPanePatches var num = 1.44f; - var filteredGroups = SelectedMod.OptionGroups.Where(x => x.GroupVisible.Invoke() && x.AdvancedRole is null); + var filteredGroups = SelectedMod.OptionGroups + .Where(x => x.GroupVisible.Invoke() && x is not IOptionableGroup); foreach (var group in filteredGroups) { @@ -277,8 +279,9 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one; viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f); - var advancedRoleOptions = SelectedMod.Options - .Where(x => x.AdvancedRole == customRole.GetType()) + var advancedRoleOptions = SelectedMod.OptionGroups + .Where(x => x is IOptionableGroup optionGroup && optionGroup.OptionableType == customRole.GetType()) + .SelectMany(x => x.Options) .ToList(); if (numPerGame > 0 && advancedRoleOptions.Count > 0) @@ -386,8 +389,9 @@ public static class LobbyViewPanePatches var num = viewPanel.yPosStart; var num2 = 1.08f; - var filteredOptions = SelectedMod.Options - .Where(x => x.AdvancedRole == roleType) + var filteredOptions = SelectedMod.OptionGroups + .Where(x => x is IOptionableGroup optionGroup && optionGroup.OptionableType == roleType) + .SelectMany(x=>x.Options) .ToList(); for (var i = 0; i < filteredOptions.Count; i++) diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 33061d4..20ffa18 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -304,15 +305,14 @@ public static class RoleSettingMenuPatches var hasImage = role.RoleScreenshot != null; var num = hasImage ? -0.872f : -1; - var filteredOptions = GameSettingMenuPatches.SelectedMod?.Options.Where(x => x.AdvancedRole == role.GetType()) ?? []; + // TODO: create sub groups under the role settings. + var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x=> x is IOptionableGroup optionableGroup && optionableGroup.OptionableType == role.GetType()) + .SelectMany(x=>x.Options) + .ToList() ?? []; foreach (var option in filteredOptions) { - if (option.AdvancedRole is not null && option.AdvancedRole != role.GetType()) - { - continue; - } - var newOpt = option.CreateOption( __instance.checkboxOrigin, __instance.numberOptionOrigin, @@ -470,8 +470,8 @@ public static class RoleSettingMenuPatches roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left; if (GameSettingMenuPatches.SelectedMod is null || - GameSettingMenuPatches.SelectedMod.Options.Exists( - x => x.AdvancedRole != null && x.AdvancedRole.IsInstanceOfType(role))) + GameSettingMenuPatches.SelectedMod.OptionGroups + .Exists(x => x is IOptionableGroup optionGroup && optionGroup.OptionableType == role.GetType())) { var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform); newButton.name = "ConfigButton"; diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 3c52fb8..ecb4326 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,5 +1,6 @@ using System.Text; using BepInEx.Configuration; +using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; @@ -11,7 +12,7 @@ namespace MiraAPI.Roles; /// /// Interface for custom roles. /// -public interface ICustomRole +public interface ICustomRole : IOptionable { /// /// Gets the name of the role. From 6f248221d64c1d6199d1d1a8756677232f9bb8e0 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 1 Mar 2025 12:38:04 -0500 Subject: [PATCH 61/71] remove extra log --- MiraAPI/PluginLoading/MiraPluginManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 1fc6be7..eced634 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -54,7 +54,6 @@ public sealed class MiraPluginManager var eventAttribute = method.GetCustomAttribute(); if (eventAttribute != null) { - Logger.Message("Registering event"); var parameters = method.GetParameters(); if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) { From d9081ed17bebcc29af1d75d173d1446abb2d9cfc Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 1 Mar 2025 15:30:14 -0500 Subject: [PATCH 62/71] Outline text method --- .../Buttons/Teleporter/TeleportButton.cs | 2 +- MiraAPI/Hud/CustomActionButton.cs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index 208ad6a..76a5b9e 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -19,7 +19,7 @@ public class TeleportButton : CustomActionButton public override int MaxUses => 2; public override LoadableAsset Sprite => ExampleAssets.TeleportButton; - + public override Color TextOutlineColor => new Color32(221, 176, 152, 255); public static bool IsZoom { get; private set; } public override bool Enabled(RoleBehaviour? role) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 4981e15..4136e99 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -46,6 +46,11 @@ public abstract class CustomActionButton /// public virtual int MaxUses => 0; + /// + /// Gets the button's text outline color. + /// + public virtual Color TextOutlineColor => Color.clear; + /// /// Gets the location of the button on the screen. /// @@ -114,6 +119,11 @@ public abstract class CustomActionButton Button.SetInfiniteUses(); } + if (TextOutlineColor != Color.clear) + { + SetTextOutline(TextOutlineColor); + } + var pb = Button.GetComponent(); pb.OnClick = new Button.ButtonClickedEvent(); pb.OnClick.AddListener((UnityAction)(() => @@ -207,6 +217,16 @@ public abstract class CustomActionButton EffectActive = false; } + /// + /// A utility function to change the outline color of the button's text. + /// + /// The new color. + /// The thickness of the outline. Set to 0 if you want to remove it. + public virtual void SetTextOutline(Color color) + { + Button?.buttonLabelText.SetOutlineColor(color); + } + /// /// A utility function to override the sprite of the button. /// From bbff2dd2d9ce457d045508554b47922af2303431 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:34:18 -0500 Subject: [PATCH 63/71] modifier options (WIP) --- MiraAPI.Example/Modifiers/CaptainModifier.cs | 10 - .../Modifiers/HighPriorityModifier.cs | 9 - .../Modifiers/CaptainModifierSettings.cs | 13 + MiraAPI/GameOptions/ModifierOptionGroup.cs | 15 + .../OptionTypes/ModdedNumberOption.cs | 10 +- MiraAPI/Modifiers/AssignmentConfiguration.cs | 10 + MiraAPI/Modifiers/BaseModifier.cs | 14 +- MiraAPI/Modifiers/ModifierManager.cs | 18 +- MiraAPI/Modifiers/Types/GameModifier.cs | 67 +++- .../Patches/Options/GameOptionsMenuPatch.cs | 352 ++++++++++-------- .../Patches/Options/GameSettingMenuPatches.cs | 116 +++++- .../Patches/Options/RoleSettingMenuPatches.cs | 2 +- MiraAPI/PluginLoading/MiraPluginInfo.cs | 3 + MiraAPI/Roles/ICustomRole.cs | 4 +- MiraAPI/Utilities/Extensions.cs | 10 + 15 files changed, 458 insertions(+), 195 deletions(-) create mode 100644 MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs create mode 100644 MiraAPI/GameOptions/ModifierOptionGroup.cs create mode 100644 MiraAPI/Modifiers/AssignmentConfiguration.cs diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 63b7c41..5845e5a 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -5,14 +5,4 @@ namespace MiraAPI.Example.Modifiers; public class CaptainModifier : GameModifier { public override string ModifierName => "Captain"; - - public override int GetAmountPerGame() - { - return 1; - } - - public override int GetAssignmentChance() - { - return 100; - } } diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index c640ed8..1d1ddd5 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -5,15 +5,6 @@ namespace MiraAPI.Example.Modifiers; public class HighPriorityModifier : GameModifier { public override string ModifierName => "High Priority"; - public override int GetAssignmentChance() - { - return 100; - } - - public override int GetAmountPerGame() - { - return 1; - } public override int Priority() { diff --git a/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs new file mode 100644 index 0000000..2ae297b --- /dev/null +++ b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs @@ -0,0 +1,13 @@ +using MiraAPI.Example.Modifiers; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Options.Modifiers; + +public class CaptainModifierSettings : AbstractOptionGroup +{ + public override string GroupName => "Captain Settings"; + + [ModdedNumberOption("Uses", 1, 5)] + public float NumUses { get; set; } = 3; +} diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs new file mode 100644 index 0000000..f845952 --- /dev/null +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace MiraAPI.GameOptions; + +internal class ModifierOptionGroup : AbstractOptionGroup +{ + public override string GroupName { get; } + + public ModifierOptionGroup(string name, IModdedOption[] options, params AbstractOptionGroup[] others) + { + GroupName = name; + Options.AddRange(options); + Options.AddRange(others.SelectMany(x=>x.Options)); + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index 0174326..4d38c04 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -75,11 +75,11 @@ public class ModdedNumberOption : ModdedOption data.Increment = Increment; data.ValidRange = new FloatRange(Min, Max); data.FormatString = formatString ?? - (!Mathf.Approximately(defaultValue, 0) || - !Mathf.Approximately(Increment % 1, 0) || - !Mathf.Approximately(Value % 1, 0) || - !Mathf.Approximately(Min % 1, 0) || - !Mathf.Approximately(Max % 1, 0) ? "0.0" : "0"); + (defaultValue.IsInteger() && + Increment.IsInteger() && + Value.IsInteger() && + Min.IsInteger() && + Max.IsInteger() ? "0" : "0.0"); data.ZeroIsInfinity = ZeroInfinity; data.SuffixType = (NumberSuffixes)SuffixType; diff --git a/MiraAPI/Modifiers/AssignmentConfiguration.cs b/MiraAPI/Modifiers/AssignmentConfiguration.cs new file mode 100644 index 0000000..fd4f3ba --- /dev/null +++ b/MiraAPI/Modifiers/AssignmentConfiguration.cs @@ -0,0 +1,10 @@ +namespace MiraAPI.Modifiers; + +/// +/// Represents the assignment configuration for a game modifier. +/// +/// The default amount of players that can have this modifier in a game. +/// The default chance of the modifier being assigned to a player. +/// Whether Mira should create an amount option for the modifier. +/// Whether Mira should create a chance option for the modifier. +public record struct AssignmentConfiguration(int DefaultAmount, int DefaultChance, bool CreateAmountOption, bool CreateChanceOption); diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 473c78f..1bd1082 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,11 +1,13 @@ using System; +using MiraAPI.GameOptions; +using MiraAPI.PluginLoading; namespace MiraAPI.Modifiers; /// /// Base class for all modifiers. /// -public abstract class BaseModifier +public abstract class BaseModifier : IOptionable { /// /// Gets the player that the modifier is attached to. @@ -32,6 +34,16 @@ public abstract class BaseModifier /// public uint TypeId => ModifierManager.GetModifierTypeId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered."); + /// + /// Gets the parent mod of the modifier. + /// + public MiraPluginInfo ParentMod => Array.Find( + MiraPluginManager.Instance.RegisteredPlugins(), + x => x.Modifiers.Exists(y => y.TypeId == TypeId) + ) ?? throw new InvalidOperationException("Modifier is not registered."); + + internal ModifierOptionGroup? ModifierOptionsGroup { get; set; } + /// /// Gets the modifier name. /// diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 4285a73..fee96ab 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -71,7 +71,23 @@ public static class ModifierManager return true; } - var mod = Activator.CreateInstance(modifierType) as GameModifier; + if (modifierType.GetConstructor(Type.EmptyTypes) == null) + { + Logger.Error($"Modifier {modifierType.FullName} does not have a parameterless constructor!"); + return false; + } + + if (Activator.CreateInstance(modifierType) is not GameModifier mod) + { + Logger.Error($"Modifier {modifierType.FullName} could not be cast to GameModifier!"); + return false; + } + + info.GameModifiers.Add(mod); + + info.PluginConfig.Bind(mod.AmountDefinition, 0); + info.PluginConfig.Bind(mod.ChanceDefinition, 0); + var priority = mod!.Priority(); if (!PrioritiesToIdsMap.TryGetValue(priority, out var list)) diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index abcd1f7..7e4cccb 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -1,4 +1,10 @@ -namespace MiraAPI.Modifiers.Types; +using System; +using BepInEx.Configuration; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; +using Reactor.Utilities; + +namespace MiraAPI.Modifiers.Types; /// /// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role. @@ -8,17 +14,72 @@ public abstract class GameModifier : BaseModifier /// public override bool ShowInFreeplay => true; + /// + /// Gets the assignment configuration for the modifier. + /// + public virtual AssignmentConfiguration AssignmentConfiguration => new(0, 0, true, true); + + internal ConfigDefinition AmountDefinition => new("Modifiers", $"Amount {GetType().FullName}"); + + internal ConfigDefinition ChanceDefinition => new("Modifiers", $"Chance {GetType().FullName}"); + + internal ModdedNumberOption AmountOption { get; } = new("Amount", 0, 0, 15, 1, MiraNumberSuffixes.None); + + internal ModdedNumberOption ChanceOption { get; } = new("Chance", 0, 0, 100, 1, MiraNumberSuffixes.Percent); + /// /// Gets the chance of the modifier being assigned to a player. /// /// An int value between 0 and 100 representing percent. - public abstract int GetAssignmentChance(); + public virtual int GetAssignmentChance() + { + return AssignmentConfiguration.CreateChanceOption && + ParentMod.PluginConfig.TryGetEntry(ChanceDefinition, out ConfigEntry entry) + ? entry.Value + : AssignmentConfiguration.DefaultChance; + } /// /// Gets the amount of players that can have this modifier in a game. /// /// An int value greater than or equal to zero. - public abstract int GetAmountPerGame(); + public virtual int GetAmountPerGame() + { + return AssignmentConfiguration.CreateAmountOption && + ParentMod.PluginConfig.TryGetEntry(AmountDefinition, out ConfigEntry entry) + ? entry.Value + : AssignmentConfiguration.DefaultAmount; + } + + /// + /// Sets the chance of the modifier being assigned to a player. + /// + /// The chance between 0 and 100. + public virtual void SetAssignmentChance(int chance) + { + if (ParentMod.PluginConfig.TryGetEntry(ChanceDefinition, out ConfigEntry entry)) + { + entry.Value = Math.Clamp(chance, 0, 100); + return; + } + + Logger.Error($"Error getting chance configuration for modifier: {ModifierName}"); + } + + /// + /// Sets the amount of players that can have this modifier in a game. + /// + /// The amount of players between zero and the max player count. + public virtual void SetAmountPerGame(int amount) + { + if (ParentMod.PluginConfig.TryGetEntry(AmountDefinition, out ConfigEntry entry)) + { + entry.Value = Math.Max(amount, 0); + return; + } + + Logger.Error($"Error getting amount configuration for modifier: {ModifierName}"); + } /// /// Gets the priority at which the modifier will spawn. The higher the value, the higher up on the assignment list. diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index c0aef14..7b24619 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,14 +1,18 @@ -using System.Linq; +using System; +using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; +using MiraAPI.Modifiers.Types; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; @@ -32,64 +36,105 @@ public static class GameOptionsMenuPatch } var num = 2.1f; - var filteredGroups = - GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x=> x is not IOptionableGroup) ?? []; - - foreach (var group in filteredGroups) + if (__instance.name == "MODIFIERS TAB") { - if (group.Options.Count == 0 || group.Header is null) + ModifiersUpdate(ref num); + } + else + { + var filteredGroups = + GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x is not IOptionableGroup) ?? []; + + foreach (var group in filteredGroups) { - continue; - } - - if (!group.GroupVisible.Invoke()) - { - group.Header.gameObject.SetActive(false); - foreach (var option in group.Options) - { - option.OptionBehaviour?.gameObject.SetActive(false); - } - continue; - } - - group.Header.gameObject.SetActive(true); - group.Header.transform.localScale = Vector3.one * 0.63f; - group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); - - num -= 0.58f; - - foreach (var opt in group.Options) - { - var newOpt = opt.OptionBehaviour; - - if (newOpt is null) - { - continue; - } - - if (!opt.Visible.Invoke()) - { - newOpt.gameObject.SetActive(false); - continue; - } - - if (!group.AllOptionsHidden) - { - newOpt.gameObject.SetActive(true); - newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); - num -= 0.45f; - } - else - { - newOpt.gameObject.SetActive(false); - } + UpdateGroup(group, ref num); } } __instance.scrollBar.SetYBoundsMax(-num - 1.65f); } + private static void UpdateGroup(AbstractOptionGroup group, ref float num) + { + if (group.Options.Count == 0 || group.Header is null) + { + return; + } + + if (!group.GroupVisible.Invoke()) + { + group.Header.gameObject.SetActive(false); + foreach (var option in group.Options) + { + option.OptionBehaviour?.gameObject.SetActive(false); + } + + return; + } + + group.Header.gameObject.SetActive(true); + group.Header.transform.localScale = Vector3.one * 0.63f; + group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + + num -= 0.58f; + + foreach (var opt in group.Options) + { + var newOpt = opt.OptionBehaviour; + + if (newOpt is null) + { + continue; + } + + if (!opt.Visible.Invoke()) + { + newOpt.gameObject.SetActive(false); + continue; + } + + if (!group.AllOptionsHidden) + { + newOpt.gameObject.SetActive(true); + newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); + num -= 0.45f; + } + else + { + newOpt.gameObject.SetActive(false); + } + } + } + + private static void ModifiersUpdate(ref float num) + { + var modifiers = GameSettingMenuPatches.SelectedMod?.Modifiers.Select(x=>x.ModifierOptionsGroup) ?? []; + + foreach (var modGroup in modifiers) + { + if (modGroup != null) + { + UpdateGroup(modGroup, ref num); + } + } + } + + private static void ModifiersCreate(GameOptionsMenu menu) + { + var modifiers = GameSettingMenuPatches.SelectedMod?.GameModifiers ?? []; + + var optLookup = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x is IOptionableGroup group && group.OptionableType.IsAssignableTo(typeof(GameModifier))) + .ToLookup(x => (x as IOptionableGroup)!.OptionableType); + + foreach (var mod in modifiers) + { + mod.ModifierOptionsGroup = new ModifierOptionGroup(mod.ModifierName, [mod.AmountOption, mod.ChanceOption], [.. optLookup?[mod.GetType()] ?? []]); + CreateGroup(menu, mod.ModifierOptionsGroup); + } + } + [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))] public static bool SettingsPatch(GameOptionsMenu __instance) @@ -101,107 +146,19 @@ public static class GameOptionsMenuPatch __instance.MapPicker.gameObject.SetActive(false); + if (__instance.name == "MODIFIERS TAB") + { + Logger.Error("Creating modifiers"); + ModifiersCreate(__instance); + return false; + } + var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups .Where(x => x is not IOptionableGroup) ?? []; foreach (var group in filteredGroups) { - var categoryHeaderMasked = Object.Instantiate( - __instance.categoryHeaderOrigin, - Vector3.zero, - Quaternion.identity, - __instance.settingsContainer); - categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); - if (group.GroupColor != Color.clear) - { - categoryHeaderMasked.Background.color = group.GroupColor; - categoryHeaderMasked.Divider.color = group.GroupColor; - categoryHeaderMasked.Title.color = group.GroupColor.GetAlternateColor(); - } - - categoryHeaderMasked.Background.size = new Vector2( - categoryHeaderMasked.Background.size.x + 1.5f, - categoryHeaderMasked.Background.size.y); - categoryHeaderMasked.gameObject.SetActive(false); - group.Header = categoryHeaderMasked; - - var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); - newText.text = "(Click to close)"; - newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); - newText.gameObject.GetComponent().Destroy(); - - var options = group.Options.Select( - opt => opt.CreateOption( - __instance.checkboxOrigin, - __instance.numberOptionOrigin, - __instance.stringOptionOrigin, - __instance.settingsContainer)); - - foreach (var newOpt in options) - { - newOpt.SetClickMask(__instance.ButtonClickMask); - - SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); - foreach (var renderer in componentsInChildren) - { - if (group.GroupColor != Color.clear) - { - renderer.color = group.GroupColor.GetAlternateColor(); - if (renderer.transform.parent.TryGetComponent(out var btn)) - { - btn.interactableColor = group.GroupColor.GetAlternateColor(); - btn.interactableHoveredColor = Color.white; - } - } - - renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); - } - - foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) - { - if (group.GroupColor != Color.clear) - { - textMeshPro.color = group.GroupColor; - } - - textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); - textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); - } - - if (newOpt is ToggleOption toggle) - { - toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); - toggle.CheckMark.color = - group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; - var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") - .GetComponent(); - rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); - rend.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; - } - - __instance.Children.Add(newOpt); - - newOpt.Initialize(); - newOpt.gameObject.SetActive(false); - } - - var boxCol = categoryHeaderMasked.gameObject.AddComponent(); - boxCol.size = new Vector2(7, 0.7f); - boxCol.offset = new Vector2(1.5f, -0.3f); - - var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); - headerBtn.ClickSound = __instance.BackButton.GetComponent().ClickSound; - headerBtn.OnMouseOver = new UnityEvent(); - headerBtn.OnMouseOut = new UnityEvent(); - headerBtn.OnClick.AddListener( - (UnityAction)(() => - { - group.AllOptionsHidden = !group.AllOptionsHidden; - newText.text = group.AllOptionsHidden - ? "(Click to open)" - : "(Click to close)"; - })); - headerBtn.SetButtonEnableState(true); + CreateGroup(__instance, group); } return false; @@ -241,4 +198,105 @@ public static class GameOptionsMenuPatch return false; } + + private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group) + { + var categoryHeaderMasked = Object.Instantiate( + menu.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + menu.settingsContainer); + + categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); + if (group.GroupColor != Color.clear) + { + categoryHeaderMasked.Background.color = group.GroupColor; + categoryHeaderMasked.Divider.color = group.GroupColor; + categoryHeaderMasked.Title.color = group.GroupColor.GetAlternateColor(); + } + + categoryHeaderMasked.Background.size = new Vector2( + categoryHeaderMasked.Background.size.x + 1.5f, + categoryHeaderMasked.Background.size.y); + categoryHeaderMasked.gameObject.SetActive(false); + group.Header = categoryHeaderMasked; + + var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); + newText.text = "(Click to close)"; + newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); + newText.gameObject.GetComponent().Destroy(); + + var options = group.Options.Select( + opt => opt.CreateOption( + menu.checkboxOrigin, + menu.numberOptionOrigin, + menu.stringOptionOrigin, + menu.settingsContainer)); + + foreach (var newOpt in options) + { + newOpt.SetClickMask(menu.ButtonClickMask); + + SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); + foreach (var renderer in componentsInChildren) + { + if (group.GroupColor != Color.clear) + { + renderer.color = group.GroupColor.GetAlternateColor(); + if (renderer.transform.parent.TryGetComponent(out var btn)) + { + btn.interactableColor = group.GroupColor.GetAlternateColor(); + btn.interactableHoveredColor = Color.white; + } + } + + renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); + } + + foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) + { + if (group.GroupColor != Color.clear) + { + textMeshPro.color = group.GroupColor; + } + + textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); + } + + if (newOpt is ToggleOption toggle) + { + toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); + toggle.CheckMark.color = + group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; + var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") + .GetComponent(); + rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); + rend.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; + } + + menu.Children.Add(newOpt); + + newOpt.Initialize(); + newOpt.gameObject.SetActive(false); + } + + var boxCol = categoryHeaderMasked.gameObject.AddComponent(); + boxCol.size = new Vector2(7, 0.7f); + boxCol.offset = new Vector2(1.5f, -0.3f); + + var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); + headerBtn.ClickSound = menu.BackButton.GetComponent().ClickSound; + headerBtn.OnMouseOver = new UnityEvent(); + headerBtn.OnMouseOut = new UnityEvent(); + headerBtn.OnClick.AddListener( + (UnityAction)(() => + { + group.AllOptionsHidden = !group.AllOptionsHidden; + newText.text = group.AllOptionsHidden + ? "(Click to open)" + : "(Click to close)"; + })); + headerBtn.SetButtonEnableState(true); + } } diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 4f87780..ab3ead7 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; @@ -12,26 +11,41 @@ using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; -/// -/// Patches for the to add support for custom options. -/// [HarmonyPatch(typeof(GameSettingMenu))] -public static class GameSettingMenuPatches +internal static class GameSettingMenuPatches { - /// - /// Gets the currently selected mod index. - /// public static int SelectedModIdx { get; private set; } - /// - /// Gets the currently selected mod. - /// public static MiraPluginInfo? SelectedMod { get; private set; } private static TextMeshPro? _text; private static Vector3 _roleBtnOgPos; + private static GameOptionsMenu? _modifiersTab; + private static PassiveButton? _modifiersButton; + private static PassiveButton? _smallRolesButton; + + [HarmonyPostfix] + [HarmonyPatch(nameof(GameSettingMenu.ChangeTab))] + public static void ChangeTabPostfix(GameSettingMenu __instance, int tabNum, bool previewOnly) + { + if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly) + { + _modifiersTab?.gameObject.SetActive(tabNum == 3); + _modifiersButton?.SelectButton(tabNum == 3); + _smallRolesButton?.SelectButton(tabNum == 2); + } + + if (previewOnly) + { + return; + } + + _modifiersButton?.SelectButton(tabNum == 3); + _smallRolesButton?.SelectButton(tabNum == 2); + } + /// /// Prefix for the method. Sets up the custom options. /// @@ -54,8 +68,6 @@ public static class GameSettingMenuPatches _text.fontSizeMax = 3.2f; _text.overflowMode = TextOverflowModes.Overflow; - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); - _text.alignment = TextAlignmentOptions.Center; var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject; @@ -91,6 +103,61 @@ public static class GameSettingMenuPatches } UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); })); + + // clone game settings tab for modifiers + _modifiersTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent); + _modifiersTab.name = "MODIFIERS TAB"; + + // create button for modifiers + __instance.RoleSettingsButton.gameObject.SetActive(false); + _smallRolesButton = Object.Instantiate(__instance.RoleSettingsButton, __instance.RoleSettingsButton.transform.parent); + var pos = new Vector3(-3.65f, _smallRolesButton.transform.localPosition.y, _smallRolesButton.transform.localPosition.z); + _smallRolesButton.transform.localPosition = pos; + _smallRolesButton.OnClick.AddListener((UnityAction)(() => + { + __instance.ChangeTab(2, false); + })); + _smallRolesButton.OnMouseOver.AddListener((UnityAction)(() => + { + __instance.ChangeTab(2, true); + })); + + var roleText = _smallRolesButton.buttonText; + roleText.text = "Roles"; + roleText.GetComponent().Destroy(); + roleText.alignment = TextAlignmentOptions.Center; + roleText.transform.parent.localPosition = new Vector3(-.525f, roleText.transform.parent.localPosition.y, roleText.transform.parent.localPosition.z); + + foreach (var collider in _smallRolesButton.Colliders) + { + if (collider.TryCast() is { } col) + { + col.size = new Vector2(col.size.x / 2, col.size.y); + } + } + foreach (var rend in _smallRolesButton.GetComponentsInChildren(true)) + { + rend.size = new Vector2(rend.size.x / 2, rend.size.y); + } + + _modifiersButton = Object.Instantiate(_smallRolesButton, _smallRolesButton.transform.parent); + _modifiersButton.OnClick = new ButtonClickedEvent(); + _modifiersButton.OnClick.AddListener((UnityAction)(() => + { + __instance.ChangeTab(3, false); + })); + _modifiersButton.OnMouseOver = new UnityEvent(); + _modifiersButton.OnMouseOver.AddListener((UnityAction)(() => + { + __instance.ChangeTab(3, true); + })); + + _modifiersButton.buttonText.text = "Modifiers"; + pos.x = -2.27f; + _modifiersButton.transform.localPosition = pos; + _modifiersButton.name = "ModifiersButton"; + + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); } private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles) @@ -109,9 +176,26 @@ public static class GameSettingMenuPatches _text.text = name[..Math.Min(name.Length, 25)]; } - menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; - menu.GameSettingsButton.gameObject.SetActive(true); - menu.RoleSettingsButton.gameObject.SetActive(true); + _modifiersButton.gameObject.SetActive(false); + _smallRolesButton.gameObject.SetActive(false); + menu.RoleSettingsButton.gameObject.SetActive(false); + if (SelectedModIdx != 0 && SelectedMod?.GameModifiers.Count != 0) + { + _modifiersButton.gameObject.SetActive(true); + _smallRolesButton.gameObject.SetActive(true); + } + else + { + menu.RoleSettingsButton.gameObject.SetActive(true); + if (SelectedModIdx == 0 && _modifiersTab.gameObject.active) + { + menu.ChangeTab(0, false); + } + } + + //menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; + //menu.GameSettingsButton.gameObject.SetActive(true); + //menu.RoleSettingsButton.gameObject.SetActive(true); // TODO: reimplement this /* diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 20ffa18..13c190f 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -504,7 +504,7 @@ public static class RoleSettingMenuPatches if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1) { - ScrollerNum += -0.43f; + ScrollerNum -= 0.43f; } return roleOptionSetting; diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 68d3ff0..2dfe479 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -6,6 +6,7 @@ using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; namespace MiraAPI.PluginLoading; @@ -73,6 +74,8 @@ public class MiraPluginInfo internal List Modifiers { get; } = []; + internal List GameModifiers { get; } = []; + internal Dictionary CustomRoles { get; } = []; internal Dictionary GameModes { get; } = []; diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index ecb4326..27df3cd 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -47,7 +47,7 @@ public interface ICustomRole : IOptionable /// /// Gets the role options group. /// - public RoleOptionsGroup RoleOptionsGroup => Team switch + RoleOptionsGroup RoleOptionsGroup => Team switch { ModdedRoleTeams.Crewmate => RoleOptionsGroup.Crewmate, ModdedRoleTeams.Impostor => RoleOptionsGroup.Impostor, @@ -58,7 +58,7 @@ public interface ICustomRole : IOptionable /// /// Gets the role's TeamIntroCutscene configuration. /// - public TeamIntroConfiguration? IntroConfiguration => Team switch + TeamIntroConfiguration? IntroConfiguration => Team switch { ModdedRoleTeams.Custom => TeamIntroConfiguration.Neutral, _ => null, diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 0a4fee3..a85da1d 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -39,6 +39,16 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Determines if a float is an integer. + /// + /// The float number. + /// True if the float is an integer, false otherwise. + public static bool IsInteger(this float number) + { + return Mathf.Approximately(number, Mathf.Round(number)); + } + /// /// Gets the best constructor for a type based on the specified arguments. /// From f2add2a65be0e89af3c96651a04240382155a6d7 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:21:37 -0500 Subject: [PATCH 64/71] this is very broken idk why --- .../Options/Modifiers/FreezeModifierSettings.cs | 13 +++++++++++++ MiraAPI/GameOptions/ModifierOptionGroup.cs | 4 ++-- MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 15 +++++++++++---- 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs diff --git a/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs new file mode 100644 index 0000000..13cdef9 --- /dev/null +++ b/MiraAPI.Example/Options/Modifiers/FreezeModifierSettings.cs @@ -0,0 +1,13 @@ +using MiraAPI.Example.Modifiers.Freezer; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Options.Modifiers; + +public class FreezeModifierSettings : AbstractOptionGroup +{ + public override string GroupName => "Freeze"; + + [ModdedToggleOption("Use Color")] + public bool UseColor { get; set; } = true; +} diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs index f845952..cfa2eff 100644 --- a/MiraAPI/GameOptions/ModifierOptionGroup.cs +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -6,10 +6,10 @@ internal class ModifierOptionGroup : AbstractOptionGroup { public override string GroupName { get; } - public ModifierOptionGroup(string name, IModdedOption[] options, params AbstractOptionGroup[] others) + public ModifierOptionGroup(string name, IModdedOption[] options, params AbstractOptionGroup[] groups) { GroupName = name; Options.AddRange(options); - Options.AddRange(others.SelectMany(x=>x.Options)); + Options.AddRange(groups.SelectMany(x=>x.Options)); } } diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 7b24619..7867c20 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -122,15 +122,22 @@ public static class GameOptionsMenuPatch private static void ModifiersCreate(GameOptionsMenu menu) { - var modifiers = GameSettingMenuPatches.SelectedMod?.GameModifiers ?? []; + var modifiers = GameSettingMenuPatches.SelectedMod?.Modifiers ?? []; var optLookup = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x => x is IOptionableGroup group && group.OptionableType.IsAssignableTo(typeof(GameModifier))) - .ToLookup(x => (x as IOptionableGroup)!.OptionableType); + .OfType() + .ToLookup(x => x.OptionableType); foreach (var mod in modifiers) { - mod.ModifierOptionsGroup = new ModifierOptionGroup(mod.ModifierName, [mod.AmountOption, mod.ChanceOption], [.. optLookup?[mod.GetType()] ?? []]); + IModdedOption[] opts = mod is GameModifier gm ? [gm.AmountOption, gm.ChanceOption] : []; + if (optLookup?[mod.GetType()].Any() == false && opts.Length == 0) + { + continue; + } + + var groups = optLookup?[mod.GetType()] ?? []; + mod.ModifierOptionsGroup = new ModifierOptionGroup(mod.ModifierName, opts, [.. groups.OfType()]); CreateGroup(menu, mod.ModifierOptionsGroup); } } From 68304a5eb83bc36f5bc9377c8c677c7ff2a2c162 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:10:14 -0500 Subject: [PATCH 65/71] simplify + partial unscuff --- MiraAPI.Example/Modifiers/CaptainModifier.cs | 14 ++- .../Modifiers/HighPriorityModifier.cs | 13 ++- .../Modifiers/CaptainModifierSettings.cs | 6 + MiraAPI/GameOptions/AbstractOptionGroup.cs | 13 ++- MiraAPI/GameOptions/IOptionableGroup.cs | 14 --- MiraAPI/GameOptions/ModdedOptionsManager.cs | 2 +- MiraAPI/Modifiers/BaseModifier.cs | 2 - MiraAPI/Modifiers/ModifierManager.cs | 32 +++--- MiraAPI/Modifiers/Types/GameModifier.cs | 67 +---------- .../Patches/Options/GameOptionsMenuPatch.cs | 35 ++---- .../Patches/Options/GameSettingMenuPatches.cs | 106 +++++++++++------- .../Patches/Options/LobbyViewPanePatches.cs | 6 +- .../Patches/Options/RoleSettingMenuPatches.cs | 4 +- MiraAPI/PluginLoading/MiraPluginInfo.cs | 2 - MiraAPI/PluginLoading/MiraPluginManager.cs | 5 +- 15 files changed, 144 insertions(+), 177 deletions(-) delete mode 100644 MiraAPI/GameOptions/IOptionableGroup.cs diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 5845e5a..ca8a570 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -1,8 +1,20 @@ -using MiraAPI.Modifiers.Types; +using MiraAPI.Example.Options.Modifiers; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; public class CaptainModifier : GameModifier { public override string ModifierName => "Captain"; + + public override int GetAssignmentChance() + { + return (int)OptionGroupSingleton.Instance.Chance; + } + + public override int GetAmountPerGame() + { + return (int)OptionGroupSingleton.Instance.Amount; + } } diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index 1d1ddd5..f4dc30b 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -1,4 +1,5 @@ -using MiraAPI.Modifiers.Types; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; @@ -6,6 +7,16 @@ public class HighPriorityModifier : GameModifier { public override string ModifierName => "High Priority"; + public override int GetAssignmentChance() + { + return 100; + } + + public override int GetAmountPerGame() + { + return 1; + } + public override int Priority() { return 100; diff --git a/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs index 2ae297b..0246a1d 100644 --- a/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs +++ b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs @@ -8,6 +8,12 @@ public class CaptainModifierSettings : AbstractOptionGroup { public override string GroupName => "Captain Settings"; + [ModdedNumberOption("Amount", 0, 5)] + public float Amount { get; set; } = 1; + + [ModdedNumberOption("Chance", 0, 100, 10)] + public float Chance { get; set; } = 50; + [ModdedNumberOption("Uses", 1, 5)] public float NumUses { get; set; } = 3; } diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 96dcdd7..c3e9cde 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -16,6 +16,11 @@ public abstract class AbstractOptionGroup /// public abstract string GroupName { get; } + /// + /// Gets the Optionable type of the group. + /// + public virtual Type? OptionableType => null; + /// /// Gets the function that determines whether the group should be visible or not. /// @@ -41,10 +46,8 @@ public abstract class AbstractOptionGroup /// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. /// /// The type of the optionable that this group contains. -public abstract class AbstractOptionGroup : AbstractOptionGroup, IOptionableGroup where T : IOptionable +public abstract class AbstractOptionGroup : AbstractOptionGroup where T : IOptionable { - /// - /// Gets the Optionable type of the group. - /// - public Type OptionableType => typeof(T); + /// + public override Type OptionableType => typeof(T); } diff --git a/MiraAPI/GameOptions/IOptionableGroup.cs b/MiraAPI/GameOptions/IOptionableGroup.cs deleted file mode 100644 index e476548..0000000 --- a/MiraAPI/GameOptions/IOptionableGroup.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace MiraAPI.GameOptions; - -/// -/// Interface for groups that have some type of "Optionable" class. -/// -public interface IOptionableGroup -{ - /// - /// Gets the type of the Optionable class. - /// - Type OptionableType { get; } -} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index cbb6459..7aaa154 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -108,7 +108,7 @@ public static class ModdedOptionsManager RegisterOption(option, group, property.Name, pluginInfo); } - private static void RegisterOption( + internal static void RegisterOption( IModdedOption option, AbstractOptionGroup group, string propertyName, diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 1bd1082..0dd2149 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -42,8 +42,6 @@ public abstract class BaseModifier : IOptionable x => x.Modifiers.Exists(y => y.TypeId == TypeId) ) ?? throw new InvalidOperationException("Modifier is not registered."); - internal ModifierOptionGroup? ModifierOptionsGroup { get; set; } - /// /// Gets the modifier name. /// diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index fee96ab..5f6bbff 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -63,32 +63,32 @@ public static class ModifierManager IdToTypeModifierMap.Add(GetNextTypeId(), modifierType); TypeToIdModifierMap.Add(modifierType, _nextTypeId); - var bm = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); // this probably isn't a great idea - info.Modifiers.Add(bm); - if (!typeof(GameModifier).IsAssignableFrom(modifierType)) + BaseModifier modifier; + if (modifierType.GetConstructor(Type.EmptyTypes) != null) + { + modifier = (BaseModifier)Activator.CreateInstance(modifierType)!; + } + else + { + // this is probably terrible but its good enough for now. + modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); + } + + info.Modifiers.Add(modifier); + + if (modifier is not GameModifier gameModifier) { return true; } if (modifierType.GetConstructor(Type.EmptyTypes) == null) { - Logger.Error($"Modifier {modifierType.FullName} does not have a parameterless constructor!"); + Logger.Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!"); return false; } - if (Activator.CreateInstance(modifierType) is not GameModifier mod) - { - Logger.Error($"Modifier {modifierType.FullName} could not be cast to GameModifier!"); - return false; - } - - info.GameModifiers.Add(mod); - - info.PluginConfig.Bind(mod.AmountDefinition, 0); - info.PluginConfig.Bind(mod.ChanceDefinition, 0); - - var priority = mod!.Priority(); + var priority = gameModifier.Priority(); if (!PrioritiesToIdsMap.TryGetValue(priority, out var list)) { diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index 7e4cccb..abcd1f7 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -1,10 +1,4 @@ -using System; -using BepInEx.Configuration; -using MiraAPI.GameOptions.OptionTypes; -using MiraAPI.Utilities; -using Reactor.Utilities; - -namespace MiraAPI.Modifiers.Types; +namespace MiraAPI.Modifiers.Types; /// /// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role. @@ -14,72 +8,17 @@ public abstract class GameModifier : BaseModifier /// public override bool ShowInFreeplay => true; - /// - /// Gets the assignment configuration for the modifier. - /// - public virtual AssignmentConfiguration AssignmentConfiguration => new(0, 0, true, true); - - internal ConfigDefinition AmountDefinition => new("Modifiers", $"Amount {GetType().FullName}"); - - internal ConfigDefinition ChanceDefinition => new("Modifiers", $"Chance {GetType().FullName}"); - - internal ModdedNumberOption AmountOption { get; } = new("Amount", 0, 0, 15, 1, MiraNumberSuffixes.None); - - internal ModdedNumberOption ChanceOption { get; } = new("Chance", 0, 0, 100, 1, MiraNumberSuffixes.Percent); - /// /// Gets the chance of the modifier being assigned to a player. /// /// An int value between 0 and 100 representing percent. - public virtual int GetAssignmentChance() - { - return AssignmentConfiguration.CreateChanceOption && - ParentMod.PluginConfig.TryGetEntry(ChanceDefinition, out ConfigEntry entry) - ? entry.Value - : AssignmentConfiguration.DefaultChance; - } + public abstract int GetAssignmentChance(); /// /// Gets the amount of players that can have this modifier in a game. /// /// An int value greater than or equal to zero. - public virtual int GetAmountPerGame() - { - return AssignmentConfiguration.CreateAmountOption && - ParentMod.PluginConfig.TryGetEntry(AmountDefinition, out ConfigEntry entry) - ? entry.Value - : AssignmentConfiguration.DefaultAmount; - } - - /// - /// Sets the chance of the modifier being assigned to a player. - /// - /// The chance between 0 and 100. - public virtual void SetAssignmentChance(int chance) - { - if (ParentMod.PluginConfig.TryGetEntry(ChanceDefinition, out ConfigEntry entry)) - { - entry.Value = Math.Clamp(chance, 0, 100); - return; - } - - Logger.Error($"Error getting chance configuration for modifier: {ModifierName}"); - } - - /// - /// Sets the amount of players that can have this modifier in a game. - /// - /// The amount of players between zero and the max player count. - public virtual void SetAmountPerGame(int amount) - { - if (ParentMod.PluginConfig.TryGetEntry(AmountDefinition, out ConfigEntry entry)) - { - entry.Value = Math.Max(amount, 0); - return; - } - - Logger.Error($"Error getting amount configuration for modifier: {ModifierName}"); - } + public abstract int GetAmountPerGame(); /// /// Gets the priority at which the modifier will spawn. The higher the value, the higher up on the assignment list. diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 7867c20..744c754 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -2,6 +2,7 @@ using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; +using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -44,7 +45,7 @@ public static class GameOptionsMenuPatch { var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x => x is not IOptionableGroup) ?? []; + .Where(x => x.OptionableType == null) ?? []; foreach (var group in filteredGroups) { @@ -109,36 +110,22 @@ public static class GameOptionsMenuPatch private static void ModifiersUpdate(ref float num) { - var modifiers = GameSettingMenuPatches.SelectedMod?.Modifiers.Select(x=>x.ModifierOptionsGroup) ?? []; + var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? []; - foreach (var modGroup in modifiers) + foreach (var modGroup in groups) { - if (modGroup != null) - { - UpdateGroup(modGroup, ref num); - } + UpdateGroup(modGroup, ref num); } } private static void ModifiersCreate(GameOptionsMenu menu) { - var modifiers = GameSettingMenuPatches.SelectedMod?.Modifiers ?? []; - - var optLookup = GameSettingMenuPatches.SelectedMod?.OptionGroups - .OfType() - .ToLookup(x => x.OptionableType); - - foreach (var mod in modifiers) + var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; + foreach (var group in groups) { - IModdedOption[] opts = mod is GameModifier gm ? [gm.AmountOption, gm.ChanceOption] : []; - if (optLookup?[mod.GetType()].Any() == false && opts.Length == 0) - { - continue; - } - - var groups = optLookup?[mod.GetType()] ?? []; - mod.ModifierOptionsGroup = new ModifierOptionGroup(mod.ModifierName, opts, [.. groups.OfType()]); - CreateGroup(menu, mod.ModifierOptionsGroup); + CreateGroup(menu, group); } } @@ -161,7 +148,7 @@ public static class GameOptionsMenuPatch } var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x => x is not IOptionableGroup) ?? []; + .Where(x => x.OptionableType == null) ?? []; foreach (var group in filteredGroups) { diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index ab3ead7..bb52c4a 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,5 +1,8 @@ using System; +using System.Linq; using HarmonyLib; +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; @@ -73,36 +76,43 @@ internal static class GameSettingMenuPatches var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject; nextButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f); nextButton.name = "RightArrowButton"; - nextButton.transform.FindChild("Inactive").gameObject.GetComponent().sprite = MiraAssets.NextButton.LoadAsset(); - nextButton.transform.FindChild("Active").gameObject.GetComponent().sprite = MiraAssets.NextButtonActive.LoadAsset(); + nextButton.transform.FindChild("Inactive").gameObject.GetComponent().sprite = + MiraAssets.NextButton.LoadAsset(); + nextButton.transform.FindChild("Active").gameObject.GetComponent().sprite = + MiraAssets.NextButtonActive.LoadAsset(); nextButton.gameObject.GetComponent().DestroyImmediate(); var passiveButton = nextButton.gameObject.GetComponent(); passiveButton.OnClick = new ButtonClickedEvent(); - passiveButton.OnClick.AddListener((UnityAction)(() => - { - SelectedModIdx += 1; - if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) + passiveButton.OnClick.AddListener( + (UnityAction)(() => { - SelectedModIdx = 0; - } - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); - })); + SelectedModIdx += 1; + if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) + { + SelectedModIdx = 0; + } + + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); + })); var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject; backButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f); backButton.name = "LeftArrowButton"; backButton.gameObject.GetComponent().Destroy(); - backButton.transform.FindChild("Active").gameObject.GetComponent().flipX = backButton.transform.FindChild("Inactive").gameObject.GetComponent().flipX = true; - backButton.gameObject.GetComponent().OnClick.AddListener((UnityAction)(() => - { - SelectedModIdx -= 1; - if (SelectedModIdx < 0) + backButton.transform.FindChild("Active").gameObject.GetComponent().flipX = + backButton.transform.FindChild("Inactive").gameObject.GetComponent().flipX = true; + backButton.gameObject.GetComponent().OnClick.AddListener( + (UnityAction)(() => { - SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; - } - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); - })); + SelectedModIdx -= 1; + if (SelectedModIdx < 0) + { + SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; + } + + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); + })); // clone game settings tab for modifiers _modifiersTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent); @@ -110,23 +120,33 @@ internal static class GameSettingMenuPatches // create button for modifiers __instance.RoleSettingsButton.gameObject.SetActive(false); - _smallRolesButton = Object.Instantiate(__instance.RoleSettingsButton, __instance.RoleSettingsButton.transform.parent); - var pos = new Vector3(-3.65f, _smallRolesButton.transform.localPosition.y, _smallRolesButton.transform.localPosition.z); + _smallRolesButton = Object.Instantiate( + __instance.RoleSettingsButton, + __instance.RoleSettingsButton.transform.parent); + var pos = new Vector3( + -3.65f, + _smallRolesButton.transform.localPosition.y, + _smallRolesButton.transform.localPosition.z); _smallRolesButton.transform.localPosition = pos; - _smallRolesButton.OnClick.AddListener((UnityAction)(() => - { - __instance.ChangeTab(2, false); - })); - _smallRolesButton.OnMouseOver.AddListener((UnityAction)(() => - { - __instance.ChangeTab(2, true); - })); + _smallRolesButton.OnClick.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(2, false); + })); + _smallRolesButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(2, true); + })); var roleText = _smallRolesButton.buttonText; roleText.text = "Roles"; roleText.GetComponent().Destroy(); roleText.alignment = TextAlignmentOptions.Center; - roleText.transform.parent.localPosition = new Vector3(-.525f, roleText.transform.parent.localPosition.y, roleText.transform.parent.localPosition.z); + roleText.transform.parent.localPosition = new Vector3( + -.525f, + roleText.transform.parent.localPosition.y, + roleText.transform.parent.localPosition.z); foreach (var collider in _smallRolesButton.Colliders) { @@ -135,6 +155,7 @@ internal static class GameSettingMenuPatches col.size = new Vector2(col.size.x / 2, col.size.y); } } + foreach (var rend in _smallRolesButton.GetComponentsInChildren(true)) { rend.size = new Vector2(rend.size.x / 2, rend.size.y); @@ -142,15 +163,17 @@ internal static class GameSettingMenuPatches _modifiersButton = Object.Instantiate(_smallRolesButton, _smallRolesButton.transform.parent); _modifiersButton.OnClick = new ButtonClickedEvent(); - _modifiersButton.OnClick.AddListener((UnityAction)(() => - { - __instance.ChangeTab(3, false); - })); + _modifiersButton.OnClick.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(3, false); + })); _modifiersButton.OnMouseOver = new UnityEvent(); - _modifiersButton.OnMouseOver.AddListener((UnityAction)(() => - { - __instance.ChangeTab(3, true); - })); + _modifiersButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(3, true); + })); _modifiersButton.buttonText.text = "Modifiers"; pos.x = -2.27f; @@ -179,7 +202,8 @@ internal static class GameSettingMenuPatches _modifiersButton.gameObject.SetActive(false); _smallRolesButton.gameObject.SetActive(false); menu.RoleSettingsButton.gameObject.SetActive(false); - if (SelectedModIdx != 0 && SelectedMod?.GameModifiers.Count != 0) + if (SelectedModIdx != 0 && + SelectedMod?.OptionGroups.Exists(x => x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) == true) { _modifiersButton.gameObject.SetActive(true); _smallRolesButton.gameObject.SetActive(true); @@ -225,11 +249,13 @@ internal static class GameSettingMenuPatches { child.gameObject.DestroyImmediate(); } + roles.advancedSettingChildren.Clear(); roles.advancedSettingChildren = null; } - foreach (var header in roles.RoleChancesSettings.transform.GetComponentsInChildren()) + foreach (var header in roles.RoleChancesSettings.transform + .GetComponentsInChildren()) { header.gameObject.DestroyImmediate(); } diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index bda1c2b..fedded2 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -127,7 +127,7 @@ public static class LobbyViewPanePatches var num = 1.44f; var filteredGroups = SelectedMod.OptionGroups - .Where(x => x.GroupVisible.Invoke() && x is not IOptionableGroup); + .Where(x => x.OptionableType == null && x.GroupVisible.Invoke()); foreach (var group in filteredGroups) { @@ -280,7 +280,7 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f); var advancedRoleOptions = SelectedMod.OptionGroups - .Where(x => x is IOptionableGroup optionGroup && optionGroup.OptionableType == customRole.GetType()) + .Where(x => x.OptionableType == customRole.GetType()) .SelectMany(x => x.Options) .ToList(); @@ -390,7 +390,7 @@ public static class LobbyViewPanePatches var num2 = 1.08f; var filteredOptions = SelectedMod.OptionGroups - .Where(x => x is IOptionableGroup optionGroup && optionGroup.OptionableType == roleType) + .Where(x => x.OptionableType == roleType) .SelectMany(x=>x.Options) .ToList(); diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 13c190f..af17682 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -307,7 +307,7 @@ public static class RoleSettingMenuPatches // TODO: create sub groups under the role settings. var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x=> x is IOptionableGroup optionableGroup && optionableGroup.OptionableType == role.GetType()) + .Where(x=> x.OptionableType == role.GetType()) .SelectMany(x=>x.Options) .ToList() ?? []; @@ -471,7 +471,7 @@ public static class RoleSettingMenuPatches if (GameSettingMenuPatches.SelectedMod is null || GameSettingMenuPatches.SelectedMod.OptionGroups - .Exists(x => x is IOptionableGroup optionGroup && optionGroup.OptionableType == role.GetType())) + .Exists(x => x.OptionableType == role.GetType())) { var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform); newButton.name = "ConfigButton"; diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 2dfe479..efbf20c 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -74,8 +74,6 @@ public class MiraPluginInfo internal List Modifiers { get; } = []; - internal List GameModifiers { get; } = []; - internal Dictionary CustomRoles { get; } = []; internal Dictionary GameModes { get; } = []; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index eced634..15fe686 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using BepInEx.Unity.IL2CPP; @@ -78,7 +79,7 @@ public sealed class MiraPluginManager if (RegisterRoleAttribute(type, info, out var role)) { - roles.Add(role!); + roles.Add(role); continue; } @@ -172,7 +173,7 @@ public sealed class MiraPluginManager return false; } - private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, out Type? role) + private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role) { role = null; try From d97588b356142d5cb802901913ac98caa8f842fd Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:12:09 -0500 Subject: [PATCH 66/71] add lobby view pane settings --- .../Patches/Options/LobbyViewPanePatches.cs | 116 ++++++++++++------ 1 file changed, 76 insertions(+), 40 deletions(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index fedded2..f5895be 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; +using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -18,17 +20,38 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(LobbyViewSettingsPane))] public static class LobbyViewPanePatches { - private static int CurrentSelectedMod { get; set; } + private static int SelectedModIdx { get; set; } - private static MiraPluginInfo? SelectedMod => CurrentSelectedMod == 0 + private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0 ? null - : MiraPluginManager.Instance.RegisteredPlugins()[CurrentSelectedMod - 1]; + : MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1]; + + private static PassiveButton? ModifiersTabButton { get; set; } + + private static StringNames ModifiersTabName { get; } = CustomStringName.CreateAndRegister("ModifiersTab"); [HarmonyPostfix] [HarmonyPatch(nameof(LobbyViewSettingsPane.Awake))] public static void AwakePatch(LobbyViewSettingsPane __instance) { __instance.gameModeText.transform.localPosition = new Vector3(-2.3f, 2.4f, -2); + __instance.gameModeText.GetComponent().Destroy(); + + // create modifiers button + ModifiersTabButton = Object.Instantiate(__instance.rolesTabButton, __instance.rolesTabButton.transform.parent); + ModifiersTabButton.buttonText.gameObject.GetComponent().Destroy(); + ModifiersTabButton.name = "ModifiersTabButton"; + var pos = ModifiersTabButton.transform.localPosition; + pos.x = 2.1f; + ModifiersTabButton.transform.localPosition = pos; + ModifiersTabButton.buttonText.text = "Modifiers"; + ModifiersTabButton.OnClick = new Button.ButtonClickedEvent(); + ModifiersTabButton.OnClick.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(ModifiersTabName); + })); + ModifiersTabButton.gameObject.SetActive(SelectedModIdx!=0); // Create the next button var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject; @@ -50,14 +73,13 @@ public static class LobbyViewPanePatches passiveButton.OnClick.AddListener( (UnityAction)(() => { - CurrentSelectedMod += 1; - if (CurrentSelectedMod > MiraPluginManager.Instance.RegisteredPlugins().Length) + SelectedModIdx += 1; + if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) { - CurrentSelectedMod = 0; + SelectedModIdx = 0; } - __instance.RefreshTab(); - __instance.scrollBar.ScrollToTop(); + Refresh(__instance); })); // Create the back button @@ -73,34 +95,56 @@ public static class LobbyViewPanePatches passiveButton2.OnClick.AddListener( (UnityAction)(() => { - CurrentSelectedMod -= 1; - if (CurrentSelectedMod < 0) + SelectedModIdx -= 1; + if (SelectedModIdx < 0) { - CurrentSelectedMod = MiraPluginManager.Instance.RegisteredPlugins().Length; + SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; } - __instance.RefreshTab(); - __instance.scrollBar.ScrollToTop(); + Refresh(__instance); })); } - [HarmonyPostfix] - [HarmonyPatch(nameof(LobbyViewSettingsPane.Update))] - public static void UpdatePatch(LobbyViewSettingsPane __instance) + private static void Refresh(LobbyViewSettingsPane menu) { - __instance.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Default"; + ModifiersTabButton?.gameObject.SetActive(SelectedModIdx != 0); + menu.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Default"; + menu.RefreshTab(); + menu.scrollBar.ScrollToTop(); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(LobbyViewSettingsPane.SetTab))] + public static bool SetTabPatch(LobbyViewSettingsPane __instance) + { + if (__instance.currentTab != ModifiersTabName || SelectedMod == null) + { + ModifiersTabButton?.SelectButton(false); + return true; + } + + __instance.taskTabButton.SelectButton(false); + __instance.rolesTabButton.SelectButton(false); + + ModifiersTabButton?.SelectButton(true); + var filteredGroups = SelectedMod.OptionGroups + .Where(x => x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true); + DrawOptions(__instance, filteredGroups); + return false; } [HarmonyPrefix] [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))] public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance) { - if (CurrentSelectedMod == 0) + if (SelectedModIdx == 0 || SelectedMod == null) { return true; } - DrawOptionsTab(__instance); + var filteredGroups = SelectedMod.OptionGroups + .Where(x => x.OptionableType == null && x.GroupVisible.Invoke()); + DrawOptions(__instance, filteredGroups); return false; } @@ -108,7 +152,7 @@ public static class LobbyViewPanePatches [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawRolesTab))] public static bool DrawRolesTabPatch(LobbyViewSettingsPane __instance) { - if (CurrentSelectedMod == 0) + if (SelectedModIdx == 0) { return true; } @@ -117,30 +161,22 @@ public static class LobbyViewPanePatches return false; } - private static void DrawOptionsTab(LobbyViewSettingsPane instance) + private static void DrawOptions(LobbyViewSettingsPane menu, IEnumerable groups) { - if (SelectedMod == null) - { - return; - } - var num = 1.44f; - var filteredGroups = SelectedMod.OptionGroups - .Where(x => x.OptionableType == null && x.GroupVisible.Invoke()); - - foreach (var group in filteredGroups) + foreach (var group in groups) { var categoryHeaderMasked = Object.Instantiate( - instance.categoryHeaderOrigin, - instance.settingsContainer, + menu.categoryHeaderOrigin, + menu.settingsContainer, true); categoryHeaderMasked.SetHeader(StringNames.Name, 61); categoryHeaderMasked.Title.text = group.GroupName; categoryHeaderMasked.transform.localScale = Vector3.one; categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, num, -2f); - instance.settingsInfo.Add(categoryHeaderMasked.gameObject); - num -= 0.85f; + menu.settingsInfo.Add(categoryHeaderMasked.gameObject); + num -= 1.05f; var i = 0; @@ -152,8 +188,8 @@ public static class LobbyViewPanePatches } var viewSettingsInfoPanel = Object.Instantiate( - instance.infoPanelOrigin, - instance.settingsContainer, + menu.infoPanelOrigin, + menu.settingsContainer, true); viewSettingsInfoPanel.transform.localScale = Vector3.one; float num2; @@ -162,7 +198,7 @@ public static class LobbyViewPanePatches num2 = -8.95f; if (i > 0) { - num -= 0.59f; + num -= 0.85f; } } else @@ -186,14 +222,14 @@ public static class LobbyViewPanePatches viewSettingsInfoPanel.SetInfo(data.Title, data.GetValueString(option.GetFloatData()), 61); } - instance.settingsInfo.Add(viewSettingsInfoPanel.gameObject); + menu.settingsInfo.Add(viewSettingsInfoPanel.gameObject); i++; } - num -= 0.59f; + num -= 0.85f; } - instance.scrollBar.CalculateAndSetYBounds(instance.settingsInfo.Count + 10, 2f, 6f, 0.59f); + menu.scrollBar.CalculateAndSetYBounds(menu.settingsInfo.Count + 10, 2f, 6f, 0.85f); } private static void DrawRolesTab(LobbyViewSettingsPane instance) From e52f48c20d870e996d10a855274c6dafeee5c3d3 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:28:20 -0500 Subject: [PATCH 67/71] allow any groups to be shown in modifiers menu --- .../Modifiers/GeneralModifierOptions.cs | 19 +++++++++++++++++++ MiraAPI/GameOptions/AbstractOptionGroup.cs | 6 ++++++ .../Patches/Options/GameOptionsMenuPatch.cs | 4 ++-- .../Patches/Options/GameSettingMenuPatches.cs | 2 +- .../Patches/Options/LobbyViewPanePatches.cs | 2 +- 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 MiraAPI.Example/Modifiers/GeneralModifierOptions.cs diff --git a/MiraAPI.Example/Modifiers/GeneralModifierOptions.cs b/MiraAPI.Example/Modifiers/GeneralModifierOptions.cs new file mode 100644 index 0000000..378c460 --- /dev/null +++ b/MiraAPI.Example/Modifiers/GeneralModifierOptions.cs @@ -0,0 +1,19 @@ +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Modifiers; + +public class GeneralModifierOptions : AbstractOptionGroup +{ + public override string GroupName => "General"; + + public override uint GroupPriority => 0; + + public override bool ShowInModifiersMenu => true; + + [ModdedToggleOption("Some Boolean Option")] + public bool SomeBooleanOption { get; set; } = true; + + [ModdedNumberOption("Number Option", 0, 10, 1)] + public float NumberOption { get; set; } = 5; +} diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index c3e9cde..45f9bd4 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -21,6 +21,12 @@ public abstract class AbstractOptionGroup /// public virtual Type? OptionableType => null; + /// + /// Gets a value indicating whether the group should be shown in the modifiers menu. + /// + // TODO: make this not a boolean + public virtual bool ShowInModifiersMenu => false; + /// /// Gets the function that determines whether the group should be visible or not. /// diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 744c754..825ecc6 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -111,7 +111,7 @@ public static class GameOptionsMenuPatch private static void ModifiersUpdate(ref float num) { var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x => x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? []; + .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? []; foreach (var modGroup in groups) { @@ -122,7 +122,7 @@ public static class GameOptionsMenuPatch private static void ModifiersCreate(GameOptionsMenu menu) { var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x => x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; + .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; foreach (var group in groups) { CreateGroup(menu, group); diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index bb52c4a..cb84518 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -203,7 +203,7 @@ internal static class GameSettingMenuPatches _smallRolesButton.gameObject.SetActive(false); menu.RoleSettingsButton.gameObject.SetActive(false); if (SelectedModIdx != 0 && - SelectedMod?.OptionGroups.Exists(x => x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) == true) + SelectedMod?.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) == true) { _modifiersButton.gameObject.SetActive(true); _smallRolesButton.gameObject.SetActive(true); diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index f5895be..9b2a934 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -128,7 +128,7 @@ public static class LobbyViewPanePatches ModifiersTabButton?.SelectButton(true); var filteredGroups = SelectedMod.OptionGroups - .Where(x => x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true); + .Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true)); DrawOptions(__instance, filteredGroups); return false; } From b6f304fe1dfcdb2d93d9f629736836a2727e30e1 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:59:55 -0500 Subject: [PATCH 68/71] organize example --- .../{ => Options}/Modifiers/GeneralModifierOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename MiraAPI.Example/{ => Options}/Modifiers/GeneralModifierOptions.cs (91%) diff --git a/MiraAPI.Example/Modifiers/GeneralModifierOptions.cs b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs similarity index 91% rename from MiraAPI.Example/Modifiers/GeneralModifierOptions.cs rename to MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs index 378c460..1414799 100644 --- a/MiraAPI.Example/Modifiers/GeneralModifierOptions.cs +++ b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs @@ -1,7 +1,7 @@ using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; -namespace MiraAPI.Example.Modifiers; +namespace MiraAPI.Example.Options.Modifiers; public class GeneralModifierOptions : AbstractOptionGroup { From e680b5c87244c4209431e293f9fedb74264cb800 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:00:05 -0500 Subject: [PATCH 69/71] unity lifetime check --- MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 2 +- MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 825ecc6..fa49d2b 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -111,7 +111,7 @@ public static class GameOptionsMenuPatch private static void ModifiersUpdate(ref float num) { var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? []; + .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; foreach (var modGroup in groups) { diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index cb84518..8676d4b 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -35,7 +35,7 @@ internal static class GameSettingMenuPatches { if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly) { - _modifiersTab?.gameObject.SetActive(tabNum == 3); + if (_modifiersTab) _modifiersTab?.gameObject.SetActive(tabNum == 3); _modifiersButton?.SelectButton(tabNum == 3); _smallRolesButton?.SelectButton(tabNum == 2); } From e43cd0cefffc430ff41091f86bb6403bd1a4de8b Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:00:16 -0500 Subject: [PATCH 70/71] remove debug log --- MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index fa49d2b..f207d3b 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -142,7 +142,6 @@ public static class GameOptionsMenuPatch if (__instance.name == "MODIFIERS TAB") { - Logger.Error("Creating modifiers"); ModifiersCreate(__instance); return false; } From e02c06ca07d13d2acd4dd810664dac1e849df03d Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:29:30 -0500 Subject: [PATCH 71/71] final cleanup --- MiraAPI.Example/Modifiers/HighPriorityModifier.cs | 3 +-- MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs | 3 +-- MiraAPI.Example/Options/Roles/TeleporterOptions.cs | 3 +-- MiraAPI/GameOptions/OptionTypes/ModdedOption.cs | 1 - MiraAPI/Patches/Options/GameOptionsMenuPatch.cs | 5 +---- MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 2 -- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 1 - MiraAPI/PluginLoading/MiraPluginInfo.cs | 1 - 8 files changed, 4 insertions(+), 15 deletions(-) diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index f4dc30b..9f13b18 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -1,5 +1,4 @@ -using MiraAPI.GameOptions; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; diff --git a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs index 6113408..1473068 100644 --- a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs +++ b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs @@ -1,5 +1,4 @@ -using System; -using MiraAPI.Example.Roles; +using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Utilities; diff --git a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs index 1b0c1da..b6d02a4 100644 --- a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs +++ b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs @@ -1,5 +1,4 @@ -using System; -using MiraAPI.Example.Roles; +using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.GameOptions.OptionTypes; diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 0cfefa8..670fb3c 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -2,7 +2,6 @@ using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; -using MiraAPI.Roles; using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; using UnityEngine; diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index f207d3b..aca0f2e 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -3,12 +3,9 @@ using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; -using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; -using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -186,7 +183,7 @@ public static class GameOptionsMenuPatch continue; } - optionBehaviour.OnValueChanged = new System.Action(__instance.ValueChanged); + optionBehaviour.OnValueChanged = new Action(__instance.ValueChanged); } return false; diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 8676d4b..7781bf8 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,8 +1,6 @@ using System; -using System.Linq; using HarmonyLib; using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index af17682..126f0b5 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; -using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; using MiraAPI.Utilities; diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index efbf20c..68d3ff0 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -6,7 +6,6 @@ using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; namespace MiraAPI.PluginLoading;