diff --git a/MiraAPI.Example/CustomRole.cs b/MiraAPI.Example/CustomRole.cs new file mode 100644 index 0000000..5c547c4 --- /dev/null +++ b/MiraAPI.Example/CustomRole.cs @@ -0,0 +1,15 @@ +using MiraAPI.Roles; +using UnityEngine; + +namespace MiraAPI.Example +{ + [RegisterCustomRole(ExamplePlugin.Id)] + public class CustomRole : CrewmateRole, ICustomRole + { + public string RoleName => "Sussy Baka"; + public string RoleDescription => "Role decs?"; + public string RoleLongDescription => "Extra role decs?"; + public Color RoleColor => Palette.Orange; + public RoleTeamTypes Team => RoleTeamTypes.Crewmate; + } +} diff --git a/MiraAPI.Example/ExampleOptions.cs b/MiraAPI.Example/ExampleOptions.cs new file mode 100644 index 0000000..1f8af5d --- /dev/null +++ b/MiraAPI.Example/ExampleOptions.cs @@ -0,0 +1,16 @@ +using MiraAPI.API.GameOptions; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example +{ + [RegisterCustomOptions(ExamplePlugin.Id)] + public class ExampleOptions : IModOptions + { + public LoadableAsset OptionsTabIcon => MiraAssets.Empty; + public static CustomToggleOption YeOrNa = new CustomToggleOption("Ye or Na", true); + } +} diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs new file mode 100644 index 0000000..ed9a112 --- /dev/null +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -0,0 +1,24 @@ +using BepInEx; +using BepInEx.Unity.IL2CPP; +using HarmonyLib; +using Reactor; +using Reactor.Networking; +using Reactor.Networking.Attributes; + +namespace MiraAPI.Example; + +[BepInAutoPlugin("mira.example", "MiraAPIExample")] +[BepInProcess("Among Us.exe")] +[BepInDependency(ReactorPlugin.Id)] +[ReactorModFlags(ModFlags.RequireOnAllClients)] +public partial class ExamplePlugin : BasePlugin +{ + public Harmony Harmony { get; } = new(Id); + public static ExamplePlugin Instance { get; private set; } + public override void Load() + { + Instance = this; + Harmony.PatchAll(); + + } +} diff --git a/MiraAPI.Example/MiraAPI.Example.csproj b/MiraAPI.Example/MiraAPI.Example.csproj new file mode 100644 index 0000000..0f097ae --- /dev/null +++ b/MiraAPI.Example/MiraAPI.Example.csproj @@ -0,0 +1,37 @@ + + + net6.0 + latest + embedded + + 1.0.0 + dev + Simple and easy to use Among Us modding API. + Angxl, XtraCube + + + + Steam + 2024.3.5 + + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/MiraAPI.sln b/MiraAPI.sln index 56145e3..5b2a2c0 100644 --- a/MiraAPI.sln +++ b/MiraAPI.sln @@ -1,6 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI", "MiraAPI\MiraAPI.csproj", "{11FBC798-BAF5-4EE5-9511-BE6DB0592F99}" +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiraAPI", "MiraAPI\MiraAPI.csproj", "{11FBC798-BAF5-4EE5-9511-BE6DB0592F99}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example", "MiraAPI.Example\MiraAPI.Example.csproj", "{19B76855-16CC-43CC-A0A6-5A7FFB43F077}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,5 +17,12 @@ Global {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Debug|Any CPU.Build.0 = Debug|Any CPU {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|Any CPU.ActiveCfg = Release|Any CPU {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|Any CPU.Build.0 = Release|Any CPU + {19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/MiraAPI/GameOptions/AbstractGameOption.cs b/MiraAPI/GameOptions/AbstractGameOption.cs index af01326..94ac265 100644 --- a/MiraAPI/GameOptions/AbstractGameOption.cs +++ b/MiraAPI/GameOptions/AbstractGameOption.cs @@ -1,4 +1,5 @@ -using MiraAPI.Roles; +using BepInEx; +using MiraAPI.Roles; using Reactor.Localization.Utilities; using System; @@ -14,6 +15,7 @@ public abstract class AbstractGameOption public CustomOptionGroup Group { get; set; } public Func Hidden { get; set; } public OptionBehaviour OptionBehaviour { get; protected set; } + public PluginInfo ParentMod { get; set; } public void ValueChanged(OptionBehaviour optionBehaviour) { OnValueChanged(optionBehaviour); @@ -33,6 +35,5 @@ public abstract class AbstractGameOption Save = save; Hidden = () => false; - CustomOptionsManager.CustomOptions.Add(this); } } \ No newline at end of file diff --git a/MiraAPI/GameOptions/Attributes/RegisterCustomOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/RegisterCustomOptionAttribute.cs new file mode 100644 index 0000000..8044d55 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/RegisterCustomOptionAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes +{ + [AttributeUsage(AttributeTargets.Field)] + public class RegisterCustomOptionAttribute : Attribute + { + private static readonly HashSet RegisteredAssemblies = []; + public string ModId { get; } + + public RegisterCustomOptionAttribute(string modId) + { + ModId = modId; + } + } +} diff --git a/MiraAPI/GameOptions/Attributes/RegisterCustomOptionsAttribute.cs b/MiraAPI/GameOptions/Attributes/RegisterCustomOptionsAttribute.cs new file mode 100644 index 0000000..ea07a08 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/RegisterCustomOptionsAttribute.cs @@ -0,0 +1,43 @@ +using BepInEx.Unity.IL2CPP; +using MiraAPI.API.GameOptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class RegisterCustomOptionsAttribute : Attribute + { + private static readonly HashSet RegisteredAssemblies = []; + public string ModId { get; } + public List Options; + + public RegisterCustomOptionsAttribute(string modId) + { + ModId = modId; + } + + public static void Initialize() + { + IL2CPPChainloader.Instance.PluginLoad += (_, assembly, plugin) => Register(assembly); + } + + public static void Register(Assembly assembly) + { + if (!RegisteredAssemblies.Add(assembly)) return; + + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttribute(); + if (attribute != null) + { + attribute.Options = type.GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(fi => fi.FieldType.BaseType == typeof(AbstractGameOption) || fi.FieldType.BaseType == typeof(CustomOptionGroup)).ToList(); + CustomOptionsManager.RegisterOptionsForMod(type, attribute.Options, attribute.ModId); + } + } + } + } +} diff --git a/MiraAPI/GameOptions/CustomNumberOption.cs b/MiraAPI/GameOptions/CustomNumberOption.cs index cadf17c..131d569 100644 --- a/MiraAPI/GameOptions/CustomNumberOption.cs +++ b/MiraAPI/GameOptions/CustomNumberOption.cs @@ -42,8 +42,6 @@ public class CustomNumberOption : AbstractGameOption Logger.Warning(e.ToString()); } } - - CustomOptionsManager.CustomNumberOptions.Add(this); } public void SetValue(float newValue) diff --git a/MiraAPI/GameOptions/CustomOptionGroup.cs b/MiraAPI/GameOptions/CustomOptionGroup.cs index 672aa89..70fba6a 100644 --- a/MiraAPI/GameOptions/CustomOptionGroup.cs +++ b/MiraAPI/GameOptions/CustomOptionGroup.cs @@ -1,4 +1,5 @@ -using MiraAPI.Roles; +using BepInEx; +using MiraAPI.Roles; using System; using System.Collections.Generic; using UnityEngine; @@ -11,6 +12,7 @@ public class CustomOptionGroup public GameObject Header; public Type AdvancedRole { get; set; } + public PluginInfo ParentMod { get; set; } public readonly List Options = []; public readonly List CustomNumberOptions; @@ -40,7 +42,5 @@ public class CustomOptionGroup { option.Group = this; } - - CustomOptionsManager.CustomGroups.Add(this); } } \ No newline at end of file diff --git a/MiraAPI/GameOptions/CustomOptionsManager.cs b/MiraAPI/GameOptions/CustomOptionsManager.cs index 65be421..2f34102 100644 --- a/MiraAPI/GameOptions/CustomOptionsManager.cs +++ b/MiraAPI/GameOptions/CustomOptionsManager.cs @@ -1,8 +1,14 @@ -using MiraAPI.Networking.Options; +using BepInEx; +using BepInEx.Unity.IL2CPP; +using MiraAPI.GameOptions; +using MiraAPI.Networking.Options; using Reactor.Networking.Rpc; using Reactor.Utilities; +using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using UnityEngine; namespace MiraAPI.API.GameOptions; @@ -13,6 +19,56 @@ public static class CustomOptionsManager public static readonly List CustomToggleOptions = []; public static readonly List CustomStringOptions = []; public static readonly List CustomGroups = []; + public static readonly List RegisteredMods = []; + + public static void RegisterOptionsForMod(Type type, List fields, string modId) + { + if (!typeof(IModOptions).IsAssignableFrom(type)) + { + Debug.LogError($"Cannot register options for {modId}! Missing IModOptions interface."); + return; + } + PluginInfo info = IL2CPPChainloader.Instance.Plugins[modId]; + + if (info == null) + { + return; + } + + Debug.Log($"Registering Options for {modId}"); + + RegisteredMods.Add(info); + foreach (FieldInfo field in fields) + { + AbstractGameOption option = (AbstractGameOption)field.GetValue(null); + if (option == null) + { + CustomOptionGroup group = (CustomOptionGroup)field.GetValue(null); + group.ParentMod = info; + CustomGroups.Add(group); + } + + option.ParentMod = info; + + CustomOptions.Add(option); + switch (field.FieldType.Name) + { + case nameof(CustomNumberOption): + CustomNumberOptions.Add((CustomNumberOption)option); + break; + + case nameof(CustomToggleOption): + CustomToggleOptions.Add((CustomToggleOption)option); + break; + + case nameof(CustomStringOption): + CustomStringOptions.Add((CustomStringOption)option); + break; + + } + } + } + public static void SyncOptions() { if (!AmongUsClient.Instance.AmHost) diff --git a/MiraAPI/GameOptions/CustomOptionsTab.cs b/MiraAPI/GameOptions/CustomOptionsTab.cs index 160a7bf..91bc614 100644 --- a/MiraAPI/GameOptions/CustomOptionsTab.cs +++ b/MiraAPI/GameOptions/CustomOptionsTab.cs @@ -1,38 +1,40 @@ using Reactor.Utilities.Extensions; using System; +using System.Collections.Generic; using TMPro; using UnityEngine; -using UnityEngine.Events; using Object = UnityEngine.Object; namespace MiraAPI.API.GameOptions; public static class CustomOptionsTab { - public static GameObject CustomTab; - public static GameObject CustomScreen; + public static List CustomTabs; + public static List CustomScreens; public static SpriteRenderer Rend; public static GameObject Initialize(GameSettingMenu __instance) { var gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject; var roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject; + var screen = CreateNewMenu(__instance); + var tab = CreateCustomTab(__instance, screen, gameBtn, roleBtn); - CustomScreen = CreateNewMenu(__instance); - CustomTab = CreateCustomTab(__instance, CustomScreen, gameBtn, roleBtn); + CustomScreens.Add(screen); + CustomTabs.Add(tab); - Rend = CustomTab.transform.FindChild("LaunchpadBtn/Tab Background").GetComponent(); + Rend = tab.transform.FindChild("Btn/Tab Background").GetComponent(); Rend.enabled = false; UpdateListeners(__instance, gameBtn.GetComponentInChildren(), roleBtn.GetComponentInChildren(), Rend); - var container = CustomScreen.transform.FindChild("GameGroup/SliderInner"); + var container = screen.transform.FindChild("GameGroup/SliderInner"); container.DestroyChildren(); - CreateNewResetButton(__instance, container); + //CreateNewResetButton(__instance, container); return container.gameObject; } - private static void CreateNewResetButton(GameSettingMenu __instance, Transform container) + /*private static void CreateNewResetButton(GameSettingMenu __instance, Transform container) { var resetBtn = __instance.RegularGameSettings.transform.FindChild("GameGroup/SliderInner/ResetToDefault"); var newResetBtn = Object.Instantiate(resetBtn.gameObject, container); @@ -54,35 +56,35 @@ public static class CustomOptionsTab toggle.OnClick.AddListener((UnityAction)(() => { CustomOptionsManager.ResetToDefault(); })); toggle.OnMouseOver.AddListener((UnityAction)(() => { tmp.text = "Reset Options"; })); toggle.OnMouseOut.AddListener((UnityAction)(() => { tmp.text = "Reset Options"; })); - } + }*/ private static void UpdateListeners(GameSettingMenu __instance, PassiveButton gameB, PassiveButton roleB, SpriteRenderer rend) { gameB.OnClick.RemoveAllListeners(); - gameB.OnClick.AddListener((UnityAction)(() => - { - __instance.RegularGameSettings.SetActive(true); - __instance.RolesSettings.gameObject.SetActive(false); + /* gameB.OnClick.AddListener((UnityAction)(() => + { + __instance.RegularGameSettings.SetActive(true); + __instance.RolesSettings.gameObject.SetActive(false); - CustomScreen.gameObject.SetActive(false); - rend.enabled = false; + CustomScreen.gameObject.SetActive(false); + rend.enabled = false; - __instance.GameSettingsHightlight.enabled = true; - __instance.RolesSettingsHightlight.enabled = false; - })); + __instance.GameSettingsHightlight.enabled = true; + __instance.RolesSettingsHightlight.enabled = false; + })); - roleB.OnClick.RemoveAllListeners(); - roleB.OnClick.AddListener((UnityAction)(() => - { - __instance.RegularGameSettings.SetActive(false); - __instance.RolesSettings.gameObject.SetActive(true); + roleB.OnClick.RemoveAllListeners(); + roleB.OnClick.AddListener((UnityAction)(() => + { + __instance.RegularGameSettings.SetActive(false); + __instance.RolesSettings.gameObject.SetActive(true); - CustomScreen.gameObject.SetActive(false); - rend.enabled = false; + CustomScreen.gameObject.SetActive(false); + rend.enabled = false; - __instance.GameSettingsHightlight.enabled = false; - __instance.RolesSettingsHightlight.enabled = true; - })); + __instance.GameSettingsHightlight.enabled = false; + __instance.RolesSettingsHightlight.enabled = true; + }));*/ } public static GameObject CreateHeader(ToggleOption toggleOpt, Transform container, string title) @@ -113,11 +115,11 @@ public static class CustomOptionsTab GameObject gameTab, GameObject roleTab) { var newTab = Object.Instantiate(gameTab, gameTab.transform.parent); - newTab.name = "LaunchpadTab"; + newTab.name = "Tab"; gameTab.transform.position += new Vector3(-1, 0, 0); var inside = newTab.transform.FindChild("ColorButton"); - inside.name = "LaunchpadBtn"; + inside.name = "Btn"; var btn = inside.GetComponentInChildren(); @@ -135,6 +137,12 @@ public static class CustomOptionsTab { __instance.RegularGameSettings.SetActive(false); __instance.RolesSettings.gameObject.SetActive(false); + + foreach (var screen in CustomScreens) + { + screen.gameObject.SetActive(false); + } + newSettings.gameObject.SetActive(true); var rend = inside.transform.FindChild("Tab Background").GetComponent(); @@ -142,6 +150,11 @@ public static class CustomOptionsTab gameTab.transform.FindChild("ColorButton/Tab Background").gameObject.GetComponent().enabled = false; roleTab.transform.FindChild("Hat Button/Tab Background").gameObject.GetComponent().enabled = false; + + foreach (var tab in CustomTabs) + { + tab.transform.FindChild("Btn/Tab Background").gameObject.GetComponent().enabled = false; + } } } @@ -149,13 +162,13 @@ public static class CustomOptionsTab { var gameSettings = __instance.RegularGameSettings; var newSettings = Object.Instantiate(gameSettings, gameSettings.transform.parent); - newSettings.name = "Launchpad Settings"; + newSettings.name = "Settings"; newSettings.SetActive(false); var launchpadGroup = newSettings.transform.FindChild("GameGroup").gameObject; var text = launchpadGroup.transform.FindChild("Text").gameObject.GetComponent(); text.gameObject.GetComponent().Destroy(); - text.text = "Launchpad Settings"; + text.text = "Settings"; return newSettings; } diff --git a/MiraAPI/GameOptions/CustomStringOption.cs b/MiraAPI/GameOptions/CustomStringOption.cs index 2350dae..48d6d53 100644 --- a/MiraAPI/GameOptions/CustomStringOption.cs +++ b/MiraAPI/GameOptions/CustomStringOption.cs @@ -22,7 +22,6 @@ public class CustomStringOption : AbstractGameOption Options = options; Default = defaultValue; - CustomOptionsManager.CustomStringOptions.Add(this); if (Save) { try diff --git a/MiraAPI/GameOptions/CustomToggleOption.cs b/MiraAPI/GameOptions/CustomToggleOption.cs index 3f655ad..68e8031 100644 --- a/MiraAPI/GameOptions/CustomToggleOption.cs +++ b/MiraAPI/GameOptions/CustomToggleOption.cs @@ -27,7 +27,6 @@ public class CustomToggleOption : AbstractGameOption Logger.Warning(e.ToString()); } } - CustomOptionsManager.CustomToggleOptions.Add(this); } public void SetValue(bool newValue) diff --git a/MiraAPI/GameOptions/IModOptions.cs b/MiraAPI/GameOptions/IModOptions.cs new file mode 100644 index 0000000..807e5af --- /dev/null +++ b/MiraAPI/GameOptions/IModOptions.cs @@ -0,0 +1,10 @@ +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.GameOptions +{ + public interface IModOptions + { + LoadableAsset OptionsTabIcon { get; } + } +} diff --git a/MiraAPI/MiraAPIPlugin.cs b/MiraAPI/MiraAPIPlugin.cs index 3c96bf2..b352a9e 100644 --- a/MiraAPI/MiraAPIPlugin.cs +++ b/MiraAPI/MiraAPIPlugin.cs @@ -2,6 +2,7 @@ using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.GameModes; +using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; using MiraAPI.Roles; using Reactor; @@ -11,7 +12,7 @@ using System.Reflection; namespace MiraAPI; -[BepInAutoPlugin("dev.xtracube.launchpad", "MiraAPI")] +[BepInAutoPlugin("mira.api", "MiraAPI")] [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] [ReactorModFlags(ModFlags.RequireOnAllClients)] @@ -25,8 +26,10 @@ public partial class MiraAPIPlugin : BasePlugin Instance = this; Harmony.PatchAll(); - RegisterGameModeAttribute.Register(Assembly.GetExecutingAssembly()); - RegisterCustomRoleAttribute.Register(Assembly.GetExecutingAssembly()); + RegisterCustomRoleAttribute.Initialize(); + RegisterCustomOptionsAttribute.Initialize(); + + RegisterGameModeAttribute.Register(Assembly.GetCallingAssembly()); RegisterButtonAttribute.Register(Assembly.GetExecutingAssembly()); } } diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs new file mode 100644 index 0000000..d551d0a --- /dev/null +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -0,0 +1,184 @@ +using HarmonyLib; +using InnerNet; +using MiraAPI.API.GameOptions; +using MiraAPI.Hud; +using MiraAPI.Patches.Options; +using MiraAPI.Roles; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Patches +{ + [HarmonyPatch(typeof(HudManager))] + public static class HudManagerPatches + { + /// Custom buttons parent + private static GameObject _bottomLeft; + + /// Custom role tab + private static TaskPanelBehaviour _roleTab; + + /// Scrolling increment + private const float Increment = 0.3f; + + /// Bounds for scrolling + private static FloatRange _bounds = new FloatRange(2.9f, 4.6f); + + /// + /// Add options scrolling (on the hud text) + /// + public static void OptionsScrollingLogic(HudManager __instance) + { + if (Input.GetKeyDown(KeyCode.Tab)) + { + if (ToHudStringPatch.CurrentPage >= CustomOptionsManager.RegisteredMods.Count) + { + ToHudStringPatch.CurrentPage = 0; + } + else + { + ToHudStringPatch.CurrentPage += 1; + } + } + + var numPlayers = GameData.Instance ? GameData.Instance.PlayerCount : 10; + HudManager.Instance.GameSettings.text = GameOptionsManager.Instance.CurrentGameOptions.ToHudString(numPlayers); + var pos = __instance.GameSettings.transform.localPosition; + + if (!PlayerControl.LocalPlayer.CanMove) + { + return; + } + + if (ToHudStringPatch.CurrentPage == 0) + { + __instance.GameSettings.transform.localPosition = new Vector3(pos.x, _bounds.min, pos.z); + return; + } + + _bounds.max = __instance.GameSettings.rectTransform.sizeDelta.y + _bounds.min; + + var delta = Input.mouseScrollDelta.y switch + { + > 0f => -Increment, + < 0f => Increment, + _ => 0f + }; + + pos = new Vector3(pos.x, Mathf.Clamp(pos.y + delta, _bounds.min, _bounds.max), pos.z); + + __instance.GameSettings.transform.localPosition = pos; + } + + [HarmonyPostfix, HarmonyPatch("Update")] + public static void UpdatePostfix(HudManager __instance) + { + var local = PlayerControl.LocalPlayer; + if (LobbyBehaviour.Instance) + { + OptionsScrollingLogic(__instance); + } + + if (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started && !ShipStatus.Instance) + { + return; + } + + //CustomGameModeManager.ActiveMode.HudUpdate(__instance); + + if (local.Data.Role is ICustomRole customRole) + { + customRole.HudUpdate(__instance); + + if (customRole.SetTabText() != null) + { + if (_roleTab == null) + { + _roleTab = CustomRoleManager.CreateRoleTab(customRole); + } + else + { + CustomRoleManager.UpdateRoleTab(_roleTab, customRole); + } + } + else if (customRole.SetTabText() == null && _roleTab) + { + _roleTab.gameObject.Destroy(); + } + } + else if (_roleTab) + { + _roleTab.gameObject.Destroy(); + } + } + + /* /// + /// Trigger hudstart on current custom gamemode + /// + [HarmonyPostfix, HarmonyPatch("OnGameStart")] + public static void GameStartPatch(HudManager __instance) + { + CustomGameModeManager.ActiveMode.HudStart(__instance); + }*/ + + /// + /// Create custom buttons parent + /// + [HarmonyPostfix, HarmonyPatch("Start")] + public static void StartPostfix(HudManager __instance) + { + if (!_bottomLeft) + { + var buttons = __instance.transform.Find("Buttons"); + _bottomLeft = UnityEngine.Object.Instantiate(buttons.Find("BottomRight").gameObject, buttons); + } + + foreach (var t in _bottomLeft.GetComponentsInChildren(true)) + { + t.gameObject.Destroy(); + } + + var gridArrange = _bottomLeft.GetComponent(); + var aspectPosition = _bottomLeft.GetComponent(); + + _bottomLeft.name = "BottomLeft"; + gridArrange.Alignment = GridArrange.StartAlign.Right; + aspectPosition.Alignment = AspectPosition.EdgeAlignments.LeftBottom; + + foreach (var button in CustomButtonManager.CustomButtons) + { + button.CreateButton(_bottomLeft.transform); + } + + gridArrange.Start(); + gridArrange.ArrangeChilds(); + + aspectPosition.AdjustPosition(); + } + + /// + /// Make sure all launchpad hud elements are inactive/active when appropriate + /// + [HarmonyPostfix, HarmonyPatch("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) + { + if (player.Data == null) + { + return; + } + + if (_roleTab) + { + _roleTab.gameObject.SetActive(isActive); + } + + foreach (var button in CustomButtonManager.CustomButtons) + button.SetActive(isActive, roleBehaviour); + + if (roleBehaviour is ICustomRole role) + { + __instance.ImpostorVentButton.gameObject.SetActive(isActive && role.CanUseVent); + } + } + } +} diff --git a/MiraAPI/Patches/Options/GameSettingsMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingsMenuPatches.cs index 03c4f74..1531a7d 100644 --- a/MiraAPI/Patches/Options/GameSettingsMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingsMenuPatches.cs @@ -18,11 +18,13 @@ public static class GameSettingsMenuPatches [HarmonyPrefix, HarmonyPatch("Start")] public static void StartPrefix(GameSettingMenu __instance) { - if (CustomOptionsTab.CustomTab) + if (CustomOptionsTab.CustomTabs != null) { return; } + CustomOptionsTab.CustomTabs = new(); + __instance.Tabs.transform.position += new Vector3(0.5f, 0, 0); _gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject; _roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject; @@ -81,7 +83,7 @@ public static class GameSettingsMenuPatches __instance.RegularGameSettings.SetActive(false); __instance.RolesSettings.gameObject.SetActive(false); - CustomOptionsTab.CustomScreen.gameObject.SetActive(true); + //CustomOptionsTab.CustomScreen.gameObject.SetActive(true); CustomOptionsTab.Rend.enabled = true; __instance.GameSettingsHightlight.enabled = false; diff --git a/MiraAPI/Patches/Options/ToHudStringPatch.cs b/MiraAPI/Patches/Options/ToHudStringPatch.cs index bd70182..8e2c633 100644 --- a/MiraAPI/Patches/Options/ToHudStringPatch.cs +++ b/MiraAPI/Patches/Options/ToHudStringPatch.cs @@ -1,7 +1,7 @@ using AmongUs.GameOptions; +using BepInEx; using HarmonyLib; using MiraAPI.API.GameOptions; -using MiraAPI.GameModes; using MiraAPI.Roles; using MiraAPI.Utilities; using System.Collections.Generic; @@ -13,11 +13,7 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(IGameOptionsExtensions), "ToHudString")] public static class ToHudStringPatch { - /// - /// Show custom Launchpad options or vanilla game options - /// - public static bool ShowCustom = false; - + public static int CurrentPage = 0; /// /// Generic method to add options /// @@ -82,28 +78,32 @@ public static class ToHudStringPatch return; } - foreach (var role in CustomRoleManager.CustomRoles.Values) + foreach (var pair in CustomRoleManager.CustomRoles) { - var customRole = role as ICustomRole; + var customRole = pair.Value as ICustomRole; if (customRole.IsGhostRole) { - role.Role = (RoleTypes)customRole.RoleId; + pair.Value.Role = (RoleTypes)pair.Key; } } - if (ShowCustom || !CustomGameModeManager.ActiveMode.CanAccessSettingsTab()) + if (CurrentPage != 0) { - var sb = new StringBuilder("Launchpad Options:\n"); - var groupsWithRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole != null); - var groupsWithoutRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null); + PluginInfo currentPlugin = CustomOptionsManager.RegisteredMods[CurrentPage - 1]; + if (currentPlugin == null) + { + CurrentPage = 0; + return; + } - var suffix = CustomGameModeManager.ActiveMode.CanAccessSettingsTab() ? "\nPress Tab to view Normal Options" : - $"\nYou can not access Normal Options on {CustomGameModeManager.ActiveMode.Name} mode."; + var sb = new StringBuilder($"{currentPlugin.Metadata.Name} Options:\n"); + var groupsWithRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole != null && group.ParentMod == currentPlugin); + var groupsWithoutRoles = CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null && group.ParentMod == currentPlugin); AddOptions(sb, - CustomOptionsManager.CustomNumberOptions.Where(option => option.Group == null && !option.Hidden()), - CustomOptionsManager.CustomStringOptions.Where(option => option.Group == null && !option.Hidden()), - CustomOptionsManager.CustomToggleOptions.Where(option => option.Group == null && !option.Hidden()) + CustomOptionsManager.CustomNumberOptions.Where(option => option.Group == null && option.ParentMod == currentPlugin && !option.Hidden()), + CustomOptionsManager.CustomStringOptions.Where(option => option.Group == null && option.ParentMod == currentPlugin && !option.Hidden()), + CustomOptionsManager.CustomToggleOptions.Where(option => option.Group == null && option.ParentMod == currentPlugin && !option.Hidden()) ); foreach (var group in groupsWithoutRoles) @@ -117,14 +117,8 @@ public static class ToHudStringPatch AddOptions(sb, group.CustomNumberOptions, group.CustomStringOptions, group.CustomToggleOptions); } - if (GameManager.Instance.IsHideAndSeek()) - { - __result = sb + suffix; - return; - } - var customOptionGroups = groupsWithRoles as CustomOptionGroup[] ?? groupsWithRoles.ToArray(); - if (customOptionGroups.Any() && CustomGameModeManager.ActiveMode.CanAccessRolesTab()) + if (customOptionGroups.Any()) { sb.AppendLine("\nRoles"); @@ -141,10 +135,13 @@ public static class ToHudStringPatch } } - __result = sb + suffix; - return; + __result = sb.ToString(); } - __result = "Normal Options:\n" + __result + "\nPress Tab to view Launchpad Options"; + if (CurrentPage == 0) + { + __result = "Normal Options:\n" + __result; + } + __result += $"\nUse TAB to switch pages (Page {CurrentPage}/{CustomOptionsManager.RegisteredMods.Count})"; } } \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/RolesSettingsMenuPatches.cs b/MiraAPI/Patches/Roles/RolesSettingsMenuPatches.cs index d32b9df..9cd5562 100644 --- a/MiraAPI/Patches/Roles/RolesSettingsMenuPatches.cs +++ b/MiraAPI/Patches/Roles/RolesSettingsMenuPatches.cs @@ -141,7 +141,7 @@ public static class RolesSettingsMenuPatches if (AmongUsClient.Instance.AmHost) { - Rpc.Instance.Send(new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); + Rpc.Instance.Send(new SyncRoleOptionsRpc.Data((ushort)roleSetting.Role.Role, numEntry.Value, chanceEntry.Value)); } GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions; return false; diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs index 4750db3..976d831 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -13,6 +13,7 @@ namespace MiraAPI.Patches.Roles; public static class TaskAdderPatch { public static TaskFolder RolesFolder; + public static System.Collections.Generic.Dictionary ModsFolders = new System.Collections.Generic.Dictionary(); [HarmonyPostfix, HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))] public static void AddRolesFolder(TaskAdderGame __instance) @@ -21,7 +22,22 @@ public static class TaskAdderPatch RolesFolder.gameObject.SetActive(false); RolesFolder.FolderName = "Roles"; RolesFolder.name = "RolesFolder"; + foreach (var pair in CustomRoleManager.ModRoles) + { + var newFolder = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform); + newFolder.FolderName = pair.Key.Metadata.Name; + newFolder.name = pair.Key.Metadata.Name; + newFolder.gameObject.SetActive(false); + RolesFolder.SubFolders.Add(newFolder); + + if (!ModsFolders.ContainsKey(pair.Key.Metadata.Name)) + { + ModsFolders.Add(pair.Key.Metadata.Name, pair.Key.Metadata.GUID); + } + } + __instance.Root.SubFolders.Add(RolesFolder); + __instance.GoToRoot(); } @@ -134,11 +150,11 @@ public static class TaskAdderPatch } } - if (taskFolder.FolderName == "Roles") // idk why only this works??? + if (ModsFolders.TryGetValue(taskFolder.FolderName, out string guid)) { - for (var m = 0; m < DestroyableSingleton.Instance.AllRoles.Length; m++) + for (var m = 0; m < CustomRoleManager.GetRolesRegistered(guid).Count; m++) { - var roleBehaviour = DestroyableSingleton.Instance.AllRoles[m]; + var roleBehaviour = CustomRoleManager.GetRolesRegistered(guid)[m]; if (roleBehaviour.Role != RoleTypes.ImpostorGhost && roleBehaviour.Role != RoleTypes.CrewmateGhost && !roleBehaviour.IsDead) { var taskAddButton2 = Object.Instantiate(__instance.RoleButton); diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 76f4578..e964814 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -1,4 +1,6 @@ using AmongUs.GameOptions; +using BepInEx; +using BepInEx.Unity.IL2CPP; using Il2CppInterop.Runtime; using MiraAPI.Networking.Options; using Reactor.Localization.Utilities; @@ -17,14 +19,21 @@ namespace MiraAPI.Roles; public static class CustomRoleManager { public static readonly Dictionary CustomRoles = []; + public static readonly Dictionary> ModRoles = []; public static void RegisterInRoleManager() { RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray(); } - internal static void RegisterRole(Type roleType) + internal static void RegisterRole(Type roleType, ushort roleId, string modId) { + if (!IL2CPPChainloader.Instance.Plugins.TryGetValue(modId, out PluginInfo info)) + { + Debug.LogError($"{modId} is not a valid mod!"); + return; + } + if (!(typeof(RoleBehaviour).IsAssignableFrom(roleType) && typeof(ICustomRole).IsAssignableFrom(roleType))) { return; @@ -42,7 +51,13 @@ public static class CustomRoleManager return; } - roleBehaviour.Role = (RoleTypes)customRole.RoleId; + if (CustomRoles.ContainsKey(roleId)) + { + Debug.LogError($"Role ID conflict for {customRole.RoleName}. Use a different ID."); + return; + } + + roleBehaviour.Role = (RoleTypes)roleId; roleBehaviour.TeamType = customRole.Team; roleBehaviour.NameColor = customRole.RoleColor; roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName); @@ -61,7 +76,16 @@ public static class CustomRoleManager RoleManager.GhostRoles.Add(roleBehaviour.Role); } - CustomRoles.Add(customRole.RoleId, roleBehaviour); + if (ModRoles.TryGetValue(info, out List roles)) + { + roles.Add(roleBehaviour); + } + else + { + ModRoles.TryAdd(info, new List() { roleBehaviour }); + } + + CustomRoles.Add(roleId, roleBehaviour); if (customRole.HideSettings) { @@ -74,6 +98,19 @@ public static class CustomRoleManager } + public static List GetRolesRegistered(string modId) + { + PluginInfo info = ModRoles.Keys.First(key => key.Metadata.GUID == modId); + if (ModRoles.TryGetValue(info, out List roles)) + { + return roles; + } + else + { + Debug.LogError($"{modId} has no roles registered."); + return null; + } + } public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole result) { CustomRoles.TryGetValue((ushort)roleType, out var temp); @@ -123,33 +160,35 @@ public static class CustomRoleManager public static void SyncRoleSettings() { - foreach (var role in CustomRoles.Values.Select(x => (ICustomRole)x)) + foreach (var role in CustomRoles.Values) { - if (role.HideSettings) + ICustomRole customRole = role as ICustomRole; + if (customRole.HideSettings) { continue; } - PluginSingleton.Instance.Config.TryGetEntry(role.NumConfigDefinition, out var numEntry); - PluginSingleton.Instance.Config.TryGetEntry(role.ChanceConfigDefinition, out var chanceEntry); + PluginSingleton.Instance.Config.TryGetEntry(customRole.NumConfigDefinition, out var numEntry); + PluginSingleton.Instance.Config.TryGetEntry(customRole.ChanceConfigDefinition, out var chanceEntry); - Rpc.Instance.Send(new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); + Rpc.Instance.Send(new SyncRoleOptionsRpc.Data((ushort)role.Role, numEntry.Value, chanceEntry.Value)); } } public static void SyncRoleSettings(int targetId) { - foreach (var role in CustomRoles.Values.Select(x => (ICustomRole)x)) + foreach (var role in CustomRoles.Values) { - if (role.HideSettings) + ICustomRole customRole = role as ICustomRole; + if (customRole.HideSettings) { continue; } - PluginSingleton.Instance.Config.TryGetEntry(role.NumConfigDefinition, out var numEntry); - PluginSingleton.Instance.Config.TryGetEntry(role.ChanceConfigDefinition, out var chanceEntry); + PluginSingleton.Instance.Config.TryGetEntry(customRole.NumConfigDefinition, out var numEntry); + PluginSingleton.Instance.Config.TryGetEntry(customRole.ChanceConfigDefinition, out var chanceEntry); - Rpc.Instance.SendTo(targetId, new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); + Rpc.Instance.SendTo(targetId, new SyncRoleOptionsRpc.Data((ushort)role.Role, numEntry.Value, chanceEntry.Value)); } } } \ No newline at end of file diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 9538eab..4a0a881 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -11,8 +11,6 @@ public interface ICustomRole { string RoleName { get; } - ushort RoleId { get; } - string RoleDescription { get; } string RoleLongDescription { get; } diff --git a/MiraAPI/Roles/RegisterCustomRoleAttribute.cs b/MiraAPI/Roles/RegisterCustomRoleAttribute.cs index 7ef6b68..0c49aeb 100644 --- a/MiraAPI/Roles/RegisterCustomRoleAttribute.cs +++ b/MiraAPI/Roles/RegisterCustomRoleAttribute.cs @@ -1,4 +1,6 @@ -using System; +using BepInEx.Unity.IL2CPP; +using MiraAPI.Utilities; +using System; using System.Collections.Generic; using System.Reflection; @@ -8,6 +10,24 @@ namespace MiraAPI.Roles; public class RegisterCustomRoleAttribute : Attribute { private static readonly HashSet RegisteredAssemblies = []; + public ushort RoleId { get; } + public string ModId { get; } + + public RegisterCustomRoleAttribute(string modId) + { + ModId = modId; + RoleId = (ushort)(20 + CustomRoleManager.CustomRoles.Count); + } + public RegisterCustomRoleAttribute(string modId, ushort roleId) + { + ModId = modId; + RoleId = roleId; + } + + public static void Initialize() + { + IL2CPPChainloader.Instance.PluginLoad += (_, assembly, plugin) => Register(assembly); + } public static void Register(Assembly assembly) { @@ -18,7 +38,8 @@ public class RegisterCustomRoleAttribute : Attribute var attribute = type.GetCustomAttribute(); if (attribute != null) { - CustomRoleManager.RegisterRole(type); + Helpers.RegisterType(type); + CustomRoleManager.RegisterRole(type, attribute.RoleId, attribute.ModId); } } } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 2eb2130..5cef351 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,4 +1,7 @@ -using MiraAPI.Roles; +using HarmonyLib; +using Il2CppInterop.Runtime.Injection; +using MiraAPI.Roles; +using System; using System.Linq; using System.Text; using UnityEngine; @@ -14,6 +17,24 @@ namespace MiraAPI.Utilities return ShipStatus.Instance.AllRooms.ToList().Find(room => room.roomArea.OverlapPoint(pos)); } + public static void RegisterType(Type type) + { + if (ClassInjector.IsTypeRegisteredInIl2Cpp(type)) + { + return; + } + + try + { + ClassInjector.RegisterTypeInIl2Cpp(type); + } + catch (Exception e) + { + Debug.LogError($"Failed to register {type.FullDescription()}: {e}"); + } + } + + public static string GetSuffix(NumberSuffixes suffix) { return suffix switch