From 1c25fd401b20f7980a800af499260399e7ac8c6a Mon Sep 17 00:00:00 2001 From: ang-xd Date: Mon, 13 May 2024 18:40:06 -0400 Subject: [PATCH] Begin seperation --- MiraAPI/GameModes/CustomGameMode.cs | 138 ++++++++++++++ MiraAPI/GameModes/CustomGameModeManager.cs | 71 +++++++ MiraAPI/GameModes/DefaultMode.cs | 9 + .../GameModes/RegisterGameModeAttribute.cs | 24 +++ MiraAPI/GameOptions/AbstractGameOption.cs | 38 ++++ MiraAPI/GameOptions/CustomNumberOption.cs | 102 ++++++++++ MiraAPI/GameOptions/CustomOptionGroup.cs | 46 +++++ MiraAPI/GameOptions/CustomOptionsManager.cs | 108 +++++++++++ MiraAPI/GameOptions/CustomOptionsTab.cs | 162 ++++++++++++++++ MiraAPI/GameOptions/CustomStringOption.cs | 93 ++++++++++ MiraAPI/GameOptions/CustomToggleOption.cs | 79 ++++++++ MiraAPI/Hud/CustomActionButton.cs | 165 +++++++++++++++++ MiraAPI/Hud/CustomButtonManager.cs | 25 +++ MiraAPI/Hud/CustomButtonSingleton.cs | 21 +++ MiraAPI/Hud/RegisterButtonAttribute.cs | 24 +++ MiraAPI/MiraAPIPlugin.cs | 10 + MiraAPI/Networking/MiraRpc.cs | 8 + MiraAPI/Networking/SyncOptionsRpc.cs | 82 +++++++++ MiraAPI/Networking/SyncRoleOptionsRpc.cs | 58 ++++++ .../Patches/Options/GameOptionsMenuPatch.cs | 109 +++++++++++ .../Options/GameSettingsMenuPatches.cs | 171 +++++++++++++++++ MiraAPI/Patches/Options/ToHudStringPatch.cs | 150 +++++++++++++++ MiraAPI/Patches/Roles/GameStartupPatch.cs | 24 +++ MiraAPI/Patches/Roles/NameTagPatch.cs | 35 ++++ MiraAPI/Patches/Roles/RoleBehaviourPatches.cs | 24 +++ MiraAPI/Patches/Roles/RoleManagerPatches.cs | 31 ++++ .../Patches/Roles/RoleOptionSettingPatch.cs | 24 +++ .../Roles/RoleOptionsCollectionPatch.cs | 52 ++++++ .../Patches/Roles/RolesSettingsMenuPatches.cs | 149 +++++++++++++++ MiraAPI/Patches/Roles/TaskAdderPatch.cs | 174 ++++++++++++++++++ MiraAPI/Patches/Roles/TaskPanelPatch.cs | 58 ++++++ MiraAPI/Roles/CustomRoleManager.cs | 155 ++++++++++++++++ MiraAPI/Roles/ICustomRole.cs | 68 +++++++ MiraAPI/Roles/RegisterCustomRoleAttribute.cs | 25 +++ MiraAPI/Utilities/Assets/LoadableAsset.cs | 8 + .../Utilities/Assets/LoadableBundleAsset.cs | 20 ++ .../Utilities/Assets/LoadableResourceAsset.cs | 19 ++ MiraAPI/Utilities/Assets/MiraAssets.cs | 7 + MiraAPI/Utilities/Assets/SpriteTools.cs | 39 ++++ MiraAPI/Utilities/Extensions.cs | 86 +++++++++ MiraAPI/Utilities/Helpers.cs | 37 ++++ 41 files changed, 2728 insertions(+) create mode 100644 MiraAPI/GameModes/CustomGameMode.cs create mode 100644 MiraAPI/GameModes/CustomGameModeManager.cs create mode 100644 MiraAPI/GameModes/DefaultMode.cs create mode 100644 MiraAPI/GameModes/RegisterGameModeAttribute.cs create mode 100644 MiraAPI/GameOptions/AbstractGameOption.cs create mode 100644 MiraAPI/GameOptions/CustomNumberOption.cs create mode 100644 MiraAPI/GameOptions/CustomOptionGroup.cs create mode 100644 MiraAPI/GameOptions/CustomOptionsManager.cs create mode 100644 MiraAPI/GameOptions/CustomOptionsTab.cs create mode 100644 MiraAPI/GameOptions/CustomStringOption.cs create mode 100644 MiraAPI/GameOptions/CustomToggleOption.cs create mode 100644 MiraAPI/Hud/CustomActionButton.cs create mode 100644 MiraAPI/Hud/CustomButtonManager.cs create mode 100644 MiraAPI/Hud/CustomButtonSingleton.cs create mode 100644 MiraAPI/Hud/RegisterButtonAttribute.cs create mode 100644 MiraAPI/Networking/MiraRpc.cs create mode 100644 MiraAPI/Networking/SyncOptionsRpc.cs create mode 100644 MiraAPI/Networking/SyncRoleOptionsRpc.cs create mode 100644 MiraAPI/Patches/Options/GameOptionsMenuPatch.cs create mode 100644 MiraAPI/Patches/Options/GameSettingsMenuPatches.cs create mode 100644 MiraAPI/Patches/Options/ToHudStringPatch.cs create mode 100644 MiraAPI/Patches/Roles/GameStartupPatch.cs create mode 100644 MiraAPI/Patches/Roles/NameTagPatch.cs create mode 100644 MiraAPI/Patches/Roles/RoleBehaviourPatches.cs create mode 100644 MiraAPI/Patches/Roles/RoleManagerPatches.cs create mode 100644 MiraAPI/Patches/Roles/RoleOptionSettingPatch.cs create mode 100644 MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs create mode 100644 MiraAPI/Patches/Roles/RolesSettingsMenuPatches.cs create mode 100644 MiraAPI/Patches/Roles/TaskAdderPatch.cs create mode 100644 MiraAPI/Patches/Roles/TaskPanelPatch.cs create mode 100644 MiraAPI/Roles/CustomRoleManager.cs create mode 100644 MiraAPI/Roles/ICustomRole.cs create mode 100644 MiraAPI/Roles/RegisterCustomRoleAttribute.cs create mode 100644 MiraAPI/Utilities/Assets/LoadableAsset.cs create mode 100644 MiraAPI/Utilities/Assets/LoadableBundleAsset.cs create mode 100644 MiraAPI/Utilities/Assets/LoadableResourceAsset.cs create mode 100644 MiraAPI/Utilities/Assets/MiraAssets.cs create mode 100644 MiraAPI/Utilities/Assets/SpriteTools.cs create mode 100644 MiraAPI/Utilities/Extensions.cs create mode 100644 MiraAPI/Utilities/Helpers.cs diff --git a/MiraAPI/GameModes/CustomGameMode.cs b/MiraAPI/GameModes/CustomGameMode.cs new file mode 100644 index 0000000..8bbfdb4 --- /dev/null +++ b/MiraAPI/GameModes/CustomGameMode.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; + +namespace MiraAPI.GameModes; + +/// +/// Base class for custom gamemodes +/// +public abstract class CustomGameMode +{ + /// + /// Game mode name + /// + public abstract string Name { get; } + + /// + /// Game mode description + /// + public abstract string Description { get; } + + /// + /// Game mode ID + /// + public abstract int Id { get; } + + /// + /// Called when Intro Cutscene is destroyed + /// + public virtual void Initialize() { } + + /// + /// Called on HudManager.Start() + /// + /// Instance of HudManager + public virtual void HudStart(HudManager instance) { } + + /// + /// Called every frame on HudManager.Update() + /// + /// Instance of HudManager + public virtual void HudUpdate(HudManager instance) { } + + /// + /// Called when a player is killed + /// + /// PlayerControl that was killed + public virtual void OnDeath(PlayerControl player) { } + + /// + /// Calculate Game End logic + /// + /// Should original game end logic be used + /// Instance of LogicGameFlowNormal + public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance) + { + runOriginal = true; + } + + /// + /// Custom role assignment + /// + /// Should original role assignment be used + /// Instance of LogicRoleSelection + public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance) + { + runOriginal = true; + } + + /// + /// Custom murder logic + /// + /// Should 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; + runOriginal = true; + } + + /// + /// Should Roles Settings be available when this gamemode is selected? + /// + public virtual bool CanAccessRolesTab() => true; + + /// + /// Should Game Settings be available when this gamemode is selected? + /// + public virtual bool CanAccessSettingsTab() => true; + + /// + /// Custom winner selection + /// + /// List of winners + public virtual List CalculateWinners() => null; + + /// + /// Show gamemode in Intro Cutscene + /// + public virtual bool ShowCustomRoleScreen() => false; + + /// + /// Can Admin be used in this gamemode + /// + /// Admin Console + public virtual bool CanUseMapConsole(MapConsole console) => true; + + /// + /// Can a body be reported in this gamemode + /// + /// Target body for reporting + public virtual bool CanReport(DeadBody body) => true; + + /// + /// Can system consoles be used in this gamemode + /// + /// System Console + public virtual bool CanUseSystemConsole(SystemConsole console) => true; + + /// + /// Can tasks be interacted with in this gamemode + /// + /// Task console + public virtual bool CanUseConsole(Console console) => true; + + /// + /// Should the sabotage map be used when attempting to open sabotage overlay + /// + /// MapBehaviour object + public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true; + + /// + /// Can a player vent in this gamemode + /// + /// Target vent + /// Player attempting to vent + public virtual bool CanVent(Vent vent, GameData.PlayerInfo playerInfo) => true; + +} \ No newline at end of file diff --git a/MiraAPI/GameModes/CustomGameModeManager.cs b/MiraAPI/GameModes/CustomGameModeManager.cs new file mode 100644 index 0000000..6a30b96 --- /dev/null +++ b/MiraAPI/GameModes/CustomGameModeManager.cs @@ -0,0 +1,71 @@ +using Reactor.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiraAPI.GameModes; + +/// +/// Manages custom gamemodes +/// +public static class CustomGameModeManager +{ + /// + /// List of registered gamemodes + /// + public static readonly Dictionary GameModes = []; + + public static bool IsDefault() + { + return ActiveMode.Id == 0; + } + + /// + /// Current gamemode + /// + public static CustomGameMode ActiveMode; + + /// + /// Set current gamemode + /// + /// gamemode ID + public static void SetGameMode(int id) + { + if (GameModes.TryGetValue(id, out var gameMode)) + { + ActiveMode = gameMode; + return; + } + + Logger.Error($"No gamemode with id {id} found!"); + } + + /// + /// Register gamemode from type + /// + /// Type of gamemode class, should inherit from + public static void RegisterGameMode(Type gameModeType) + { + if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType)) + { + Logger.Warning($"{gameModeType?.Name} does not inherit CustomGameMode!"); + return; + } + + var gameMode = (CustomGameMode)Activator.CreateInstance(gameModeType); + + if (GameModes.Any(x => x.Key == gameMode?.Id)) + { + Logger.Error($"ID for gamemode {gameMode?.Name} already exists!"); + return; + } + + if (gameMode == null) + { + Logger.Error($"GAMEMODE WITH TYPE {gameModeType.Name} IS NULL"); + return; + } + + GameModes.Add(gameMode.Id, gameMode); + } +} \ No newline at end of file diff --git a/MiraAPI/GameModes/DefaultMode.cs b/MiraAPI/GameModes/DefaultMode.cs new file mode 100644 index 0000000..2e7fcb0 --- /dev/null +++ b/MiraAPI/GameModes/DefaultMode.cs @@ -0,0 +1,9 @@ +namespace MiraAPI.GameModes; + +[RegisterGameMode] +public class DefaultMode : CustomGameMode +{ + public override string Name => "Default"; + public override string Description => "Default Among Us GameMode"; + public override int Id => 0; +} \ No newline at end of file diff --git a/MiraAPI/GameModes/RegisterGameModeAttribute.cs b/MiraAPI/GameModes/RegisterGameModeAttribute.cs new file mode 100644 index 0000000..54f134a --- /dev/null +++ b/MiraAPI/GameModes/RegisterGameModeAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MiraAPI.GameModes; + +public class RegisterGameModeAttribute : Attribute +{ + private static readonly HashSet RegisteredAssemblies = []; + + public static void Register(Assembly assembly) + { + if (!RegisteredAssemblies.Add(assembly)) return; + + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttribute(); + if (attribute != null) + { + CustomGameModeManager.RegisterGameMode(type); + } + } + } +} \ No newline at end of file diff --git a/MiraAPI/GameOptions/AbstractGameOption.cs b/MiraAPI/GameOptions/AbstractGameOption.cs new file mode 100644 index 0000000..af01326 --- /dev/null +++ b/MiraAPI/GameOptions/AbstractGameOption.cs @@ -0,0 +1,38 @@ +using MiraAPI.Roles; +using Reactor.Localization.Utilities; +using System; + +namespace MiraAPI.API.GameOptions; + +public abstract class AbstractGameOption +{ + public string Title { get; } + public StringNames StringName { get; } + public Type AdvancedRole { get; } + public bool Save { get; } + public bool ShowInHideNSeek { get; init; } + public CustomOptionGroup Group { get; set; } + public Func Hidden { get; set; } + public OptionBehaviour OptionBehaviour { get; protected set; } + public void ValueChanged(OptionBehaviour optionBehaviour) + { + OnValueChanged(optionBehaviour); + CustomOptionsManager.SyncOptions(); + } + + protected abstract void OnValueChanged(OptionBehaviour optionBehaviour); + + protected AbstractGameOption(string title, Type roleType, bool save) + { + Title = title; + StringName = CustomStringName.CreateAndRegister(Title); + if (roleType is not null && roleType.IsAssignableTo(typeof(ICustomRole))) + { + AdvancedRole = roleType; + } + + Save = save; + Hidden = () => false; + CustomOptionsManager.CustomOptions.Add(this); + } +} \ No newline at end of file diff --git a/MiraAPI/GameOptions/CustomNumberOption.cs b/MiraAPI/GameOptions/CustomNumberOption.cs new file mode 100644 index 0000000..cadf17c --- /dev/null +++ b/MiraAPI/GameOptions/CustomNumberOption.cs @@ -0,0 +1,102 @@ +using BepInEx.Configuration; +using Reactor.Utilities; +using System; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.API.GameOptions; + +public class CustomNumberOption : AbstractGameOption +{ + public float Value { get; private set; } + public float Min { get; } + public float Max { get; } + public float Increment { get; } + public NumberSuffixes SuffixType { get; } + public string NumberFormat { get; } + public float Default { get; } + public bool ZeroInfinity { get; } + public ConfigEntry Config { get; } + public Action ChangedEvent { get; set; } + + public CustomNumberOption(string title, float defaultValue, float min, float max, float increment, NumberSuffixes suffixType, bool zeroInfinity = false, string numberFormat = "0", Type role = null, bool save = true) : base(title, role, save) + { + Value = defaultValue; + Default = defaultValue; + Min = min; + Max = max; + Increment = increment; + SuffixType = suffixType; + NumberFormat = numberFormat; + ZeroInfinity = zeroInfinity; + + if (Save) + { + try + { + Config = MiraAPIPlugin.Instance.Config.Bind("Number Options", title, defaultValue); + SetValue(Save ? Config.Value : defaultValue); + } + catch (Exception e) + { + Logger.Warning(e.ToString()); + } + } + + CustomOptionsManager.CustomNumberOptions.Add(this); + } + + public void SetValue(float newValue) + { + newValue = Mathf.Clamp(newValue, Min, Max); + + if (Save && (!AmongUsClient.Instance || AmongUsClient.Instance.AmHost)) + { + try + { + Config.Value = newValue; + + } + catch (Exception e) + { + Logger.Warning(e.ToString()); + } + } + + Value = newValue; + + var behaviour = (NumberOption)OptionBehaviour; + if (behaviour) + { + behaviour.Value = Value; + } + + ChangedEvent?.Invoke(Value); + } + + public NumberOption CreateNumberOption(NumberOption original, Transform container) + { + var numberOption = Object.Instantiate(original, container); + + numberOption.name = Title; + numberOption.Title = StringName; + numberOption.Value = Value; + numberOption.Increment = Increment; + numberOption.SuffixType = SuffixType; + numberOption.FormatString = NumberFormat; + numberOption.ValidRange = new FloatRange(Min, Max); + numberOption.ZeroIsInfinity = ZeroInfinity; + numberOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + numberOption.OnEnable(); + + OptionBehaviour = numberOption; + + return numberOption; + } + + protected override void OnValueChanged(OptionBehaviour optionBehaviour) + { + SetValue(optionBehaviour.GetFloat()); + } + +} \ No newline at end of file diff --git a/MiraAPI/GameOptions/CustomOptionGroup.cs b/MiraAPI/GameOptions/CustomOptionGroup.cs new file mode 100644 index 0000000..672aa89 --- /dev/null +++ b/MiraAPI/GameOptions/CustomOptionGroup.cs @@ -0,0 +1,46 @@ +using MiraAPI.Roles; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace MiraAPI.API.GameOptions; +public class CustomOptionGroup +{ + public string Title { get; } + public Func Hidden { get; set; } + + public GameObject Header; + public Type AdvancedRole { get; set; } + + public readonly List Options = []; + public readonly List CustomNumberOptions; + public readonly List CustomToggleOptions; + public readonly List CustomStringOptions; + public CustomOptionGroup(string title, List numberOpt, + List toggleOpt, List stringOpt, Type role = null) + { + Title = title; + + if (role is not null && role.IsAssignableTo(typeof(ICustomRole))) + { + AdvancedRole = role; + } + + + Hidden = () => false; + CustomNumberOptions = numberOpt; + CustomToggleOptions = toggleOpt; + CustomStringOptions = stringOpt; + + Options.AddRange(CustomNumberOptions); + Options.AddRange(CustomToggleOptions); + Options.AddRange(CustomStringOptions); + + foreach (var option in Options) + { + 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 new file mode 100644 index 0000000..65be421 --- /dev/null +++ b/MiraAPI/GameOptions/CustomOptionsManager.cs @@ -0,0 +1,108 @@ +using MiraAPI.Networking.Options; +using Reactor.Networking.Rpc; +using Reactor.Utilities; +using System.Collections.Generic; +using System.Linq; + +namespace MiraAPI.API.GameOptions; + +public static class CustomOptionsManager +{ + public static readonly List CustomOptions = []; + public static readonly List CustomNumberOptions = []; + public static readonly List CustomToggleOptions = []; + public static readonly List CustomStringOptions = []; + public static readonly List CustomGroups = []; + public static void SyncOptions() + { + if (!AmongUsClient.Instance.AmHost) + { + return; + } + + var toggles = CustomToggleOptions.Select(x => x.Value).ToArray(); + var numbers = CustomNumberOptions.Select(x => x.Value).ToArray(); + var strings = CustomStringOptions.Select(x => x.IndexValue).ToArray(); + + Rpc.Instance.Send(new SyncOptionsRpc.Data(toggles, numbers, strings)); + } + + public static void SyncOptions(int targetId) + { + if (!AmongUsClient.Instance.AmHost) + { + return; + } + + var toggles = CustomToggleOptions.Select(x => x.Value).ToArray(); + var numbers = CustomNumberOptions.Select(x => x.Value).ToArray(); + var strings = CustomStringOptions.Select(x => x.IndexValue).ToArray(); + + Rpc.Instance.SendTo(targetId, new SyncOptionsRpc.Data(toggles, numbers, strings)); + } + + public static void UpdateToConfig() + { + foreach (var numberOpt in CustomNumberOptions) + { + numberOpt.SetValue(numberOpt.Save ? numberOpt.Config.Value : numberOpt.Default); + } + + foreach (var toggleOpt in CustomToggleOptions) + { + toggleOpt.SetValue(toggleOpt.Save ? toggleOpt.Config.Value : toggleOpt.Default); + } + + foreach (var stringOpt in CustomStringOptions) + { + stringOpt.SetValue(stringOpt.Save ? stringOpt.Config.Value : stringOpt.Default); + } + } + + + public static void ResetToDefault() + { + foreach (var numberOpt in CustomNumberOptions) + { + numberOpt.SetValue(numberOpt.Default); + } + + foreach (var stringOpt in CustomStringOptions) + { + stringOpt.SetValue(stringOpt.Default); + } + + foreach (var toggleOpt in CustomToggleOptions) + { + toggleOpt.SetValue(toggleOpt.Default); + } + + foreach (var option in CustomOptions) + { + if (!option.OptionBehaviour) + { + continue; + } + + option.ValueChanged(option.OptionBehaviour); + } + } + + public static void HandleOptionsSync(bool[] toggles, float[] numbers, int[] strings) + { + Logger.Warning("syncing options!"); + for (var i = 0; i < toggles.Length; i++) + { + CustomToggleOptions[i].SetValue(toggles[i]); + } + for (var i = 0; i < numbers.Length; i++) + { + CustomNumberOptions[i].SetValue(numbers[i]); + } + for (var i = 0; i < strings.Length; i++) + { + CustomStringOptions[i].SetValue(strings[i]); + } + } + +} \ No newline at end of file diff --git a/MiraAPI/GameOptions/CustomOptionsTab.cs b/MiraAPI/GameOptions/CustomOptionsTab.cs new file mode 100644 index 0000000..160a7bf --- /dev/null +++ b/MiraAPI/GameOptions/CustomOptionsTab.cs @@ -0,0 +1,162 @@ +using Reactor.Utilities.Extensions; +using System; +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 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; + + CustomScreen = CreateNewMenu(__instance); + CustomTab = CreateCustomTab(__instance, CustomScreen, gameBtn, roleBtn); + + Rend = CustomTab.transform.FindChild("LaunchpadBtn/Tab Background").GetComponent(); + Rend.enabled = false; + + UpdateListeners(__instance, gameBtn.GetComponentInChildren(), roleBtn.GetComponentInChildren(), Rend); + + var container = CustomScreen.transform.FindChild("GameGroup/SliderInner"); + container.DestroyChildren(); + CreateNewResetButton(__instance, container); + + return container.gameObject; + } + + private static void CreateNewResetButton(GameSettingMenu __instance, Transform container) + { + var resetBtn = __instance.RegularGameSettings.transform.FindChild("GameGroup/SliderInner/ResetToDefault"); + var newResetBtn = Object.Instantiate(resetBtn.gameObject, container); + newResetBtn.gameObject.name = "LaunchpadReset"; + + var tmp = newResetBtn.GetComponentInChildren(); + tmp.text = "Reset Options"; + tmp.alignment = TextAlignmentOptions.Center; + tmp.gameObject.transform.localPosition = new Vector3(0, 0, 0); + + newResetBtn.GetComponent().Destroy(); + newResetBtn.transform.FindChild("CheckBox").gameObject.Destroy(); + + var toggle = newResetBtn.GetComponent(); + + toggle.OnClick.RemoveAllListeners(); + toggle.OnMouseOver.RemoveAllListeners(); + toggle.OnMouseOut.RemoveAllListeners(); + 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); + + CustomScreen.gameObject.SetActive(false); + rend.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); + + CustomScreen.gameObject.SetActive(false); + rend.enabled = false; + + __instance.GameSettingsHightlight.enabled = false; + __instance.RolesSettingsHightlight.enabled = true; + })); + } + + public static GameObject CreateHeader(ToggleOption toggleOpt, Transform container, string title) + { + var header = Object.Instantiate(toggleOpt, container); + + header.Title = StringNames.None; + header.TitleText.text = title; + header.name = "Header"; + + var checkBox = header.transform.FindChild("CheckBox")?.gameObject; + if (checkBox) + { + checkBox.Destroy(); + } + + var background = header.transform.FindChild("Background")?.gameObject; + if (background) + { + background.Destroy(); + } + + header.GetComponent().Destroy(); + return header.gameObject; + } + + public static GameObject CreateCustomTab(GameSettingMenu __instance, GameObject newSettings, + GameObject gameTab, GameObject roleTab) + { + var newTab = Object.Instantiate(gameTab, gameTab.transform.parent); + newTab.name = "LaunchpadTab"; + gameTab.transform.position += new Vector3(-1, 0, 0); + + var inside = newTab.transform.FindChild("ColorButton"); + inside.name = "LaunchpadBtn"; + + var btn = inside.GetComponentInChildren(); + + btn.OnClick.RemoveAllListeners(); + + btn.OnClick.AddListener((Action)TabAction); + + var spriteRend = inside.GetComponentInChildren(); + //spriteRend.sprite = LaunchpadAssets.HackButton.LoadAsset(); + spriteRend.gameObject.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f); + + return newTab; + + void TabAction() + { + __instance.RegularGameSettings.SetActive(false); + __instance.RolesSettings.gameObject.SetActive(false); + newSettings.gameObject.SetActive(true); + + var rend = inside.transform.FindChild("Tab Background").GetComponent(); + rend.enabled = true; + + gameTab.transform.FindChild("ColorButton/Tab Background").gameObject.GetComponent().enabled = false; + roleTab.transform.FindChild("Hat Button/Tab Background").gameObject.GetComponent().enabled = false; + } + } + + public static GameObject CreateNewMenu(GameSettingMenu __instance) + { + var gameSettings = __instance.RegularGameSettings; + var newSettings = Object.Instantiate(gameSettings, gameSettings.transform.parent); + newSettings.name = "Launchpad 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"; + + return newSettings; + } +} \ No newline at end of file diff --git a/MiraAPI/GameOptions/CustomStringOption.cs b/MiraAPI/GameOptions/CustomStringOption.cs new file mode 100644 index 0000000..2350dae --- /dev/null +++ b/MiraAPI/GameOptions/CustomStringOption.cs @@ -0,0 +1,93 @@ +using BepInEx.Configuration; +using Reactor.Localization.Utilities; +using Reactor.Utilities; +using System; +using System.Linq; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.API.GameOptions; + +public class CustomStringOption : AbstractGameOption +{ + public int IndexValue { get; private set; } + public string Value => Options[IndexValue]; + public int Default { get; } + public string[] Options { get; private set; } + public ConfigEntry Config { get; } + public Action ChangedEvent { get; init; } + public CustomStringOption(string title, int defaultValue, string[] options, Type role = null, bool save = true) : base(title, role, save) + { + IndexValue = defaultValue; + Options = options; + Default = defaultValue; + + CustomOptionsManager.CustomStringOptions.Add(this); + if (Save) + { + try + { + Config = MiraAPIPlugin.Instance.Config.Bind("String Options", title, defaultValue); + SetValue(Save ? Config.Value : defaultValue); + } + catch (Exception e) + { + Logger.Warning(e.ToString()); + } + } + + } + + public void SetValue(int newValue) + { + if (Save && (!AmongUsClient.Instance || AmongUsClient.Instance.AmHost)) + { + try + { + Config.Value = newValue; + + } + catch (Exception e) + { + Logger.Warning(e.ToString()); + } + } + + var oldValue = IndexValue; + IndexValue = newValue; + + var behaviour = (StringOption)OptionBehaviour; + if (behaviour) + { + behaviour.Value = newValue; + } + + if (oldValue != newValue) + { + ChangedEvent?.Invoke(newValue, Value); + } + } + + public void SetValue(string newValue) => SetValue(Options.ToList().IndexOf(newValue)); + + protected override void OnValueChanged(OptionBehaviour optionBehaviour) + { + SetValue(optionBehaviour.GetInt()); + } + + public StringOption CreateStringOption(StringOption original, Transform container) + { + var stringOption = Object.Instantiate(original, container); + + stringOption.name = Title; + stringOption.Title = StringName; + stringOption.Value = Options.ToList().IndexOf(Value); + stringOption.Values = Options.Select(CustomStringName.CreateAndRegister).ToArray(); + stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + stringOption.OnEnable(); + + OptionBehaviour = stringOption; + + return stringOption; + } +} \ No newline at end of file diff --git a/MiraAPI/GameOptions/CustomToggleOption.cs b/MiraAPI/GameOptions/CustomToggleOption.cs new file mode 100644 index 0000000..3f655ad --- /dev/null +++ b/MiraAPI/GameOptions/CustomToggleOption.cs @@ -0,0 +1,79 @@ +using BepInEx.Configuration; +using Reactor.Utilities; +using System; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.API.GameOptions; + +public class CustomToggleOption : AbstractGameOption +{ + public bool Value { get; private set; } + public bool Default { get; } + public ConfigEntry Config { get; } + public Action ChangedEvent { get; init; } + public CustomToggleOption(string title, bool defaultValue, Type role = null, bool save = true) : base(title, role, save) + { + Default = defaultValue; + if (Save) + { + try + { + Config = MiraAPIPlugin.Instance.Config.Bind("Toggle Options", title, defaultValue); + SetValue(Save ? Config.Value : defaultValue); + } + catch (Exception e) + { + Logger.Warning(e.ToString()); + } + } + CustomOptionsManager.CustomToggleOptions.Add(this); + } + + public void SetValue(bool newValue) + { + if (Save && (!AmongUsClient.Instance || AmongUsClient.Instance.AmHost)) + { + try + { + Config.Value = newValue; + } + catch (Exception e) + { + Logger.Warning(e.ToString()); + } + } + + var oldValue = Value; + Value = newValue; + + var behaviour = (ToggleOption)OptionBehaviour; + if (behaviour) + { + behaviour.CheckMark.enabled = newValue; + } + + if (newValue != oldValue) + { + ChangedEvent?.Invoke(newValue); + } + } + + protected override void OnValueChanged(OptionBehaviour optionBehaviour) + { + SetValue(optionBehaviour.GetBool()); + } + + public ToggleOption CreateToggleOption(ToggleOption original, Transform container) + { + var toggleOption = Object.Instantiate(original, container); + + toggleOption.name = Title; + toggleOption.Title = StringName; + toggleOption.CheckMark.enabled = Value; + toggleOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + toggleOption.OnEnable(); + OptionBehaviour = toggleOption; + return toggleOption; + } +} \ No newline at end of file diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs new file mode 100644 index 0000000..39ecd00 --- /dev/null +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -0,0 +1,165 @@ +using MiraAPI.Utilities; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.Hud; + +public abstract class CustomActionButton +{ + public abstract string Name { get; } + + public abstract float Cooldown { get; } + + public abstract float EffectDuration { get; } + + public abstract int MaxUses { get; } + + public abstract LoadableAsset Sprite { get; } + + public bool HasEffect => EffectDuration > 0; + + public bool LimitedUses => MaxUses > 0; + + protected bool EffectActive; + + protected float Timer; + + protected int UsesLeft; + + protected ActionButton Button; + + protected DeadBody DeadBodyTarget; + + public void CreateButton(Transform parent) + { + if (Button) + { + return; + } + + UsesLeft = MaxUses; + Timer = Cooldown; + EffectActive = false; + + Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent); + Button.name = Name + "Button"; + Button.OverrideText(Name.ToUpper()); + + Button.graphic.sprite = Sprite.LoadAsset(); + + Button.SetUsesRemaining(MaxUses); + if (MaxUses <= 0) + { + Button.SetInfiniteUses(); + } + + var pb = Button.GetComponent(); + pb.OnClick.RemoveAllListeners(); + pb.OnClick.AddListener((UnityAction)ClickHandler); + } + + public void ResetCooldown() + { + Timer = Cooldown; + if (EffectActive) + { + OnEffectEnd(); + } + + EffectActive = false; + } + + public void OverrideSprite(Sprite sprite) + { + Button.graphic.sprite = sprite; + } + + public void OverrideName(string name) + { + Button.OverrideText(name); + } + + protected virtual void FixedUpdate(PlayerControl playerControl) { } + + protected abstract void OnClick(); + + public abstract bool Enabled(RoleBehaviour role); + + public virtual void OnEffectEnd() { } + + private bool CanUseHandler() + { + return Timer <= 0 && !EffectActive && (!LimitedUses || UsesLeft > 0) && CanUse(); + } + + public virtual bool CanUse() + { + return true; + } + + public virtual void SetActive(bool visible, RoleBehaviour role) + { + Button.ToggleVisible(visible && Enabled(role)); + } + + private void ClickHandler() + { + if (!CanUseHandler()) + { + return; + } + + if (LimitedUses) + { + UsesLeft--; + Button.SetUsesRemaining(UsesLeft); + } + + OnClick(); + Button.SetDisabled(); + if (HasEffect) + { + EffectActive = true; + Timer = EffectDuration; + } + else + { + Timer = Cooldown; + } + } + + public void FixedUpdateHandler(PlayerControl playerControl) + { + if (Timer >= 0) + { + Timer -= Time.deltaTime; + } + else + { + if (HasEffect && EffectActive) + { + EffectEndHandler(); + } + } + + if (CanUseHandler()) + { + Button.SetEnabled(); + } + else + { + Button.SetDisabled(); + } + Button.SetCoolDown(Timer, EffectActive ? EffectDuration : Cooldown); + + playerControl.UpdateBodies(playerControl.Data.Role.TeamColor, ref DeadBodyTarget); + FixedUpdate(playerControl); + } + + private void EffectEndHandler() + { + EffectActive = false; + Timer = Cooldown; + OnEffectEnd(); + } +} \ No newline at end of file diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs new file mode 100644 index 0000000..3b0a31b --- /dev/null +++ b/MiraAPI/Hud/CustomButtonManager.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MiraAPI.Hud; + +public static class CustomButtonManager +{ + public static readonly List CustomButtons = []; + + + public static void RegisterButton(Type buttonType) + { + if (!typeof(CustomActionButton).IsAssignableFrom(buttonType)) + { + return; + } + + var button = (CustomActionButton)Activator.CreateInstance(buttonType); + 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 new file mode 100644 index 0000000..23c2eef --- /dev/null +++ b/MiraAPI/Hud/CustomButtonSingleton.cs @@ -0,0 +1,21 @@ +#nullable enable +using System; +using System.Linq; + +namespace MiraAPI.Hud; + +public 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 diff --git a/MiraAPI/Hud/RegisterButtonAttribute.cs b/MiraAPI/Hud/RegisterButtonAttribute.cs new file mode 100644 index 0000000..818fec5 --- /dev/null +++ b/MiraAPI/Hud/RegisterButtonAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MiraAPI.Hud; + +public class RegisterButtonAttribute : Attribute +{ + private static readonly HashSet RegisteredAssemblies = []; + + public static void Register(Assembly assembly) + { + if (!RegisteredAssemblies.Add(assembly)) return; + + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttribute(); + if (attribute != null) + { + CustomButtonManager.RegisterButton(type); + } + } + } +} \ No newline at end of file diff --git a/MiraAPI/MiraAPIPlugin.cs b/MiraAPI/MiraAPIPlugin.cs index 44fceac..3c96bf2 100644 --- a/MiraAPI/MiraAPIPlugin.cs +++ b/MiraAPI/MiraAPIPlugin.cs @@ -1,9 +1,13 @@ using BepInEx; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.GameModes; +using MiraAPI.Hud; +using MiraAPI.Roles; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; +using System.Reflection; namespace MiraAPI; @@ -14,9 +18,15 @@ namespace MiraAPI; public partial class MiraAPIPlugin : BasePlugin { public Harmony Harmony { get; } = new(Id); + public static MiraAPIPlugin Instance { get; private set; } public override void Load() { + Instance = this; Harmony.PatchAll(); + + RegisterGameModeAttribute.Register(Assembly.GetExecutingAssembly()); + RegisterCustomRoleAttribute.Register(Assembly.GetExecutingAssembly()); + RegisterButtonAttribute.Register(Assembly.GetExecutingAssembly()); } } diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs new file mode 100644 index 0000000..519ce40 --- /dev/null +++ b/MiraAPI/Networking/MiraRpc.cs @@ -0,0 +1,8 @@ +namespace MiraAPI.Networking +{ + public enum MiraRpc : uint + { + SyncGameOptions, + SyncRoleOptions + } +} diff --git a/MiraAPI/Networking/SyncOptionsRpc.cs b/MiraAPI/Networking/SyncOptionsRpc.cs new file mode 100644 index 0000000..d8a10c1 --- /dev/null +++ b/MiraAPI/Networking/SyncOptionsRpc.cs @@ -0,0 +1,82 @@ +using Hazel; +using MiraAPI.API.GameOptions; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; + +namespace MiraAPI.Networking.Options; + +// METHOD RPC DOESNT WORK WITH THE ARRAYS AND STUFF SO THIS IS HOW WE WILL DO IT FOR NOW +[RegisterCustomRpc((uint)MiraRpc.SyncGameOptions)] +public class SyncOptionsRpc(MiraAPIPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +{ + public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; + + public readonly struct Data + { + public readonly bool[] Toggles; + public readonly float[] Numbers; + public readonly int[] StringIDs; + + public Data(bool[] toggles, float[] numbers, int[] stringIDs) + { + Toggles = toggles; + Numbers = numbers; + StringIDs = stringIDs; + } + } + + public override void Write(MessageWriter writer, Data data) + { + writer.WritePacked((uint)data.Toggles.Length); + foreach (var t in data.Toggles) + { + writer.Write(t); + } + + writer.WritePacked((uint)data.Numbers.Length); + foreach (var n in data.Numbers) + { + writer.Write(n); + } + + writer.WritePacked((uint)data.StringIDs.Length); + foreach (var n in data.StringIDs) + { + writer.WritePacked(n); + } + } + + public override Data Read(MessageReader reader) + { + var toggles = new bool[reader.ReadPackedUInt32()]; + for (var i = 0; i < toggles.Length; i++) + { + toggles[i] = reader.ReadBoolean(); + } + + var numbers = new float[reader.ReadPackedUInt32()]; + for (var i = 0; i < numbers.Length; i++) + { + numbers[i] = reader.ReadSingle(); + } + + var strings = new int[reader.ReadPackedUInt32()]; + for (var i = 0; i < strings.Length; i++) + { + strings[i] = reader.ReadPackedInt32(); + } + + + return new Data(toggles, numbers, strings); + } + + public override void Handle(PlayerControl playerControl, Data data) + { + if (AmongUsClient.Instance.HostId != playerControl.OwnerId) + { + return; + } + + CustomOptionsManager.HandleOptionsSync(data.Toggles, data.Numbers, data.StringIDs); + } +} \ No newline at end of file diff --git a/MiraAPI/Networking/SyncRoleOptionsRpc.cs b/MiraAPI/Networking/SyncRoleOptionsRpc.cs new file mode 100644 index 0000000..eb259da --- /dev/null +++ b/MiraAPI/Networking/SyncRoleOptionsRpc.cs @@ -0,0 +1,58 @@ +using Hazel; +using MiraAPI.Roles; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; +using Reactor.Utilities; +using System; + +namespace MiraAPI.Networking.Options; + +[RegisterCustomRpc((uint)MiraRpc.SyncRoleOptions)] +public class SyncRoleOptionsRpc(MiraAPIPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +{ + + public struct Data(ushort roleId, int number, int chance) + { + public readonly ushort RoleId = roleId; + public readonly int Number = number; + public readonly int Chance = chance; + } + + public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; + + public override void Write(MessageWriter writer, Data data) + { + writer.Write(data.RoleId); + writer.WritePacked(data.Number); + writer.WritePacked(data.Chance); + } + + public override Data Read(MessageReader reader) + { + return new Data(reader.ReadUInt16(), reader.ReadPackedInt32(), reader.ReadPackedInt32()); + } + + public override void Handle(PlayerControl playerControl, Data data) + { + if (AmongUsClient.Instance.HostId != playerControl.OwnerId) + { + return; + } + + if (!CustomRoleManager.CustomRoles.TryGetValue(data.RoleId, out var roleBehaviour) || + !CustomRoleManager.GetCustomRoleBehaviour(roleBehaviour.Role, out var role)) return; + + PluginSingleton.Instance.Config.TryGetEntry(role.NumConfigDefinition, out var numEntry); + PluginSingleton.Instance.Config.TryGetEntry(role.ChanceConfigDefinition, out var chanceEntry); + + try + { + numEntry.Value = data.Number; + chanceEntry.Value = data.Chance; + } + catch (Exception e) + { + Logger.Warning(e.ToString()); + } + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs new file mode 100644 index 0000000..157b231 --- /dev/null +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -0,0 +1,109 @@ +using HarmonyLib; +using Il2CppSystem; +using MiraAPI.API.GameOptions; +using System.Linq; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.Patches.Options; + +[HarmonyPatch(typeof(GameOptionsMenu))] +public static class GameOptionsMenuPatch +{ + /// + /// Change the OnValueChanged action for all custom game options + /// + [HarmonyPostfix, HarmonyPatch("Start")] + public static void StartPostfix(GameOptionsMenu __instance) + { + foreach (var customOption in CustomOptionsManager.CustomOptions) + { + if (customOption.AdvancedRole is not null || !customOption.OptionBehaviour) + { + continue; + } + + customOption.OptionBehaviour.OnValueChanged = (Action)customOption.ValueChanged; + } + } + + private static GameSettingMenu menu; + + + /// + /// Set the position and offset of all custom game options + /// + [HarmonyPostfix, HarmonyPatch("Update")] + public static void UpdatePostfix(GameOptionsMenu __instance) + { + if (!menu) + { + menu = Object.FindObjectsOfType().First(); + } + + if (menu.RegularGameSettings.active || menu.RolesSettings.gameObject.active || menu.HideNSeekSettings.active) + { + return; + } + + var startOffset = 2.15f; + __instance.GetComponentInParent().ContentYBounds.max = startOffset + __instance.Children.Count * 0.5f; + + foreach (var option in CustomOptionsManager.CustomOptions.Where(option => option.Group == null)) + { + if (!option.OptionBehaviour) continue; + + option.OptionBehaviour.enabled = !option.Hidden(); + option.OptionBehaviour.gameObject.SetActive(!option.Hidden()); + + if (!option.Hidden()) + { + startOffset -= 0.55f; + } + + var transform = option.OptionBehaviour.transform; + var optionPosition = transform.localPosition; + transform.localPosition = new Vector3(optionPosition.x, startOffset, optionPosition.z); + } + + foreach (var group in CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null)) + { + if (group.Header == null) + { + continue; + } + + group.Header.SetActive(!group.Hidden()); + + if (!group.Hidden()) + { + startOffset -= 0.5f; + } + + var position = group.Header.transform.localPosition; + group.Header.transform.localPosition = new Vector3(position.x, startOffset, position.z); + + foreach (var option in group.Options) + { + if (!option.OptionBehaviour) + { + continue; + } + + var enabled = !group.Hidden() && !option.Hidden(); + + option.OptionBehaviour.enabled = enabled; + option.OptionBehaviour.gameObject.SetActive(enabled); + + if (!group.Hidden() && !option.Hidden()) + { + startOffset -= 0.5f; + } + + var transform = option.OptionBehaviour.transform; + var optionPosition = transform.localPosition; + transform.localPosition = new Vector3(optionPosition.x, startOffset, optionPosition.z); + } + } + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Options/GameSettingsMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingsMenuPatches.cs new file mode 100644 index 0000000..03c4f74 --- /dev/null +++ b/MiraAPI/Patches/Options/GameSettingsMenuPatches.cs @@ -0,0 +1,171 @@ +using HarmonyLib; +using MiraAPI.API.GameOptions; +using MiraAPI.GameModes; +using Reactor.Utilities; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.Patches.Options; + +[HarmonyPatch(typeof(GameSettingMenu))] +public static class GameSettingsMenuPatches +{ + private static GameObject _gameBtn; + private static GameObject _roleBtn; + + [HarmonyPrefix, HarmonyPatch("Start")] + public static void StartPrefix(GameSettingMenu __instance) + { + if (CustomOptionsTab.CustomTab) + { + return; + } + + __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; + + var numberOpt = __instance.RegularGameSettings.GetComponentInChildren(); + var toggleOpt = Object.FindObjectOfType(); + var stringOpt = __instance.RegularGameSettings.GetComponentInChildren(); + var container = GameManager.Instance.IsNormal() ? CustomOptionsTab.Initialize(__instance).transform : __instance.HideAndSeekScroller.Inner; + + foreach (var group in CustomOptionsManager.CustomGroups.Where(group => group.AdvancedRole == null)) + { + if (GameManager.Instance.IsNormal()) + { + group.Header = CustomOptionsTab.CreateHeader(toggleOpt, container, group.Title); + CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container, + group.CustomToggleOptions, group.CustomNumberOptions, group.CustomStringOptions); + continue; + } + + if (!group.Options.Any(x => x.ShowInHideNSeek)) + { + continue; + } + + group.Header = CustomOptionsTab.CreateHeader(toggleOpt, container, group.Title); + __instance.AllHideAndSeekItems = __instance.AllHideAndSeekItems.Append(group.Header.transform).ToArray(); + + CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container, + group.CustomToggleOptions, group.CustomNumberOptions, group.CustomStringOptions); + } + + CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container, + CustomOptionsManager.CustomToggleOptions.Where(option => option.Group == null), + CustomOptionsManager.CustomNumberOptions.Where(option => option.Group == null), + CustomOptionsManager.CustomStringOptions.Where(option => option.Group == null)); + + if (!numberOpt || !toggleOpt || !stringOpt) + { + Logger.Error("OPTION PREFABS MISSING"); + } + } + + [HarmonyPostfix, HarmonyPatch("Update")] + public static void UpdatePatch(GameSettingMenu __instance) + { + if (CustomGameModeManager.ActiveMode == null) + { + return; + } + + _gameBtn.SetActive(CustomGameModeManager.ActiveMode.CanAccessSettingsTab()); + _roleBtn.SetActive(CustomGameModeManager.ActiveMode.CanAccessRolesTab()); + + if (!CustomGameModeManager.ActiveMode.CanAccessSettingsTab()) + { + __instance.RegularGameSettings.SetActive(false); + __instance.RolesSettings.gameObject.SetActive(false); + + CustomOptionsTab.CustomScreen.gameObject.SetActive(true); + CustomOptionsTab.Rend.enabled = true; + + __instance.GameSettingsHightlight.enabled = false; + __instance.RolesSettingsHightlight.enabled = false; + } + } + + public static void CreateOptionsFor(GameSettingMenu __instance, ToggleOption togglePrefab, NumberOption numberPrefab, StringOption stringPrefab, Transform container, + IEnumerable toggles, IEnumerable numbers, IEnumerable strings) + { + foreach (var customToggleOption in toggles) + { + if (customToggleOption.AdvancedRole is not null || customToggleOption.OptionBehaviour) + { + continue; + } + + ToggleOption toggleOption; + + if (GameManager.Instance.IsNormal()) + { + toggleOption = customToggleOption.CreateToggleOption(togglePrefab, container); + __instance.AllItems = __instance.AllItems.AddItem(toggleOption.transform).ToArray(); + continue; + } + + if (!customToggleOption.ShowInHideNSeek) + { + continue; + } + + toggleOption = customToggleOption.CreateToggleOption(togglePrefab, container); + __instance.AllHideAndSeekItems = __instance.AllHideAndSeekItems.AddItem(toggleOption.transform).ToArray(); + + } + + foreach (var customNumberOption in numbers) + { + if (customNumberOption.AdvancedRole is not null || customNumberOption.OptionBehaviour) + { + continue; + } + + NumberOption numberOption; + + if (GameManager.Instance.IsNormal()) + { + numberOption = customNumberOption.CreateNumberOption(numberPrefab, container); + __instance.AllItems = __instance.AllItems.AddItem(numberOption.transform).ToArray(); + continue; + } + + if (!customNumberOption.ShowInHideNSeek) + { + continue; + } + + numberOption = customNumberOption.CreateNumberOption(numberPrefab, container); + __instance.AllHideAndSeekItems = __instance.AllHideAndSeekItems.AddItem(numberOption.transform).ToArray(); + } + + foreach (var customStringOption in strings) + { + if (customStringOption.AdvancedRole is not null || customStringOption.OptionBehaviour) + { + continue; + } + + StringOption stringOption; + + if (GameManager.Instance.IsNormal()) + { + stringOption = customStringOption.CreateStringOption(stringPrefab, container); + __instance.AllItems = __instance.AllItems.AddItem(stringOption.transform).ToArray(); + continue; + } + + if (!customStringOption.ShowInHideNSeek) + { + continue; + } + + stringOption = customStringOption.CreateStringOption(stringPrefab, container); + __instance.AllHideAndSeekItems = __instance.AllHideAndSeekItems.AddItem(stringOption.transform).ToArray(); + } + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Options/ToHudStringPatch.cs b/MiraAPI/Patches/Options/ToHudStringPatch.cs new file mode 100644 index 0000000..c245d2d --- /dev/null +++ b/MiraAPI/Patches/Options/ToHudStringPatch.cs @@ -0,0 +1,150 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using MiraAPI.API.GameOptions; +using MiraAPI.GameModes; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +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; + + /// + /// Generic method to add options + /// + public static void AddOptions(StringBuilder sb, + IEnumerable numberOptions, IEnumerable stringOptions, IEnumerable toggleOptions) + { + foreach (var numberOption in numberOptions.Where(x => !x.Hidden())) + { + if (GameManager.Instance.IsHideAndSeek() && !numberOption.ShowInHideNSeek) + { + continue; + } + + sb.AppendLine(numberOption.Title + ": " + numberOption.Value + Helpers.GetSuffix(numberOption.SuffixType)); + } + + foreach (var toggleOption in toggleOptions.Where(x => !x.Hidden())) + { + if (GameManager.Instance.IsHideAndSeek() && !toggleOption.ShowInHideNSeek) + { + continue; + } + + sb.AppendLine(toggleOption.Title + ": " + (toggleOption.Value ? "On" : "Off")); + } + + foreach (var stringOption in stringOptions.Where(x => !x.Hidden())) + { + if (GameManager.Instance.IsHideAndSeek() && !stringOption.ShowInHideNSeek) + { + continue; + } + + sb.AppendLine(stringOption.Title + ": " + stringOption.Value); + } + } + + public static void Prefix() + { + if (GameManager.Instance is null || GameManager.Instance.IsHideAndSeek()) + { + return; + } + + foreach (var role in CustomRoleManager.CustomRoles.Values) + { + var customRole = role as ICustomRole; + if (customRole.IsGhostRole) + { + role.Role = RoleTypes.CrewmateGhost; + } + } + } + + /// + /// Update the HudOptions on the left of the screen if player is using Launchpad options + /// + public static void Postfix(IGameOptions __instance, ref string __result) + { + if (GameManager.Instance is null) + { + return; + } + + foreach (var role in CustomRoleManager.CustomRoles.Values) + { + var customRole = role as ICustomRole; + if (customRole.IsGhostRole) + { + role.Role = (RoleTypes)customRole.RoleId; + } + } + + if (ShowCustom || !CustomGameModeManager.ActiveMode.CanAccessSettingsTab()) + { + 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); + + var suffix = CustomGameModeManager.ActiveMode.CanAccessSettingsTab() ? "\nPress Tab to view Normal Options" : + $"\nYou can not access Normal Options on {Cus tomGameModeManager.ActiveMode.Name} mode."; + + 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()) + ); + + foreach (var group in groupsWithoutRoles) + { + if (group.Hidden() || (GameManager.Instance.IsHideAndSeek() && !group.Options.Any(x => x.ShowInHideNSeek))) + { + continue; + } + + sb.AppendLine($"\n{group.Title}"); + 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()) + { + sb.AppendLine("\nRoles"); + + foreach (var group in customOptionGroups) + { + if (group.Hidden()) + { + continue; + } + + sb.AppendLine($"{group.Title}"); + AddOptions(sb, group.CustomNumberOptions, group.CustomStringOptions, group.CustomToggleOptions); + sb.Append("\n"); + } + } + + __result = sb + suffix; + return; + } + + __result = "Normal Options:\n" + __result + "\nPress Tab to view Launchpad Options"; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/GameStartupPatch.cs b/MiraAPI/Patches/Roles/GameStartupPatch.cs new file mode 100644 index 0000000..6adc10d --- /dev/null +++ b/MiraAPI/Patches/Roles/GameStartupPatch.cs @@ -0,0 +1,24 @@ +using HarmonyLib; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(MainMenuManager))] +public static class GameStartupPatch +{ + private static bool _runOnce; + + /// + /// This is used for registering roles when the game opens, might be a janky solution but it works + /// + [HarmonyPostfix, HarmonyPatch("Start")] + public static void StartPostfix() + { + if (_runOnce) + { + return; + } + + _runOnce = true; + CustomRoleManager.RegisterInRoleManager(); + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/NameTagPatch.cs b/MiraAPI/Patches/Roles/NameTagPatch.cs new file mode 100644 index 0000000..86eeba3 --- /dev/null +++ b/MiraAPI/Patches/Roles/NameTagPatch.cs @@ -0,0 +1,35 @@ +using HarmonyLib; +using UnityEngine; + +namespace MiraAPI.Patches.Roles; + +/// +/// Set nametag color depending on option +/// +[HarmonyPatch(typeof(PlayerNameColor))] +public static class NameTagPatch +{ + [HarmonyPrefix, HarmonyPatch("Get", typeof(RoleBehaviour))] + public static bool GetPatch([HarmonyArgument(0)] RoleBehaviour otherPlayerRole, ref Color __result) + { + if (PlayerControl.LocalPlayer.Data.IsDead) + { + __result = otherPlayerRole.NameColor; + return false; + } + + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && otherPlayerRole.IsImpostor) + { + return true; + } + + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null || PlayerControl.LocalPlayer.Data.Role == null || otherPlayerRole == null) + { + __result = Color.white; + } + + __result = PlayerControl.LocalPlayer.Data?.Role == otherPlayerRole ? otherPlayerRole.NameColor : Color.white; + + return false; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/RoleBehaviourPatches.cs b/MiraAPI/Patches/Roles/RoleBehaviourPatches.cs new file mode 100644 index 0000000..3169c3a --- /dev/null +++ b/MiraAPI/Patches/Roles/RoleBehaviourPatches.cs @@ -0,0 +1,24 @@ +using HarmonyLib; +using MiraAPI.Roles; +using UnityEngine; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(RoleBehaviour))] +public static class RoleBehaviourPatches +{ + /// + /// Update TeamColor text for Launchpad roles + /// + [HarmonyPrefix, HarmonyPatch("TeamColor", MethodType.Getter)] + public static bool PrefixTeamColorGetter(RoleBehaviour __instance, ref Color __result) + { + if (__instance is not ICustomRole behaviour) + { + return true; + } + + __result = behaviour.RoleColor; + return false; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs new file mode 100644 index 0000000..953b9bb --- /dev/null +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -0,0 +1,31 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using MiraAPI.Roles; +using UnityEngine; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(RoleManager))] +public static class RoleManagerPatches +{ + [HarmonyPrefix, HarmonyPatch("AssignRoleOnDeath")] + public static bool AssignRoleOnDeath(RoleManager __instance, [HarmonyArgument(0)] PlayerControl plr) + { + if (plr == null || !plr.Data.IsDead) + { + return false; + } + + if (plr.Data.Role is ICustomRole role) + { + Debug.Log(role.RoleName); + if (role.GhostRole != RoleTypes.CrewmateGhost && role.GhostRole != RoleTypes.ImpostorGhost) + { + plr.RpcSetRole(role.GhostRole); + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/RoleOptionSettingPatch.cs b/MiraAPI/Patches/Roles/RoleOptionSettingPatch.cs new file mode 100644 index 0000000..6104869 --- /dev/null +++ b/MiraAPI/Patches/Roles/RoleOptionSettingPatch.cs @@ -0,0 +1,24 @@ +using HarmonyLib; +using MiraAPI.Roles; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(RoleOptionSetting))] +public static class RoleOptionSettingPatch +{ + /// + /// Update the role details screen in role settings for Launchpad roles + /// + [HarmonyPrefix, HarmonyPatch("ShowRoleDetails")] + public static bool ShowRoleDetailsPrefix(RoleOptionSetting __instance) + { + if (__instance.Role is ICustomRole customRole) + { + GameSettingMenu.Instance.RoleName.text = __instance.Role.NiceName; + GameSettingMenu.Instance.RoleBlurb.text = __instance.Role.BlurbLong; + GameSettingMenu.Instance.RoleIcon.sprite = customRole.Icon.LoadAsset(); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs new file mode 100644 index 0000000..4b2d1bc --- /dev/null +++ b/MiraAPI/Patches/Roles/RoleOptionsCollectionPatch.cs @@ -0,0 +1,52 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using MiraAPI.Roles; +using Reactor.Utilities; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(RoleOptionsCollectionV07))] +public static class RoleOptionsCollectionPatch +{ + /// + /// Set the role chance for custom Launchpad roles based on config + /// + [HarmonyPrefix, HarmonyPatch("GetChancePerGame")] + public static bool GetChancePrefix([HarmonyArgument(0)] RoleTypes roleType, ref int __result) + { + if (CustomRoleManager.GetCustomRoleBehaviour(roleType, out var customRole)) + { + if (customRole.HideSettings) + { + return false; + } + + PluginSingleton.Instance.Config.TryGetEntry(customRole.ChanceConfigDefinition, out var entry); + __result = entry.Value; + return false; + } + + return true; + } + + /// + /// Set the amount for custom Launchpad roles based on config + /// + [HarmonyPrefix, HarmonyPatch("GetNumPerGame")] + public static bool GetNumPrefix([HarmonyArgument(0)] RoleTypes roleType, ref int __result) + { + if (CustomRoleManager.GetCustomRoleBehaviour(roleType, out var customRole)) + { + if (customRole.HideSettings) + { + return false; + } + + PluginSingleton.Instance.Config.TryGetEntry(customRole.NumConfigDefinition, out var entry); + __result = entry.Value; + return false; + } + + return true; + } +} diff --git a/MiraAPI/Patches/Roles/RolesSettingsMenuPatches.cs b/MiraAPI/Patches/Roles/RolesSettingsMenuPatches.cs new file mode 100644 index 0000000..d32b9df --- /dev/null +++ b/MiraAPI/Patches/Roles/RolesSettingsMenuPatches.cs @@ -0,0 +1,149 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using Il2CppInterop.Runtime; +using Il2CppSystem; +using MiraAPI.API.GameOptions; +using MiraAPI.Networking.Options; +using MiraAPI.Roles; +using Reactor.Networking.Rpc; +using Reactor.Utilities.Extensions; +using System.Linq; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(RolesSettingsMenu))] +public static class RolesSettingsMenuPatches +{ + /// + /// Create role buttons and advanced role settings menu for every custom Launchpad role + /// + [HarmonyPrefix, HarmonyPatch("OnEnable")] + public static void OnEnablePrefix(RolesSettingsMenu __instance) + { + var tabPrefab = __instance.AllAdvancedSettingTabs.ToArray()[1].Tab; + foreach (var (key, role) in CustomRoleManager.CustomRoles) + { + if (__instance.AllAdvancedSettingTabs.ToArray().Any(x => (ushort)x.Type == key)) + { + continue; + } + + if (role is ICustomRole { HideSettings: true }) + { + continue; + } + + var numChanceOption = Object.Instantiate(__instance.SettingPrefab, __instance.ItemParent); + numChanceOption.transform.localPosition = new Vector3(0.044f, 0, 0); + numChanceOption.name = role.NiceName; + numChanceOption.Role = role; + __instance.AllRoleSettings.Add(numChanceOption); + + + var newTab = Object.Instantiate(tabPrefab, __instance.AdvancedRolesSettings.transform); + newTab.name = role.NiceName + " Settings"; + var toggleSet = Object.Instantiate(newTab.GetComponentInChildren(true)); + var numberSet = Object.Instantiate(newTab.GetComponentInChildren(true)); + + foreach (var option in newTab.GetComponentsInChildren()) + { + option.gameObject.Destroy(); + } + + var startOffset = 0.5f; + + foreach (var customOption in CustomOptionsManager.CustomOptions) + { + if (customOption.AdvancedRole is not null && customOption.AdvancedRole == role.GetType()) + { + startOffset -= 0.5f; + + switch (customOption) + { + case CustomNumberOption numberOption: + var numOpt = numberOption.CreateNumberOption(numberSet, newTab.transform); + numOpt.transform.localPosition = new Vector3(-1.25f, startOffset, 0); + break; + + case CustomToggleOption toggleOption: + var togOpt = toggleOption.CreateToggleOption(toggleSet, newTab.transform); + togOpt.transform.localPosition = new Vector3(-1.25f, startOffset, 0); + break; + } + } + } + + var tmp = newTab.GetComponentInChildren(); + tmp.defaultStr = role.NiceName; + tmp.TargetText = role.StringName; + + var newAdvSet = new AdvancedRoleSettingsButton + { + Tab = newTab, + Type = (RoleTypes)key + }; + + __instance.AllAdvancedSettingTabs.Add(newAdvSet); + + toggleSet.gameObject.Destroy(); + numberSet.gameObject.Destroy(); + } + } + + /// + /// Set custom role options OnValueChanged and re-enable the role settings menu scrolling + /// + [HarmonyPostfix, HarmonyPatch("Start")] + public static void StartPostfix(RolesSettingsMenu __instance) + { + foreach (var customOption in CustomOptionsManager.CustomOptions) + { + if (customOption.AdvancedRole is not null) + { + customOption.OptionBehaviour.OnValueChanged = (Action)customOption.ValueChanged; + } + } + + var scroll = __instance.GetComponentInChildren(); + scroll.active = true; + scroll.ContentYBounds.max = 2 * __instance.AllRoleSettings.Count / 10f; + + scroll.transform.FindChild("UI_Scrollbar").gameObject.SetActive(true); + } + + /// + /// Update config when value changed + /// + [HarmonyPrefix, HarmonyPatch("ValueChanged")] + public static bool ValueChangedPrefix(RolesSettingsMenu __instance, [HarmonyArgument(0)] OptionBehaviour obj) + { + if (obj.GetIl2CppType() != Il2CppType.Of()) + { + return true; + } + + var roleSetting = obj.Cast(); + + if (roleSetting.Role is not ICustomRole role || role.HideSettings) + { + return true; + } + + MiraAPIPlugin.Instance.Config.TryGetEntry(role.NumConfigDefinition, out var numEntry); + numEntry.Value = roleSetting.RoleMaxCount; + + MiraAPIPlugin.Instance.Config.TryGetEntry(role.ChanceConfigDefinition, out var chanceEntry); + chanceEntry.Value = roleSetting.RoleChance; + + roleSetting.UpdateValuesAndText(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions); + + if (AmongUsClient.Instance.AmHost) + { + Rpc.Instance.Send(new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); + } + GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions; + return false; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Roles/TaskAdderPatch.cs new file mode 100644 index 0000000..4750db3 --- /dev/null +++ b/MiraAPI/Patches/Roles/TaskAdderPatch.cs @@ -0,0 +1,174 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using Il2CppSystem; +using Il2CppSystem.Collections.Generic; +using Il2CppSystem.Text; +using MiraAPI.Roles; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch] +public static class TaskAdderPatch +{ + public static TaskFolder RolesFolder; + + [HarmonyPostfix, HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))] + public static void AddRolesFolder(TaskAdderGame __instance) + { + RolesFolder = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform); + RolesFolder.gameObject.SetActive(false); + RolesFolder.FolderName = "Roles"; + RolesFolder.name = "RolesFolder"; + __instance.Root.SubFolders.Add(RolesFolder); + __instance.GoToRoot(); + } + + [HarmonyPostfix, HarmonyPatch(typeof(TaskAddButton), "Role", MethodType.Setter)] + public static void RoleGetterPatch(TaskAddButton __instance) + { + if (__instance.role is ICustomRole lpRole) + { + if (lpRole.IsOutcast) + { + __instance.FileImage.color = Color.gray; + } + } + + __instance.RolloverHandler.OutColor = __instance.FileImage.color; + } + + // yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry: + [HarmonyPrefix, HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))] + public static bool ShowPatch(TaskAdderGame __instance, [HarmonyArgument(0)] TaskFolder taskFolder) + { + var stringBuilder = new StringBuilder(64); + __instance.Hierarchy.Add(taskFolder); + for (var i = 0; i < __instance.Hierarchy.Count; i++) + { + stringBuilder.Append(__instance.Hierarchy.ToArray()[i].FolderName); + stringBuilder.Append("\\"); + } + __instance.PathText.text = stringBuilder.ToString(); + for (var j = 0; j < __instance.ActiveItems.Count; j++) + { + Object.Destroy(__instance.ActiveItems.ToArray()[j].gameObject); + } + __instance.ActiveItems.Clear(); + var num = 0f; + var num2 = 0f; + var num3 = 0f; + for (var k = 0; k < taskFolder.SubFolders.Count; k++) + { + var taskFolder2 = Object.Instantiate(taskFolder.SubFolders.ToArray()[k], __instance.TaskParent); + var folderTransform = taskFolder2.transform; + + taskFolder2.gameObject.SetActive(true); + taskFolder2.Parent = __instance; + + folderTransform.localPosition = new Vector3(num, num2, 0f); + folderTransform.localScale = Vector3.one; + num3 = Mathf.Max(num3, taskFolder2.Text.bounds.size.y + 1.1f); + num += __instance.folderWidth; + if (num > __instance.lineWidth) + { + num = 0f; + num2 -= num3; + num3 = 0f; + } + __instance.ActiveItems.Add(folderTransform); + if (!taskFolder2 || !taskFolder2.Button) + { + continue; + } + + ControllerManager.Instance.AddSelectableUiElement(taskFolder2.Button); + if (!string.IsNullOrEmpty(__instance.restorePreviousSelectionByFolderName) && taskFolder2.FolderName.Equals(__instance.restorePreviousSelectionByFolderName)) + { + __instance.restorePreviousSelectionFound = taskFolder2.Button; + } + } + + var flag = false; + var list = new List(); + + foreach (var item in taskFolder.Children) + { + list.Add(item); + } + + for (var l = 0; l < list.Count; l++) + { + var taskAddButton = Object.Instantiate(__instance.TaskPrefab); + taskAddButton.MyTask = list.ToArray()[l]; + if (taskAddButton.MyTask.TaskType == TaskTypes.DivertPower) + { + var targetSystem = taskAddButton.MyTask.Cast().TargetSystem; + taskAddButton.Text.text = DestroyableSingleton.Instance.GetString(StringNames.DivertPowerTo, DestroyableSingleton.Instance.GetString(targetSystem)); + } + else if (taskAddButton.MyTask.TaskType == TaskTypes.FixWeatherNode) + { + var nodeId = ((WeatherNodeTask)taskAddButton.MyTask).NodeId; + taskAddButton.Text.text = DestroyableSingleton.Instance.GetString(StringNames.FixWeatherNode, Array.Empty()) + " " + DestroyableSingleton.Instance.GetString(WeatherSwitchGame.ControlNames[nodeId], + Array.Empty()); + } + else + { + taskAddButton.Text.text = DestroyableSingleton.Instance.GetString(taskAddButton.MyTask.TaskType); + } + __instance.AddFileAsChild(taskFolder, taskAddButton, ref num, ref num2, ref num3); + if (taskAddButton != null && taskAddButton.Button != null) + { + ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); + if (__instance.Hierarchy.Count != 1 && !flag) + { + var component = ControllerManager.Instance.CurrentUiState.CurrentSelection.GetComponent(); + if (component != null) + { + __instance.restorePreviousSelectionByFolderName = component.FolderName; + } + ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button); + flag = true; + } + } + } + + if (taskFolder.FolderName == "Roles") // idk why only this works??? + { + for (var m = 0; m < DestroyableSingleton.Instance.AllRoles.Length; m++) + { + var roleBehaviour = DestroyableSingleton.Instance.AllRoles[m]; + if (roleBehaviour.Role != RoleTypes.ImpostorGhost && roleBehaviour.Role != RoleTypes.CrewmateGhost && !roleBehaviour.IsDead) + { + var taskAddButton2 = Object.Instantiate(__instance.RoleButton); + taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; + taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; + __instance.AddFileAsChild(RolesFolder, taskAddButton2, ref num, ref num2, ref num3); + taskAddButton2.Role = roleBehaviour; + if (taskAddButton2 != null && taskAddButton2.Button != null) + { + ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); + if (m == 0 && __instance.restorePreviousSelectionFound != null) + { + ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound); + __instance.restorePreviousSelectionByFolderName = string.Empty; + __instance.restorePreviousSelectionFound = null; + } + else if (m == 0) + { + ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); + } + } + } + } + } + if (__instance.Hierarchy.Count == 1) + { + ControllerManager.Instance.SetBackButton(__instance.BackButton); + return false; + } + ControllerManager.Instance.SetBackButton(__instance.FolderBackButton); + return false; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/TaskPanelPatch.cs b/MiraAPI/Patches/Roles/TaskPanelPatch.cs new file mode 100644 index 0000000..24c2a75 --- /dev/null +++ b/MiraAPI/Patches/Roles/TaskPanelPatch.cs @@ -0,0 +1,58 @@ +using HarmonyLib; +using UnityEngine; + +namespace MiraAPI.Patches.Roles; + +[HarmonyPatch(typeof(TaskPanelBehaviour))] +public static class TaskPanelPatch +{ + /// + /// This patch is to override the automatic updating of the y position on the tab (which is in base game), + /// because I can't change the custom tab y pos if it's being overriden every frame. + /// Im sure there is an easier/better way, but this is the fix that worked for me + /// + [HarmonyPrefix] + [HarmonyPatch(nameof(TaskPanelBehaviour.Update))] + public static bool UpdatePrefix(TaskPanelBehaviour __instance) + { + if (__instance.gameObject.name != "RolePanel") + { + return true; + } + + var transform = __instance.background.transform; + var vector = __instance.background.sprite.bounds.extents; + var vector2 = __instance.tab.sprite.bounds.extents; + + transform.localScale = __instance.taskText.textBounds.size.x > 0f ? new Vector3(__instance.taskText.textBounds.size.x + 0.4f, __instance.taskText.textBounds.size.y + 0.3f, 1f) : Vector3.zero; + + vector.y = -vector.y; + vector = vector.Mul(transform.localScale); + __instance.background.transform.localPosition = vector; + + vector2 = vector2.Mul(__instance.tab.transform.localScale); + vector2.y = -vector2.y; + vector2.x += vector.x * 2f; + __instance.tab.transform.localPosition = vector2; + + if (GameManager.Instance == null) + { + return false; + } + var closePosition = new Vector3(-__instance.background.sprite.bounds.size.x * __instance.background.transform.localScale.x, __instance.closedPosition.y, __instance.closedPosition.z); + __instance.closedPosition = closePosition; + if (__instance.open) + { + __instance.timer = Mathf.Min(1f, __instance.timer + Time.deltaTime / __instance.animationTimeSeconds); + } + else + { + __instance.timer = Mathf.Max(0f, __instance.timer - Time.deltaTime / __instance.animationTimeSeconds); + } + Vector3 relativePos; + relativePos = new Vector3(Mathf.SmoothStep(__instance.closedPosition.x, __instance.openPosition.x, __instance.timer), Mathf.SmoothStep(__instance.closedPosition.y, __instance.openPosition.y, __instance.timer), __instance.openPosition.z); + __instance.transform.localPosition = AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos); + return false; + } + +} \ No newline at end of file diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs new file mode 100644 index 0000000..76f4578 --- /dev/null +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -0,0 +1,155 @@ +using AmongUs.GameOptions; +using Il2CppInterop.Runtime; +using MiraAPI.Networking.Options; +using Reactor.Localization.Utilities; +using Reactor.Networking.Rpc; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using TMPro; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.Roles; + +public static class CustomRoleManager +{ + public static readonly Dictionary CustomRoles = []; + + public static void RegisterInRoleManager() + { + RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray(); + } + + internal static void RegisterRole(Type roleType) + { + if (!(typeof(RoleBehaviour).IsAssignableFrom(roleType) && typeof(ICustomRole).IsAssignableFrom(roleType))) + { + return; + } + + if (CustomRoles.Any(behaviour => behaviour.GetType() == roleType)) + { + return; + } + + var roleBehaviour = (RoleBehaviour)new GameObject(roleType.Name).DontDestroy().AddComponent(Il2CppType.From(roleType)); + + if (roleBehaviour is not ICustomRole customRole) + { + return; + } + + roleBehaviour.Role = (RoleTypes)customRole.RoleId; + roleBehaviour.TeamType = customRole.Team; + roleBehaviour.NameColor = customRole.RoleColor; + roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName); + roleBehaviour.BlurbName = CustomStringName.CreateAndRegister(customRole.RoleDescription); + roleBehaviour.BlurbNameLong = CustomStringName.CreateAndRegister(customRole.RoleLongDescription); + roleBehaviour.AffectedByLightAffectors = customRole.AffectedByLight; + roleBehaviour.CanBeKilled = customRole.CanGetKilled; + roleBehaviour.CanUseKillButton = customRole.CanKill; + roleBehaviour.TasksCountTowardProgress = customRole.TasksCount; + roleBehaviour.CanVent = customRole.CanUseVent; + roleBehaviour.DefaultGhostRole = customRole.GhostRole; + roleBehaviour.MaxCount = 15; + + if (customRole.IsGhostRole) + { + RoleManager.GhostRoles.Add(roleBehaviour.Role); + } + + CustomRoles.Add(customRole.RoleId, roleBehaviour); + + if (customRole.HideSettings) + { + return; + } + + var config = PluginSingleton.Instance.Config; + config.Bind(customRole.NumConfigDefinition, 1); + config.Bind(customRole.ChanceConfigDefinition, 100); + + } + + public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole result) + { + CustomRoles.TryGetValue((ushort)roleType, out var temp); + if (temp is ICustomRole role) + { + result = role; + return true; + } + + result = null; + return false; + } + + public static TaskPanelBehaviour CreateRoleTab(ICustomRole role) + { + var ogPanel = HudManager.Instance.TaskStuff.transform.FindChild("TaskPanel").gameObject.GetComponent(); + var clonePanel = Object.Instantiate(ogPanel.gameObject, ogPanel.transform.parent); + clonePanel.name = "RolePanel"; + + var newPanel = clonePanel.GetComponent(); + newPanel.open = false; + + var tab = newPanel.tab.gameObject; + tab.GetComponentInChildren().Destroy(); + + newPanel.transform.localPosition = ogPanel.transform.localPosition - new Vector3(0, 1, 0); + + UpdateRoleTab(newPanel, role); + return newPanel; + } + + public static void UpdateRoleTab(TaskPanelBehaviour panel, ICustomRole role) + { + var tabText = panel.tab.gameObject.GetComponentInChildren(); + var ogPanel = HudManager.Instance.TaskStuff.transform.FindChild("TaskPanel").gameObject.GetComponent(); + if (tabText.text != role.RoleName) + { + tabText.text = role.RoleName; + } + + var y = ogPanel.taskText.textBounds.size.y + 1; + panel.closedPosition = new Vector3(ogPanel.closedPosition.x, ogPanel.open ? y + 0.2f : 2f, ogPanel.closedPosition.z); + panel.openPosition = new Vector3(ogPanel.openPosition.x, ogPanel.open ? y : 2f, ogPanel.openPosition.z); + + panel.SetTaskText(role.SetTabText().ToString()); + } + + public static void SyncRoleSettings() + { + foreach (var role in CustomRoles.Values.Select(x => (ICustomRole)x)) + { + if (role.HideSettings) + { + continue; + } + + PluginSingleton.Instance.Config.TryGetEntry(role.NumConfigDefinition, out var numEntry); + PluginSingleton.Instance.Config.TryGetEntry(role.ChanceConfigDefinition, out var chanceEntry); + + Rpc.Instance.Send(new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); + } + } + + public static void SyncRoleSettings(int targetId) + { + foreach (var role in CustomRoles.Values.Select(x => (ICustomRole)x)) + { + if (role.HideSettings) + { + continue; + } + + PluginSingleton.Instance.Config.TryGetEntry(role.NumConfigDefinition, out var numEntry); + PluginSingleton.Instance.Config.TryGetEntry(role.ChanceConfigDefinition, out var chanceEntry); + + Rpc.Instance.SendTo(targetId, new SyncRoleOptionsRpc.Data(role.RoleId, numEntry.Value, chanceEntry.Value)); + } + } +} \ No newline at end of file diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs new file mode 100644 index 0000000..9538eab --- /dev/null +++ b/MiraAPI/Roles/ICustomRole.cs @@ -0,0 +1,68 @@ +using AmongUs.GameOptions; +using BepInEx.Configuration; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using System.Text; +using UnityEngine; + +namespace MiraAPI.Roles; + +public interface ICustomRole +{ + string RoleName { get; } + + ushort RoleId { get; } + + string RoleDescription { get; } + + string RoleLongDescription { get; } + + Color RoleColor { get; } + + RoleTeamTypes Team { get; } + + LoadableAsset Icon => MiraAssets.Empty; + + ConfigDefinition NumConfigDefinition => new("Roles", $"Num{RoleName}"); + + ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance{RoleName}"); + + bool AffectedByLight => Team == RoleTeamTypes.Crewmate; + + bool CanGetKilled => Team == RoleTeamTypes.Crewmate; + + bool IsOutcast => false; + + bool CanKill => Team == RoleTeamTypes.Impostor; + + bool CanUseVent => Team == RoleTeamTypes.Impostor; + + bool TasksCount => Team == RoleTeamTypes.Crewmate; + + bool IsGhostRole => false; + + bool TargetsBodies => false; + + bool CreateCustomTab => true; + + bool HideSettings => IsGhostRole; + + RoleTypes GhostRole => Team == RoleTeamTypes.Crewmate ? RoleTypes.CrewmateGhost : RoleTypes.ImpostorGhost; + + void CreateOptions() { } + + void PlayerControlFixedUpdate(PlayerControl playerControl) { } + + void HudUpdate(HudManager hudManager) { } + + string GetCustomEjectionMessage(GameData.PlayerInfo player) + { + return Team == RoleTeamTypes.Impostor ? $"{player.PlayerName} was The {RoleName}" : null; + } + + StringBuilder SetTabText() + { + var taskStringBuilder = Helpers.CreateForRole(this); + return taskStringBuilder; + } +} \ No newline at end of file diff --git a/MiraAPI/Roles/RegisterCustomRoleAttribute.cs b/MiraAPI/Roles/RegisterCustomRoleAttribute.cs new file mode 100644 index 0000000..7ef6b68 --- /dev/null +++ b/MiraAPI/Roles/RegisterCustomRoleAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MiraAPI.Roles; + +[AttributeUsage(AttributeTargets.Class)] +public class RegisterCustomRoleAttribute : Attribute +{ + private static readonly HashSet RegisteredAssemblies = []; + + public static void Register(Assembly assembly) + { + if (!RegisteredAssemblies.Add(assembly)) return; + + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttribute(); + if (attribute != null) + { + CustomRoleManager.RegisterRole(type); + } + } + } +} \ No newline at end of file diff --git a/MiraAPI/Utilities/Assets/LoadableAsset.cs b/MiraAPI/Utilities/Assets/LoadableAsset.cs new file mode 100644 index 0000000..537aa38 --- /dev/null +++ b/MiraAPI/Utilities/Assets/LoadableAsset.cs @@ -0,0 +1,8 @@ +namespace MiraAPI.Utilities +{ + public abstract class LoadableAsset where T : UnityEngine.Object + { + protected T _loadedAsset; + public abstract T LoadAsset(); + } +} diff --git a/MiraAPI/Utilities/Assets/LoadableBundleAsset.cs b/MiraAPI/Utilities/Assets/LoadableBundleAsset.cs new file mode 100644 index 0000000..a1ace01 --- /dev/null +++ b/MiraAPI/Utilities/Assets/LoadableBundleAsset.cs @@ -0,0 +1,20 @@ +using Reactor.Utilities.Extensions; +using System; +using UnityEngine; + +namespace MiraAPI.Utilities +{ + public class LoadableBundleAsset(string name, AssetBundle bundle) : LoadableAsset where T : UnityEngine.Object + { + public override T LoadAsset() + { + if (_loadedAsset != null) + { + return _loadedAsset; + } + + return _loadedAsset = bundle.LoadAsset(name); + throw new Exception($"INVALID ASSET: {name}"); + } + } +} diff --git a/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs b/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs new file mode 100644 index 0000000..0288b6e --- /dev/null +++ b/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs @@ -0,0 +1,19 @@ +using System; +using UnityEngine; + +namespace MiraAPI.Utilities.Assets +{ + public class LoadableResourceAsset(string name, string resourcesFolder) : LoadableAsset + { + public override Sprite LoadAsset() + { + if (_loadedAsset != null) + { + return _loadedAsset; + } + + return _loadedAsset = SpriteTools.LoadSpriteFromPath(resourcesFolder + name); + throw new Exception($"INVALID ASSET: {name}"); + } + } +} diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs new file mode 100644 index 0000000..5957b9d --- /dev/null +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -0,0 +1,7 @@ +namespace MiraAPI.Utilities.Assets +{ + public static class MiraAssets + { + public static readonly LoadableResourceAsset Empty = new("", ""); + } +} diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs new file mode 100644 index 0000000..4d016c9 --- /dev/null +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -0,0 +1,39 @@ +using Il2CppInterop.Runtime; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Reactor.Utilities.Extensions; +using System; +using System.Reflection; +using UnityEngine; + +namespace MiraAPI.Utilities.Assets +{ + public static class SpriteTools + { + private delegate bool DLoadImage(IntPtr tex, IntPtr data, bool markNonReadable); + + private static DLoadImage _iCallLoadImage; + + private static bool LoadImage(Texture2D tex, byte[] data, bool markNonReadable) + { + _iCallLoadImage ??= IL2CPP.ResolveICall("UnityEngine.ImageConversion::LoadImage"); + + var il2CPPArray = (Il2CppStructArray)data; + + return _iCallLoadImage.Invoke(tex.Pointer, il2CPPArray.Pointer, markNonReadable); + } + + public static Sprite LoadSpriteFromPath(string resourcePath) + { + var tex = new Texture2D(1, 1, TextureFormat.ARGB32, false); + var assembly = Assembly.GetExecutingAssembly(); + var myStream = assembly.GetManifestResourceStream(resourcePath); + if (myStream != null) + { + var buttonTexture = myStream.ReadFully(); + LoadImage(tex, buttonTexture, false); + } + + return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f)); + } + } +} diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs new file mode 100644 index 0000000..00a4c7d --- /dev/null +++ b/MiraAPI/Utilities/Extensions.cs @@ -0,0 +1,86 @@ +using MiraAPI.Roles; +using Reactor.Utilities.Extensions; +using System.Linq; +using UnityEngine; + +namespace MiraAPI.Utilities +{ + public static class Extensions + { + public static void UpdateBodies(this PlayerControl playerControl, Color outlineColor, ref DeadBody target) + { + foreach (var body in Object.FindObjectsOfType()) + { + foreach (var bodyRenderer in body.bodyRenderers) + { + bodyRenderer.SetOutline(null); + } + } + + if (playerControl.Data.Role is not ICustomRole { TargetsBodies: true }) + { + return; + } + + target = playerControl.NearestDeadBody(); + if (!target) + { + return; + } + + foreach (var renderer in target.bodyRenderers) + { + renderer.SetOutline(outlineColor); + } + } + + public static DeadBody NearestDeadBody(this PlayerControl playerControl) + { + var results = new Il2CppSystem.Collections.Generic.List(); + Physics2D.OverlapCircle(playerControl.GetTruePosition(), playerControl.MaxReportDistance / 4f, Helpers.Filter, results); + return results.ToArray() + .Where(collider2D => collider2D.CompareTag("DeadBody")) + .Select(collider2D => collider2D.GetComponent()) + .FirstOrDefault(component => component && !component.Reported); + } + + public static PlayerControl GetClosestPlayer(this PlayerControl playerControl, bool includeImpostors, float distance) + { + PlayerControl result = null; + if (!ShipStatus.Instance) + { + return null; + } + + var truePosition = playerControl.GetTruePosition(); + + foreach (var playerInfo in GameData.Instance.AllPlayers) + { + if (playerInfo.Disconnected || playerInfo.PlayerId == playerControl.PlayerId || + playerInfo.IsDead || !includeImpostors && playerInfo.Role.IsImpostor) + { + continue; + } + + var @object = playerInfo.Object; + if (!@object) + { + continue; + } + + var vector = @object.GetTruePosition() - truePosition; + var magnitude = vector.magnitude; + if (!(magnitude <= distance) || PhysicsHelpers.AnyNonTriggersBetween(truePosition, + vector.normalized, + magnitude, LayerMask.GetMask("Ship", "Objects"))) + { + continue; + } + + result = @object; + distance = magnitude; + } + return result; + } + } +} diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs new file mode 100644 index 0000000..2eb2130 --- /dev/null +++ b/MiraAPI/Utilities/Helpers.cs @@ -0,0 +1,37 @@ +using MiraAPI.Roles; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace MiraAPI.Utilities +{ + public static class Helpers + { + public static readonly ContactFilter2D Filter = ContactFilter2D.CreateLegacyFilter(Constants.NotShipMask, float.MinValue, float.MaxValue); + + public static PlainShipRoom GetRoom(Vector3 pos) + { + return ShipStatus.Instance.AllRooms.ToList().Find(room => room.roomArea.OverlapPoint(pos)); + } + + public static string GetSuffix(NumberSuffixes suffix) + { + return suffix switch + { + NumberSuffixes.None => string.Empty, + NumberSuffixes.Multiplier => "x", + NumberSuffixes.Seconds => "s", + _ => string.Empty + }; + } + + public static StringBuilder CreateForRole(ICustomRole role) + { + var taskStringBuilder = new StringBuilder(); + taskStringBuilder.AppendLine($"{role.RoleColor.ToTextColor()}You are a {role.RoleName}."); + taskStringBuilder.Append(""); + taskStringBuilder.AppendLine($"{role.RoleLongDescription}"); + return taskStringBuilder; + } + } +}