diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ca76373..6c87428 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -46,6 +46,6 @@ jobs: files: MiraAPI/bin/Release/net6.0/MiraAPI.dll - name: Push NuGet package - if: github.repository == 'All-Of-Us-Mods/MiraAPI' && github.ref == 'refs/heads/master' && github.ref_type == 'tag' + if: github.repository == 'All-Of-Us-Mods/MiraAPI' && github.ref == 'refs/heads/master' run: | dotnet nuget push {MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg} --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 097874d..2431ce4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ true - 0.1.3 + 0.1.4 dev All Of Us, XtraCube, Angxl @@ -33,6 +33,11 @@ + + + all + runtime; build; native; contentfiles; analyzers + $(MSBuildThisFileDirectory)\stylecop.ruleset diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 3926316..f8de0ab 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -21,9 +21,9 @@ public class FreezeButton : CustomActionButton Target?.RpcAddModifier(); } - public override PlayerControl GetTarget() + public override PlayerControl? GetTarget() { - return PlayerControl.LocalPlayer.Data.Role.FindClosestTarget(); + return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance, false); } public override void SetOutline(bool active) @@ -31,14 +31,13 @@ public class FreezeButton : CustomActionButton Target?.cosmetics.SetOutline(active, new Il2CppSystem.Nullable(Palette.Blue)); } - public override bool IsTargetValid(PlayerControl target) + public override bool IsTargetValid(PlayerControl? target) { return true; } - public override bool Enabled(RoleBehaviour role) + public override bool Enabled(RoleBehaviour? role) { return role is FreezerRole; } - -} \ No newline at end of file +} diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index 639e663..4b4f087 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -1,10 +1,11 @@ -using MiraAPI.Example; -using MiraAPI.Example.Modifiers; +using MiraAPI.Example.Modifiers; using MiraAPI.Hud; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using UnityEngine; +namespace MiraAPI.Example.Buttons; + [RegisterButton] public class MeetingButton : CustomActionButton { @@ -18,7 +19,7 @@ public class MeetingButton : CustomActionButton public override LoadableAsset Sprite => ExampleAssets.ExampleButton; - public override bool Enabled(RoleBehaviour role) + public override bool Enabled(RoleBehaviour? role) { return PlayerControl.LocalPlayer.HasModifier(); } diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index 79ddabd..89ba051 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -22,6 +22,5 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin public override void Load() { Harmony.PatchAll(); - AddComponent(); } -} \ No newline at end of file +} diff --git a/MiraAPI.Example/MiraDebugWindow.cs b/MiraAPI.Example/MiraDebugWindow.cs index 0332776..9b0fa7e 100644 --- a/MiraAPI.Example/MiraDebugWindow.cs +++ b/MiraAPI.Example/MiraDebugWindow.cs @@ -1,6 +1,4 @@ -using MiraAPI.Example.Modifiers; -using MiraAPI.Utilities; -using Reactor.Utilities.Attributes; +using Reactor.Utilities.Attributes; using Reactor.Utilities.ImGui; using System; using UnityEngine; @@ -21,11 +19,11 @@ public class MiraDebugWindow(IntPtr ptr) : MonoBehaviour(ptr) } }) { - Enabled = true + Enabled = true, }; public void OnGUI() { DebuggingWindow.OnGUI(); } -} \ No newline at end of file +} diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 80d9224..09c733c 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -7,6 +7,7 @@ namespace MiraAPI.Example.Modifiers; public class CaptainModifier : GameModifier { public override string ModifierName => "Captain"; + public override int GetAmountPerGame() { return 1; @@ -16,4 +17,4 @@ public class CaptainModifier : GameModifier { return 100; } -} \ No newline at end of file +} diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index d112406..12a87ec 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -1,5 +1,8 @@ -using MiraAPI.Modifiers; +using Il2CppSystem; +using MiraAPI.Example.Roles; +using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; +using UnityEngine; namespace MiraAPI.Example.Modifiers.Freezer; @@ -14,15 +17,25 @@ public class FreezeModifier : TimedModifier public override void OnActivate() { - if (Player.AmOwner) + if (Player?.AmOwner == true) { Player.moveable = false; } } + public override void FixedUpdate() + { + base.FixedUpdate(); + + if (Player?.AmOwner == true || PlayerControl.LocalPlayer.Data.Role is FreezerRole) + { + Player?.cosmetics.SetOutline(true, new Nullable(Palette.LightBlue)); + } + } + public override void OnTimerComplete() { - if (Player.AmOwner) + if (Player?.AmOwner == true) { Player.moveable = true; } diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index 13a7dd7..9e9fa59 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -19,16 +19,13 @@ public class ExampleOptions : AbstractOptionGroup [ModdedNumberOption("Number Opt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Percent)] public float NumberOpt { get; set; } = 4f; - [ModdedStringOption("String Opt", ["Hello", "Hey", "Bye"])] - public int StringOpt { get; set; } = 2; - - [ModdedEnumOption("Best API", typeof(BestApiEnum), ["Mira API", "Mitochondria", "Reactor"])] - public BestApiEnum EnumOpt { get; set; } = BestApiEnum.MiraAPI; + [ModdedEnumOption("Best API", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])] + public BestApi Opt { get; set; } = BestApi.MiraAPI; } -public enum BestApiEnum +public enum BestApi { MiraAPI, Mitochondria, - Reactor -} \ No newline at end of file + Reactor, +} diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs index 594bb36..c521ea8 100644 --- a/MiraAPI.Example/Options/ExampleOptions2.cs +++ b/MiraAPI.Example/Options/ExampleOptions2.cs @@ -1,5 +1,6 @@ using MiraAPI.GameOptions; using MiraAPI.GameOptions.OptionTypes; +using Reactor.Utilities; namespace MiraAPI.Example.Options; @@ -11,17 +12,18 @@ public class ExampleOptions2 : AbstractOptionGroup public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false) { - Visible = () => OptionGroupSingleton.Instance.ToggleOpt1.Value + Visible = () => OptionGroupSingleton.Instance.ToggleOpt1.Value, }; - public ModdedStringOption StringOpt { get; } = new("String Opt", 0, ["Choice 1", "Choice 2", "Choice 3"]); - - public ModdedEnumOption EnumOpt { get; } = new("Enum Opt", 0, typeof(TestingEnum)); + public ModdedEnumOption EnumOpt { get; } = new("Enum Opt", 0, typeof(TestingData)) + { + ChangedEvent = x => Logger.Info($"changed Enum Opt to {x}"), + }; } -public enum TestingEnum +public enum TestingData { Happy, Sad, - Neutral -} \ No newline at end of file + Neutral, +} diff --git a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs index 3e51e33..b8bb823 100644 --- a/MiraAPI.Example/Options/Roles/TeleporterOptions.cs +++ b/MiraAPI.Example/Options/Roles/TeleporterOptions.cs @@ -20,4 +20,4 @@ public class TeleporterOptions : AbstractOptionGroup [ModdedNumberOption("Zoom Distance", 4, 15)] public float ZoomDistance { get; set; } = 6; -} \ No newline at end of file +} diff --git a/MiraAPI.Example/Roles/FreezerRole.cs b/MiraAPI.Example/Roles/FreezerRole.cs index e649b1a..55e22c0 100644 --- a/MiraAPI.Example/Roles/FreezerRole.cs +++ b/MiraAPI.Example/Roles/FreezerRole.cs @@ -1,4 +1,5 @@ -using MiraAPI.Roles; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -12,6 +13,7 @@ public class FreezerRole : ImpostorRole, ICustomRole public string RoleDescription => RoleLongDescription; public Color RoleColor => Palette.Blue; public ModdedRoleTeams Team => ModdedRoleTeams.Impostor; + [HideFromIl2Cpp] public LoadableAsset OptionsScreenshot => ExampleAssets.Banner; public int MaxPlayers => 2; -} \ No newline at end of file +} diff --git a/MiraAPI.Example/Roles/TeleporterRole.cs b/MiraAPI.Example/Roles/TeleporterRole.cs index be7528b..c45f942 100644 --- a/MiraAPI.Example/Roles/TeleporterRole.cs +++ b/MiraAPI.Example/Roles/TeleporterRole.cs @@ -1,4 +1,5 @@ -using MiraAPI.Roles; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Roles; using MiraAPI.Utilities.Assets; using UnityEngine; @@ -12,6 +13,6 @@ public class TeleporterRole : CrewmateRole, ICustomRole public string RoleDescription => RoleLongDescription; public Color RoleColor => new Color32(221, 176, 152, 255); public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; + [HideFromIl2Cpp] public LoadableAsset OptionsScreenshot => ExampleAssets.Banner; - -} \ No newline at end of file +} diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs index 3e3df97..7bbe87a 100644 --- a/MiraAPI/Colors/CustomColor.cs +++ b/MiraAPI/Colors/CustomColor.cs @@ -3,21 +3,42 @@ using Reactor.Localization.Utilities; using UnityEngine; namespace MiraAPI.Colors; + +/// +/// Represents a custom color with a main color and a shadow color. +/// +/// The name of the option. +/// The main color. +/// The shadow color. public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor) { + /// + /// Gets the main color. + /// public Color32 MainColor { get; } = mainColor; + + /// + /// Gets the shadow color. + /// public Color32 ShadowColor { get; } = shadowColor; + + /// + /// Gets the name of the color. + /// public StringNames Name { get; } = name; + /// public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60)) { } + /// public CustomColor(string name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60)) { } + /// public CustomColor(string name, Color32 mainColor, Color32 shadowColor) : this(CustomStringName.CreateAndRegister(name), mainColor, shadowColor) { } -} \ No newline at end of file +} diff --git a/MiraAPI/Colors/PaletteManager.cs b/MiraAPI/Colors/PaletteManager.cs index a94de66..c788353 100644 --- a/MiraAPI/Colors/PaletteManager.cs +++ b/MiraAPI/Colors/PaletteManager.cs @@ -4,18 +4,29 @@ using HarmonyLib; namespace MiraAPI.Colors; -internal static class PaletteManager +/// +/// Used to register and track custom colors. +/// +public static class PaletteManager { - public static readonly List CustomColors = []; - - public static void RegisterAllColors() - { - var colors = CustomColors.Select(x => x.MainColor); - var shadowColors = CustomColors.Select(x => x.ShadowColor); - var stringNames = CustomColors.Select(x => x.Name); + /// + /// Gets all registered custom colors. + /// + public static CustomColor[] RegisteredColors => [.. CustomColors]; - Palette.PlayerColors = Palette.PlayerColors.ToArray().AddRangeToArray(colors.ToArray()); - Palette.ShadowColors = Palette.ShadowColors.ToArray().AddRangeToArray(shadowColors.ToArray()); - Palette.ColorNames = Palette.ColorNames.ToArray().AddRangeToArray(stringNames.ToArray()); - } -} \ No newline at end of file + internal static readonly List CustomColors = []; + + internal static void RegisterAllColors() + { + var colors = CustomColors.Select(x => x.MainColor).ToArray(); + var shadowColors = CustomColors.Select(x => x.ShadowColor).ToArray(); + var stringNames = CustomColors.Select(x => x.Name).ToArray(); + + Palette.PlayerColors = Palette.PlayerColors.ToArray().AddRangeToArray(colors); + Palette.ShadowColors = Palette.ShadowColors.ToArray().AddRangeToArray(shadowColors); + Palette.ColorNames = Palette.ColorNames.ToArray().AddRangeToArray(stringNames); + + Palette.TextColors = Palette.TextColors.ToArray().AddRangeToArray(colors); + Palette.TextOutlineColors = Palette.TextOutlineColors.ToArray().AddRangeToArray(shadowColors); + } +} diff --git a/MiraAPI/Colors/RegisterCustomColorsAttribute.cs b/MiraAPI/Colors/RegisterCustomColorsAttribute.cs index 93d99b7..0e9ace7 100644 --- a/MiraAPI/Colors/RegisterCustomColorsAttribute.cs +++ b/MiraAPI/Colors/RegisterCustomColorsAttribute.cs @@ -2,5 +2,8 @@ namespace MiraAPI.Colors; +/// +/// Used to mark a class for custom colors registration. +/// [AttributeUsage(AttributeTargets.Class)] -public class RegisterCustomColorsAttribute : Attribute; \ No newline at end of file +public class RegisterCustomColorsAttribute : Attribute; diff --git a/MiraAPI/GameModes/CustomGameMode.cs b/MiraAPI/GameModes/CustomGameMode.cs index c030fb4..c7cf639 100644 --- a/MiraAPI/GameModes/CustomGameMode.cs +++ b/MiraAPI/GameModes/CustomGameMode.cs @@ -3,74 +3,82 @@ namespace MiraAPI.GameModes; /// -/// Base class for custom gamemodes +/// Base class for custom gamemodes. /// public abstract class CustomGameMode { /// - /// Game mode name + /// Gets the game mode name. /// public abstract string Name { get; } /// - /// Game mode description + /// Gets the game mode description. /// public abstract string Description { get; } /// - /// Game mode ID + /// Gets the game mode ID. /// public abstract int Id { get; } /// - /// Called when Intro Cutscene is destroyed + /// Called when Intro Cutscene is destroyed. /// - public virtual void Initialize() { } + public virtual void Initialize() + { + } /// - /// Called on HudManager.Start() + /// Called on HudManager.Start(). /// - /// Instance of HudManager - public virtual void HudStart(HudManager instance) { } + /// Instance of HudManager. + public virtual void HudStart(HudManager instance) + { + } /// - /// Called every frame on HudManager.Update() + /// Called every frame on HudManager.Update(). /// - /// Instance of HudManager - public virtual void HudUpdate(HudManager instance) { } + /// Instance of HudManager. + public virtual void HudUpdate(HudManager instance) + { + } /// - /// Called when a player is killed + /// Called when a player is killed. /// - /// PlayerControl that was killed - public virtual void OnDeath(PlayerControl player) { } + /// PlayerControl that was killed. + public virtual void OnDeath(PlayerControl player) + { + } /// - /// Calculate Game End logic + /// Calculate Game End logic. /// - /// Should original game end logic be used - /// Instance of LogicGameFlowNormal + /// Should original game end logic be used. + /// Instance of LogicGameFlowNormal. public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) { runOriginal = true; } /// - /// Custom role assignment + /// Custom role assignment. /// - /// Should original role assignment be used - /// Instance of LogicRoleSelection + /// Should original role assignment be used. + /// Instance of LogicRoleSelection. public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) { runOriginal = true; } /// - /// Custom murder logic + /// Custom murder logic. /// - /// Should original murder logic be used - /// Result of custom murder logic - /// Target player for murder + /// Should the original murder logic be used. + /// Result of custom murder logic. + /// Target player for murder. public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target) { result = false; @@ -78,61 +86,69 @@ public abstract class CustomGameMode } /// - /// Should Roles Settings be available when this gamemode is selected? + /// Should Roles Settings be available when this gamemode is selected. /// - public virtual bool CanAccessRolesTab() => true; + /// True if Role Settings are enabled in this game mode. + public virtual bool AreRoleSettingsEnabled() => true; /// - /// Should Game Settings be available when this gamemode is selected? + /// Should Game Settings be available when this gamemode is selected. /// - public virtual bool CanAccessSettingsTab() => true; + /// True if Game Settings are enabled in this mode. + public virtual bool AreGameSettingsEnabled() => true; /// - /// Custom winner selection + /// Custom winner selection. /// - /// List of winners - public virtual List CalculateWinners() => null; + /// List of winners or null. + public virtual List? CalculateWinners() => null; /// - /// Show gamemode in Intro Cutscene + /// Show gamemode in Intro Cutscene. /// - public virtual bool ShowCustomRoleScreen() => false; + /// True if the game mode should be shown in the intro cutscene. + public virtual bool ShowGameModeIntroCutscene() => false; /// - /// Can Admin be used in this gamemode + /// Can Admin be used in this gamemode. /// - /// Admin Console + /// Admin Console. + /// True if Admin console is enabled. public virtual bool CanUseMapConsole(MapConsole console) => true; /// - /// Can a body be reported in this gamemode + /// Can a body be reported in this gamemode. /// - /// Target body for reporting + /// Target body for reporting. + /// True if dead bodies can be reported. public virtual bool CanReport(DeadBody body) => true; /// - /// Can system consoles be used in this gamemode + /// Can system consoles be used in this gamemode. /// - /// System Console + /// System Console. + /// True if system consoles are enabled in this mode. public virtual bool CanUseSystemConsole(SystemConsole console) => true; /// - /// Can tasks be interacted with in this gamemode + /// Can tasks be interacted with in this gamemode. /// - /// Task console - public virtual bool CanUseConsole(Console console) => true; + /// Task console. + /// True if tasks are enabled in this mode. + public virtual bool CanUseTasks(Console console) => true; /// - /// Should the sabotage map be used when attempting to open sabotage overlay + /// Should the sabotage map be used when attempting to open sabotage overlay. /// - /// MapBehaviour object + /// MapBehaviour object. + /// True if the sabotage map should be shown. public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true; /// - /// Can a player vent in this gamemode + /// Can a player vent in this gamemode. /// - /// Target vent - /// Player attempting to vent + /// Target vent. + /// Player attempting to vent. + /// True if venting is enabled in this mode. public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true; - -} \ No newline at end of file +} diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs index e80b05c..028eb94 100644 --- a/MiraAPI/GameModes/CustomGameModeManager.cs +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -1,5 +1,4 @@ -#nullable enable -using Reactor.Utilities; +using Reactor.Utilities; using System; using System.Collections.Generic; using System.Linq; diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index ffe7672..aeae257 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -4,31 +4,34 @@ using UnityEngine; namespace MiraAPI.GameOptions; +/// +/// Base class for option groups. An option group is a collection of options that are displayed together in the options menu. +/// public abstract class AbstractOptionGroup { - public List Options { get; } = []; + internal List Options { get; } = []; /// - /// The name of the group. Visible in options menu. + /// Gets the name of the group. Visible in options menu. /// public abstract string GroupName { get; } /// - /// A function that returns whether the group should be visible or not. + /// Gets the function that determines whether the group should be visible or not. /// public virtual Func GroupVisible => () => true; /// - /// The group color. This is used to color the group in the options menu. + /// Gets the group color. This is used to color the group in the options menu. /// public virtual Color GroupColor => Color.clear; /// - /// The role the group is associated with. This is used for the advanced role options menu. + /// Gets the role the group is associated with. This is used for the advanced role options menu. /// - public virtual Type AdvancedRole => null; + public virtual Type? AdvancedRole => null; internal bool AllOptionsHidden { get; set; } - internal CategoryHeaderMasked Header { get; set; } -} \ No newline at end of file + internal CategoryHeaderMasked? Header { get; set; } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs index cc861dd..2944520 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedEnumOptionAttribute.cs @@ -4,24 +4,33 @@ using System.Reflection; namespace MiraAPI.GameOptions.Attributes; -public class ModdedEnumOptionAttribute(string title, Type enumType, string[] values = null, Type roleType = null) +/// +/// Attribute for creating an enum option. +/// +[AttributeUsage(AttributeTargets.Property)] +public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null, Type? roleType = null) : ModdedOptionAttribute(title, roleType) { - internal override IModdedOption CreateOption(object value, PropertyInfo property) + internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var opt = new ModdedEnumOption(Title, (int)value, enumType, values, RoleType); + var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values, RoleType); return opt; } + /// public override void SetValue(object value) { var opt = HolderOption as ModdedEnumOption; - opt.SetValue((int)value); + opt?.SetValue((int)value); } + /// public override object GetValue() { - var opt = HolderOption as ModdedEnumOption; - return Enum.ToObject(enumType, opt.Value); + if (HolderOption is ModdedEnumOption opt) + { + return Enum.ToObject(enumType, opt.Value); + } + throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption"); } -} \ No newline at end of file +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index ac451ad..2bd4438 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -5,6 +5,10 @@ using System.Reflection; namespace MiraAPI.GameOptions.Attributes; +/// +/// A number option attribute for the modded options system. +/// +[AttributeUsage(AttributeTargets.Property)] public class ModdedNumberOptionAttribute( string title, float min, @@ -12,24 +16,28 @@ public class ModdedNumberOptionAttribute( float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, bool zeroInfinity = false, - Type roleType = null) + Type? roleType = null) : ModdedOptionAttribute(title, roleType) { - internal override IModdedOption CreateOption(object value, PropertyInfo property) + internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var toggleOpt = new ModdedNumberOption(Title, (float)value, min, max, increment, suffixType, zeroInfinity, RoleType); - return toggleOpt; + return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, zeroInfinity, RoleType); } + /// public override void SetValue(object value) { - var toggleOpt = HolderOption as ModdedNumberOption; - toggleOpt.SetValue((float)value); + var opt = HolderOption as ModdedNumberOption; + opt?.SetValue((float)value); } + /// public override object GetValue() { - var toggleOpt = HolderOption as ModdedNumberOption; - return toggleOpt.Value; + if (HolderOption is ModdedNumberOption opt) + { + return opt.Value; + } + throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption"); } -} \ No newline at end of file +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs index 63bf017..702d75e 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -3,18 +3,37 @@ using System.Reflection; namespace MiraAPI.GameOptions.Attributes; +/// +/// Represents an attribute that is used to define a modded option. +/// +/// The option title. +/// Optional parameter to specify a role Type. [AttributeUsage(AttributeTargets.Property)] -public abstract class ModdedOptionAttribute(string title, Type roleType = null) : Attribute +public abstract class ModdedOptionAttribute(string title, Type? roleType = null) : Attribute { - public IModdedOption HolderOption { get; internal set; } - - public string Title { get; private set; } = title; - - protected Type RoleType { get; private set; } = roleType; + internal IModdedOption? HolderOption { get; set; } - public abstract void SetValue(object val); - + /// + /// Gets the title of the option. + /// + public string Title { get; private set; } = title; + + /// + /// Gets the role type of the option. + /// + protected Type? RoleType { get; private set; } = roleType; + + /// + /// Sets the value of the option. + /// + /// The new value as an object. + public abstract void SetValue(object value); + + /// + /// Gets the value of the option. + /// + /// The value of the option as an object. public abstract object GetValue(); - internal abstract IModdedOption CreateOption(object value, PropertyInfo property); -} \ No newline at end of file + internal abstract IModdedOption? CreateOption(object? value, PropertyInfo property); +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs deleted file mode 100644 index cb8d6b8..0000000 --- a/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -using MiraAPI.GameOptions.OptionTypes; -using System; -using System.Reflection; - -namespace MiraAPI.GameOptions.Attributes; - -public class ModdedStringOptionAttribute(string title, string[] values, Type roleType = null) - : ModdedOptionAttribute(title, roleType) -{ - internal override IModdedOption CreateOption(object value, PropertyInfo property) - { - var opt = new ModdedStringOption(Title, (int)value, values, RoleType); - return opt; - } - - public override void SetValue(object value) - { - var opt = HolderOption as ModdedStringOption; - opt.SetValue((int)value); - } - - public override object GetValue() - { - var opt = HolderOption as ModdedStringOption; - return opt.Value; - } -} \ No newline at end of file diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs index 2ab75fc..c389292 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs @@ -4,23 +4,34 @@ using System.Reflection; namespace MiraAPI.GameOptions.Attributes; -public class ModdedToggleOptionAttribute(string title, Type roleType = null) : ModdedOptionAttribute(title, roleType) +/// +/// Attribute for a toggle option. +/// +/// The option title. +/// An optional role type. +[AttributeUsage(AttributeTargets.Property)] +public class ModdedToggleOptionAttribute(string title, Type? roleType = null) : ModdedOptionAttribute(title, roleType) { - internal override IModdedOption CreateOption(object value, PropertyInfo property) + internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - var toggleOpt = new ModdedToggleOption(Title, (bool)value, RoleType); + var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), RoleType); return toggleOpt; } + /// public override void SetValue(object value) { - var toggleOpt = HolderOption as ModdedToggleOption; - toggleOpt.SetValue((bool)value); + var opt = HolderOption as ModdedToggleOption; + opt?.SetValue((bool)value); } + /// public override object GetValue() { - var toggleOpt = HolderOption as ModdedToggleOption; - return toggleOpt.Value; + if (HolderOption is ModdedToggleOption opt) + { + return opt.Value; + } + throw new InvalidOperationException($"Holder option for {Title} is not a ModdedToggleOption."); } -} \ No newline at end of file +} diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index c4b88c1..4f7e950 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -1,25 +1,24 @@ -using MiraAPI.PluginLoading; -using System; +using System; using BepInEx.Configuration; using MiraAPI.Networking; +using MiraAPI.PluginLoading; using UnityEngine; namespace MiraAPI.GameOptions; -public interface IModdedOption +internal interface IModdedOption { - public uint Id { get; } - public BaseGameSetting Data { get; } - public IMiraPlugin ParentMod { get; set; } - public Type AdvancedRole { get; set; } - public OptionBehaviour OptionBehaviour { get; protected set; } - public string Title { get; } - public StringNames StringName { get; } - public Func Visible { get; set; } - public ConfigDefinition ConfigDefinition { get; } - public void ValueChanged(OptionBehaviour optionBehaviour); - public OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container); - public abstract float GetFloatData(); - public NetData GetNetData(); - public void HandleNetData(byte[] data); -} \ No newline at end of file + uint Id { get; } + string Title { get; } + StringNames StringName { get; } + IMiraPlugin? ParentMod { get; set; } + BaseGameSetting? Data { get; } + Type? AdvancedRole { get; set; } + OptionBehaviour? OptionBehaviour { get; } + Func Visible { get; set; } + ConfigDefinition? ConfigDefinition { get; set; } + OptionBehaviour? CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container); + float GetFloatData(); + NetData GetNetData(); + void HandleNetData(byte[] data); +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 76410e4..b57528e 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -1,17 +1,21 @@ -using HarmonyLib; -using MiraAPI.GameOptions.Attributes; -using Reactor.Utilities; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using BepInEx.Configuration; +using HarmonyLib; +using MiraAPI.GameOptions.Attributes; using MiraAPI.Networking; using MiraAPI.PluginLoading; using MiraAPI.Utilities; using Reactor.Networking.Rpc; +using Reactor.Utilities; namespace MiraAPI.GameOptions; +/// +/// Handles modded options. +/// public static class ModdedOptionsManager { private static readonly Dictionary OptionAttributes = new(); @@ -19,10 +23,10 @@ public static class ModdedOptionsManager internal static readonly Dictionary ModdedOptions = new(); internal static readonly List Groups = []; - + internal static uint NextId => _nextId++; private static uint _nextId = 1; - + internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo) { if (Activator.CreateInstance(type) is not AbstractOptionGroup group) @@ -40,10 +44,10 @@ public static class ModdedOptionsManager Groups.Add(group); TypeToGroup.Add(type, group); pluginInfo.OptionGroups.Add(group); - + return true; } - + internal static void RegisterPropertyOption(Type type, PropertyInfo property, MiraPluginInfo pluginInfo) { if (!TypeToGroup.TryGetValue(type, out var group)) @@ -57,11 +61,15 @@ public static class ModdedOptionsManager Logger.Error($"Failed to get option for {property.Name}"); return; } - - RegisterOption(option, group, pluginInfo); + + RegisterOption(option, group, property.Name, pluginInfo); } - - internal static void RegisterAttributeOption(Type type, ModdedOptionAttribute attribute, PropertyInfo property, MiraPluginInfo pluginInfo) + + internal static void RegisterAttributeOption( + Type type, + ModdedOptionAttribute attribute, + PropertyInfo property, + MiraPluginInfo pluginInfo) { if (OptionAttributes.ContainsKey(property)) { @@ -76,13 +84,13 @@ public static class ModdedOptionsManager } var option = attribute.CreateOption(property.GetValue(group), property); - + if (option == null) { Logger.Error($"Failed to get option for {property.Name}"); return; } - + var setterOriginal = property.GetSetMethod(); var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); @@ -93,25 +101,33 @@ public static class ModdedOptionsManager OptionAttributes.Add(property, attribute); attribute.HolderOption = option; - - RegisterOption(option, group, pluginInfo); + + RegisterOption(option, group, property.Name, pluginInfo); } - private static void RegisterOption(IModdedOption option, AbstractOptionGroup group, MiraPluginInfo pluginInfo) + private static void RegisterOption( + IModdedOption option, + AbstractOptionGroup group, + string propertyName, + MiraPluginInfo pluginInfo) { + var groupName = group.GetType().FullName; + + option.ConfigDefinition = new ConfigDefinition(groupName, propertyName); + option.ParentMod = pluginInfo.MiraPlugin; option.AdvancedRole = group.AdvancedRole; - + pluginInfo.Options.Add(option); - + ModdedOptions.Add(option.Id, option); group.Options.Add(option); } - - internal static void SyncAllOptions(int targetId=-1) + + internal static void SyncAllOptions(int targetId = -1) { var chunks = ModdedOptions.Values.Select(option => option.GetNetData()).ChunkNetData(1000); - + while (chunks.Count > 0) { Rpc.Instance.SendTo(PlayerControl.LocalPlayer, targetId, chunks.Dequeue()); @@ -123,51 +139,56 @@ public static class ModdedOptionsManager // necessary to disable then re-enable this setting // we dont know how other plugins handle their configs // this way, all the options are saved at once, instead of one by one - foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins.Values) + var oldConfigSetting = new Dictionary(); + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) { + oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet); plugin.PluginConfig.SaveOnConfigSet = false; } - + foreach (var netData in data) { if (!ModdedOptions.TryGetValue(netData.Id, out var option)) { continue; } - + option.HandleNetData(netData.Data); } - - foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins.Values) + + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) { plugin.PluginConfig.Save(); - plugin.PluginConfig.SaveOnConfigSet = true; + plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin]; } - + if (LobbyInfoPane.Instance) { LobbyInfoPane.Instance.RefreshPane(); } } - - + + /// + /// Patches the setter of a property to update the value of the option. + /// + /// The original setter method. + /// The new object value. public static void PropertySetterPatch(MethodBase __originalMethod, object value) { - var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod().Equals(__originalMethod)).Value; - - attribute?.SetValue(value); + var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value; + attribute.SetValue(value); } + /// + /// Patches the getter of a property to return the value of the option. + /// + /// The original getter method. + /// The result of the property getter. + /// False so the original getter gets skipped. public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result) { - var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod().Equals(__originalMethod)).Value; - - if (attribute == null) - { - return true; - } - + var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value; __result = attribute.GetValue(); return false; } -} \ No newline at end of file +} diff --git a/MiraAPI/GameOptions/OptionGroupSingleton.cs b/MiraAPI/GameOptions/OptionGroupSingleton.cs index d7678c6..03a11fa 100644 --- a/MiraAPI/GameOptions/OptionGroupSingleton.cs +++ b/MiraAPI/GameOptions/OptionGroupSingleton.cs @@ -3,21 +3,17 @@ using System.Linq; namespace MiraAPI.GameOptions; +/// +/// Singleton for option groups. +/// +/// The option group type. public class OptionGroupSingleton where T : AbstractOptionGroup { - private static T _instance; + private static T? _instance; - public static T Instance - { - get => _instance ??= ModdedOptionsManager.Groups.OfType().Single(); - set - { - if (_instance != null) - { - throw new InvalidOperationException($"Instance for {typeof(T)} is already set"); - } - - _instance = value; - } - } -} \ No newline at end of file + /// + /// Gets the instance of the option group. + /// + /// Can't + public static T Instance => _instance ??= ModdedOptionsManager.Groups.OfType().Single(); +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs index 0be085f..c3f0f5e 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs @@ -2,18 +2,33 @@ using Reactor.Localization.Utilities; using System; using System.Linq; +using Il2CppInterop.Runtime.InteropTypes.Arrays; using UnityEngine; using Object = UnityEngine.Object; namespace MiraAPI.GameOptions.OptionTypes; +/// +/// An option for selecting an enum value. +/// public class ModdedEnumOption : ModdedOption { - public string[] Values { get; } + /// + /// Gets the string values of the enum. + /// + public string[]? Values { get; } - public ModdedEnumOption(string title, int defaultValue, Type enumType, string[] values = null, Type roleType = null) : base(title, defaultValue, roleType) + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// The default value as an int. + /// The Enum type. + /// An option list of string values to use in place of the enum name. + /// An optional role type to specify for a specific role. + public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType) { - Values = values is null ? Enum.GetNames(enumType) : values; + Values = values ?? Enum.GetNames(enumType); Data = ScriptableObject.CreateInstance(); var data = (StringGameSetting)Data; @@ -26,6 +41,7 @@ public class ModdedEnumOption : ModdedOption data.Index = Value; } + /// public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) { var stringOption = Object.Instantiate(stringOpt, container); @@ -35,7 +51,7 @@ public class ModdedEnumOption : ModdedOption // SetUpFromData method doesnt work correctly so we must set the values manually stringOption.Title = StringName; - stringOption.Values = ((StringGameSetting)Data).Values; + stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray(0); stringOption.Value = Value; OptionBehaviour = stringOption; @@ -43,43 +59,42 @@ public class ModdedEnumOption : ModdedOption return stringOption; } + /// public override float GetFloatData() { return Value; } + /// public override NetData GetNetData() { return new NetData(Id, BitConverter.GetBytes(Value)); } + /// public override void HandleNetData(byte[] data) { SetValue(BitConverter.ToInt32(data)); } - public override string GetHudStringText() - { - return Title + ": " + Values[Value]; - } - + /// public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) { return optionBehaviour.GetInt(); } - public override void OnValueChanged(int newValue) + /// + protected override void OnValueChanged(int newValue) { - DestroyableSingleton.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false); + DestroyableSingleton.Instance.Notifier.AddSettingsChangeMessage(StringName, Data?.GetValueString(newValue), false); if (!OptionBehaviour) { return; } - var opt = OptionBehaviour as StringOption; - if (opt) + if (OptionBehaviour is StringOption opt) { opt.Value = newValue; } } -} \ No newline at end of file +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index ac04d1b..65aa846 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -7,19 +7,56 @@ using Object = UnityEngine.Object; namespace MiraAPI.GameOptions.OptionTypes; +/// +/// Represents a modded number option. +/// public class ModdedNumberOption : ModdedOption { + /// + /// Gets the minimum value of the option. + /// public float Min { get; } + /// + /// Gets the maximum value of the option. + /// public float Max { get; } + /// + /// Gets the increment value of the option. + /// public float Increment { get; } + /// + /// Gets the suffix type of the option. + /// public MiraNumberSuffixes SuffixType { get; } + /// + /// Gets a value indicating whether zero is infinity. + /// public bool ZeroInfinity { get; } - public ModdedNumberOption(string title, float defaultValue, float min, float max, float increment, MiraNumberSuffixes suffixType, bool zeroInfinity = false, Type roleType = null) : base(title, defaultValue, roleType) + /// + /// Initializes a new instance of the class. + /// + /// The title of the option. + /// The default value as a float. + /// The minimum value. + /// The maximum value. + /// The increment. + /// The suffix type. + /// Whether zero is infinity or not. + /// An optional role type. + public ModdedNumberOption( + string title, + float defaultValue, + float min, + float max, + float increment, + MiraNumberSuffixes suffixType, + bool zeroInfinity = false, + Type? roleType = null) : base(title, defaultValue, roleType) { Min = min; Max = max; @@ -37,13 +74,18 @@ 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"; + 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.ZeroIsInfinity = ZeroInfinity; data.SuffixType = (NumberSuffixes)SuffixType; data.OptionName = FloatOptionNames.Invalid; } - public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) + /// + public override OptionBehaviour CreateOption( + ToggleOption toggleOpt, + NumberOption numberOpt, + StringOption stringOpt, + Transform container) { var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container); @@ -54,56 +96,52 @@ public class ModdedNumberOption : ModdedOption numberOption.Value = Value; numberOption.Increment = Increment; numberOption.ValidRange = new FloatRange(Min, Max); - numberOption.FormatString = ((FloatGameSetting)Data).FormatString; + numberOption.FormatString = (Data as FloatGameSetting)?.FormatString ?? "0.0"; numberOption.ZeroIsInfinity = ZeroInfinity; numberOption.SuffixType = (NumberSuffixes)SuffixType; numberOption.floatOptionName = FloatOptionNames.Invalid; - OptionBehaviour = numberOption; return numberOption; } + /// public override float GetFloatData() { return Value; } + /// public override NetData GetNetData() { return new NetData(Id, BitConverter.GetBytes(Value)); } + /// public override void HandleNetData(byte[] data) { SetValue(BitConverter.ToSingle(data)); } - public override string GetHudStringText() - { - return Title + ": " + Value + Helpers.GetSuffix(SuffixType); - } - + /// public override float GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) { return Mathf.Clamp(optionBehaviour.GetFloat(), Min, Max); } - public override void OnValueChanged(float newValue) + /// + protected override void OnValueChanged(float newValue) { Value = Mathf.Clamp(newValue, Min, Max); - DestroyableSingleton.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(Value), false); + DestroyableSingleton.Instance.Notifier.AddSettingsChangeMessage( + StringName, + Data?.GetValueString(Value), + false); - if (!OptionBehaviour) + if (OptionBehaviour is NumberOption opt) { - return; - } - - var opt = OptionBehaviour as NumberOption; - if (opt) - { - opt.Value = newValue; + opt.Value = Value; } } -} \ No newline at end of file +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index d93522d..e1c816e 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -9,33 +9,101 @@ using UnityEngine; namespace MiraAPI.GameOptions.OptionTypes; +/// +/// Represents a modded option. +/// +/// The value type. public abstract class ModdedOption : IModdedOption { - private IMiraPlugin _parentMod; + private IMiraPlugin? _parentMod; + /// + /// Gets the unique identifier of the option. + /// public uint Id { get; } - public BaseGameSetting Data { get; protected init; } - public IMiraPlugin ParentMod + + /// + /// Gets the title of the option. + /// + public string Title { get; } + + /// + /// Gets the StringName object of the option. + /// + public StringNames StringName { get; } + + /// + /// Gets the BaseGameSetting data of the option. + /// + public BaseGameSetting? Data { get; protected init; } + + /// + /// Gets or sets the parent mod of the option. + /// + public IMiraPlugin? ParentMod { get => _parentMod; set { + if (_parentMod != null || value == null) return; _parentMod = value; var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue); Value = entry.Value; } } - public T Value { get; protected set; } - public T DefaultValue { get; } - public Action ChangedEvent { get; set; } - public string Title { get; } - public StringNames StringName { get; } - public Func Visible { get; set; } - public Type AdvancedRole { get; set; } - public OptionBehaviour OptionBehaviour { get; set; } - public ConfigDefinition ConfigDefinition => new("Options", Title); - protected ModdedOption(string title, T defaultValue, Type roleType) + /// + /// Gets or sets the value of the option. + /// + public T Value { get; protected set; } + + /// + /// Gets the default value of the option. + /// + public T DefaultValue { get; } + + /// + /// Gets or sets the event that is invoked when the value of the option changes. + /// + public Action? ChangedEvent { get; set; } + + /// + /// Gets or sets the visibility of the option. + /// + public Func Visible { get; set; } + + /// + /// Gets or sets the advanced role of the option. + /// + public Type? AdvancedRole { get; set; } + + /// + /// Gets or sets the option behaviour of the option. + /// + public OptionBehaviour? OptionBehaviour { get; protected set; } + + /// + /// Gets or sets the config definition of the option. + /// + public ConfigDefinition? ConfigDefinition + { + get => _configDefinition; + set + { + if (_configDefinition is not null) return; + _configDefinition = value; + } + } + + private ConfigDefinition? _configDefinition; + + /// + /// Initializes a new instance of the class. + /// + /// The option title. + /// The default value. + /// The Role Type or null if it doesn't belong to a role. + protected ModdedOption(string title, T defaultValue, Type? roleType) { Id = ModdedOptionsManager.NextId; Title = title; @@ -50,18 +118,23 @@ public abstract class ModdedOption : IModdedOption } } - public void ValueChanged(OptionBehaviour optionBehaviour) + internal void ValueChanged(OptionBehaviour optionBehaviour) { SetValue(GetValueFromOptionBehaviour(optionBehaviour)); } + /// + /// Sets the value of the option. + /// + /// The new value. public void SetValue(T newValue) { var oldVal = Value; Value = newValue; - if (!Value.Equals(oldVal) && ChangedEvent != null) + + if (Value?.Equals(oldVal) == false) { - ChangedEvent.Invoke(Value); + ChangedEvent?.Invoke(Value); } if (AmongUsClient.Instance.AmHost) @@ -70,24 +143,55 @@ public abstract class ModdedOption : IModdedOption { entry.Value = Value; } - + Rpc.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true); } OnValueChanged(newValue); } + /// + /// Gets the float data of the option. + /// + /// A float object representing the option's value. public abstract float GetFloatData(); - + + /// + /// Gets the net data of the option. + /// + /// A NetData object representing this option's data. public abstract NetData GetNetData(); + /// + /// Handles incoming net data. + /// + /// The NetData's byte array. public abstract void HandleNetData(byte[] data); - public abstract void OnValueChanged(T newValue); + /// + /// Handles the value changed event. + /// + /// The new value. + protected abstract void OnValueChanged(T newValue); + /// + /// Gets the value from the option behaviour. + /// + /// The OptionBehaviour. + /// The value. public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour); - public abstract OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container); - - public abstract string GetHudStringText(); -} \ No newline at end of file + /// + /// Creates the option behaviour. + /// + /// The ToggleOption prefab. + /// The NumberOption prefab. + /// The StringOption prefab. + /// The options container. + /// A new OptionBehaviour for this modded option. + public abstract OptionBehaviour CreateOption( + ToggleOption toggleOpt, + NumberOption numberOpt, + StringOption stringOpt, + Transform container); +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs deleted file mode 100644 index 6b878b9..0000000 --- a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Reactor.Localization.Utilities; -using System; -using System.Linq; -using MiraAPI.Networking; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace MiraAPI.GameOptions.OptionTypes; - -public class ModdedStringOption : ModdedOption -{ - public string[] Values { get; } - - public ModdedStringOption(string title, int defaultValue, string[] values, Type roleType=null) : base(title, defaultValue, roleType) - { - Values = values; - Data = ScriptableObject.CreateInstance(); - var data = (StringGameSetting)Data; - - data.Title = StringName; - data.Type = global::OptionTypes.String; - data.Values = Values.Select(CustomStringName.CreateAndRegister).ToArray(); - data.Index = Value; - } - - public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) - { - var stringOption = Object.Instantiate(stringOpt, container); - - stringOption.SetUpFromData(Data, 20); - stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; - - // SetUpFromData method doesnt work correctly so we must set the values manually - stringOption.Title = StringName; - stringOption.Values = ((StringGameSetting)Data).Values; - stringOption.Value = Value; - - OptionBehaviour = stringOption; - - return stringOption; - } - - public override float GetFloatData() - { - return Value; - } - - public override NetData GetNetData() - { - return new NetData(Id, BitConverter.GetBytes(Value)); - } - - public override void HandleNetData(byte[] data) - { - SetValue(BitConverter.ToInt32(data)); - } - - public override string GetHudStringText() - { - return Title + ": " + Values[Value]; - } - - public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) - { - return optionBehaviour.GetInt(); - } - - public override void OnValueChanged(int newValue) - { - DestroyableSingleton.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false); - if (!OptionBehaviour) - { - return; - } - - var opt = OptionBehaviour as StringOption; - if (opt) - { - opt.Value = newValue; - } - } -} \ No newline at end of file diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index f90f83f..ad00c33 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -5,9 +5,18 @@ using Object = UnityEngine.Object; namespace MiraAPI.GameOptions.OptionTypes; +/// +/// A modded toggle option. +/// public class ModdedToggleOption : ModdedOption { - public ModdedToggleOption(string title, bool defaultValue, Type roleType = null) : base(title, defaultValue, roleType) + /// + /// Initializes a new instance of the class. + /// + /// The option title. + /// The default value. + /// An optional role type. + public ModdedToggleOption(string title, bool defaultValue, Type? roleType = null) : base(title, defaultValue, roleType) { Data = ScriptableObject.CreateInstance(); @@ -16,6 +25,7 @@ public class ModdedToggleOption : ModdedOption data.Type = global::OptionTypes.Checkbox; } + /// public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) { var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container); @@ -32,32 +42,32 @@ public class ModdedToggleOption : ModdedOption return toggleOption; } + /// public override float GetFloatData() { return Value ? 1 : 0; } + /// public override NetData GetNetData() { return new NetData(Id, BitConverter.GetBytes(Value)); } + /// public override void HandleNetData(byte[] data) { SetValue(BitConverter.ToBoolean(data)); } - public override string GetHudStringText() - { - return Title + ": " + (Value ? "On" : "Off"); - } - + /// public override bool GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) { return optionBehaviour.GetBool(); } - public override void OnValueChanged(bool newValue) + /// + protected override void OnValueChanged(bool newValue) { DestroyableSingleton.Instance.Notifier.AddSettingsChangeMessage(StringName, newValue ? "On" : "Off", false); if (!OptionBehaviour) @@ -65,10 +75,9 @@ public class ModdedToggleOption : ModdedOption return; } - var toggleOpt = OptionBehaviour as ToggleOption; - if (toggleOpt) + if (OptionBehaviour is ToggleOption toggleOpt) { toggleOpt.CheckMark.enabled = newValue; } } -} \ No newline at end of file +} diff --git a/MiraAPI/Hud/ButtonLocation.cs b/MiraAPI/Hud/ButtonLocation.cs index e08c9d2..da11737 100644 --- a/MiraAPI/Hud/ButtonLocation.cs +++ b/MiraAPI/Hud/ButtonLocation.cs @@ -1,7 +1,17 @@ namespace MiraAPI.Hud; +/// +/// The location of the custom button on the screen. +/// public enum ButtonLocation { + /// + /// Placed in the bottom left. + /// BottomLeft, - BottomRight -} \ No newline at end of file + + /// + /// Placed in the bottom right. + /// + BottomRight, +} diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 6cae97d..361e434 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -1,5 +1,4 @@ -#nullable enable -using MiraAPI.Utilities.Assets; +using MiraAPI.Utilities.Assets; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -7,67 +6,67 @@ using UnityEngine.UI; namespace MiraAPI.Hud; /// -/// Class for making custom action buttons. More customizable than the default Action/Ability buttons in the base game +/// Class for making custom action buttons. More customizable than the default Action/Ability buttons in the base game. /// public abstract class CustomActionButton { /// - /// The name and text of the button. + /// Gets the name and text of the button. /// public abstract string Name { get; } /// - /// How long the button takes to cooldown. + /// Gets the button's cooldown duration in seconds. /// public abstract float Cooldown { get; } /// - /// If the button has an effect ability, how long the effect should last. To disable effects, set to 0. + /// Gets the button's effect duration in seconds. If the button has no effect, set to 0. /// public abstract float EffectDuration { get; } /// - /// If the button has limited uses, how many uses it has. To make the button infinite, set to 0. + /// Gets the maximum amount of uses the button has. If the button has infinite uses, set to 0. /// public abstract int MaxUses { get; } /// - /// The sprite of the button. Use to load a sprite from a resource path. Use to load a sprite from an asset bundle. + /// Gets the sprite of the button. Use to load a sprite from a resource path. Use to load a sprite from an asset bundle. /// public abstract LoadableAsset Sprite { get; } /// - /// The location of the button on the screen + /// Gets the location of the button on the screen. /// public virtual ButtonLocation Location => ButtonLocation.BottomLeft; /// - /// Returns true if the button has an effect ability. + /// Gets a value indicating whether the button has an effect ability. /// public bool HasEffect => EffectDuration > 0; /// - /// Returns true if the button has limited uses. + /// Gets a value indicating whether the button has limited uses. /// public bool LimitedUses => MaxUses > 0; /// - /// Returns true if the effect is currently active. + /// Gets or sets a value indicating whether the effect is currently active, if there is one. /// public bool EffectActive { get; protected set; } /// - /// Returns the amount of uses left. + /// Gets or sets the amount of uses left. /// public int UsesLeft { get; protected set; } /// - /// A timer variable to measure cooldowns and effects. + /// Gets or sets the timer variable to measure cooldowns and effects. /// public float Timer { get; protected set; } /// - /// The button object in game. This is created by Mira API automatically. + /// Gets the button object in game. This is created by Mira API automatically. /// protected ActionButton? Button { get; private set; } @@ -84,7 +83,7 @@ public abstract class CustomActionButton Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent); Button.name = Name + "Button"; - Button.OverrideText(Name.ToUpper()); + Button.OverrideText(Name.ToUpperInvariant()); Button.graphic.sprite = Sprite.LoadAsset(); @@ -116,7 +115,7 @@ public abstract class CustomActionButton /// /// A utility function to override the sprite of the button. /// - /// The new sprite to override with + /// The new sprite to override with. public void OverrideSprite(Sprite sprite) { if (Button != null) @@ -128,7 +127,7 @@ public abstract class CustomActionButton /// /// A utility function to override the name of the button. /// - /// The new name to override with + /// The new name to override with. public void OverrideName(string name) { Button?.OverrideText(name); @@ -137,7 +136,7 @@ public abstract class CustomActionButton /// /// Set the button's timer. /// - /// The amount you want to set to. + /// The time you want to set to. public void SetTimer(float time) { Timer = Mathf.Clamp(time, -1, float.MaxValue); @@ -161,7 +160,6 @@ public abstract class CustomActionButton SetTimer(Timer - amount); } - /// /// Set the amount of uses this button has left. /// @@ -174,7 +172,7 @@ public abstract class CustomActionButton /// /// Increase the amount of uses this button has left. /// - /// The amount you want to increase by. Default: 1 + /// The amount you want to increase by. Default: 1. public void IncreaseUses(int amount = 1) { SetUses(UsesLeft + amount); @@ -183,7 +181,7 @@ public abstract class CustomActionButton /// /// Decrease the amount of uses this button has left. /// - /// The amount you want to decrease by. Default: 1 + /// The amount you want to decrease by. Default: 1. public void DecreaseUses(int amount = 1) { SetUses(UsesLeft - amount); @@ -192,8 +190,10 @@ public abstract class CustomActionButton /// /// A utility function that runs with the local PlayerControl's FixedUpdate if the button is enabled. /// - /// the local PlayerControl - protected virtual void FixedUpdate(PlayerControl playerControl) { } + /// the local PlayerControl. + protected virtual void FixedUpdate(PlayerControl playerControl) + { + } /// /// Callback method for the button click event. @@ -205,13 +205,16 @@ public abstract class CustomActionButton /// True means the button is active, false means the button is disabled. /// /// The role of the local player. + /// True if the button is enabled, false otherwise. public abstract bool Enabled(RoleBehaviour? role); /// /// Given that there is an effect, this method runs when the effect ends. ///

THIS IS A CALLBACK METHOD! Use if you want to end the effect. ///
- public virtual void OnEffectEnd() { } + public virtual void OnEffectEnd() + { + } /// /// When the button is enabled, this method is called to determine if the button can be used. @@ -229,7 +232,7 @@ public abstract class CustomActionButton /// The default behavior is to show the button if the parameter and the method return true. /// It can be overridden for custom behavior. /// - /// Passed in from HudManager.SetHudActive, should hud be active? + /// Passed in from HudManager.SetHudActive, should hud be active. /// Passed in from HudManager.SetHudActive, the current role of the player. public virtual void SetActive(bool visible, RoleBehaviour role) { @@ -272,7 +275,7 @@ public abstract class CustomActionButton /// By default, it handles the cooldown and effect timers, and sets the button to enabled or disabled. /// It can be overridden for custom behavior. /// - /// The local PlayerControl + /// The local PlayerControl. public virtual void FixedUpdateHandler(PlayerControl playerControl) { if (Timer >= 0) @@ -308,23 +311,20 @@ public abstract class CustomActionButton public abstract class CustomActionButton : CustomActionButton where T : MonoBehaviour { /// - /// The target object of the button. + /// Gets or sets the target object of the button. /// - public T? Target { get; private set; } + public T? Target { get; protected set; } /// - /// The distance the player must be from the target object to use the button. + /// Gets the distance the player must be from the target object to use the button. /// public virtual float Distance => PlayerControl.LocalPlayer.Data.Role.GetAbilityDistance(); - /// - /// An optional collider tag to filter the target object by. - /// - public virtual string? ColliderTag => null; - /// /// Determines if the target object is valid. /// + /// The target object being checked. + /// True if the target object is valid, false otherwise. public virtual bool IsTargetValid(T? target) { return target; @@ -333,15 +333,16 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB /// /// The method used to get the target object. /// + /// The target object or null if it isn't found. public abstract T? GetTarget(); /// /// Sets the outline of the target object. /// - /// Should the outline be active + /// Should the outline be active. public abstract void SetOutline(bool active); - + /// public override bool CanUse() { var newTarget = GetTarget(); @@ -355,4 +356,4 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB return base.CanUse() && Target; } -} \ No newline at end of file +} diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs index 90f7b6e..7dbe7ac 100644 --- a/MiraAPI/Hud/CustomButtonManager.cs +++ b/MiraAPI/Hud/CustomButtonManager.cs @@ -22,10 +22,10 @@ internal static class CustomButtonManager Logger.Error($"Failed to create button from {buttonType.Name}"); return; } - + CustomButtons.Add(button); typeof(CustomButtonSingleton<>).MakeGenericType(buttonType) .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! .SetValue(null, button); } -} \ No newline at end of file +} diff --git a/MiraAPI/Hud/CustomButtonSingleton.cs b/MiraAPI/Hud/CustomButtonSingleton.cs index 9c609e3..3e732be 100644 --- a/MiraAPI/Hud/CustomButtonSingleton.cs +++ b/MiraAPI/Hud/CustomButtonSingleton.cs @@ -1,6 +1,4 @@ -#nullable enable -using System; -using System.Linq; +using System.Linq; namespace MiraAPI.Hud; @@ -8,26 +6,12 @@ namespace MiraAPI.Hud; /// A utility class to get the instance of a custom action button. /// /// The type of the button you are trying to access. -public class CustomButtonSingleton where T : CustomActionButton +public static class CustomButtonSingleton where T : CustomActionButton { private static T? _instance; - public static T Instance - { - get => _instance ??= CustomButtonManager.CustomButtons.OfType().Single(); - set - { - if (_instance == value) - { - return; - } - - if (_instance != null) - { - throw new InvalidOperationException($"Instance for {typeof(T)} is already set"); - } - - _instance = value; - } - } -} \ No newline at end of file + /// + /// Gets the instance of the button. + /// + public static T Instance => _instance ??= CustomButtonManager.CustomButtons.OfType().Single(); +} diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 918b53b..b928565 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -2,7 +2,6 @@ using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; -using MiraAPI.Utilities; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 2e2f107..af9b491 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,14 +1,74 @@ namespace MiraAPI.Modifiers; + +/// +/// Base class for all modifiers. +/// public abstract class BaseModifier { - public abstract string ModifierName { get; } - public virtual bool HideOnUi => false; - public PlayerControl Player { get; internal set; } + /// + /// Gets the player that the modifier is attached to. + /// + public PlayerControl? Player { get; internal set; } + + /// + /// Gets the modifier id. + /// public uint ModifierId { get; internal set; } - public virtual void OnActivate() { } - public virtual void OnDeactivate() { } - public virtual void Update() { } - public virtual void FixedUpdate() { } - public virtual void OnDeath() { } - public virtual bool CanVent() => Player.Data.Role.CanVent; -} \ No newline at end of file + + /// + /// Gets the modifier name. + /// + public abstract string ModifierName { get; } + + /// + /// Gets a value indicating whether the modifier is hidden on the UI. + /// + public virtual bool HideOnUi => false; + + /// + /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true. + /// + /// The information string for the HUD. + public virtual string GetHudString() => ModifierName; + + /// + /// Called when the modifier is activated. + /// + public virtual void OnActivate() + { + } + + /// + /// Called when the modifier is deactivated. + /// + public virtual void OnDeactivate() + { + } + + /// + /// Called when the modifier is updated. Attached to the ModifierComponent's Update method. + /// + public virtual void Update() + { + } + + /// + /// Called when the modifier is updated. Attached to the ModifierComponent's FixedUpdate method. + /// + public virtual void FixedUpdate() + { + } + + /// + /// Called when the player dies. + /// + public virtual void OnDeath() + { + } + + /// + /// Determines whether the player can vent. + /// + /// True if the player can vent, false otherwise. + public virtual bool CanVent() => Player?.Data.Role.CanVent == true; +} diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index ba6f14a..eecda1a 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -1,31 +1,58 @@ -using MiraAPI.Modifiers.Types; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Text; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Modifiers.Types; using MiraAPI.Utilities; using Reactor.Utilities; using Reactor.Utilities.Attributes; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using TMPro; using UnityEngine; namespace MiraAPI.Modifiers; +/// +/// The component for handling modifiers. +/// [RegisterInIl2Cpp] public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr) { - public List ActiveModifiers { get; private set; } + /// + /// Gets the active modifiers on the player. + /// + [HideFromIl2Cpp] + public ImmutableList ActiveModifiers => Modifiers.ToImmutableList(); - public PlayerControl player; + [HideFromIl2Cpp] + private List Modifiers { get; set; } = []; - private TextMeshPro _modifierText; + private PlayerControl? _player; - public void Start() + private TextMeshPro? _modifierText; + + private readonly List _toRemove = []; + + private readonly List _toAdd = []; + + internal void ClearModifiers() { - player = GetComponent(); - ActiveModifiers = []; + foreach (var modifier in Modifiers) + { + modifier.OnDeactivate(); + } - if (!player.AmOwner) + _toRemove.AddRange(Modifiers); + } + + private void Start() + { + _player = GetComponent(); + Modifiers = []; + + if (!_player.AmOwner) { return; } @@ -34,55 +61,80 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr) _modifierText.verticalAlignment = VerticalAlignmentOptions.Top; } - public void FixedUpdate() + private void FixedUpdate() { - if (ActiveModifiers is null) return; - - foreach (var modifier in ActiveModifiers) + foreach (var modifier in _toRemove) { - if (modifier is null) continue; + modifier.OnDeactivate(); + Modifiers.Remove(modifier); + } + foreach (var modifier in _toAdd) + { + Modifiers.Add(modifier); + modifier.OnActivate(); + + if (_player?.AmOwner == true && modifier is TimedModifier { AutoStart: true } timer) + { + timer.StartTimer(); + } + } + + if (_toAdd.Count > 0 || _toRemove.Count > 0) + { + if (_player?.AmOwner == true) + { + HudManager.Instance?.SetHudActive(true); + } + _toAdd.Clear(); + _toRemove.Clear(); + } + + foreach (var modifier in Modifiers) + { modifier.FixedUpdate(); } } - public void Update() + private void Update() { - if (ActiveModifiers is null) return; - - foreach (var modifier in ActiveModifiers) + foreach (var modifier in Modifiers) { - if (modifier is null) continue; - modifier.Update(); } - var filteredModifiers = ActiveModifiers.Where(mod => !mod.HideOnUi); + if (!_modifierText || _player?.AmOwner == false) + { + return; + } + + var filteredModifiers = Modifiers.Where(mod => !mod.HideOnUi); var baseModifiers = filteredModifiers as BaseModifier[] ?? filteredModifiers.ToArray(); - if (player.AmOwner && baseModifiers.Any()) + if (baseModifiers.Length != 0) { var stringBuild = new StringBuilder(); foreach (var mod in baseModifiers) { - stringBuild.Append($"\n{mod.ModifierName}"); - if (mod is TimedModifier timer) - { - stringBuild.Append($" ({Math.Round(timer.Duration - timer.TimeRemaining, 0)}s/{timer.Duration}s)"); - } + stringBuild.Append(CultureInfo.InvariantCulture, $"\n{mod.GetHudString()}"); } - _modifierText.text = $"Modifiers:{stringBuild}"; + _modifierText!.text = $"Modifiers:{stringBuild}"; } - else if (player.AmOwner && !baseModifiers.Any() && _modifierText.text != string.Empty) + else if (_modifierText!.text != string.Empty) { _modifierText.text = string.Empty; } } + /// + /// Removes a modifier from the player. + /// + /// The modifier type. + [HideFromIl2Cpp] public void RemoveModifier(Type type) { - var modifier = ActiveModifiers.Find(x => x.GetType() == type); + var modifier = Modifiers.Find(x => x.GetType() == type); if (modifier is null) { @@ -93,14 +145,24 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr) RemoveModifier(modifier); } + /// + /// Removes a modifier from the player. + /// + /// The modifier type. + [HideFromIl2Cpp] public void RemoveModifier() where T : BaseModifier { RemoveModifier(typeof(T)); } + /// + /// Removes a modifier from the player. + /// + /// The modifier ID. + [HideFromIl2Cpp] public void RemoveModifier(uint modifierId) { - var modifier = ActiveModifiers.Find(x => x.ModifierId == modifierId); + var modifier = Modifiers.Find(x => x.ModifierId == modifierId); if (modifier is null) { @@ -111,68 +173,72 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr) RemoveModifier(modifier); } + /// + /// Removes a modifier from the player. + /// + /// The modifier object. + [HideFromIl2Cpp] public void RemoveModifier(BaseModifier modifier) { - if (!ActiveModifiers.Contains(modifier)) + if (!Modifiers.Contains(modifier)) { Logger.Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player."); return; } - modifier.OnDeactivate(); - ActiveModifiers.Remove(modifier); - - if (player.AmOwner) - { - HudManager.Instance.SetHudActive(true); - } + _toRemove.Add(modifier); } - public BaseModifier AddModifier(BaseModifier modifier) + /// + /// Adds a modifier to the player. + /// + /// The modifier to add. + /// The modifier that was added. + [HideFromIl2Cpp] + public BaseModifier? AddModifier(BaseModifier modifier) { - if (ActiveModifiers.Contains(modifier)) + if (Modifiers.Contains(modifier)) { Logger.Error($"Player already has modifier with id {modifier.ModifierId}!"); return null; } - ActiveModifiers.Add(modifier); - modifier.Player = player; - modifier.ModifierId = ModifierManager.TypeToIdModifiers[modifier.GetType()]; - modifier.OnActivate(); + var modifierId = ModifierManager.GetModifierId(modifier.GetType()); - if (!player.AmOwner) + if (modifierId == null) { - return modifier; + Logger.Error($"Cannot add modifier {modifier.GetType().Name} because it has no ID!"); + return null; } - if (modifier is TimedModifier { AutoStart: true } timer) - { - timer.StartTimer(); - } - - HudManager.Instance.SetHudActive(true); - + _toAdd.Add(modifier); + modifier.Player = _player; + modifier.ModifierId = modifierId.Value; return modifier; } - public BaseModifier AddModifier(Type type) + /// + /// Adds a modifier to the player. + /// + /// The modifier type. + /// The modifier that was added. + [HideFromIl2Cpp] + public BaseModifier? AddModifier(Type type) { - if (!ModifierManager.TypeToIdModifiers.TryGetValue(type, out var modifierId)) + var modifierId = ModifierManager.GetModifierId(type); + if (modifierId == null) { Logger.Error($"Cannot add modifier {type.Name} because it is not registered."); return null; } - if (ActiveModifiers.Find(x => x.ModifierId == modifierId) != null) + if (Modifiers.Find(x => x.ModifierId == modifierId) != null) { Logger.Error($"Player already has modifier with id {modifierId}!"); return null; } - var modifier = (BaseModifier)Activator.CreateInstance(type); - - if (modifier is null) + if (Activator.CreateInstance(type) is not BaseModifier modifier) { Logger.Error($"Cannot add modifier {type.Name} because it is null."); return null; @@ -183,8 +249,14 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr) return modifier; } - public T AddModifier() where T : BaseModifier + /// + /// Adds a modifier to the player. + /// + /// The Type of the modifier. + /// The new modifier. + [HideFromIl2Cpp] + public T? AddModifier() where T : BaseModifier { return AddModifier(typeof(T)) as T; } -} \ No newline at end of file +} diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index ad0d71d..773da63 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -1,21 +1,24 @@ -using MiraAPI.Modifiers.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Modifiers.Types; using MiraAPI.Networking; using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking.Rpc; -using Reactor.Utilities.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; using Reactor.Utilities; +using Reactor.Utilities.Extensions; using Random = System.Random; namespace MiraAPI.Modifiers; +/// +/// The manager for handling modifiers. +/// public static class ModifierManager { - public static readonly Dictionary IdToTypeModifiers = []; - public static readonly Dictionary TypeToIdModifiers = []; + private static readonly Dictionary IdToTypeModifierMap = []; + private static readonly Dictionary TypeToIdModifierMap = []; private static uint _nextId; @@ -25,6 +28,26 @@ public static class ModifierManager return _nextId; } + /// + /// Gets the modifier type from the id. + /// + /// The ID. + /// The Type of the modifier. + public static Type? GetModifierType(uint id) + { + return IdToTypeModifierMap.GetValueOrDefault(id); + } + + /// + /// Gets the modifier id from the type. + /// + /// The Type. + /// The ID of the modifier. + public static uint? GetModifierId(Type type) + { + return TypeToIdModifierMap.GetValueOrDefault(type); + } + internal static void RegisterModifier(Type modifierType) { if (!typeof(BaseModifier).IsAssignableFrom(modifierType)) @@ -32,8 +55,8 @@ public static class ModifierManager return; } - IdToTypeModifiers.Add(GetNextId(), modifierType); - TypeToIdModifiers.Add(modifierType, _nextId); + IdToTypeModifierMap.Add(GetNextId(), modifierType); + TypeToIdModifierMap.Add(modifierType, _nextId); } internal static void AssignModifiers(List plrs) @@ -42,34 +65,32 @@ public static class ModifierManager List filteredModifiers = []; - foreach (var modifier in IdToTypeModifiers.Where(pair => pair.Value.IsAssignableTo(typeof(GameModifier)))) + foreach (var modifier in IdToTypeModifierMap.Where(pair => pair.Value.IsAssignableTo(typeof(GameModifier)))) { - var mod = (GameModifier)Activator.CreateInstance(modifier.Value); - - if (mod is null) + if (Activator.CreateInstance(modifier.Value) is not GameModifier mod) { Logger.Error($"Failed to create instance of {modifier.Value.Name}"); continue; } - - if (!plrs.Any(x=>IsGameModifierValid(x, mod, modifier.Key))) + + if (!plrs.Exists(x => IsGameModifierValid(x, mod, modifier.Key))) { Logger.Warning("No players are valid for modifier: " + mod.ModifierName); continue; } - - var maxCount = plrs.Count(x=>IsGameModifierValid(x, mod, modifier.Key)); + + var maxCount = plrs.Count(x => IsGameModifierValid(x, mod, modifier.Key)); var num = Math.Clamp(mod.GetAmountPerGame(), 0, maxCount); var chance = mod.GetAssignmentChance(); - + for (var i = 0; i < num; i++) { var randomNum = rand.Next(100); if (randomNum < Math.Clamp(chance, 0, 100)) { - filteredModifiers.Add(TypeToIdModifiers[modifier.Value]); + filteredModifiers.Add(TypeToIdModifierMap[modifier.Value]); } } } @@ -79,28 +100,26 @@ public static class ModifierManager { shuffledModifiers = shuffledModifiers.GetRange(0, plrs.Count); } - + while (shuffledModifiers.Count > 0) { var id = shuffledModifiers[0]; - var mod = (GameModifier)Activator.CreateInstance(IdToTypeModifiers[id]); - - if (mod is null) + if (Activator.CreateInstance(IdToTypeModifierMap[id]) is not GameModifier mod) { - Logger.Error($"Failed to create instance of {IdToTypeModifiers[id].Name}"); + Logger.Error($"Failed to create instance of {IdToTypeModifierMap[id].Name}"); continue; } - - if (!plrs.Any(x=>IsGameModifierValid(x, mod, id))) + + if (!plrs.Exists(x => IsGameModifierValid(x, mod, id))) { shuffledModifiers.RemoveAt(0); continue; } - + var plr = plrs.Random(); - if (!IsGameModifierValid(plr, mod, id)) + if (plr == null || !IsGameModifierValid(plr, mod, id)) { continue; } @@ -116,17 +135,17 @@ public static class ModifierManager modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifier(modifierId); } - + internal static void SyncAllModifiers(int targetId = -1) { var data = new List(); - foreach (var player in GameData.Instance.AllPlayers) + foreach (var player in PlayerControl.AllPlayerControls) { - data.Add(GetPlayerModifiers(player.Object)); + data.Add(GetPlayerModifiers(player)); } - Rpc.Instance.SendTo(PlayerControl.LocalPlayer, targetId, data.ToArray()); + Rpc.Instance.SendTo(PlayerControl.LocalPlayer, targetId, [.. data]); } internal static void HandleSyncModifiers(NetData[] data) @@ -144,39 +163,40 @@ public static class ModifierManager continue; } - foreach (var modifier in modifierComponent.ActiveModifiers) - { - modifier.OnDeactivate(); - } - modifierComponent.ActiveModifiers.Clear(); + modifierComponent.ClearModifiers(); foreach (var id in ids) { - if (!IdToTypeModifiers.TryGetValue(id, out var type)) + 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) + private static NetData GetPlayerModifiers(PlayerControl? player) { - var bytes = new List(); + if (player == null || !player) + { + return new NetData(0, []); + } + + List bytes = []; var modifierComponent = player.GetComponent(); - if (!modifierComponent) + if (modifierComponent == null || !modifierComponent) { return new NetData(player.PlayerId, []); } foreach (var modifier in modifierComponent.ActiveModifiers) { - bytes.AddRange(BitConverter.GetBytes(TypeToIdModifiers[modifier.GetType()])); + bytes.AddRange(BitConverter.GetBytes(TypeToIdModifierMap[modifier.GetType()])); } - return new NetData(player.PlayerId, bytes.ToArray()); + return new NetData(player.PlayerId, [.. bytes]); } -} \ No newline at end of file +} diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index cda0844..9ec6f28 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -1,7 +1,26 @@ namespace MiraAPI.Modifiers.Types; + +/// +/// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role. +/// public abstract class GameModifier : BaseModifier { + /// + /// Gets the chance of the modifier being assigned to a player. + /// + /// An int value between 0 and 100 representing percent. public abstract int GetAssignmentChance(); + + /// + /// Gets the amount of players that can have this modifier in a game. + /// + /// An int value greater than or equal to zero. public abstract int GetAmountPerGame(); + + /// + /// Determines whether the modifier is valid on a role. + /// + /// The role to be checked. + /// True if the modifier is valid on the role, otherwise false. public virtual bool IsModifierValidOn(RoleBehaviour role) => true; -} \ No newline at end of file +} diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 44e73f7..f18b136 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -1,22 +1,60 @@ -using MiraAPI.Utilities; +using System; +using MiraAPI.Utilities; using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Modifiers.Types; + +/// +/// The base class for all timed modifiers. Timed modifiers have a duration and can be started and stopped. +/// public abstract class TimedModifier : BaseModifier { + /// + /// Gets the duration of the modifier. + /// public abstract float Duration { get; } + + /// + /// Gets a value indicating whether the timer should start automatically when added. + /// public virtual bool AutoStart => false; + + /// + /// Gets a value indicating whether the modifier should be removed when the timer completes. + /// public virtual bool RemoveOnComplete => true; + + /// + /// Called when the timer completes. + /// public abstract void OnTimerComplete(); + /// + /// Gets or sets a value indicating whether the timer is active. + /// public bool TimerActive { get; protected set; } + /// + /// Gets or sets the time remaining on the timer. + /// public float TimeRemaining { get; protected set; } + /// + /// Gets the HUD information for Timed Modifier, including the time remaining. + /// + /// A string with the hud information + timer. + public override string GetHudString() + { + return base.GetHudString() + $" ({Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s)"; + } + + /// + /// The FixedUpdate method for timed modifiers. Automatically handles the timer logic. + /// public override void FixedUpdate() { - if (!Player.AmOwner) + if (Player?.AmOwner == false) { return; } @@ -33,11 +71,14 @@ public abstract class TimedModifier : BaseModifier if (RemoveOnComplete) { - Player.RpcRemoveModifier(ModifierId); + Player?.RpcRemoveModifier(ModifierId); } } } + /// + /// Starts the timer. + /// public void StartTimer() { if (TimerActive) @@ -49,4 +90,4 @@ public abstract class TimedModifier : BaseModifier TimerActive = true; TimeRemaining = Duration; } -} \ No newline at end of file +} diff --git a/MiraAPI/Networking/CustomMurderRpcs.cs b/MiraAPI/Networking/CustomMurderRpcs.cs index 9f216ab..17fb4ae 100644 --- a/MiraAPI/Networking/CustomMurderRpcs.cs +++ b/MiraAPI/Networking/CustomMurderRpcs.cs @@ -14,8 +14,8 @@ public static class CustomMurderRpcs public static void RpcCustomMurder(this PlayerControl source, PlayerControl target, bool didSucceed = true, bool resetKillTimer = true, bool createDeadBody = true, bool teleportMurderer = true, bool showKillAnim = true, bool playKillSound = true) { - MurderResultFlags murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError; - MurderResultFlags murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags; + var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError; + var murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags; source.CustomMurder(target, murderResultFlags2, resetKillTimer, createDeadBody, teleportMurderer, showKillAnim, playKillSound); } @@ -25,7 +25,7 @@ public static class CustomMurderRpcs { source.isKilling = false; source.logger.Debug(string.Format("{0} trying to murder {1}", source.PlayerId, target.PlayerId), null); - NetworkedPlayerInfo data = target.Data; + var data = target.Data; if (resultFlags.HasFlag(MurderResultFlags.FailedError)) { return; @@ -33,7 +33,7 @@ public static class CustomMurderRpcs if (resultFlags.HasFlag(MurderResultFlags.FailedProtected) || (resultFlags.HasFlag(MurderResultFlags.DecisionByHost) && target.protectedByGuardianId > -1)) { target.protectedByGuardianThisRound = true; - bool flag = PlayerControl.LocalPlayer.Data.Role.Role == RoleTypes.GuardianAngel; + var flag = PlayerControl.LocalPlayer.Data.Role.Role == RoleTypes.GuardianAngel; if (flag && PlayerControl.LocalPlayer.Data.PlayerId == target.protectedByGuardianId) { StatsManager.Instance.IncrementStat(StringNames.StatsGuardianAngelCrewmatesProtected); @@ -116,9 +116,9 @@ public static class CustomMurderRpcs public static IEnumerator CoPerformCustomKill(this KillAnimation anim, PlayerControl source, PlayerControl target, bool createDeadBody = true, bool teleportMurderer = true) { - FollowerCamera cam = Camera.main.GetComponent(); - bool isParticipant = PlayerControl.LocalPlayer == source || PlayerControl.LocalPlayer == target; - PlayerPhysics sourcePhys = source.MyPhysics; + var cam = Camera.main.GetComponent(); + var isParticipant = PlayerControl.LocalPlayer == source || PlayerControl.LocalPlayer == target; + var sourcePhys = source.MyPhysics; if (teleportMurderer) { @@ -143,7 +143,7 @@ public static class CustomMurderRpcs deadBody.bodyRenderers.ToArray().ToList().ForEach(target.SetPlayerMaterialColors); target.SetPlayerMaterialColors(deadBody.bloodSplatter); - Vector3 vector = target.transform.position + anim.BodyOffset; + var vector = target.transform.position + anim.BodyOffset; vector.z = vector.y / 1000f; deadBody.transform.position = vector; } diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 25e2d48..c3ee9e8 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,5 +1,4 @@ -using System; -using HarmonyLib; +using HarmonyLib; using InnerNet; using MiraAPI.Hud; using MiraAPI.Roles; @@ -9,15 +8,22 @@ using Object = UnityEngine.Object; namespace MiraAPI.Patches; +/// +/// General patches for the HudManager class. +/// [HarmonyPatch(typeof(HudManager))] public static class HudManagerPatches { - /// Custom buttons parent - private static GameObject _bottomLeft; + // Custom buttons parent. + private static GameObject? _bottomLeft; - /// Custom role tab - private static TaskPanelBehaviour _roleTab; + // Custom role tab. + private static TaskPanelBehaviour? _roleTab; + /// + /// Update custom role tab and custom role hud elements. + /// + /// The HudManager instance. [HarmonyPostfix] [HarmonyPatch(nameof(HudManager.Update))] public static void UpdatePostfix(HudManager __instance) @@ -28,21 +34,25 @@ public static class HudManagerPatches { return; } - - //CustomGameModeManager.ActiveMode?.HudUpdate(__instance); - if (local?.Data?.Role is null) + // CustomGameModeManager.ActiveMode?.HudUpdate(__instance); + + switch (local?.Data?.Role) { - return; - } + case null: + return; - if (local.Data.Role is ICustomRole customRole) - { - customRole.HudUpdate(__instance); - - if (customRole.SetTabText() != null) + case ICustomRole customRole: { - if (!_roleTab) + customRole.HudUpdate(__instance); + + if (customRole.RoleHintType != RoleHintType.RoleTab) + { + _roleTab?.gameObject.Destroy(); + return; + } + + if (_roleTab == null) { _roleTab = CustomRoleManager.CreateRoleTab(customRole); } @@ -50,15 +60,16 @@ public static class HudManagerPatches { CustomRoleManager.UpdateRoleTab(_roleTab, customRole); } + + break; } - else if (customRole.SetTabText() == null && _roleTab) - { - _roleTab.gameObject.Destroy(); - } - } - else if (_roleTab) - { - _roleTab.gameObject.Destroy(); + + default: + if (_roleTab != null) + { + _roleTab.gameObject.Destroy(); + } + break; } } @@ -74,16 +85,17 @@ public static class HudManagerPatches }*/ /// - /// Create custom buttons parent + /// Create custom buttons and arrange them on the hud. /// + /// The HudManager instance. [HarmonyPostfix] [HarmonyPatch(nameof(HudManager.Start))] public static void StartPostfix(HudManager __instance) { var buttons = __instance.transform.Find("Buttons"); var bottomRight = buttons.Find("BottomRight"); - - if (!_bottomLeft) + + if (_bottomLeft == null) { _bottomLeft = Object.Instantiate(bottomRight.gameObject, buttons); } @@ -106,14 +118,14 @@ public static class HudManagerPatches { ButtonLocation.BottomLeft => _bottomLeft.transform, ButtonLocation.BottomRight => bottomRight, - _ => null + _ => null, }; if (location is null) { continue; } - + button.CreateButton(location); } @@ -124,30 +136,34 @@ public static class HudManagerPatches } /// - /// Make sure all launchpad hud elements are inactive/active when appropriate + /// Set the custom role tab and custom buttons active when the hud is active. /// + /// HudManager instance. + /// The local PlayerControl. + /// The player's RoleBehaviour. + /// Whether the Hud should be set active or not. [HarmonyPostfix] [HarmonyPatch(nameof(HudManager.SetHudActive), typeof(PlayerControl), typeof(RoleBehaviour), typeof(bool))] - public static void SetHudActivePostfix(HudManager __instance, [HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] RoleBehaviour roleBehaviour, [HarmonyArgument(2)] bool isActive) + public static void SetHudActivePostfix(HudManager __instance, PlayerControl localPlayer, RoleBehaviour role, bool isActive) { - if (!player.Data) + if (!localPlayer.Data) { return; } if (_roleTab) { - _roleTab.gameObject.SetActive(isActive); + _roleTab?.gameObject.SetActive(isActive); } foreach (var button in CustomButtonManager.CustomButtons) { - button.SetActive(isActive, roleBehaviour); + button.SetActive(isActive, role); } - if (roleBehaviour is ICustomRole role) + if (role is ICustomRole customRole) { - __instance.ImpostorVentButton.gameObject.SetActive(isActive && role.CanUseVent); + __instance.ImpostorVentButton.gameObject.SetActive(isActive && customRole.CanUseVent); } } -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index 9d95896..01066c3 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -26,7 +26,7 @@ public static class IntroCutscenePatches { if (PlayerControl.LocalPlayer.Data.Role is ICustomRole customRole) { - if (!customRole.IsNeutral) + if (customRole.Team is not ModdedRoleTeams.Neutral) { return true; } diff --git a/MiraAPI/Patches/MeetingHudPatches.cs b/MiraAPI/Patches/MeetingHudPatches.cs index e813730..6abe4c0 100644 --- a/MiraAPI/Patches/MeetingHudPatches.cs +++ b/MiraAPI/Patches/MeetingHudPatches.cs @@ -3,9 +3,28 @@ using MiraAPI.Hud; namespace MiraAPI.Patches; +/// +/// Harmony patches for the MeetingHud class. +/// [HarmonyPatch(typeof(MeetingHud))] public static class MeetingHudPatches { + /// + /// Resets the cooldown and effect of all custom buttons when the meeting starts. + /// + [HarmonyPostfix] + [HarmonyPatch(nameof(MeetingHud.Start))] + public static void StartPostfix() + { + foreach (var customActionButton in CustomButtonManager.CustomButtons) + { + customActionButton.ResetCooldownAndOrEffect(); + } + } + + /// + /// Resets the cooldown and effect of all custom buttons when the voting is complete. + /// [HarmonyPostfix] [HarmonyPatch(nameof(MeetingHud.VotingComplete))] public static void VotingCompletePostfix() @@ -15,4 +34,4 @@ public static class MeetingHudPatches customActionButton.ResetCooldownAndOrEffect(); } } -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 0bc37f1..4c47865 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -10,15 +10,21 @@ using UnityEngine.Events; namespace MiraAPI.Patches.Options; - +/// +/// Patches the GameOptionsMenu to add custom options. +/// [HarmonyPatch(typeof(GameOptionsMenu))] public static class GameOptionsMenuPatch { + /// + /// Update patch for the GameOptionsMenu. + /// + /// The GameOptionsMenu instance. [HarmonyPostfix] [HarmonyPatch(nameof(GameOptionsMenu.Update))] public static void UpdatePatch(GameOptionsMenu __instance) { - if (GameSettingMenuPatches.CurrentSelectedMod == 0) + if (GameSettingMenuPatches.SelectedModIdx == 0) { return; } @@ -71,7 +77,7 @@ public static class GameOptionsMenuPatch [HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))] public static bool SettingsPatch(GameOptionsMenu __instance) { - if (GameSettingMenuPatches.CurrentSelectedMod == 0) + if (GameSettingMenuPatches.SelectedModIdx == 0) { return true; } diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index b74581b..13c9036 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,5 +1,4 @@ -#nullable enable -using HarmonyLib; +using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; @@ -13,17 +12,30 @@ using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; +/// +/// Patches for the to add support for custom options. +/// [HarmonyPatch(typeof(GameSettingMenu))] public static class GameSettingMenuPatches { - public static int CurrentSelectedMod { get; private set; } + /// + /// 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; + /// + /// Prefix for the method. Sets up the custom options. + /// + /// The GameSettingMenu instance. [HarmonyPrefix] [HarmonyPatch(nameof(GameSettingMenu.Start))] public static void StartPrefix(GameSettingMenu __instance) @@ -57,11 +69,12 @@ public static class GameSettingMenuPatches passiveButton.OnClick = new ButtonClickedEvent(); passiveButton.OnClick.AddListener((UnityAction)(() => { - if (CurrentSelectedMod != MiraPluginManager.Instance.RegisteredPlugins.Count) + SelectedModIdx += 1; + if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length) { - CurrentSelectedMod += 1; - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); + SelectedModIdx = 0; } + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); })); var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject; @@ -71,17 +84,18 @@ public static class GameSettingMenuPatches backButton.transform.FindChild("Active").gameObject.GetComponent().flipX = backButton.transform.FindChild("Inactive").gameObject.GetComponent().flipX = true; backButton.gameObject.GetComponent().OnClick.AddListener((UnityAction)(() => { - if (CurrentSelectedMod != 0) + SelectedModIdx -= 1; + if (SelectedModIdx < 0) { - CurrentSelectedMod -= 1; - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); + SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length; } + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); })); } private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles) { - if (_text is not null && CurrentSelectedMod == 0) + if (_text is not null && SelectedModIdx == 0) { _text.text = "Default"; _text.fontSizeMax = 3.2f; @@ -89,22 +103,17 @@ public static class GameSettingMenuPatches else if (_text is not null) { _text.fontSizeMax = 2.3f; - SelectedMod = MiraPluginManager.Instance.RegisteredPlugins.ElementAt(CurrentSelectedMod - 1).Value; - if (SelectedMod == null) - { - CurrentSelectedMod = 0; - UpdateText(menu, settings, roles); - } + SelectedMod = MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1]; - var name = SelectedMod?.MiraPlugin.OptionsTitleText; - _text.text = name?[..Math.Min(name.Length, 25)]; + var name = SelectedMod.MiraPlugin.OptionsTitleText; + _text.text = name[..Math.Min(name.Length, 25)]; } menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; menu.GameSettingsButton.gameObject.SetActive(true); menu.RoleSettingsButton.gameObject.SetActive(true); - if (CurrentSelectedMod != 0) + if (SelectedModIdx != 0) { if (SelectedMod?.Options.Where(x=>x.AdvancedRole==null).ToList().Count == 0) { @@ -122,7 +131,7 @@ public static class GameSettingMenuPatches } } - if (roles?.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0) + if (roles.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0) { if (roles.advancedSettingChildren is not null) { diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index a0b79ee..a74588e 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -1,5 +1,4 @@ -#nullable enable -using System; +using System; using System.Collections.Generic; using System.Linq; using HarmonyLib; @@ -17,10 +16,9 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(LobbyViewSettingsPane))] public static class LobbyViewPanePatches { - public static int CurrentSelectedMod { get; private set; } - public static MiraPluginInfo? SelectedMod => CurrentSelectedMod == 0 ? null : MiraPluginManager.Instance.RegisteredPlugins.ElementAt(CurrentSelectedMod-1).Value; + public static MiraPluginInfo? SelectedMod => CurrentSelectedMod == 0 ? null : MiraPluginManager.Instance.RegisteredPlugins()[CurrentSelectedMod-1]; [HarmonyPostfix] @@ -49,7 +47,7 @@ public static class LobbyViewPanePatches passiveButton.OnClick.AddListener((UnityAction)(() => { CurrentSelectedMod += 1; - if (CurrentSelectedMod > MiraPluginManager.Instance.RegisteredPlugins.Count) + if (CurrentSelectedMod > MiraPluginManager.Instance.RegisteredPlugins().Length) { CurrentSelectedMod = 0; } @@ -72,7 +70,7 @@ public static class LobbyViewPanePatches CurrentSelectedMod -= 1; if (CurrentSelectedMod < 0) { - CurrentSelectedMod = MiraPluginManager.Instance.RegisteredPlugins.Count; + CurrentSelectedMod = MiraPluginManager.Instance.RegisteredPlugins().Length; } __instance.RefreshTab(); __instance.scrollBar.ScrollToTop(); @@ -186,146 +184,145 @@ public static class LobbyViewPanePatches private static void DrawRolesTab(LobbyViewSettingsPane instance) { - if (SelectedMod == null) - { - return; - } + if (SelectedMod == null) + { + return; + } - var num = 0.95f; - var num2 = -6.53f; - var categoryHeaderMasked = - Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true); - categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61); - categoryHeaderMasked.transform.localScale = Vector3.one; - categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, 1.26f, -2f); - instance.settingsInfo.Add(categoryHeaderMasked.gameObject); + var num = 0.95f; + var num2 = -6.53f; + var categoryHeaderMasked = + Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true); + categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61); + categoryHeaderMasked.transform.localScale = Vector3.one; + categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, 1.26f, -2f); + instance.settingsInfo.Add(categoryHeaderMasked.gameObject); - var list = new List(); + var list = new List(); - foreach (var team in Enum.GetValues()) - { - var filteredRoles = SelectedMod.CustomRoles.Values - .OfType() - .Where(x => !x.HideSettings && x.Team == team).ToList(); + foreach (var team in Enum.GetValues()) + { + var filteredRoles = SelectedMod.CustomRoles.Values + .OfType() + .Where(x => !x.HideSettings && x.Team == team).ToList(); - if (filteredRoles.Count == 0) - { - continue; - } + if (filteredRoles.Count == 0) + { + continue; + } - var categoryHeaderRoleVariant = - Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true); + var categoryHeaderRoleVariant = + Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true); - switch (team) - { - case ModdedRoleTeams.Crewmate: - categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61); - break; - case ModdedRoleTeams.Impostor: - categoryHeaderRoleVariant.SetHeader(StringNames.ImpostorRolesHeader, 61); - break; - case ModdedRoleTeams.Neutral: - default: - categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61); - categoryHeaderRoleVariant.Title.text = team + " Roles"; - categoryHeaderRoleVariant.Background.color = Color.gray; - categoryHeaderRoleVariant.Title.color = Color.white; - break; - } + switch (team) + { + 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; + } - categoryHeaderRoleVariant.transform.localScale = Vector3.one; - categoryHeaderRoleVariant.transform.localPosition = new Vector3(0.09f, num, -2f); - instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject); - num -= 0.696f; + categoryHeaderRoleVariant.transform.localScale = Vector3.one; + categoryHeaderRoleVariant.transform.localPosition = new Vector3(0.09f, num, -2f); + instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject); + num -= 0.696f; - foreach (var customRole in filteredRoles) - { - var roleBehaviour = customRole as RoleBehaviour; - if (roleBehaviour == null) - { - continue; - } + foreach (var customRole in filteredRoles) + { + var roleBehaviour = customRole as RoleBehaviour; + if (roleBehaviour == null) + { + continue; + } - var chancePerGame = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleBehaviour.Role); - var numPerGame = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); + var chancePerGame = + GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleBehaviour.Role); + var numPerGame = + GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); - var flag = numPerGame == 0; + var flag = numPerGame == 0; - var viewSettingsInfoPanelRoleVariant = - Object.Instantiate( - instance.infoPanelRoleOrigin, instance.settingsContainer, true); - viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one; - viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f); + var viewSettingsInfoPanelRoleVariant = + Object.Instantiate( + instance.infoPanelRoleOrigin, instance.settingsContainer, true); + viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one; + viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f); - var advancedRoleOptions = SelectedMod.Options - .Where(x => x.AdvancedRole == customRole.GetType()) - .ToList(); + var advancedRoleOptions = SelectedMod.Options + .Where(x => x.AdvancedRole == customRole.GetType()) + .ToList(); - if (!flag && advancedRoleOptions.Count > 0) - { - list.Add(customRole.GetType()); - } + if (!flag && advancedRoleOptions.Count > 0) + { + list.Add(customRole.GetType()); + } - viewSettingsInfoPanelRoleVariant.SetInfo(roleBehaviour.NiceName, numPerGame, chancePerGame, - 61, customRole.RoleColor, customRole.Icon.LoadAsset(), true); + viewSettingsInfoPanelRoleVariant.SetInfo(roleBehaviour.NiceName, numPerGame, chancePerGame, + 61, customRole.RoleColor, customRole.Icon.LoadAsset(), true); - viewSettingsInfoPanelRoleVariant.titleText.color = - viewSettingsInfoPanelRoleVariant.chanceTitle.color = - viewSettingsInfoPanelRoleVariant.chanceBackground.color = - viewSettingsInfoPanelRoleVariant.background.color = - customRole.RoleColor.GetAlternateColor(); - instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject); - num -= 0.664f; - } + viewSettingsInfoPanelRoleVariant.titleText.color = + viewSettingsInfoPanelRoleVariant.chanceTitle.color = + viewSettingsInfoPanelRoleVariant.chanceBackground.color = + viewSettingsInfoPanelRoleVariant.background.color = + customRole.RoleColor.GetAlternateColor(); + instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject); + num -= 0.664f; + } - } + } - if (list.Count > 0) - { - var categoryHeaderMasked2 = - Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true); - categoryHeaderMasked2.SetHeader(StringNames.RoleSettingsLabel, 61); - categoryHeaderMasked2.transform.localScale = Vector3.one; - categoryHeaderMasked2.transform.localPosition = new Vector3(-9.77f, num, -2f); - instance.settingsInfo.Add(categoryHeaderMasked2.gameObject); - num -= 1.7f; - var num3 = 0f; - for (var k = 0; k < list.Count; k++) - { - float num4; - if (k % 2 == 0) - { - num4 = -5.8f; - if (k > 0) - { - num -= num3 + 0.59f; - num3 = 0f; - } - } - else - { - num4 = 0.14999962f; - } + if (list.Count > 0) + { + var categoryHeaderMasked2 = + Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true); + categoryHeaderMasked2.SetHeader(StringNames.RoleSettingsLabel, 61); + categoryHeaderMasked2.transform.localScale = Vector3.one; + categoryHeaderMasked2.transform.localPosition = new Vector3(-9.77f, num, -2f); + instance.settingsInfo.Add(categoryHeaderMasked2.gameObject); + num -= 1.7f; + var num3 = 0f; + for (var k = 0; k < list.Count; k++) + { + float num4; + if (k % 2 == 0) + { + num4 = -5.8f; + if (k > 0) + { + num -= num3 + 0.59f; + num3 = 0f; + } + } + else + { + num4 = 0.14999962f; + } - var advancedRoleViewPanel = - Object.Instantiate(instance.advancedRolePanelOrigin, instance.settingsContainer, true); - advancedRoleViewPanel.transform.localScale = Vector3.one; - advancedRoleViewPanel.transform.localPosition = new Vector3(num4, num, -2f); - var num5 = SetUpAdvancedRoleViewPanel(advancedRoleViewPanel, list[k], 0.59f, 61); + var advancedRoleViewPanel = + Object.Instantiate(instance.advancedRolePanelOrigin, instance.settingsContainer, true); + advancedRoleViewPanel.transform.localScale = Vector3.one; + advancedRoleViewPanel.transform.localPosition = new Vector3(num4, num, -2f); + var num5 = SetUpAdvancedRoleViewPanel(advancedRoleViewPanel, list[k], 0.59f, 61); - if (num5 > num3) - { - num3 = num5; - } + if (num5 > num3) + { + num3 = num5; + } - instance.settingsInfo.Add(advancedRoleViewPanel.gameObject); - } - } + instance.settingsInfo.Add(advancedRoleViewPanel.gameObject); + } + } - instance.scrollBar.SetYBoundsMax(-num); + instance.scrollBar.SetYBoundsMax(-num); } private static float SetUpAdvancedRoleViewPanel(AdvancedRoleViewPanel viewPanel, Type roleType, float spacingY, int maskLayer) diff --git a/MiraAPI/Patches/Options/NotificationPopperPatch.cs b/MiraAPI/Patches/Options/NotificationPopperPatch.cs index a45bfe6..0a8fc57 100644 --- a/MiraAPI/Patches/Options/NotificationPopperPatch.cs +++ b/MiraAPI/Patches/Options/NotificationPopperPatch.cs @@ -16,8 +16,8 @@ public class NotificationPopperPatch { if (CustomRoleManager.CustomRoles.Values.Any(role => role.StringName == key)) { - string item = string.Empty; - string text = (teamType == RoleTeamTypes.Crewmate) ? Palette.CrewmateSettingChangeText.ToTextColor() : Palette.ImpostorRed.ToTextColor(); + var item = string.Empty; + var text = (teamType == RoleTeamTypes.Crewmate) ? Palette.CrewmateSettingChangeText.ToTextColor() : Palette.ImpostorRed.ToTextColor(); item = DestroyableSingleton.Instance.GetString(StringNames.LobbyChangeSettingNotificationRole, new Object[] { string.Concat( diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index e355438..420fff8 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -30,7 +30,7 @@ public static class RoleSettingMenuPatches var maskBg = __instance.scrollBar.transform.FindChild("MaskBg"); var hitbox = __instance.scrollBar.transform.FindChild("Hitbox"); - if (GameSettingMenuPatches.CurrentSelectedMod == 0) + if (GameSettingMenuPatches.SelectedModIdx == 0) { __instance.AllButton.transform.parent.gameObject.SetActive(true); __instance.AllButton.gameObject.SetActive(true); @@ -127,7 +127,7 @@ public static class RoleSettingMenuPatches [HarmonyPatch(nameof(RolesSettingsMenu.Update))] public static bool UpdatePatch() { - return GameSettingMenuPatches.CurrentSelectedMod == 0; + return GameSettingMenuPatches.SelectedModIdx == 0; } [HarmonyPostfix] diff --git a/MiraAPI/Patches/Options/SettingPatches.cs b/MiraAPI/Patches/Options/SettingPatches.cs index 2591269..1b228a3 100644 --- a/MiraAPI/Patches/Options/SettingPatches.cs +++ b/MiraAPI/Patches/Options/SettingPatches.cs @@ -1,17 +1,32 @@ -using HarmonyLib; +using System.Globalization; +using HarmonyLib; using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Patches.Options; + +/// +/// Patches for the various game settings. +/// [HarmonyPatch] public class SettingPatches { + /// + /// Prefix for the method. Adds support for custom number suffixes. + /// + /// The FloatGameSetting instance. + /// The result of the GetValueString method. + /// The float value. + /// False to skip original method. [HarmonyPrefix] [HarmonyPatch(typeof(FloatGameSetting), nameof(FloatGameSetting.GetValueString))] - public static bool ValueStringPatch(FloatGameSetting __instance, ref string __result, [HarmonyArgument(0)] float value) + public static bool ValueStringPatch( + FloatGameSetting __instance, + ref string __result, + [HarmonyArgument(0)] float value) { - string result = string.Empty; - MiraNumberSuffixes suffix = (MiraNumberSuffixes)__instance.SuffixType; + var result = string.Empty; + var suffix = (MiraNumberSuffixes)__instance.SuffixType; if (__instance.ZeroIsInfinity && Mathf.Abs(value) < 0.0001f) { @@ -31,12 +46,15 @@ public class SettingPatches } else { - result = DestroyableSingleton.Instance.GetString(StringNames.GameSecondsAbbrev, (Il2CppSystem.Object[])( - [ - value.ToString(__instance.FormatString) - ])); + result = DestroyableSingleton.Instance.GetString( + StringNames.GameSecondsAbbrev, + (Il2CppSystem.Object[]) + [ + value.ToString(__instance.FormatString, CultureInfo.InvariantCulture) + ]); } + __result = result; return false; } -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index f4f79ce..7d4d97a 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -1,14 +1,20 @@ using HarmonyLib; using MiraAPI.Hud; -using MiraAPI.Roles; using MiraAPI.Modifiers; +using MiraAPI.Roles; using Reactor.Utilities.Extensions; namespace MiraAPI.Patches; +/// +/// General patches for the PlayerControl class. +/// [HarmonyPatch(typeof(PlayerControl))] public static class PlayerControlPatches { + /// + /// Adds the modifier component to the player on start. + /// [HarmonyPrefix] [HarmonyPatch(nameof(PlayerControl.Start))] public static void PlayerControlStartPostfix(PlayerControl __instance) @@ -21,6 +27,24 @@ public static class PlayerControlPatches __instance.gameObject.AddComponent(); } + /// + /// Calls the OnDeath method for all active modifiers. + /// + [HarmonyPostfix] + [HarmonyPatch(nameof(PlayerControl.Die))] + public static void PlayerControlDiePostfix(PlayerControl __instance) + { + var modifiersComponent = __instance.GetComponent(); + + if (modifiersComponent) + { + modifiersComponent.ActiveModifiers.ForEach(x=>x.OnDeath()); + } + } + + /// + /// FixedUpdate handler for custom roles and custom buttons. + /// [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.FixedUpdate))] public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance) @@ -29,26 +53,25 @@ public static class PlayerControlPatches { customRole.PlayerControlFixedUpdate(__instance); } - + if (!__instance.AmOwner) { return; } - + foreach (var button in CustomButtonManager.CustomButtons) { if (__instance.Data?.Role == null) { continue; } - + if (!button.Enabled(__instance.Data?.Role)) { continue; } - + button.FixedUpdateHandler(__instance); } } - -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/Roles/GameStartupPatch.cs b/MiraAPI/Patches/Roles/GameStartupPatch.cs index 8455208..b452791 100644 --- a/MiraAPI/Patches/Roles/GameStartupPatch.cs +++ b/MiraAPI/Patches/Roles/GameStartupPatch.cs @@ -1,5 +1,4 @@ using HarmonyLib; -using MiraAPI.Colors; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -23,6 +22,5 @@ public static class GameStartupPatch _runOnce = true; CustomRoleManager.RegisterInRoleManager(); - PaletteManager.RegisterAllColors(); } -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index f7d0401..350767f 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -3,7 +3,6 @@ using HarmonyLib; using MiraAPI.Modifiers; using MiraAPI.Roles; using System.Linq; -using UnityEngine; namespace MiraAPI.Patches.Roles; diff --git a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs index dd938b8..c146b8a 100644 --- a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs +++ b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs @@ -18,7 +18,7 @@ public static class RoleOptionsCollectionPatch { return true; } - + if (customRole.HideSettings) { return false; @@ -27,7 +27,6 @@ public static class RoleOptionsCollectionPatch customRole.ParentMod.PluginConfig.TryGetEntry(customRole.ChanceConfigDefinition, out var entry); __result = entry.Value; return false; - } /// diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index d36efb7..9db0f92 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -2,7 +2,6 @@ using HarmonyLib; using Il2CppSystem; using Il2CppSystem.Collections.Generic; -using Il2CppSystem.Text; using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities.Assets; @@ -34,17 +33,17 @@ public static class TaskAdderPatch GameObject hitbox = new("Hitbox") { - layer = 5 + layer = 5, }; hitbox.transform.SetParent(__instance.TaskParent.transform, false); hitbox.transform.localScale = new Vector3(7.5f, 6.5f, 1); hitbox.transform.localPosition = new Vector3(2.8f, -2.2f, 0); - - SpriteMask mask = hitbox.AddComponent(); + + var mask = hitbox.AddComponent(); mask.sprite = MiraAssets.NextButton.LoadAsset(); mask.alphaCutoff = 0.0f; - - BoxCollider2D collider = hitbox.AddComponent(); + + var collider = hitbox.AddComponent(); collider.size = new Vector2(1f, 1f); collider.enabled = true; @@ -54,7 +53,7 @@ public static class TaskAdderPatch __instance.RoleButton.GetComponent().ClickMask = collider; __instance.RootFolderPrefab.GetComponent().ClickMask = collider; __instance.RootFolderPrefab.gameObject.SetActive(false); - + __instance.TaskParent = inner.transform; _rolesFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); @@ -63,7 +62,7 @@ public static class TaskAdderPatch _rolesFolder.FolderName = "Roles"; _rolesFolder.name = "RolesFolder"; - foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins.Values) + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) { var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); newFolder.FolderName = newFolder.name = plugin.PluginInfo.Metadata.Name; @@ -85,14 +84,14 @@ public static class TaskAdderPatch [HarmonyPatch(typeof(TaskAddButton), "Role", MethodType.Setter)] public static void RoleGetterPatch(TaskAddButton __instance) { - if (__instance.role is ICustomRole { IsNeutral: true }) + if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Neutral }) { __instance.FileImage.color = Color.gray; } __instance.RolloverHandler.OutColor = __instance.FileImage.color; } - + private static void AddFileAsChildCustom(this TaskAdderGame instance, TaskFolder taskFolder, TaskAddButton item, ref float xCursor, ref float yCursor, ref float maxHeight) { item.transform.SetParent(instance.TaskParent); @@ -115,7 +114,7 @@ public static class TaskAdderPatch [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))] public static bool ShowPatch(TaskAdderGame __instance, [HarmonyArgument(0)] TaskFolder taskFolder) { - var stringBuilder = new StringBuilder(64); + var stringBuilder = new Il2CppSystem.Text.StringBuilder(64); __instance.Hierarchy.Add(taskFolder); for (var i = 0; i < __instance.Hierarchy.Count; i++) { @@ -286,4 +285,4 @@ public static class TaskAdderPatch ControllerManager.Instance.SetBackButton(__instance.FolderBackButton); return false; } -} \ No newline at end of file +} diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index e6e0b5c..c6dc714 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -6,16 +6,44 @@ using System.Collections.Generic; namespace MiraAPI.PluginLoading; -public class MiraPluginInfo(IMiraPlugin miraPlugin, PluginInfo info) +/// +/// Represents a Mira plugin. +/// +public class MiraPluginInfo { - public string PluginId { get; set; } = info.Metadata.GUID; - public IMiraPlugin MiraPlugin { get; set; } = miraPlugin; - public PluginInfo PluginInfo { get; set; } = info; - public ConfigFile PluginConfig { get; set; } = miraPlugin.GetConfigFile(); + internal MiraPluginInfo(IMiraPlugin miraPlugin, PluginInfo info) + { + MiraPlugin = miraPlugin; + PluginConfig = miraPlugin.GetConfigFile(); + PluginInfo = info; + PluginId = info.Metadata.GUID; + } - public readonly List OptionGroups = []; - public readonly List Options = []; + internal List OptionGroups { get; } = []; - public readonly Dictionary CustomRoles = []; - internal readonly Dictionary GameModes = []; -} \ No newline at end of file + internal List Options { get; } = []; + + internal Dictionary CustomRoles { get; } = []; + + internal Dictionary GameModes { get; } = []; + + /// + /// Gets the plugin's ID, as defined in the plugin's BepInEx metadata. + /// + public string PluginId { get; } + + /// + /// Gets the plugin's instance as an . + /// + public IMiraPlugin MiraPlugin { get; } + + /// + /// Gets the plugin's BepInEx metadata. + /// + public PluginInfo PluginInfo { get; } + + /// + /// Gets the plugin's configuration file. + /// + public ConfigFile PluginConfig { get; } +} diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 61fa292..794d74e 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -1,61 +1,58 @@ -using BepInEx.Unity.IL2CPP; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using BepInEx.Unity.IL2CPP; +using MiraAPI.Colors; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Roles; -using Reactor.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MiraAPI.Colors; using MiraAPI.Utilities; using Reactor.Networking; +using Reactor.Utilities; namespace MiraAPI.PluginLoading; internal class MiraPluginManager { - public readonly Dictionary RegisteredPlugins = []; + private readonly Dictionary _registeredPlugins = []; - private static MiraPluginManager _instance; + internal MiraPluginInfo[] RegisteredPlugins() => [.. _registeredPlugins.Values]; - public static MiraPluginManager Instance - { - get => _instance ??= new MiraPluginManager(); - private set => _instance = value; - } + public static MiraPluginManager Instance { get; private set; } = new(); internal void Initialize() { Instance = this; IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => { - if (!plugin.GetType().GetInterfaces().Contains(typeof(IMiraPlugin))) + if (plugin is not IMiraPlugin miraPlugin) { return; } - var info = new MiraPluginInfo(plugin as IMiraPlugin, pluginInfo); + var info = new MiraPluginInfo(miraPlugin, pluginInfo); RegisterModifierAttribute(assembly); RegisterAllOptions(assembly, info); - + RegisterRoleAttribute(assembly, info); RegisterButtonAttribute(assembly); RegisterColorClasses(assembly); - RegisteredPlugins.Add(assembly, info); + _registeredPlugins.Add(assembly, info); Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); }; + IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; } public MiraPluginInfo GetPluginByGuid(string guid) { - return RegisteredPlugins.Values.First(plugin => plugin.PluginId == guid); + return _registeredPlugins.Values.First(plugin => plugin.PluginId == guid); } private static void RegisterAllOptions(Assembly assembly, MiraPluginInfo pluginInfo) @@ -68,7 +65,7 @@ internal class MiraPluginManager { continue; } - + foreach (var property in type.GetProperties()) { if (property.PropertyType.IsAssignableTo(typeof(IModdedOption))) @@ -76,7 +73,7 @@ internal class MiraPluginManager ModdedOptionsManager.RegisterPropertyOption(type, property, pluginInfo); continue; } - + var attribute = property.GetCustomAttribute(); if (attribute == null) { @@ -98,22 +95,22 @@ internal class MiraPluginManager { continue; } - + if (!(typeof(RoleBehaviour).IsAssignableFrom(type) && typeof(ICustomRole).IsAssignableFrom(type))) { Logger.Error($"{type.Name} does not inherit from RoleBehaviour or ICustomRole."); continue; } - + if (!ModList.GetById(pluginInfo.PluginId).IsRequiredOnAllClients) { Logger.Error("Custom roles are only supported on all clients."); return; } - + roles.Add(type); } - + CustomRoleManager.RegisterRoleTypes(roles, pluginInfo); } @@ -138,14 +135,13 @@ internal class MiraPluginManager { continue; } - - var color = (CustomColor)property.GetValue(null); - if (color == null) + + if (property.GetValue(null) is not CustomColor color) { - Logger.Error($"Color property {property.Name} in {type.Name} is null."); + Logger.Error($"Color property {property.Name} in {type.Name} is not a CustomColor."); continue; } - + PaletteManager.CustomColors.Add(color); } } @@ -163,7 +159,6 @@ internal class MiraPluginManager } } - private static void RegisterButtonAttribute(Assembly assembly) { foreach (var type in assembly.GetTypes()) @@ -175,4 +170,4 @@ internal class MiraPluginManager } } } -} \ No newline at end of file +} diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 798c662..ca34213 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -17,11 +17,13 @@ using Object = UnityEngine.Object; namespace MiraAPI.Roles; +/// +/// Custom role manager for handling custom roles. +/// public static class CustomRoleManager { - public static readonly Dictionary CustomRoles = new(); - - public static readonly Dictionary RoleIds = new(); + internal static readonly Dictionary CustomRoles = []; + internal static readonly Dictionary RoleIds = []; private static ushort _roleId = 100; @@ -35,7 +37,7 @@ public static class CustomRoleManager RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray(); } - public static void RegisterRoleTypes(List roles, MiraPluginInfo pluginInfo) + internal static void RegisterRoleTypes(List roles, MiraPluginInfo pluginInfo) { roles.ForEach(x => RoleIds.Add(x, GetNextRoleId())); @@ -53,11 +55,11 @@ public static class CustomRoleManager } } - private static RoleBehaviour RegisterRole(Type roleType, MiraPluginInfo parentMod) + private static RoleBehaviour? RegisterRole(Type roleType, MiraPluginInfo parentMod) { if (!(typeof(RoleBehaviour).IsAssignableFrom(roleType) && typeof(ICustomRole).IsAssignableFrom(roleType))) { - Logger.Error($"{roleType?.Name} does not inherit from RoleBehaviour or ICustomRole."); + Logger.Error($"{roleType.Name} does not inherit from RoleBehaviour or ICustomRole."); return null; } @@ -91,6 +93,14 @@ public static class CustomRoleManager RoleManager.GhostRoles.Add(roleBehaviour.Role); } + var useTaskHint = customRole.RoleHintType is RoleHintType.TaskHint; + var overridesTaskText = customRole.GetType().GetMethod("SpawnTaskHeader")?.DeclaringType == customRole.GetType(); + + if (useTaskHint && !overridesTaskText) + { + Logger.Error($"Role {customRole.RoleName} is using RoleHintType.TaskHint but does not override SpawnTaskHeader!"); + } + CustomRoles.Add(roleId, roleBehaviour); if (customRole.HideSettings) @@ -105,12 +115,23 @@ public static class CustomRoleManager return roleBehaviour; } + /// + /// Finds the parent mod of a custom role. + /// + /// The ICustomRole object. + /// A MiraPluginInfo object representing the parent mod of the role. public static MiraPluginInfo FindParentMod(ICustomRole role) { - return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.Value.CustomRoles.ContainsValue(role as RoleBehaviour)).Value; + return MiraPluginManager.Instance.RegisteredPlugins().First(plugin => plugin.CustomRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException())); } - public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole result) + /// + /// Gets a custom role behaviour by role type. + /// + /// The role type enum. + /// The ICustomRole result. + /// True if the role was found. + public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole? result) { CustomRoles.TryGetValue((ushort)roleType, out var temp); if (temp) @@ -176,7 +197,7 @@ public static class CustomRoleManager // we dont know how other plugins handle their configs // this way, all the options are saved at once, instead of one by one var oldConfigSetting = new Dictionary(); - foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins.Values) + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) { oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet); plugin.PluginConfig.SaveOnConfigSet = false; @@ -202,9 +223,11 @@ public static class CustomRoleManager try { - customRole.ParentMod.PluginConfig.TryGetEntry(customRole.NumConfigDefinition, + customRole.ParentMod.PluginConfig.TryGetEntry( + customRole.NumConfigDefinition, out var numEntry); - customRole.ParentMod.PluginConfig.TryGetEntry(customRole.ChanceConfigDefinition, + customRole.ParentMod.PluginConfig.TryGetEntry( + customRole.ChanceConfigDefinition, out var chanceEntry); numEntry.Value = num; @@ -216,7 +239,7 @@ public static class CustomRoleManager } } - foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins.Values) + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) { plugin.PluginConfig.Save(); plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin]; @@ -227,4 +250,4 @@ public static class CustomRoleManager LobbyInfoPane.Instance.RefreshPane(); } } -} \ No newline at end of file +} diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index f8c3e8d..ffe2327 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,94 +1,161 @@ -using AmongUs.GameOptions; +using System.Text; +using AmongUs.GameOptions; using BepInEx.Configuration; -using HarmonyLib; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.Modifiers; -using MiraAPI.Networking; using MiraAPI.PluginLoading; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; -using System; -using System.Text; using UnityEngine; namespace MiraAPI.Roles; +/// +/// Interface for custom roles. +/// public interface ICustomRole { + /// + /// Gets the name of the role. + /// string RoleName { get; } + /// + /// Gets the description of the role. Used in the Intro Cutscene. + /// string RoleDescription { get; } + /// + /// Gets the long description of the role. Used in the Role Tab and Role Options. + /// string RoleLongDescription { get; } + /// + /// Gets the color of the role. + /// Color RoleColor { get; } + /// + /// Gets the team of the role. + /// ModdedRoleTeams Team { get; } + /// + /// Gets the **maximum amount** of players that can have this role at a time. This is not the same as the **number** of players that can have this role in a game. + /// int MaxPlayers => 15; + /// + /// Gets the Sprite used for the Role Options menu screenshot. + /// + [HideFromIl2Cpp] LoadableAsset OptionsScreenshot => MiraAssets.Empty; + /// + /// Gets the Sprite used for the Role Icon. + /// + [HideFromIl2Cpp] LoadableAsset Icon => MiraAssets.Empty; - ConfigDefinition NumConfigDefinition => new("Roles", $"Num{RoleName}"); + /// + /// Gets the BepInEx ConfigDefinition for the amount of players that can have this role. + /// + ConfigDefinition NumConfigDefinition => new("Roles", $"Num {GetType().FullName}"); - ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance{RoleName}"); + /// + /// Gets the BepInEx ConfigDefinition for the chance of this role being selected. + /// + ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance {GetType().FullName}"); + /// + /// Gets a value indicating whether the role is affected by light sabotages. + /// bool AffectedByLight => Team == ModdedRoleTeams.Crewmate; + /// + /// Gets a value indicating whether the role can be killed by others. + /// bool CanGetKilled => Team == ModdedRoleTeams.Crewmate; - bool IsNeutral => Team == ModdedRoleTeams.Neutral; - + /// + /// Gets a value indicating whether the role can kill others. + /// bool CanKill => Team == ModdedRoleTeams.Impostor; + /// + /// Gets a value indicating whether the role can use vents. + /// bool CanUseVent => Team == ModdedRoleTeams.Impostor; + /// + /// Gets a value indicating whether the role's tasks count towards task progress. + /// bool TasksCount => Team == ModdedRoleTeams.Crewmate; + /// + /// Gets a value indicating whether the role is a Ghost. + /// bool IsGhostRole => false; - bool CreateCustomTab => true; - + /// + /// Gets a value indicating whether the role should show up in the Role Options menu. + /// bool HideSettings => IsGhostRole; - RoleTypes GhostRole => Team == ModdedRoleTeams.Crewmate ? RoleTypes.CrewmateGhost : RoleTypes.ImpostorGhost; + /// + /// Gets the role hint style. See enum for all options. + /// + RoleHintType RoleHintType => RoleHintType.RoleTab; + /// + /// Gets the Ghost role that is applied when the player is killed. + /// + RoleTypes GhostRole => Team == ModdedRoleTeams.Impostor ? RoleTypes.ImpostorGhost : RoleTypes.CrewmateGhost; + + /// + /// Gets the parent mod of this role. + /// MiraPluginInfo ParentMod => CustomRoleManager.FindParentMod(this); /// - /// Runs on the PlayerControl FixedUpdate method for any player with this role + /// This method runs on the PlayerControl.FixedUpdate method for ALL players with this role. /// - /// The PlayerControl that has this role - void PlayerControlFixedUpdate(PlayerControl playerControl) { } + /// The PlayerControl that has this role. + void PlayerControlFixedUpdate(PlayerControl playerControl) + { + } /// - /// Only runs for local player on HudManager Update method + /// This method runs on the HudManager.Update method ONLY when the LOCAL player has this role. /// - /// Reference to HudManager instance - void HudUpdate(HudManager hudManager) { } + /// Reference to HudManager instance. + void HudUpdate(HudManager hudManager) + { + } - string GetCustomEjectionMessage(NetworkedPlayerInfo player) + /// + /// Gets a custom ejection message for the role. Return null to use the default message. + /// + /// The NetworkedPlayerInfo object for this player. + /// A string with a custom ejection message or null. + string? GetCustomEjectionMessage(NetworkedPlayerInfo player) { return Team == ModdedRoleTeams.Impostor ? $"{player.PlayerName} was The {RoleName}" : null; } - StringBuilder SetTabText() - { - var taskStringBuilder = Helpers.CreateForRole(this); - return taskStringBuilder; - } + /// + /// Get the custom Role Tab text for this role. + /// + /// A StringBuilder with the role tab text. + StringBuilder SetTabText() => Helpers.CreateForRole(this); + /// + /// Determine whether a given modifier can be applied to this role. + /// + /// The modifier to be tested. + /// True if the modifier is valid on this role, false otherwise. bool IsModifierApplicable(BaseModifier modifier) { return true; } - - NetData GetNetData() - { - ParentMod.PluginConfig.TryGetEntry(NumConfigDefinition, out var numEntry); - ParentMod.PluginConfig.TryGetEntry(ChanceConfigDefinition, out var chanceEntry); - - return new NetData(RoleId.Get(GetType()), BitConverter.GetBytes(numEntry.Value).AddRangeToArray(BitConverter.GetBytes(chanceEntry.Value))); - } -} \ No newline at end of file +} diff --git a/MiraAPI/Roles/RoleHintType.cs b/MiraAPI/Roles/RoleHintType.cs new file mode 100644 index 0000000..8994016 --- /dev/null +++ b/MiraAPI/Roles/RoleHintType.cs @@ -0,0 +1,24 @@ +// Copyright (c) PlaceholderCompany. All rights reserved. + +namespace MiraAPI.Roles; + +/// +/// The type of hint style for a role to use. +/// +public enum RoleHintType +{ + /// + /// No hint. + /// + None, + + /// + /// Original game style (hint above tasks). + /// + TaskHint, + + /// + /// Use custom role tab. + /// + RoleTab, +} diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs index 3ed203c..d086a9e 100644 --- a/MiraAPI/Utilities/Assets/SpriteTools.cs +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -13,10 +13,23 @@ namespace MiraAPI.Utilities.Assets; /// public static class SpriteTools { + /// + /// Delegate for LoadImage. + /// public delegate bool DLoadImage(IntPtr tex, IntPtr data, bool markNonReadable); - public static DLoadImage ICallLoadImage; + /// + /// The ICall for LoadImage. + /// + public static DLoadImage? ICallLoadImage; + /// + /// Load an image into a texture. + /// + /// The texture. + /// Byte data of image. + /// Mark nonreadable. + /// True if succeeded. public static bool LoadImage(Texture2D tex, byte[] data, bool markNonReadable) { ICallLoadImage ??= IL2CPP.ResolveICall("UnityEngine.ImageConversion::LoadImage"); @@ -48,4 +61,4 @@ public static class SpriteTools return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f)); } -} \ No newline at end of file +} diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index ffe0c23..8374649 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -1,29 +1,63 @@ -#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.Networking; +using MiraAPI.Roles; using Reactor.Networking.Attributes; using Reactor.Utilities; -using System.Collections.Generic; -using System.Linq; using UnityEngine; namespace MiraAPI.Utilities; +/// +/// Extension methods for various classes. +/// public static class Extensions { - public static bool IsStatic(this System.Type type) + internal static NetData GetNetData(this ICustomRole role) + { + role.ParentMod.PluginConfig.TryGetEntry(role.NumConfigDefinition, out var numEntry); + role.ParentMod.PluginConfig.TryGetEntry(role.ChanceConfigDefinition, out var chanceEntry); + + return new NetData(RoleId.Get(role.GetType()), BitConverter.GetBytes(numEntry.Value).AddRangeToArray(BitConverter.GetBytes(chanceEntry.Value))); + } + + /// + /// Checks if a type is static. + /// + /// The type being checked. + /// True if the type is static, false otherwise. + public static bool IsStatic(this Type type) { return type is { IsClass: true, IsAbstract: true, IsSealed: true }; } + /// + /// Gets a darkened version of a color. + /// + /// The original color. + /// A darkness amount between 0 and 255. + /// The darkened color. public static Color32 GetShadowColor(this Color32 color, byte darknessAmount) { return - new Color32((byte)Mathf.Clamp(color.r - darknessAmount, 0, 255), (byte)Mathf.Clamp(color.g - darknessAmount, 0, 255), - (byte)Mathf.Clamp(color.b - darknessAmount, 0, 255), byte.MaxValue); + new Color32( + (byte)Mathf.Clamp(color.r - darknessAmount, 0, 255), + (byte)Mathf.Clamp(color.g - darknessAmount, 0, 255), + (byte)Mathf.Clamp(color.b - darknessAmount, 0, 255), + byte.MaxValue); } + /// + /// Truncates a string to a specified length. + /// + /// The original string. + /// The maximum length. + /// An option suffix to attach at the end of the truncated string. + /// A truncated string of maxLength with the attached suffix. public static string? Truncate(this string? value, int maxLength, string truncationSuffix = "…") { return value?.Length > maxLength @@ -31,6 +65,12 @@ public static class Extensions : value; } + /// + /// Chunks a collection of NetData into smaller arrays. + /// + /// A collection of NetData objects. + /// The max chunk size in bytes. + /// A Queue of NetData arrays. public static Queue ChunkNetData(this IEnumerable dataCollection, int chunkSize) { Queue chunks = []; @@ -59,20 +99,29 @@ public static class Extensions if (current.Count > 0) { - chunks.Enqueue(current.ToArray()); + chunks.Enqueue([.. current]); } return chunks; } - + /// + /// Determines if a given OptionBehaviour is for a custom option. + /// + /// The OptionBehaviour to be tested. + /// True if the OptionBehaviour is for a custom options, false otherwise. public static bool IsCustom(this OptionBehaviour optionBehaviour) { - return ModdedOptionsManager.ModdedOptions.Values.Any(opt => opt.OptionBehaviour && opt.OptionBehaviour.Equals(optionBehaviour)); + return ModdedOptionsManager.ModdedOptions.Values.Any(opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour); } - public static readonly Dictionary ModifierComponents = new(); + private static readonly Dictionary ModifierComponents = []; + /// + /// 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)) @@ -89,6 +138,12 @@ public static class Extensions return component; } + /// + /// Randomizes a list. + /// + /// The list object. + /// The type of object the list contains. + /// A randomized list made from the original list. public static List Randomize(this List list) { List randomizedList = []; @@ -102,101 +157,184 @@ 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()!.ActiveModifiers.Exists(x => x is T); } + /// + /// 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()!.ActiveModifiers.Exists(x => x.ModifierId == 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); } - [MethodRpc((uint)MiraRpc.AddModifier)] - public static BaseModifier? RpcAddModifier(this PlayerControl target, uint modifierId) - { - if (ModifierManager.IdToTypeModifiers.TryGetValue(modifierId, out var type)) - { - return target.GetModifierComponent()?.AddModifier(type); - } - - Logger.Error($"Cannot add modifier with id {modifierId} because it is not registered."); - return null; - } - - public static void RpcAddModifier(this PlayerControl player) where T : BaseModifier - { - if (!ModifierManager.TypeToIdModifiers.TryGetValue(typeof(T), out var id)) - { - Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); - return; - } - - player.RpcAddModifier(id); - } - + /// + /// 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 { - if (!ModifierManager.TypeToIdModifiers.TryGetValue(typeof(T), out var id)) + 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); + 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. + /// + /// The original color. + /// A float amount between 0 and 1. + /// The darkened color. public static Color DarkenColor(this Color color, float amount = 0.45f) { return new Color(color.r - amount, color.g - amount, color.b - amount); } + + /// + /// Gets an alternate color based on the original color. + /// + /// The original color. + /// The amount to darken or lighten the original color by between 0.0 and 1.0. + /// An alternate color that has been darkened or lightened. public static Color GetAlternateColor(this Color color, float amount = 0.45f) { return color.IsColorDark() ? LightenColor(color, amount) : DarkenColor(color, amount); } + /// + /// Lightens a color by a specified amount. + /// + /// The original color. + /// A float amount between 0.0 and 1.0. + /// The lightened color. public static Color LightenColor(this Color color, float amount = 0.45f) { return new Color(color.r + amount, color.g + amount, color.b + amount); } + /// + /// Checks if a color is dark. + /// + /// The color to check. + /// True if the color is dark, false otherwise. public static bool IsColorDark(this Color color) { return color.r < 0.5f && color is { g: < 0.5f, b: < 0.5f }; } + /// + /// Gets the nearest dead body to a player. + /// + /// The player object. + /// The radius to search within. + /// The dead body if it is found, or null there is none within the radius. public static DeadBody? GetNearestDeadBody(this PlayerControl playerControl, float radius) { - return Helpers.GetNearestDeadBodies(playerControl.GetTruePosition(), radius).FirstOrDefault(component => component && !component.Reported); + return Helpers.GetNearestDeadBodies(playerControl.GetTruePosition(), radius).Find(component => component && !component.Reported); } - public static T? GetNearestObjectOfType(this PlayerControl playerControl, float radius, string? colliderTag = null, System.Func? predicate = null) where T : Component + /// + /// Finds the nearest object of a specified type to a player. It will only work if the object has a collider. + /// + /// The player object. + /// The radius to search within. + /// An optional collider tag. + /// Optional predicate to test if the object is valid. + /// The type of the object. + /// The object if it was found, or null if there is none within the radius. + public static T? GetNearestObjectOfType(this PlayerControl playerControl, float radius, string? colliderTag = null, Predicate? predicate = null) where T : Component { - return Helpers.GetNearestObjectsOfType(playerControl.GetTruePosition(), radius, colliderTag).FirstOrDefault(predicate ?? (component => component)); + return Helpers.GetNearestObjectsOfType(playerControl.GetTruePosition(), radius, colliderTag).Find(predicate ?? (component => component)); } + /// + /// Gets the nearest player to a player. + /// + /// The player object. + /// Whether impostors should be included in the search. + /// The radius to search within. + /// Whether colliders should be ignored when searching. + /// The closest player if there is one, false otherwise. public static PlayerControl? GetClosestPlayer(this PlayerControl playerControl, bool includeImpostors, float distance, bool ignoreColliders = false) { - if (!ShipStatus.Instance) - { - return null; - } - var filteredPlayers = Helpers.GetClosestPlayers(playerControl, distance, ignoreColliders) .Where(playerInfo => !playerInfo.Data.Disconnected && playerInfo.PlayerId != playerControl.PlayerId && !playerInfo.Data.IsDead && (includeImpostors || !playerInfo.Data.Role.IsImpostor)); - return filteredPlayers.First(); + return filteredPlayers.FirstOrDefault(); } -} \ No newline at end of file +} diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 10085f4..e860431 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,5 +1,6 @@ using MiraAPI.Roles; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using TMPro; @@ -8,14 +9,35 @@ using Object = UnityEngine.Object; namespace MiraAPI.Utilities; +/// +/// A class that contains helper methods. +/// public static class Helpers { - public static readonly ContactFilter2D Filter = ContactFilter2D.CreateLegacyFilter(Constants.NotShipMask, float.MinValue, float.MaxValue); - public static PlainShipRoom GetRoom(Vector3 pos) + /// + /// NotShipMask filter for collisions. + /// + public static readonly ContactFilter2D Filter = ContactFilter2D.CreateLegacyFilter( + Constants.NotShipMask, + float.MinValue, + float.MaxValue); + + /// + /// Get the room at a specific position. + /// + /// The position. + /// The ship room if its found. + public static PlainShipRoom? GetRoom(Vector3 pos) { return ShipStatus.Instance.AllRooms.ToList().Find(room => room.roomArea.OverlapPoint(pos)); } + /// + /// Gets a list of dead bodies within a radius. + /// + /// The source location. + /// The radius to search in. + /// A list of dead bodies. public static List GetNearestDeadBodies(Vector2 source, float radius) { var results = new Il2CppSystem.Collections.Generic.List(); @@ -25,7 +47,16 @@ public static class Helpers .Select(collider2D => collider2D.GetComponent()).ToList(); } - public static List GetNearestObjectsOfType(Vector2 source, float radius, string colliderTag = null) where T : Component + /// + /// Gets a list of objects within a radius. + /// + /// The source point. + /// The radius to search in. + /// An optional collider tag. + /// The type of the object. + /// A list of objects of type T. + public static List GetNearestObjectsOfType(Vector2 source, float radius, string? colliderTag = null) + where T : Component { var results = new Il2CppSystem.Collections.Generic.List(); Physics2D.OverlapCircle(source, radius, Filter, results); @@ -34,95 +65,139 @@ public static class Helpers .Select(collider2D => collider2D.GetComponent()).ToList(); } - public static List GetClosestPlayersInCircle(Vector2 source, float radius, bool ignoreColliders = true) + /// + /// Gets the closest players to a specific point. + /// + /// The source point. + /// The radius to search in. + /// Whether colliders should be ignored. + /// A list of Player Controls in the radius. + public static List GetClosestPlayersInCircle( + Vector2 source, + float radius, + bool ignoreColliders = true) { - List newList = GetNearestObjectsOfType(source, radius); + var newList = GetNearestObjectsOfType(source, radius); - if (ignoreColliders) + if (!ignoreColliders) { - List filteredList = new List(); - foreach (var player in newList) - { - Vector2 vector = player.GetTruePosition() - source; - float magnitude = vector.magnitude; - if (!PhysicsHelpers.AnyNonTriggersBetween(source, vector.normalized, magnitude, Constants.ShipAndObjectsMask)) - { - filteredList.Add(player); - } - } - - return filteredList; + return newList; } - return newList; + return (from player in newList + let vector = player.GetTruePosition() - source + let magnitude = vector.magnitude + where !PhysicsHelpers.AnyNonTriggersBetween( + source, + vector.normalized, + magnitude, + Constants.ShipAndObjectsMask) + select player).ToList(); } - public static List GetClosestPlayers(PlayerControl source, float distance = 2f, bool ignoreColliders = true, bool ignoreSource = true) + + /// + /// Gets the closest players to a specific player. + /// + /// The source player. + /// Distance to search in. + /// Whether to ignore colliders. + /// Whether to ignore the source player. + /// A list of PlayerControls. + public static List GetClosestPlayers( + PlayerControl source, + float distance = 2f, + bool ignoreColliders = true, + bool ignoreSource = true) { if (!ShipStatus.Instance) { - return null; + return []; } - Vector2 myPos = source.GetTruePosition(); - List players = GetClosestPlayers(myPos, distance, ignoreColliders); + var myPos = source.GetTruePosition(); + var players = GetClosestPlayers(myPos, distance, ignoreColliders); return ignoreSource ? players.Where(plr => plr.PlayerId != source.PlayerId).ToList() : players; } - public static List GetClosestPlayers(Vector2 source, float distance = 2f, bool ignoreColliders = true) + /// + /// Gets the closest players to a specific point. + /// + /// The source point. + /// The distance to search in. + /// Whether to ignore colliders. + /// A list of Player Controls. + public static List GetClosestPlayers( + Vector2 source, + float distance = 2f, + bool ignoreColliders = true) { if (!ShipStatus.Instance) { - return null; + return []; } - List outputList = new List(); + List outputList = []; outputList.Clear(); - List allPlayers = GameData.Instance.AllPlayers.ToArray().ToList(); + var allPlayers = GameData.Instance.AllPlayers.ToArray().Select(x => x.Object); - for (int i = 0; i < allPlayers.Count; i++) - { - NetworkedPlayerInfo networkedPlayerInfo = allPlayers[i]; + outputList.AddRange( + from playerControl in allPlayers + where playerControl && playerControl.Collider.enabled + let vector = playerControl.GetTruePosition() - source + let magnitude = vector.magnitude + where magnitude <= distance && (ignoreColliders || !PhysicsHelpers.AnyNonTriggersBetween( + source, + vector.normalized, + magnitude, + Constants.ShipAndObjectsMask)) + select playerControl); - PlayerControl @object = networkedPlayerInfo.Object; - if (@object && @object.Collider.enabled) + outputList.Sort( + delegate(PlayerControl a, PlayerControl b) { - Vector2 vector = @object.GetTruePosition() - source; - float magnitude = vector.magnitude; - if (magnitude <= distance && (ignoreColliders || !PhysicsHelpers.AnyNonTriggersBetween(source, vector.normalized, magnitude, Constants.ShipAndObjectsMask))) + var magnitude2 = (a.GetTruePosition() - source).magnitude; + var magnitude3 = (b.GetTruePosition() - source).magnitude; + if (magnitude2 > magnitude3) { - outputList.Add(@object); + return 1; } - } - } - outputList.Sort(delegate (PlayerControl a, PlayerControl b) - { - float magnitude2 = (a.GetTruePosition() - source).magnitude; - float magnitude3 = (b.GetTruePosition() - source).magnitude; - if (magnitude2 > magnitude3) - { - return 1; - } - if (magnitude2 < magnitude3) - { - return -1; - } - return 0; - }); + + if (magnitude2 < magnitude3) + { + return -1; + } + + return 0; + }); return outputList; } - public static TextMeshPro CreateTextLabel(string name, Transform parent, -AspectPosition.EdgeAlignments alignment, Vector3 distance, float fontSize = 2f, -TextAlignmentOptions textAlignment = TextAlignmentOptions.Center) + /// + /// Creates a TextMeshPro object with the specified parameters. + /// + /// The name of the object. + /// The object parent. + /// The alignment of the TMP object. + /// The distance from the edge. + /// The font size. + /// The text alignment. + /// A new TMP object. + public static TextMeshPro CreateTextLabel( + string name, + Transform parent, + AspectPosition.EdgeAlignments alignment, + Vector3 distance, + float fontSize = 2f, + TextAlignmentOptions textAlignment = TextAlignmentOptions.Center) { var textObj = new GameObject(name) { transform = { - parent = parent + parent = parent, }, - layer = LayerMask.NameToLayer("UI") + layer = LayerMask.NameToLayer("UI"), }; var textMeshPro = textObj.AddComponent(); @@ -139,11 +214,21 @@ TextAlignmentOptions textAlignment = TextAlignmentOptions.Center) return textMeshPro; } - public static DeadBody GetBodyById(byte id) + /// + /// Gets a DeadBody by its parent ID. + /// + /// The player ID. + /// A dead body or null if its not found. + public static DeadBody? GetBodyById(byte id) { return Object.FindObjectsOfType().FirstOrDefault(body => body.ParentId == id); } + /// + /// Gets the suffix for a MiraNumberSuffixes enum. + /// + /// The MiraNumberSuffixes enum. + /// A suffix based on the enum. public static string GetSuffix(MiraNumberSuffixes suffix) { return suffix switch @@ -152,16 +237,21 @@ TextAlignmentOptions textAlignment = TextAlignmentOptions.Center) MiraNumberSuffixes.Multiplier => "x", MiraNumberSuffixes.Seconds => "s", MiraNumberSuffixes.Percent => "%", - _ => string.Empty + _ => string.Empty, }; } + /// + /// Creates a string builder for the Role Tab. + /// + /// The ICustomRole object. + /// A StringBuilder. public static StringBuilder CreateForRole(ICustomRole role) { var taskStringBuilder = new StringBuilder(); - taskStringBuilder.AppendLine($"{role.RoleColor.ToTextColor()}You are a {role.RoleName}.
"); + taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a {role.RoleName}.
"); taskStringBuilder.Append(""); - taskStringBuilder.AppendLine($"{role.RoleLongDescription}"); + taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}"); return taskStringBuilder; } -} \ No newline at end of file +} diff --git a/README.md b/README.md index ee83945..383a41d 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ public float SussyLevel { get; set; } = 4f; // You can set a default value here. Here are the available Option Attributes and their signatures: ```csharp -ModdedEnumOption(string name, Type enumType, Type roleType = null) +ModdedEnumOption(string name, Type enumType, string[]? values = null, Type? roleType = null) ModdedNumberOption( string name, @@ -127,11 +127,9 @@ ModdedNumberOption( float increment=1 NumberSuffixes suffixType = NumberSuffixes.None, bool zeroInfinity = false, - Type roleType = null) - -ModdedStringOption(string title, string[] values, Type roleType = null) + Type? roleType = null) -ModdedToggleOption(string name, Type roleType = null) +ModdedToggleOption(string name, Type? roleType = null) ``` ### ModdedOption Properties @@ -144,7 +142,6 @@ public ModdedToggleOption YeezusAbility { get; } = new ModdedToggleOption("Yeezu Here is a full list of ModdedOption classes you can use: - `ModdedEnumOption` - `ModdedNumberOption` -- `ModdedStringOption` - `ModdedToggleOption` To see a full example of an options class, see [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/ExampleOptions.cs).