From bc188191de568fe62dc5eb192b709464ede49527 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 18 May 2025 11:34:41 -0400 Subject: [PATCH] Initial commit --- .../Buttons/NeutralKillerButton.cs | 9 +- .../ExampleExtensionPlugin.cs | 31 ++++ .../GameOver/NeutralKillerGameOver.cs | 4 +- .../MiraAPI.Example.Extension.csproj | 14 ++ .../Modifiers/ExtensionModifier.cs | 16 ++ .../Options/ExtendedFreezerOptions.cs | 13 ++ .../Options/ExtensionModifierOptions.cs | 17 ++ .../Patches/FreezeButtonPatch.cs | 15 ++ .../Roles/NeutralKillerRole.cs | 10 +- .../Buttons/Freezer/FreezeButton.cs | 2 +- MiraAPI.Example/ExampleEventHandlers.cs | 2 +- .../Options/Roles/FreezerRoleSettings.cs | 3 - MiraAPI.sln | 6 + MiraAPI/PluginLoading/IMiraExtension.cs | 8 + MiraAPI/PluginLoading/MiraPluginManager.cs | 163 ++++++++++-------- 15 files changed, 227 insertions(+), 86 deletions(-) rename {MiraAPI.Example => MiraAPI.Example.Extension}/Buttons/NeutralKillerButton.cs (81%) create mode 100644 MiraAPI.Example.Extension/ExampleExtensionPlugin.cs rename {MiraAPI.Example => MiraAPI.Example.Extension}/GameOver/NeutralKillerGameOver.cs (86%) create mode 100644 MiraAPI.Example.Extension/MiraAPI.Example.Extension.csproj create mode 100644 MiraAPI.Example.Extension/Modifiers/ExtensionModifier.cs create mode 100644 MiraAPI.Example.Extension/Options/ExtendedFreezerOptions.cs create mode 100644 MiraAPI.Example.Extension/Options/ExtensionModifierOptions.cs create mode 100644 MiraAPI.Example.Extension/Patches/FreezeButtonPatch.cs rename {MiraAPI.Example => MiraAPI.Example.Extension}/Roles/NeutralKillerRole.cs (87%) create mode 100644 MiraAPI/PluginLoading/IMiraExtension.cs diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example.Extension/Buttons/NeutralKillerButton.cs similarity index 81% rename from MiraAPI.Example/Buttons/NeutralKillerButton.cs rename to MiraAPI.Example.Extension/Buttons/NeutralKillerButton.cs index d38253a..b4825dc 100644 --- a/MiraAPI.Example/Buttons/NeutralKillerButton.cs +++ b/MiraAPI.Example.Extension/Buttons/NeutralKillerButton.cs @@ -1,17 +1,20 @@ -using MiraAPI.Example.GameOver; -using MiraAPI.Example.Roles; +using MiraAPI.Example.Extension.GameOver; +using MiraAPI.Example.Extension.Roles; using MiraAPI.GameEnd; using MiraAPI.Hud; using MiraAPI.Utilities.Assets; using UnityEngine; -namespace MiraAPI.Example.Buttons; +namespace MiraAPI.Example.Extension.Buttons; public class NeutralKillerButton : CustomActionButton { public override string Name => "Win Game"; + public override float Cooldown => 0f; + public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + protected override void OnClick() { CustomGameOver.Trigger([PlayerControl.LocalPlayer.Data]); diff --git a/MiraAPI.Example.Extension/ExampleExtensionPlugin.cs b/MiraAPI.Example.Extension/ExampleExtensionPlugin.cs new file mode 100644 index 0000000..38f022c --- /dev/null +++ b/MiraAPI.Example.Extension/ExampleExtensionPlugin.cs @@ -0,0 +1,31 @@ +using System; +using BepInEx; +using BepInEx.Unity.IL2CPP; +using HarmonyLib; +using MiraAPI.PluginLoading; +using Reactor; +using Reactor.Networking; +using Reactor.Networking.Attributes; + +namespace MiraAPI.Example.Extension; + +[BepInAutoPlugin("mira.example.extension", "MiraExampleExtensionMod")] +[BepInProcess("Among Us.exe")] +[BepInDependency(ReactorPlugin.Id)] +[BepInDependency(MiraApiPlugin.Id)] +[BepInDependency(ExamplePlugin.Id)] +[ReactorModFlags(ModFlags.RequireOnAllClients)] +public partial class ExampleExtensionPlugin : BasePlugin, IMiraExtension +{ + public Harmony Harmony { get; } = new(Id); + + public override void Load() + { + Harmony.PatchAll(); + } + + public Type GetBasePluginType() + { + return typeof(ExamplePlugin); + } +} diff --git a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs b/MiraAPI.Example.Extension/GameOver/NeutralKillerGameOver.cs similarity index 86% rename from MiraAPI.Example/GameOver/NeutralKillerGameOver.cs rename to MiraAPI.Example.Extension/GameOver/NeutralKillerGameOver.cs index 351d593..0ffc26f 100644 --- a/MiraAPI.Example/GameOver/NeutralKillerGameOver.cs +++ b/MiraAPI.Example.Extension/GameOver/NeutralKillerGameOver.cs @@ -1,9 +1,9 @@ -using MiraAPI.Example.Roles; +using MiraAPI.Example.Extension.Roles; using MiraAPI.GameEnd; using MiraAPI.Utilities; using UnityEngine; -namespace MiraAPI.Example.GameOver; +namespace MiraAPI.Example.Extension.GameOver; public class NeutralKillerGameOver : CustomGameOver { diff --git a/MiraAPI.Example.Extension/MiraAPI.Example.Extension.csproj b/MiraAPI.Example.Extension/MiraAPI.Example.Extension.csproj new file mode 100644 index 0000000..8970ff9 --- /dev/null +++ b/MiraAPI.Example.Extension/MiraAPI.Example.Extension.csproj @@ -0,0 +1,14 @@ + + + 1.0.0 + Example extension for Mira API + Angxl, XtraCube + + + + + + + + + diff --git a/MiraAPI.Example.Extension/Modifiers/ExtensionModifier.cs b/MiraAPI.Example.Extension/Modifiers/ExtensionModifier.cs new file mode 100644 index 0000000..1569039 --- /dev/null +++ b/MiraAPI.Example.Extension/Modifiers/ExtensionModifier.cs @@ -0,0 +1,16 @@ +using MiraAPI.Example.Extension.Options; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers.Types; + +namespace MiraAPI.Example.Extension.Modifiers; + +public class ExtensionModifier : GameModifier +{ + public override string ModifierName => "Extension Modifier"; + + public override string GetDescription() => "This is from the extension mod!"; + + public override int GetAssignmentChance() => (int)OptionGroupSingleton.Instance.ModifierChance; + + public override int GetAmountPerGame() => (int)OptionGroupSingleton.Instance.ModifierLimit; +} diff --git a/MiraAPI.Example.Extension/Options/ExtendedFreezerOptions.cs b/MiraAPI.Example.Extension/Options/ExtendedFreezerOptions.cs new file mode 100644 index 0000000..4222194 --- /dev/null +++ b/MiraAPI.Example.Extension/Options/ExtendedFreezerOptions.cs @@ -0,0 +1,13 @@ +using MiraAPI.Example.Roles; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Extension.Options; + +public class ExtendedFreezerOptions : AbstractOptionGroup +{ + public override string GroupName => "Extended Freezer Role Options"; + + [ModdedNumberOption("Freeze Uses", 1, 5)] + public float FreezeUses { get; set; } = 1; +} diff --git a/MiraAPI.Example.Extension/Options/ExtensionModifierOptions.cs b/MiraAPI.Example.Extension/Options/ExtensionModifierOptions.cs new file mode 100644 index 0000000..33b6d41 --- /dev/null +++ b/MiraAPI.Example.Extension/Options/ExtensionModifierOptions.cs @@ -0,0 +1,17 @@ +using MiraAPI.Example.Extension.Modifiers; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; +using MiraAPI.Utilities; + +namespace MiraAPI.Example.Extension.Options; + +public class ExtensionModifierOptions : AbstractOptionGroup +{ + public override string GroupName => "Extension Modifier Options"; + + [ModdedNumberOption("Modifier Chance", 0, 100, 10, MiraNumberSuffixes.Percent)] + public float ModifierChance { get; set; } = 100f; + + [ModdedNumberOption("Modifier Limit", 0, 10)] + public float ModifierLimit { get; set; } = 0; +} diff --git a/MiraAPI.Example.Extension/Patches/FreezeButtonPatch.cs b/MiraAPI.Example.Extension/Patches/FreezeButtonPatch.cs new file mode 100644 index 0000000..adadd1e --- /dev/null +++ b/MiraAPI.Example.Extension/Patches/FreezeButtonPatch.cs @@ -0,0 +1,15 @@ +using HarmonyLib; +using MiraAPI.Example.Buttons.Freezer; +using MiraAPI.Example.Extension.Options; +using MiraAPI.GameOptions; + +namespace MiraAPI.Example.Extension.Patches; + +[HarmonyPatch(typeof(FreezeButton), nameof(FreezeButton.MaxUses), MethodType.Getter)] +public static class FreezeButtonPatch +{ + public static void Postfix(ref int __result) + { + __result = (int)OptionGroupSingleton.Instance.FreezeUses; + } +} diff --git a/MiraAPI.Example/Roles/NeutralKillerRole.cs b/MiraAPI.Example.Extension/Roles/NeutralKillerRole.cs similarity index 87% rename from MiraAPI.Example/Roles/NeutralKillerRole.cs rename to MiraAPI.Example.Extension/Roles/NeutralKillerRole.cs index 55fdc51..79c6d6d 100644 --- a/MiraAPI.Example/Roles/NeutralKillerRole.cs +++ b/MiraAPI.Example.Extension/Roles/NeutralKillerRole.cs @@ -1,16 +1,20 @@ -using MiraAPI.Example.GameOver; +using MiraAPI.Example.Extension.GameOver; using MiraAPI.GameEnd; using MiraAPI.Roles; using UnityEngine; -namespace MiraAPI.Example.Roles; +namespace MiraAPI.Example.Extension.Roles; public class NeutralKillerRole : ImpostorRole, ICustomRole { public string RoleName => "Outcast Killer"; + public string RoleDescription => "Outcast who can kill."; - public string RoleLongDescription => RoleDescription; + + public string RoleLongDescription => this.RoleDescription; + public Color RoleColor => Color.magenta; + public ModdedRoleTeams Team => ModdedRoleTeams.Custom; public CustomRoleConfiguration Configuration => new(this) diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 3b7363a..323bb2f 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -16,7 +16,7 @@ public class FreezeButton : CustomActionButton public override float Cooldown => OptionGroupSingleton.Instance.FreezeDuration; - public override int MaxUses => (int)OptionGroupSingleton.Instance.FreezeUses; + public override int MaxUses => 1; public override LoadableAsset Sprite => ExampleAssets.ExampleButton; diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs index 94f90ed..0a48ed1 100644 --- a/MiraAPI.Example/ExampleEventHandlers.cs +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -51,7 +51,7 @@ public static class ExampleEventHandlers [RegisterEvent(15)] public static void HandleVoteEvent(HandleVoteEvent @event) { - if (@event.VoteData.Owner.Data.Role is not NeutralKillerRole) return; + if (@event.VoteData.Owner.Data.Role is not FreezerRole) return; @event.VoteData.SetRemainingVotes(0); diff --git a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs index 1473068..1f9677d 100644 --- a/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs +++ b/MiraAPI.Example/Options/Roles/FreezerRoleSettings.cs @@ -11,7 +11,4 @@ public class FreezerRoleSettings : AbstractOptionGroup [ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)] public float FreezeDuration { get; set; } = 5; - - [ModdedNumberOption("Freeze Uses", 1, 5)] - public float FreezeUses { get; set; } = 1; } diff --git a/MiraAPI.sln b/MiraAPI.sln index 5b2a2c0..7e73464 100644 --- a/MiraAPI.sln +++ b/MiraAPI.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiraAPI", "MiraAPI\MiraAPI. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example", "MiraAPI.Example\MiraAPI.Example.csproj", "{19B76855-16CC-43CC-A0A6-5A7FFB43F077}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example.Extension", "MiraAPI.Example.Extension\MiraAPI.Example.Extension.csproj", "{526FF017-80E1-4094-804D-741AF35AA226}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Debug|Any CPU.Build.0 = Debug|Any CPU {19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.ActiveCfg = Release|Any CPU {19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.Build.0 = Release|Any CPU + {526FF017-80E1-4094-804D-741AF35AA226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {526FF017-80E1-4094-804D-741AF35AA226}.Debug|Any CPU.Build.0 = Debug|Any CPU + {526FF017-80E1-4094-804D-741AF35AA226}.Release|Any CPU.ActiveCfg = Release|Any CPU + {526FF017-80E1-4094-804D-741AF35AA226}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MiraAPI/PluginLoading/IMiraExtension.cs b/MiraAPI/PluginLoading/IMiraExtension.cs new file mode 100644 index 0000000..1ab75d0 --- /dev/null +++ b/MiraAPI/PluginLoading/IMiraExtension.cs @@ -0,0 +1,8 @@ +using System; + +namespace MiraAPI.PluginLoading; + +public interface IMiraExtension +{ + public Type GetBasePluginType(); +} diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index c68c8ce..8a16ea4 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using BepInEx; using BepInEx.Unity.IL2CPP; using MiraAPI.Colors; using MiraAPI.Events; @@ -35,86 +36,24 @@ public sealed class MiraPluginManager Instance = this; IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => { - if (plugin is not IMiraPlugin miraPlugin) + if (plugin is IMiraPlugin miraPlugin) { - return; + var miraInfo = new MiraPluginInfo(miraPlugin, pluginInfo); + RegisterPlugin(miraInfo, assembly); + _registeredPlugins.Add(assembly, miraInfo); } - - var info = new MiraPluginInfo(miraPlugin, pluginInfo); - var roles = new List(); - - var oldConfigSetting = info.PluginConfig.SaveOnConfigSet; - info.PluginConfig.SaveOnConfigSet = false; - - foreach (var type in assembly.GetTypes()) + if (plugin is IMiraExtension miraExtension) { - if (type.GetCustomAttribute() != null) + var basePlugin = miraExtension.GetBasePluginType(); + var basePluginInfo = _registeredPlugins.Values.FirstOrDefault(x => x.MiraPlugin.GetType() == basePlugin); + if (basePluginInfo == null) { - continue; + Logger.Error($"Base plugin {basePlugin.Name} not found for extension {pluginInfo.Metadata.GUID}."); + return; } - foreach (var method in type.GetMethods()) - { - var eventAttribute = method.GetCustomAttribute(); - if (eventAttribute == null) - { - continue; - } - - if (!method.IsStatic) - { - Logger.Error($"Event method {method.Name} in {type.Name} must be static."); - continue; - } - - var parameters = method.GetParameters(); - if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) - { - Logger.Error($"Invalid event registration method {method.Name} in {type.Name}"); - continue; - } - - var paramType = parameters[0].ParameterType; - MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); - } - - if (RegisterModifier(type, info)) - { - continue; - } - - if (RegisterOptions(type, info)) - { - continue; - } - - if (RegisterRole(type, info, out var role)) - { - roles.Add(role); - continue; - } - - if (RegisterButton(type, info)) - { - continue; - } - - if (RegisterGameOver(type)) - { - continue; - } - - RegisterColorClasses(type); + RegisterPlugin(basePluginInfo, assembly); } - - info.PluginConfig.Save(); - info.PluginConfig.SaveOnConfigSet = oldConfigSetting; - - info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); - QueuedRoleRegistrations.Add(info, roles); - - _registeredPlugins.Add(assembly, info); - Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); }; IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; IL2CPPChainloader.Instance.Finished += () => @@ -133,6 +72,84 @@ public sealed class MiraPluginManager return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId); } + private void RegisterPlugin(MiraPluginInfo info, Assembly assembly) + { + var roles = new List(); + + var oldConfigSetting = info.PluginConfig.SaveOnConfigSet; + info.PluginConfig.SaveOnConfigSet = false; + + foreach (var type in assembly.GetTypes()) + { + if (type.GetCustomAttribute() != null) + { + continue; + } + + foreach (var method in type.GetMethods()) + { + var eventAttribute = method.GetCustomAttribute(); + if (eventAttribute == null) + { + continue; + } + + if (!method.IsStatic) + { + Logger.Error($"Event method {method.Name} in {type.Name} must be static."); + continue; + } + + var parameters = method.GetParameters(); + if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent))) + { + Logger.Error($"Invalid event registration method {method.Name} in {type.Name}"); + continue; + } + + var paramType = parameters[0].ParameterType; + MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority); + } + + if (RegisterModifier(type, info)) + { + continue; + } + + if (RegisterOptions(type, info)) + { + continue; + } + + if (RegisterRole(type, info, out var role)) + { + roles.Add(role); + continue; + } + + if (RegisterButton(type, info)) + { + continue; + } + + if (RegisterGameOver(type)) + { + continue; + } + + RegisterColorClasses(type); + } + + info.PluginConfig.Save(); + info.PluginConfig.SaveOnConfigSet = oldConfigSetting; + + info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + QueuedRoleRegistrations.TryAdd(info, []); + QueuedRoleRegistrations[info].AddRange(roles); + + Logger.Info($"Registering mod {info.PluginId} with Mira API."); + } + private static bool RegisterGameOver(Type type) { try