diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index e26405e..3b7363a 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 fed4e1f..c4d59e3 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.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/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index ce20ddb..76a5b9e 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; @@ -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.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.Example/GameOver/NeutralKillerGameOver.cs b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs new file mode 100644 index 0000000..351d593 --- /dev/null +++ b/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs @@ -0,0 +1,21 @@ +using MiraAPI.Example.Roles; +using MiraAPI.GameEnd; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Example.GameOver; + +public class NeutralKillerGameOver : CustomGameOver +{ + public override bool VerifyCondition(PlayerControl playerControl, NetworkedPlayerInfo[] winners) + { + return winners is [{ Role: NeutralKillerRole }]; + } + + 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/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 63b7c41..ca8a570 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -1,4 +1,6 @@ -using MiraAPI.Modifiers.Types; +using MiraAPI.Example.Options.Modifiers; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; @@ -6,13 +8,13 @@ public class CaptainModifier : GameModifier { public override string ModifierName => "Captain"; - public override int GetAmountPerGame() - { - return 1; - } - public override int GetAssignmentChance() { - return 100; + 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 c640ed8..9f13b18 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -5,6 +5,7 @@ namespace MiraAPI.Example.Modifiers; public class HighPriorityModifier : GameModifier { public override string ModifierName => "High Priority"; + public override int GetAssignmentChance() { return 100; 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.Example/Options/Modifiers/CaptainModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs new file mode 100644 index 0000000..0246a1d --- /dev/null +++ b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs @@ -0,0 +1,19 @@ +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("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.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.Example/Options/Modifiers/GeneralModifierOptions.cs b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs new file mode 100644 index 0000000..1414799 --- /dev/null +++ b/MiraAPI.Example/Options/Modifiers/GeneralModifierOptions.cs @@ -0,0 +1,19 @@ +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Options.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.Example/Options/Roles/FreezerRoleSettings.cs b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs index 0e26665..1473068 100644 --- a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs +++ b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs @@ -1,17 +1,14 @@ using MiraAPI.Example.Roles; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; -using System; 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..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; @@ -7,12 +6,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.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/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/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/GameEnd/CustomGameOver.cs b/MiraAPI/GameEnd/CustomGameOver.cs new file mode 100644 index 0000000..70d7b27 --- /dev/null +++ b/MiraAPI/GameEnd/CustomGameOver.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using MiraAPI.Networking; +using Reactor.Networking.Rpc; + +namespace MiraAPI.GameEnd; + +/// +/// Custom Game Over. +/// +public abstract class CustomGameOver +{ + /// + /// Gets the current CustomGameOver. + /// + public static CustomGameOver? Instance { get; internal set; } + + /// + /// 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, NetworkedPlayerInfo[] winners) + { + return true; + } + + /// + /// Runs before the base game calls EndGameManager.SetEverythingUp. + /// + /// The EndGameManager instance. + /// Return True to use the run the original method, return False to skip it. + public virtual bool BeforeEndGameSetup(EndGameManager endGameManager) + { + return true; + } + + /// + /// Runs after the base game calls EndGameManager.SetEverythingUp. + /// + /// The EndGameManager instance. + 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 Trigger(IEnumerable winners) where T : CustomGameOver + { + 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..6144d8a --- /dev/null +++ b/MiraAPI/GameEnd/GameOverManager.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Reactor.Utilities; + +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. + /// >True if the game over was registered successfully, false otherwise. + public static bool RegisterGameOver(Type gameOverType) + { + if (!typeof(CustomGameOver).IsAssignableFrom(gameOverType)) + { + return false; + } + + if (gameOverType.IsAbstract) + { + Logger.Error("The type must not be abstract."); + return false; + } + + if (gameOverType.GetConstructor(Type.EmptyTypes) == null) + { + Logger.Error("The type must have a parameterless constructor."); + return false; + } + + GameOverIds.Add(gameOverType, _nextId); + GameOverTypes.Add(_nextId, gameOverType); + _nextId++; + return true; + } + + /// + /// 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, [NotNullWhen(true)] 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/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 6eac8bd..45f9bd4 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using MiraAPI.PluginLoading; using UnityEngine; namespace MiraAPI.GameOptions; @@ -8,7 +7,6 @@ namespace MiraAPI.GameOptions; /// /// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. /// -[MiraIgnore] public abstract class AbstractOptionGroup { internal List Options { get; } = []; @@ -18,6 +16,17 @@ public abstract class AbstractOptionGroup /// public abstract string GroupName { get; } + /// + /// Gets the Optionable type of the group. + /// + 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. /// @@ -34,17 +43,17 @@ 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; - - /// - /// Gets the role the group is associated with. This is used for the advanced role options menu. - /// - public virtual Type? AdvancedMode => 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 where T : IOptionable +{ + /// + public override Type OptionableType => typeof(T); +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index 721710f..54680f2 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -1,24 +1,19 @@ -using MiraAPI.GameOptions.OptionTypes; -using System; +using System; using System.Reflection; +using MiraAPI.GameOptions.OptionTypes; namespace MiraAPI.GameOptions.Attributes; /// /// Attribute for creating an enum option. /// -/// The title of the option. -/// The Enum type. -/// An option list of string values to use in place of the enum name. -/// An optional role type. -/// An optional game mode type. [AttributeUsage(AttributeTargets.Property)] -public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null, Type? roleType = null, Type? modeType = null) - : ModdedOptionAttribute(title, roleType, modeType) +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, ModeType); + 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 50db882..07b5cfc 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -1,21 +1,13 @@ -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; /// /// A number option attribute for the modded options system. /// -/// The title of the option. -/// The minimum value. -/// The maximum value. -/// The increment. -/// The suffix type. -/// Whether zero is infinity or not. -/// An optional role type. -/// An optional game mode type. [AttributeUsage(AttributeTargets.Property)] public class ModdedNumberOptionAttribute( string title, @@ -23,14 +15,13 @@ public class ModdedNumberOptionAttribute( float max, float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, - bool zeroInfinity = false, - Type? roleType = null, - Type? modeType = null) - : ModdedOptionAttribute(title, roleType, modeType) + string? formatString = null, + 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, zeroInfinity, RoleType, ModeType); + 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 5adfe77..0ad64da 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; @@ -8,14 +8,12 @@ namespace MiraAPI.GameOptions.Attributes; /// Attribute for a toggle option. /// /// The option title. -/// An optional role type. -/// An optional game mode type. [AttributeUsage(AttributeTargets.Property)] -public class ModdedToggleOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : ModdedOptionAttribute(title, roleType, modeType) +public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(title) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), RoleType, ModeType); + var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false)); return toggleOpt; } diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index b2c542e..f40f3cf 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -13,8 +13,6 @@ public interface IModdedOption StringNames StringName { get; } IMiraPlugin? ParentMod { get; set; } BaseGameSetting Data { get; } - Type? AdvancedRole { get; set; } - Type? AdvancedMode { 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/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 977d059..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, @@ -119,11 +119,7 @@ public static class ModdedOptionsManager option.ConfigDefinition = new ConfigDefinition(groupName, propertyName); option.ParentMod = pluginInfo.MiraPlugin; - option.AdvancedRole = group.AdvancedRole; - option.AdvancedMode = group.AdvancedMode; - pluginInfo.Options.Add(option); - ModdedOptions.Add(option.Id, option); group.Options.Add(option); } diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs new file mode 100644 index 0000000..cfa2eff --- /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[] groups) + { + GroupName = name; + Options.AddRange(options); + Options.AddRange(groups.SelectMany(x=>x.Options)); + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 0f7eb80..ff422f5 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; @@ -26,9 +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. - /// An optional game mode type to specify for a specific mode. - public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, Type? roleType = null, Type? modeType = null) : base(title, defaultValue, roleType, modeType) + 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. /// @@ -119,9 +117,7 @@ public class ModdedEnumOption : ModdedOption where T : Enum /// The title of the option. /// The default value as an int. /// An option list of string values to use in place of the enum name. - /// An optional role type to specify for a specific role. - /// An optional mode type to specify for a specific gamemode. - public ModdedEnumOption(string title, T defaultValue, string[]? values = null, Type? roleType = null, Type? modeType = null) : base(title, defaultValue, roleType, modeType) + public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue) { Values = values ?? Enum.GetNames(typeof(T)); Data = ScriptableObject.CreateInstance(); @@ -147,7 +143,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; @@ -157,13 +153,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)!)); } /// @@ -175,13 +171,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; @@ -189,7 +185,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/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index 6320919..4d38c04 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; @@ -46,9 +46,8 @@ 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. - /// An optional game mode type. public ModdedNumberOption( string title, float defaultValue, @@ -56,9 +55,8 @@ public class ModdedNumberOption : ModdedOption float max, float increment, MiraNumberSuffixes suffixType, - bool zeroInfinity = false, - Type? roleType = null, - Type? modeType = null) : base(title, defaultValue, roleType, modeType) + string? formatString = null, + bool zeroInfinity = false) : base(title, defaultValue) { Min = min; Max = max; @@ -76,7 +74,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 ?? + (defaultValue.IsInteger() && + Increment.IsInteger() && + Value.IsInteger() && + Min.IsInteger() && + Max.IsInteger() ? "0" : "0.0"); + data.ZeroIsInfinity = ZeroInfinity; data.SuffixType = (NumberSuffixes)SuffixType; data.OptionName = FloatOptionNames.Invalid; @@ -138,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 2b74219..9e98467 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -1,11 +1,9 @@ -using BepInEx.Configuration; -using MiraAPI.GameModes; +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; @@ -73,16 +71,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 advanced mode of the option. - /// - public Type? AdvancedMode { get; set; } - /// /// Gets or sets the option behaviour of the option. /// @@ -108,9 +96,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. - /// The Game Mode Type or null if it doesn't belong to a mode. - protected ModdedOption(string title, T defaultValue, Type? roleType, Type? modeType) + protected ModdedOption(string title, T defaultValue) { Id = ModdedOptionsManager.NextId; Title = title; @@ -118,16 +104,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; - } - - if (modeType is not null && modeType.IsAssignableTo(typeof(AbstractGameMode))) - { - AdvancedMode = modeType; - } } internal void ValueChanged(OptionBehaviour optionBehaviour) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 79e8281..3495e86 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; @@ -15,9 +15,7 @@ public class ModdedToggleOption : ModdedOption /// /// The option title. /// The default value. - /// An optional role type. - /// An optional game mode type. - public ModdedToggleOption(string title, bool defaultValue, Type? roleType = null, Type? modeType = null) : base(title, defaultValue, roleType, modeType) + public ModdedToggleOption(string title, bool defaultValue) : base(title, defaultValue) { Data = ScriptableObject.CreateInstance(); diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 73603ea..4136e99 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.PluginLoading; -using MiraAPI.Utilities.Assets; -using System; using MiraAPI.Patches; +using MiraAPI.Utilities.Assets; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -14,7 +14,6 @@ namespace MiraAPI.Hud; /// /// Class for making custom action buttons. More customizable than the default Action/Ability buttons in the base game. /// -[MiraIgnore] public abstract class CustomActionButton { /// @@ -47,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. /// @@ -115,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)(() => @@ -208,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. /// @@ -423,7 +442,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 @@ -439,7 +458,6 @@ public abstract class CustomActionButton /// Custom action button that has a target object. /// /// The type of the target object. -[MiraIgnore] public abstract class CustomActionButton : CustomActionButton where T : MonoBehaviour { /// 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/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 6efca17..3380820 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,5 +1,4 @@ using BepInEx; -using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; @@ -11,26 +10,22 @@ 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, IMiraPlugin +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 static MiraApiPlugin Instance { get; private set; } - - public string OptionsTitleText => "Mira API"; - + /// public override void Load() { - Instance = this; - Harmony.PatchAll(); ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow); @@ -38,6 +33,4 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin PluginManager = new MiraPluginManager(); PluginManager.Initialize(); } - - public ConfigFile GetConfigFile() => Config; } 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 98aace9..0dd2149 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,12 +1,13 @@ -using MiraAPI.PluginLoading; +using System; +using MiraAPI.GameOptions; +using MiraAPI.PluginLoading; namespace MiraAPI.Modifiers; /// /// Base class for all modifiers. /// -[MiraIgnore] -public abstract class BaseModifier +public abstract class BaseModifier : IOptionable { /// /// Gets the player that the modifier is attached to. @@ -19,9 +20,27 @@ public abstract class BaseModifier public ModifierComponent? ModifierComponent { get; internal set; } /// - /// Gets the modifier id. + /// Gets a value indicating whether the modifier has been initialized. /// - public uint ModifierId { get; internal set; } + 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 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."); /// /// Gets the modifier name. @@ -33,6 +52,16 @@ 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 is unique. If true, the player can only have one instance of this modifier. + /// + 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 7db81f5..8c7f6f2 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -1,14 +1,15 @@ -using Il2CppInterop.Runtime.Attributes; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +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 System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using System.Text; using TMPro; using UnityEngine; @@ -67,6 +68,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) @@ -103,14 +105,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()}"); } @@ -123,45 +123,178 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } /// - /// Removes a modifier from the player. + /// Gets a collection of modifiers by type. + /// + /// The predicate to check the modifier by. + /// The Type of the Modifier. + /// A collection of modifiers. + [HideFromIl2Cpp] + public IEnumerable GetModifiers(Func? predicate=null) where T : BaseModifier + { + return Modifiers.OfType().Where(x => predicate == null || predicate(x)); + } + + /// + /// Gets a collection of modifiers by type. /// /// The modifier type. + /// The predicate to check the modifier by. + /// A collection of modifiers. [HideFromIl2Cpp] - public void RemoveModifier(Type type) + public IEnumerable GetModifiers(Type type, Func? predicate=null) { - var modifier = Modifiers.Find(x => x.GetType() == type); + return Modifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))); + } - if (modifier is null) - { - Logger.Error($"Cannot remove modifier {type.Name} because it is not active."); - return; - } + /// + /// Gets a collection of modifiers by type ID. + /// + /// The modifier's type ID. + /// The predicate to check the modifier by. + /// A collection of modifiers. + [HideFromIl2Cpp] + 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."); - RemoveModifier(modifier); + return GetModifiers(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. + [HideFromIl2Cpp] + public bool TryGetModifier([NotNullWhen(true)] out T? modifier, Func? predicate = null) where T : BaseModifier + { + 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, [NotNullWhen(true)] out BaseModifier? modifier, Func? predicate = null) + { + modifier = GetModifier(type, predicate); + return modifier != null; + } + + /// + /// Tries to get a modifier by its type 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, [NotNullWhen(true)] out BaseModifier? modifier, Func? predicate = null) + { + modifier = GetModifier(id, predicate); + return modifier != null; + } + + /// + /// 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 TryGetModifier(Guid modifierGuid, [NotNullWhen(true)] out BaseModifier? modifier) + { + modifier = GetModifier(modifierGuid); + return modifier != null; + } + + /// + /// Gets a modifier by its type. + /// + /// 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 GetModifiers(predicate).FirstOrDefault(); + } + + /// + /// Gets a modifier by its type. + /// + /// 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 GetModifiers(type).FirstOrDefault(predicate ?? (_ => true)); + } + + /// + /// 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) + { + var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException( + $"Cannot get modifier with id {id} because it is not registered."); + + 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? GetModifier(Guid modifierGuid) + { + return Modifiers.Find(x => x.UniqueId == modifierGuid); } /// /// Removes a modifier from the player. /// /// The modifier type. + /// The predicate to check the modifier by. [HideFromIl2Cpp] - public void RemoveModifier() where T : BaseModifier + public void RemoveModifier(Func? predicate = null) where T : BaseModifier { - RemoveModifier(typeof(T)); + RemoveModifier(typeof(T), x => predicate == null || predicate((T)x)); } /// /// Removes a modifier from the player. /// - /// The modifier ID. + /// The modifier type. + /// The predicate to check the modifier by. [HideFromIl2Cpp] - public void RemoveModifier(uint modifierId) + public void RemoveModifier(Type type, Func? predicate = null) { - var modifier = Modifiers.Find(x => x.ModifierId == modifierId); + 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 with id {modifierId} because it is not active."); + Logger.Error($"Cannot remove modifier {type.Name} because it is not active."); return; } @@ -184,6 +317,48 @@ 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 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."); + RemoveModifier(type, predicate); + } + + /// + /// Removes a modifier from the player. + /// + /// The modifier's unique ID. + [HideFromIl2Cpp] + public void RemoveModifier(Guid uniqueId) + { + var modifier = GetModifier(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. + /// + /// 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. /// @@ -192,24 +367,32 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? AddModifier(BaseModifier modifier) { - if (Modifiers.Contains(modifier)) + // TODO: Make a proper synchronization system. + if (LobbyBehaviour.Instance) { - Logger.Error($"Player already has modifier with id {modifier.ModifierId}!"); + Logger.Warning($"Modifiers added in the lobby won't sync to new players!"); + } + + 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; } - 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; + if (modifier.UniqueId == Guid.Empty) + { + modifier.UniqueId = Guid.NewGuid(); + } return modifier; } @@ -217,62 +400,90 @@ 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) != 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. - /// True if the Modifier is present, false otherwise. - public bool HasModifier(uint id) - { - return ActiveModifiers.Exists(x => x.ModifierId == id) || _toAdd.Exists(x => x.ModifierId == id); - } - - /// - /// 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. - public bool HasModifier() + [HideFromIl2Cpp] + public bool HasModifier(Func? predicate=null) where T : BaseModifier { - return ActiveModifiers.Exists(x => x is T) || _toAdd.Exists(x => x is T); + return Modifiers.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 Modifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x))); + } + + /// + /// Checks if a player has an active modifier by its type ID. + /// + /// The modifier's 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) + { + 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's guid. + /// True if the modifier is present, false otherwise. + [HideFromIl2Cpp] + public bool HasModifier(Guid id) + { + return Modifiers.Exists(x => x.UniqueId == id); } } diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs new file mode 100644 index 0000000..06d9f06 --- /dev/null +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Networking; +using MiraAPI.Networking.Modifiers; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; +using Reactor.Utilities; + +namespace MiraAPI.Modifiers; + +/// +/// 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 type ID. + /// The arguments to initialize the modifier constructor with. + public static void RpcAddModifier(this PlayerControl target, uint typeId, params object[] args) + { + _ = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException( + $"Modifier with ID {typeId} is not registered."); + + Rpc.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), 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.GetModifierTypeId(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 unique ID of the modifier. + [MethodRpc((uint)MiraRpc.RemoveModifier)] + public static void RpcRemoveModifier(this PlayerControl target, Guid uniqueId) + { + target.RemoveModifier(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 RpcRemoveModifier( + this PlayerControl target, + uint typeId, + Func? predicate = null) + { + 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.RpcRemoveModifier(modifier.UniqueId); + } + + /// + /// Remote Procedure Call to remove a modifier from a player. + /// + /// The player to remove the modifier from. + /// 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.GetModifierTypeId(type) ?? throw new InvalidOperationException( + $"Modifier {type.Name} is not registered."); + + player.RpcRemoveModifier(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); + } + + /// + /// 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) + { + throw new InvalidOperationException("ModifierComponent is not attached to the player."); + } + + ModifierComponents[player] = component; + return component; + } + + /// + /// Checks if the player has a specific modifier. + /// + /// 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().HasModifier(predicate); + } + + /// + /// Checks if the player has a specific modifier by type. + /// + /// 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().HasModifier(type, predicate); + } + + /// + /// Checks if the player has a specific modifier by type ID. + /// + /// The PlayerControl instance. + /// 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 typeId, + Func? predicate = null) + { + return player.GetModifierComponent().HasModifier(typeId, predicate); + } + + /// + /// Checks if the player has a specific modifier by its GUID. + /// + /// The PlayerControl instance. + /// The unique ID of the modifier. + /// True if the player has the modifier, false otherwise. + public static bool HasModifier(this PlayerControl player, Guid uniqueId) + { + 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. + /// + /// 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 its 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? GetModifier( + this PlayerControl player, + uint typeId, + Func? predicate = null) + { + return player.GetModifierComponent().GetModifier(typeId, predicate); + } + + /// + /// Gets a specific modifier from the player by its GUID. + /// + /// The PlayerControl instance. + /// The GUID of the modifier. + /// The modifier if found, null otherwise. + public static BaseModifier? GetModifier( + this PlayerControl player, + Guid uniqueId) + { + return player.GetModifierComponent().GetModifier(uniqueId); + } + + /// + /// Gets all modifiers of a specific type from the player. + /// + /// The type of the modifiers. + /// The PlayerControl instance. + /// Optional predicate to filter the modifiers. + /// A collection of modifiers. + public static IEnumerable GetModifiers(this PlayerControl player, Func? predicate = null) + where T : BaseModifier + { + return player.GetModifierComponent().GetModifiers(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. + /// A collection of modifiers. + public static IEnumerable GetModifiers( + this PlayerControl player, + Type type, + Func? predicate = null) + { + return player.GetModifierComponent().GetModifiers(type, predicate); + } + + /// + /// Gets all modifiers of a specific type from the player by type ID. + /// + /// The PlayerControl instance. + /// The type ID of the modifiers. + /// Optional predicate to filter the modifiers. + /// A collection of modifiers. + public static IEnumerable GetModifiers( + this PlayerControl player, + uint typeId, + Func? predicate = null) + { + return player.GetModifierComponent().GetModifiers(typeId, 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); + } + + /// + /// 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 its type ID. + /// + /// The PlayerControl instance. + /// The type ID of the modifier. + /// Optional predicate to filter the modifiers. + public static void RemoveModifier( + this PlayerControl player, + uint typeId, + Func? predicate = null) + { + player.GetModifierComponent().RemoveModifier(typeId, predicate); + } + + /// + /// Removes a specific modifier from the player by its GUID. + /// + /// The PlayerControl instance. + /// The GUID of the modifier. + public static void RemoveModifier( + this PlayerControl player, + Guid uniqueId) + { + player.GetModifierComponent().RemoveModifier(uniqueId); + } + + /// + /// Adds a specific modifier to the player. + /// + /// The type of the modifier. + /// The PlayerControl instance. + /// The arguments to initialize the modifier constructor with. + /// The added modifier. + public static T? AddModifier(this PlayerControl player, params object[] args) where T : BaseModifier + { + return player.GetModifierComponent().AddModifier(args); + } + + /// + /// Adds a specific modifier to the player. + /// + /// The PlayerControl instance. + /// The modifier to add. + /// The added modifier. + public static BaseModifier? AddModifier(this PlayerControl player, BaseModifier modifier) + { + return player.GetModifierComponent().AddModifier(modifier); + } + + /// + /// 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 type ID. + /// + /// The PlayerControl instance. + /// 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 typeId, params object[] args) + { + return player.GetModifierComponent().AddModifier(typeId, args); + } +} diff --git a/MiraAPI/Modifiers/ModifierFactory.cs b/MiraAPI/Modifiers/ModifierFactory.cs new file mode 100644 index 0000000..8f8d005 --- /dev/null +++ b/MiraAPI/Modifiers/ModifierFactory.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using MiraAPI.Utilities; + +namespace MiraAPI.Modifiers; + +/// +/// Factory for creating instances of a modifier. More efficient than using reflection. +/// +public static class ModifierFactory +{ + private static readonly Dictionary<(Type, Type[]), Func> _constructorCache = []; + + private static Func CreateConstructor(Type type, params object[] args) + { + var constructorInfo = type.GetBestConstructor(args) ?? 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"); + 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 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) + { + 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); + } +} + +/// +/// 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(params object[] args) + { + 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(); + + 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/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index ddb3c87..5f6bbff 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -1,11 +1,11 @@ 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; using Reactor.Utilities; using Reactor.Utilities.Extensions; using Random = System.Random; @@ -21,16 +21,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. @@ -44,35 +44,58 @@ public static class ModifierManager /// /// The Type. /// The ID of the modifier. - public static uint? GetModifierId(Type type) + public static uint? GetModifierTypeId(Type type) { - return TypeToIdModifierMap.GetValueOrDefault(type); + if (!TypeToIdModifierMap.TryGetValue(type, out var id)) + { + return null; + } + + return id; } - internal static bool RegisterModifier(Type modifierType) + internal static bool RegisterModifier(Type modifierType, MiraPluginInfo info) { if (!typeof(BaseModifier).IsAssignableFrom(modifierType)) { return false; } - IdToTypeModifierMap.Add(GetNextId(), modifierType); - TypeToIdModifierMap.Add(modifierType, _nextId); + IdToTypeModifierMap.Add(GetNextTypeId(), modifierType); + TypeToIdModifierMap.Add(modifierType, _nextTypeId); - 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; } - var mod = Activator.CreateInstance(modifierType) as GameModifier; - var priority = mod!.Priority(); + if (modifierType.GetConstructor(Type.EmptyTypes) == null) + { + Logger.Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!"); + return false; + } + + var priority = gameModifier.Priority(); if (!PrioritiesToIdsMap.TryGetValue(priority, out var list)) { PrioritiesToIdsMap[priority] = list = []; } - list.Add(_nextId); + list.Add(_nextTypeId); return true; } @@ -134,7 +157,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"); } @@ -149,71 +172,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.HasModifier(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/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index 6274917..c05ee58 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -8,6 +8,9 @@ namespace MiraAPI.Modifiers.Types; [MiraIgnore] 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/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index ef60056..0f8b759 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -19,7 +19,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. @@ -29,7 +29,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. @@ -104,7 +106,7 @@ public abstract class TimedModifier : BaseModifier if (RemoveOnComplete) { - ModifierComponent?.RemoveModifier(ModifierId); + ModifierComponent?.RemoveModifier(this); } } diff --git a/MiraAPI/Networking/CustomGameOverRpc.cs b/MiraAPI/Networking/CustomGameOverRpc.cs new file mode 100644 index 0000000..34dbe6a --- /dev/null +++ b/MiraAPI/Networking/CustomGameOverRpc.cs @@ -0,0 +1,70 @@ +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.ReadPackedInt32(); + 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)) + { + if (!gameOver.VerifyCondition(innerNetObject, [.. data.Winners])) + { + Logger.Info($"Game over condition not met for {gameOver.GetType().Name}"); + return; + } + + 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/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 7f1e60f..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; @@ -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); } } 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/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/Networking/Modifiers/AddModifierRpc.cs b/MiraAPI/Networking/Modifiers/AddModifierRpc.cs new file mode 100644 index 0000000..2e7b19e --- /dev/null +++ b/MiraAPI/Networking/Modifiers/AddModifierRpc.cs @@ -0,0 +1,76 @@ +using System; +using Hazel; +using MiraAPI.Modifiers; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; +using Reactor.Networking.Serialization; + +namespace MiraAPI.Networking.Modifiers; + +/// +/// 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) +{ + /// + public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before; + + /// + public override void Write(MessageWriter writer, ModifierData data) + { + writer.WritePacked(data.TypeId); + writer.WriteBytesAndSize(data.UniqueId.ToByteArray()); + 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 guid = new Guid(reader.ReadBytesAndSize()); + var argCount = reader.ReadPackedUInt32(); + var objects = new object[argCount]; + + if (argCount > 0) + { + 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]); + } + } + + return new ModifierData(modId, guid, objects); + } + + /// + public override void Handle(PlayerControl player, ModifierData data) + { + 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/ModifierConverter.cs b/MiraAPI/Networking/Modifiers/ModifierConverter.cs new file mode 100644 index 0000000..34fd714 --- /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 with ID: {player.PlayerId}"); + } +} 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/SyncModifiersRpc.cs b/MiraAPI/Networking/SyncModifiersRpc.cs deleted file mode 100644 index 288e2c1..0000000 --- a/MiraAPI/Networking/SyncModifiersRpc.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Hazel; -using MiraAPI.Modifiers; -using Reactor.Networking.Attributes; -using Reactor.Networking.Rpc; - -namespace MiraAPI.Networking; - -[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/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/EndGamePatches.cs b/MiraAPI/Patches/Events/EndGamePatches.cs index 4f7118b..2c04d6b 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.BeforeEndGameSetup(__instance); + return result; + } + + public static void Postfix(EndGameManager __instance) + { + if (CustomGameOver.Instance is { } gameOver) + { + gameOver.AfterEndGameSetup(__instance); + CustomGameOver.Instance = null; + } + var @event = new GameEndEvent(__instance); MiraEventManager.InvokeEvent(@event); } 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..a6c4b1d 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.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/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs new file mode 100644 index 0000000..184bafb --- /dev/null +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -0,0 +1,36 @@ +using HarmonyLib; +using MiraAPI.Modifiers; +using MiraAPI.Roles; +using UnityEngine; + +namespace MiraAPI.Patches.Freeplay; + +[HarmonyPatch(typeof(TaskAddButton))] +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.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; + } + __instance.RolloverHandler.OutColor = __instance.FileImage.color; + } +} diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs similarity index 61% rename from MiraAPI/Patches/Roles/TaskAdderPatch.cs rename to MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index 98e93bc..666a1ea 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -1,32 +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; using MiraAPI.Utilities.Assets; -using System; -using System.Collections.Generic; -using System.Linq; using TMPro; using UnityEngine; +using UnityEngine.UI; using Object = UnityEngine.Object; -namespace MiraAPI.Patches.Roles; +namespace MiraAPI.Patches.Freeplay; -/// -/// Patch TaskAdder for organizing Roles and adding scroller. -/// -[HarmonyPatch] -public static class TaskAdderPatch +[HarmonyPatch(typeof(TaskAdderGame))] +internal static class TaskAdderPatches { - private static readonly Dictionary ModsFolders = []; 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"); @@ -67,45 +62,38 @@ 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(); } - /// - /// 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, @@ -130,8 +118,8 @@ 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) + [HarmonyPatch(nameof(TaskAdderGame.ShowFolder))] + public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder) { var stringBuilder = new Il2CppSystem.Text.StringBuilder(64); __instance.Hierarchy.Add(taskFolder); @@ -277,46 +265,31 @@ public static class TaskAdderPatch } } - if (ModsFolders.TryGetValue(taskFolder.FolderName, out var guid)) + if (taskFolder) { - var plugin = MiraPluginManager.GetPluginByGuid(guid); - for (var m = 0; m < plugin.CustomRoles.Count; m++) + var split = taskFolder.name.Split(':'); + if (split.Length == 2) { - var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value; - if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || - (roleBehaviour is ICustomRole customRole && !customRole.Configuration.ShowInFreeplay)) - { - continue; - } + var pluginId = split[0]; + var folderName = split[1]; - 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) + if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) { - 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; + 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); + } } } } foreach (var chip in __instance.ActiveItems) { - chip.GetComponentInChildren().EnableMasking(); + chip.GetComponentInChildren().EnableStencilMasking(); chip.GetComponent().maskInteraction = SpriteMaskInteraction.VisibleInsideMask; } @@ -336,4 +309,98 @@ 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.TypeId.ToString(NumberFormatInfo.InvariantInfo); + 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)(() => + { + var id = modifier.TypeId; + if (PlayerControl.LocalPlayer.HasModifier(id)) + { + PlayerControl.LocalPlayer.RemoveModifier(id); + taskAddButton.Overlay.enabled = false; + } + else + { + PlayerControl.LocalPlayer.AddModifier(id); + taskAddButton.Overlay.enabled = true; + } + })); + + 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"; + taskAddButton2.Text.EnableMasking(); + + 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/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index b98c58e..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; @@ -32,7 +31,16 @@ public static class IntroCutscenePatches [HarmonyPrefix] [HarmonyPatch(nameof(IntroCutscene.BeginImpostor))] - public static void BeginImpostorPatch(IntroCutscene __instance) + [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] + 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) { @@ -48,43 +56,6 @@ public static class IntroCutscenePatches } } - [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/Patches/Modifiers/EndGameDidWinPatch.cs b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs index d7a99d5..a86383d 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; @@ -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/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 7e75ede..007ea01 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,13 +1,16 @@ -using HarmonyLib; -using MiraAPI.GameModes; +using System; +using System.Linq; +using HarmonyLib; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers; 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; +using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; @@ -31,64 +34,98 @@ public static class GameOptionsMenuPatch } var num = 2.1f; - var filteredGroups = - GameSettingMenuPatches.SelectedMod?.OptionGroups.Where( - x => x.AdvancedRole is null && (x.AdvancedMode is null || x.AdvancedMode == CustomGameModeManager.ActiveMode?.GetType())) ?? []; - - 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.OptionableType == null) ?? []; + + 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 groups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; + + foreach (var modGroup in groups) + { + UpdateGroup(modGroup, ref num); + } + } + + private static void ModifiersCreate(GameOptionsMenu menu) + { + var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? []; + foreach (var group in groups) + { + CreateGroup(menu, group); + } + } + [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))] public static bool SettingsPatch(GameOptionsMenu __instance) @@ -100,107 +137,18 @@ public static class GameOptionsMenuPatch __instance.MapPicker.gameObject.SetActive(false); - var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(x => x.AdvancedRole is null && (x.AdvancedMode is null || x.AdvancedMode == - CustomGameModeManager.ActiveMode?.GetType())) ?? []; + if (__instance.name == "MODIFIERS TAB") + { + ModifiersCreate(__instance); + return false; + } + + var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x.OptionableType == null) ?? []; 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; @@ -235,9 +183,110 @@ public static class GameOptionsMenuPatch continue; } - optionBehaviour.OnValueChanged = new System.Action(__instance.ValueChanged); + optionBehaviour.OnValueChanged = new Action(__instance.ValueChanged); } 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 0cdca29..7781bf8 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,9 +1,9 @@ -using HarmonyLib; +using System; +using HarmonyLib; +using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; -using System; -using System.Linq; using TMPro; using UnityEngine; using UnityEngine.Events; @@ -12,26 +12,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) + { + if (_modifiersTab) _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,43 +69,116 @@ 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; 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; + 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); + _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); } - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); - })); + } + + 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,10 +197,30 @@ 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?.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) == true) + { + _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 + /* if (SelectedModIdx != 0) { if (SelectedMod?.Options.Where(x=>x.AdvancedRole==null).ToList().Count == 0) @@ -129,7 +237,7 @@ public static class GameSettingMenuPatches { menu.RoleSettingsButton.transform.localPosition = menu.GameSettingsButton.transform.localPosition; } - } + }*/ if (roles.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0) { @@ -139,11 +247,13 @@ public 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 6d5bbc5..053422f 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -1,13 +1,16 @@ -using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; using MiraAPI.GameModes; +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 System; -using System.Collections.Generic; -using System.Linq; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -18,17 +21,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 +74,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 +96,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.GroupVisible() && (x.ShowInModifiersMenu || 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 +153,7 @@ public static class LobbyViewPanePatches [HarmonyPatch(nameof(LobbyViewSettingsPane.DrawRolesTab))] public static bool DrawRolesTabPatch(LobbyViewSettingsPane __instance) { - if (CurrentSelectedMod == 0) + if (SelectedModIdx == 0) { return true; } @@ -117,29 +162,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.GroupVisible.Invoke() && x.AdvancedRole is null); - - 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; @@ -151,8 +189,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; @@ -161,7 +199,7 @@ public static class LobbyViewPanePatches num2 = -8.95f; if (i > 0) { - num -= 0.59f; + num -= 0.85f; } } else @@ -192,14 +230,14 @@ public static class LobbyViewPanePatches } } - 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) @@ -220,34 +258,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; @@ -255,7 +302,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) @@ -268,8 +315,6 @@ public static class LobbyViewPanePatches var numPerGame = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); - var flag = numPerGame == 0; - var viewSettingsInfoPanelRoleVariant = Object.Instantiate( instance.infoPanelRoleOrigin, @@ -278,11 +323,12 @@ 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() && (x.AdvancedMode is null || x.AdvancedMode == customRole.Configuration.AssociatedGameMode)) + var advancedRoleOptions = SelectedMod.OptionGroups + .Where(x => x.OptionableType == customRole.GetType()) + .SelectMany(x => x.Options) .ToList(); - if (!flag && advancedRoleOptions.Count > 0) + if (numPerGame > 0 && advancedRoleOptions.Count > 0) { list.Add(customRole.GetType()); } @@ -295,6 +341,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 = @@ -379,13 +427,15 @@ 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; var num2 = 1.08f; - var filteredOptions = SelectedMod.Options - .Where(x => x.AdvancedRole == roleType) + var filteredOptions = SelectedMod.OptionGroups + .Where(x => x.OptionableType == roleType) + .SelectMany(x=>x.Options) .ToList(); for (var i = 0; i < filteredOptions.Count; i++) 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 97dd6f4..5f60ccf 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -23,9 +23,9 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(RolesSettingsMenu))] internal 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; @@ -33,10 +33,11 @@ internal 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(); @@ -80,6 +81,10 @@ internal 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.All(role => role.Configuration.AssociatedGameMode != CustomGameModeManager.ActiveMode?.GetType())) @@ -98,39 +103,73 @@ internal 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) @@ -149,11 +188,11 @@ internal 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(); @@ -164,12 +203,11 @@ internal 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(); @@ -180,7 +218,7 @@ internal static class RoleSettingMenuPatches })); headerBtn.SetButtonEnableState(true); - if (!RoleGroupHidden[group]) + if (RoleGroupHidden.TryGetValue(group, out var value) && !value) { ScrollerNum -= 0.4f; } @@ -191,7 +229,7 @@ internal 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] @@ -258,32 +296,31 @@ internal 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() && (x.AdvancedMode is null || x.AdvancedMode == - CustomGameModeManager.ActiveMode?.GetType())) ?? []; + // TODO: create sub groups under the role settings. + var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x=> x.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, __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); @@ -321,15 +358,63 @@ internal 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); @@ -373,7 +458,7 @@ internal 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; @@ -386,8 +471,8 @@ internal 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.OptionableType == role.GetType())) { var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform); newButton.name = "ConfigButton"; @@ -420,7 +505,7 @@ internal static class RoleSettingMenuPatches if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1) { - ScrollerNum += -0.43f; + ScrollerNum -= 0.43f; } return roleOptionSetting; diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index c7918fe..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; @@ -126,6 +126,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/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/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/Patches/VentPatches.cs b/MiraAPI/Patches/VentPatches.cs index 621abbc..2f8991d 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; @@ -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) { diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 905342c..570516f 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -1,10 +1,11 @@ -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; namespace MiraAPI.PluginLoading; @@ -21,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. /// @@ -70,6 +80,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 7e7cf00..c2e2e61 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using BepInEx; @@ -15,7 +17,6 @@ using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; -using System.Collections.ObjectModel; namespace MiraAPI.PluginLoading; @@ -33,14 +34,10 @@ public sealed class MiraPluginManager internal void Initialize() { Instance = this; - RegisterPlugin( - IL2CPPChainloader.Instance.Plugins[MiraApiPlugin.Id], - typeof(MiraApiPlugin).Assembly, - MiraApiPlugin.Instance); CustomGameModeManager.RegisterDefaultMode(); CustomGameModeManager.GetAndSetGameMode(); - IL2CPPChainloader.Instance.PluginLoad += RegisterPlugin; + IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; IL2CPPChainloader.Instance.Finished += MiraEventManager.SortAllHandlers; IL2CPPChainloader.Instance.Finished += () => @@ -66,34 +63,56 @@ public sealed class MiraPluginManager continue; } - if (RegisterModifier(type)) - { - continue; - } + foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)) + { + var eventAttribute = method.GetCustomAttribute(); + if (eventAttribute != null) + { + 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; + } - if (RegisterOptions(type, info)) - { - continue; - } + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + } + } - if (RegisterRoleAttribute(type, info, out var role)) - { - roles.Add(role!); - continue; - } + if (RegisterModifier(type, info)) + { + continue; + } - if (RegisterButtonAttribute(type, info)) - { - continue; - } + if (RegisterOptions(type, info)) + { + continue; + } - if (RegisterGameModeAttribute(type, info)) - { - continue; - } + if (RegisterRoleAttribute(type, info, out var role)) + { + roles.Add(role); + continue; + } - RegisterColorClasses(type); - } + if (RegisterButtonAttribute(type, info)) + { + continue; + } + + if (RegisterGameOver(type)) + { + continue; + } + + if (RegisterGameModeAttribute(type, info)) + { + continue; + } + + RegisterColorClasses(type); + } info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); CustomRoleManager.RegisterRoleTypes(roles, info); @@ -107,9 +126,22 @@ 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 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) @@ -152,7 +184,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 @@ -215,11 +247,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/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 38cc4dd..af2bcea 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -34,6 +34,7 @@ public record struct CustomRoleConfiguration TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate; HideSettings = roleBehaviour?.IsDead == true; ShowInFreeplay = roleBehaviour?.IsDead == false; + OptionsScreenshot = null; } /// @@ -59,7 +60,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..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; @@ -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) { diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index a452d1e..27df3cd 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,9 +1,10 @@ -using BepInEx.Configuration; +using System.Text; +using BepInEx.Configuration; +using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; using Reactor.Utilities; -using System.Text; using UnityEngine; namespace MiraAPI.Roles; @@ -11,7 +12,7 @@ namespace MiraAPI.Roles; /// /// Interface for custom roles. /// -public interface ICustomRole +public interface ICustomRole : IOptionable { /// /// Gets the name of the role. @@ -46,7 +47,7 @@ public interface ICustomRole /// /// Gets the role options group. /// - public RoleOptionsGroup RoleOptionsGroup => Team switch + RoleOptionsGroup RoleOptionsGroup => Team switch { ModdedRoleTeams.Crewmate => RoleOptionsGroup.Crewmate, ModdedRoleTeams.Impostor => RoleOptionsGroup.Impostor, @@ -57,7 +58,7 @@ public interface ICustomRole /// /// Gets the role's TeamIntroCutscene configuration. /// - public TeamIntroConfiguration? IntroConfiguration => Team switch + TeamIntroConfiguration? IntroConfiguration => Team switch { ModdedRoleTeams.Custom => TeamIntroConfiguration.Neutral, _ => null, @@ -158,6 +159,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. /// 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; + } } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index dc3bba1..a85da1d 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -1,13 +1,13 @@ -using HarmonyLib; -using MiraAPI.GameOptions; -using MiraAPI.Modifiers; -using MiraAPI.Networking; -using MiraAPI.Roles; -using Reactor.Networking.Attributes; -using Reactor.Utilities; 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 TMPro; using UnityEngine; namespace MiraAPI.Utilities; @@ -39,6 +39,84 @@ 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. + /// + /// 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. + /// + /// The TMP text. + public static void EnableStencilMasking(this TMP_Text text) + { + text.fontMaterial.SetFloat(ShaderID.Stencil, 1); + text.fontMaterial.SetFloat(ShaderID.StencilComp, 4); + } + /// /// Checks if a type is static. /// @@ -130,30 +208,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 +228,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 +283,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 +332,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 +339,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/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; 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 = [];