From 947a202c5596c4b15c9cad9dd557d09bac8aa30a Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 18 May 2024 00:10:07 -0400 Subject: [PATCH] Attribute based options API (wip) --- MiraAPI.Example/ExOptions.cs | 9 -- MiraAPI.Example/ExampleOptions.cs | 14 ++ MiraAPI.Example/ExamplePlugin.cs | 12 +- MiraAPI.Example/SomeRandomNonGroupClass.cs | 9 ++ .../Attributes/ModdedNumberOptionAttribute.cs | 44 +++++ .../Attributes/ModdedOptionAttribute.cs | 39 +++++ .../Attributes/ModdedStringOptionAttribute.cs | 33 ++++ .../Attributes/ModdedToggleOptionAttribute.cs | 33 ++++ .../RegisterModdedOptionAttribute.cs | 41 ----- MiraAPI/GameOptions/IModdedOption.cs | 16 +- MiraAPI/GameOptions/IModdedOptionGroup.cs | 13 ++ MiraAPI/GameOptions/ModdedGroupSingleton.cs | 20 +++ MiraAPI/GameOptions/ModdedOptionGroup.cs | 15 ++ MiraAPI/GameOptions/ModdedOptionsManager.cs | 121 +++++++++----- .../OptionTypes/ModdedNumberOption.cs | 64 ++++++++ .../GameOptions/OptionTypes/ModdedOption.cs | 31 ++-- .../OptionTypes/ModdedStringOption.cs | 49 ++++++ .../OptionTypes/ModdedToggleOption.cs | 32 +++- MiraAPI/GameOptions/UI/ModdedOptionsTab.cs | 153 ++++++++++++++++++ MiraAPI/IMiraConfig.cs | 16 ++ MiraAPI/MiraAPI.csproj | 4 - MiraAPI/MiraAPIPlugin.cs | 4 +- MiraAPI/Patches/HudManagerPatches.cs | 61 ++++++- .../Patches/Options/GameOptionsMenuPatch.cs | 109 +++++++++++++ .../Options/GameSettingsMenuPatches.cs | 68 ++++++++ MiraAPI/Patches/Options/HudStringPatch.cs | 118 ++++++++++++++ .../Utilities/Assets/LoadableResourceAsset.cs | 14 +- 27 files changed, 1024 insertions(+), 118 deletions(-) delete mode 100644 MiraAPI.Example/ExOptions.cs create mode 100644 MiraAPI.Example/ExampleOptions.cs create mode 100644 MiraAPI.Example/SomeRandomNonGroupClass.cs create mode 100644 MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs create mode 100644 MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs create mode 100644 MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs create mode 100644 MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs delete mode 100644 MiraAPI/GameOptions/Attributes/RegisterModdedOptionAttribute.cs create mode 100644 MiraAPI/GameOptions/IModdedOptionGroup.cs create mode 100644 MiraAPI/GameOptions/ModdedGroupSingleton.cs create mode 100644 MiraAPI/GameOptions/ModdedOptionGroup.cs create mode 100644 MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs create mode 100644 MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs create mode 100644 MiraAPI/GameOptions/UI/ModdedOptionsTab.cs create mode 100644 MiraAPI/IMiraConfig.cs create mode 100644 MiraAPI/Patches/Options/GameOptionsMenuPatch.cs create mode 100644 MiraAPI/Patches/Options/GameSettingsMenuPatches.cs create mode 100644 MiraAPI/Patches/Options/HudStringPatch.cs diff --git a/MiraAPI.Example/ExOptions.cs b/MiraAPI.Example/ExOptions.cs deleted file mode 100644 index 487ac02..0000000 --- a/MiraAPI.Example/ExOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MiraAPI.GameOptions.Attributes; - -namespace MiraAPI.Example -{ - public class ExOptions - { - [RegisterModdedOption("Use Thing")] public bool useThing = false; - } -} diff --git a/MiraAPI.Example/ExampleOptions.cs b/MiraAPI.Example/ExampleOptions.cs new file mode 100644 index 0000000..0ee7d49 --- /dev/null +++ b/MiraAPI.Example/ExampleOptions.cs @@ -0,0 +1,14 @@ +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example +{ + public class ExampleOptions : IModdedOptionGroup + { + public string GroupName => "General Group"; + [ModdedToggleOption("Use Thing")] public bool useThing { get; set; } = true; + [ModdedToggleOption("Use another thing")] public bool useAnotherThing { get; set; } = false; + [ModdedNumberOption("Sussy level", min: 0, max: 10)] public float sussyLevel { get; set; } = 4f; + [ModdedStringOption("Sus choices", ["hello", "hello 2", "hello 3"])] public int susChoices { get; set; } = 0; + } +} diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index ed9a112..3371c5d 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -1,6 +1,7 @@ using BepInEx; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.Utilities.Assets; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; @@ -11,14 +12,19 @@ namespace MiraAPI.Example; [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] [ReactorModFlags(ModFlags.RequireOnAllClients)] -public partial class ExamplePlugin : BasePlugin +public partial class ExamplePlugin : BasePlugin, IMiraConfig { public Harmony Harmony { get; } = new(Id); public static ExamplePlugin Instance { get; private set; } + public ModdedOptionTabSettings TabSettings => new ModdedOptionTabSettings() + { + Title = "Example Mod", + TabIcon = MiraAssets.Empty + }; + public override void Load() { Instance = this; Harmony.PatchAll(); - } -} +} \ No newline at end of file diff --git a/MiraAPI.Example/SomeRandomNonGroupClass.cs b/MiraAPI.Example/SomeRandomNonGroupClass.cs new file mode 100644 index 0000000..76504f9 --- /dev/null +++ b/MiraAPI.Example/SomeRandomNonGroupClass.cs @@ -0,0 +1,9 @@ +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example +{ + public class SomeRandomNonGroupClass + { + [ModdedStringOption("Yeah, idk", ["Idk 1", "idk 2", "idk 3", "idk 4"])] public static int yeaIdk { get; set; } = 2; + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs new file mode 100644 index 0000000..fe0943d --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -0,0 +1,44 @@ +using MiraAPI.GameOptions.OptionTypes; +using System; +using System.Reflection; +using UnityEngine; + +namespace MiraAPI.GameOptions.Attributes +{ + public class ModdedNumberOptionAttribute : ModdedOptionAttribute + { + public float Min { get; private set; } + public float Max { get; private set; } + public float Increment { get; private set; } + public NumberSuffixes SuffixType { get; private set; } + public bool ZeroInfinity { get; private set; } + + public ModdedNumberOptionAttribute(string title, float min, float max, float increment = 1, NumberSuffixes suffixType = NumberSuffixes.None, bool zeroInfinity = false, Type roleType = null) : base(title, roleType) + { + Min = min; + Max = max; + Increment = increment; + SuffixType = suffixType; + ZeroInfinity = zeroInfinity; + } + + public override IModdedOption CreateOption(object value, PropertyInfo property) + { + Debug.LogError(value.ToString()); + var toggleOpt = new ModdedNumberOption(Title, (float)value, Min, Max, Increment, SuffixType, ZeroInfinity, RoleType); + return toggleOpt; + } + + public override void SetValue(object value) + { + ModdedNumberOption toggleOpt = HolderOption as ModdedNumberOption; + toggleOpt.SetValue((float)value); + } + + public override object GetValue() + { + ModdedNumberOption toggleOpt = HolderOption as ModdedNumberOption; + return toggleOpt.Value; + } + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs new file mode 100644 index 0000000..e2379b5 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedOptionAttribute.cs @@ -0,0 +1,39 @@ +using System; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public abstract class ModdedOptionAttribute : Attribute + { + public IModdedOption HolderOption { get; set; } + public string Title { get; private set; } + public Type RoleType { get; private set; } + public ModdedOptionAttribute(string title, Type roleType = null) + { + Title = title; + RoleType = roleType; + } + + public static void Register(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + { + foreach (PropertyInfo property in type.GetProperties()) + { + foreach (var attribute in property.GetCustomAttributes()) + { + if (attribute.GetType().BaseType == typeof(ModdedOptionAttribute)) + { + ModdedOptionsManager.RegisterOption(assembly, type, (ModdedOptionAttribute)attribute, property); + } + } + } + } + } + + public abstract void SetValue(object val); + public abstract object GetValue(); + public abstract IModdedOption CreateOption(object value, PropertyInfo property); + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs new file mode 100644 index 0000000..d9cfeb7 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedStringOptionAttribute.cs @@ -0,0 +1,33 @@ +using MiraAPI.GameOptions.OptionTypes; +using System; +using System.Reflection; + +namespace MiraAPI.GameOptions.Attributes +{ + public class ModdedStringOptionAttribute : ModdedOptionAttribute + { + public string[] Values; + public ModdedStringOptionAttribute(string title, string[] strings, Type roleType = null) : base(title, roleType) + { + Values = strings; + } + + public override IModdedOption CreateOption(object value, PropertyInfo property) + { + var opt = new ModdedStringOption(Title, (int)value, Values, RoleType); + return opt; + } + + public override void SetValue(object value) + { + ModdedStringOption opt = HolderOption as ModdedStringOption; + opt.SetValue((int)value); + } + + public override object GetValue() + { + ModdedStringOption opt = HolderOption as ModdedStringOption; + return opt.Value; + } + } +} diff --git a/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs new file mode 100644 index 0000000..5e0ea54 --- /dev/null +++ b/MiraAPI/GameOptions/Attributes/ModdedToggleOptionAttribute.cs @@ -0,0 +1,33 @@ +using MiraAPI.GameOptions.OptionTypes; +using System; +using System.Reflection; +using UnityEngine; + +namespace MiraAPI.GameOptions.Attributes +{ + public class ModdedToggleOptionAttribute : ModdedOptionAttribute + { + public ModdedToggleOptionAttribute(string title, Type roleType = null) : base(title, roleType) + { + } + + public override IModdedOption CreateOption(object value, PropertyInfo property) + { + Debug.LogError(value.ToString()); + var toggleOpt = new ModdedToggleOption(Title, (bool)value, RoleType); + return toggleOpt; + } + + public override void SetValue(object value) + { + ModdedToggleOption toggleOpt = HolderOption as ModdedToggleOption; + toggleOpt.SetValue((bool)value); + } + + public override object GetValue() + { + ModdedToggleOption toggleOpt = HolderOption as ModdedToggleOption; + return toggleOpt.Value; + } + } +} diff --git a/MiraAPI/GameOptions/Attributes/RegisterModdedOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/RegisterModdedOptionAttribute.cs deleted file mode 100644 index b2e7b79..0000000 --- a/MiraAPI/GameOptions/Attributes/RegisterModdedOptionAttribute.cs +++ /dev/null @@ -1,41 +0,0 @@ -using BepInEx.Unity.IL2CPP; -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace MiraAPI.GameOptions.Attributes -{ - [AttributeUsage(AttributeTargets.Property)] - public class RegisterModdedOptionAttribute : Attribute - { - private static readonly HashSet RegisteredAssemblies = []; - - public string Title { get; private set; } - public RegisterModdedOptionAttribute(string title) - { - Title = title; - } - - 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()) - { - foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public)) - { - var attribute = property.GetCustomAttribute(); - if (attribute != null) - { - ModdedOptionsManager.Instance.CreateOption(type, attribute, property); - } - } - } - } - } -} diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 205a142..d5628a0 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -1,7 +1,19 @@ -namespace MiraAPI.GameOptions +using System; +using UnityEngine; + +namespace MiraAPI.GameOptions { public interface IModdedOption { - + public Type AdvancedRole { get; } + public OptionBehaviour OptionBehaviour { get; protected set; } + public string Title { get; } + public StringNames StringName { get; } + public Func Visible { get; set; } + public ModdedOptionGroup Group { get; set; } + public IMiraConfig ParentMod { get; set; } + public void ValueChanged(OptionBehaviour optionBehaviour); + public string GetHudStringText(); + public OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container); } } diff --git a/MiraAPI/GameOptions/IModdedOptionGroup.cs b/MiraAPI/GameOptions/IModdedOptionGroup.cs new file mode 100644 index 0000000..41c0d36 --- /dev/null +++ b/MiraAPI/GameOptions/IModdedOptionGroup.cs @@ -0,0 +1,13 @@ +using System; +using UnityEngine; + +namespace MiraAPI.GameOptions +{ + public interface IModdedOptionGroup + { + public string GroupName { get; } + public Func GroupVisible => () => true; + public Color GroupColor => Color.white; + public Type AdvancedRole => null; + } +} diff --git a/MiraAPI/GameOptions/ModdedGroupSingleton.cs b/MiraAPI/GameOptions/ModdedGroupSingleton.cs new file mode 100644 index 0000000..07dd84e --- /dev/null +++ b/MiraAPI/GameOptions/ModdedGroupSingleton.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; + +namespace MiraAPI.GameOptions +{ + public class ModdedGroupSingleton where T : ModdedOptionGroup + { + private static T _instance; + + public static T Instance + { + get => _instance ??= ModdedOptionsManager.Groups.OfType().Single(); + set + { + if (_instance != null) throw new InvalidOperationException($"Instance for {typeof(T)} is already set"); + _instance = value; + } + } + } +} diff --git a/MiraAPI/GameOptions/ModdedOptionGroup.cs b/MiraAPI/GameOptions/ModdedOptionGroup.cs new file mode 100644 index 0000000..6e7742b --- /dev/null +++ b/MiraAPI/GameOptions/ModdedOptionGroup.cs @@ -0,0 +1,15 @@ +using System; +using UnityEngine; + +namespace MiraAPI.GameOptions +{ + public class ModdedOptionGroup + { + public IMiraConfig ParentMod; + public GameObject Header; + public string GroupName; + public Func GroupVisible; + public Color GroupColor; + public Type AdvancedRole; + } +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 16637cd..5ea9378 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -1,6 +1,6 @@ -using HarmonyLib; +using BepInEx.Unity.IL2CPP; +using HarmonyLib; using MiraAPI.GameOptions.Attributes; -using MiraAPI.GameOptions.OptionTypes; using Reactor.Utilities; using System; using System.Collections.Generic; @@ -12,62 +12,101 @@ namespace MiraAPI.GameOptions { public class ModdedOptionsManager { - public static ModdedOptionsManager Instance; - public List Options = new List(); + public static List Options = new(); + public static List Groups = new(); + private static Dictionary OptionAttributes = new(); + private static Dictionary OriginalTypes = new(); + public static Dictionary RegisteredMods = new(); - private Dictionary PatchedOptions = new Dictionary(); - - public void CreateOption(Type type, RegisterModdedOptionAttribute attribute, PropertyInfo property) + public static void Initialize() { - Debug.LogError(property.GetConstantValue().GetType().ToString()); - - switch (property.GetConstantValue()) + IL2CPPChainloader.Instance.PluginLoad += (_, assembly, plugin) => { - case bool: - CreateToggleOption(type, attribute, property); - break; - default: - Debug.LogError($"Mira API does not support type {property.GetConstantValue().GetType()}"); - break; - } + if (plugin.GetType().GetInterfaces().Contains(typeof(IMiraConfig))) + { + IMiraConfig config = (IMiraConfig)Activator.CreateInstance(plugin.GetType()); + RegisteredMods.Add(assembly, config); + } + + RegisterOptionGroups(assembly); + + ModdedOptionAttribute.Register(assembly); + }; } - public static void PropertySetterPatch(MethodBase __originalMethod, object[] args) + private static void RegisterOptionGroups(Assembly assembly) { - IModdedOption opt = Instance.PatchedOptions.First(pair => pair.Key.GetSetMethod().Equals(__originalMethod)).Value; - - if (opt != null) + foreach (var type in assembly.GetTypes()) { - object value = args[0]; - switch (value) + if (typeof(IModdedOptionGroup).IsAssignableFrom(type)) { - case bool: - ModdedToggleOption toggleOpt = opt as ModdedToggleOption; - toggleOpt.SetValue((bool)value); - break; + IModdedOptionGroup group = (IModdedOptionGroup)Activator.CreateInstance(type); + ModdedOptionGroup newGroup = new ModdedOptionGroup() + { + AdvancedRole = group.AdvancedRole, + GroupColor = group.GroupColor, + GroupName = group.GroupName, + GroupVisible = group.GroupVisible + }; + + Groups.Add(newGroup); + OriginalTypes.Add(type, newGroup); + + newGroup.ParentMod = RegisteredMods[assembly]; } } } - public ModdedToggleOption CreateToggleOption(Type type, RegisterModdedOptionAttribute attribute, PropertyInfo property) + public static void RegisterOption(Assembly assembly, Type type, ModdedOptionAttribute attribute, PropertyInfo property) { - var toggleOpt = new ModdedToggleOption(attribute.Title, (bool)property.GetValue(type)); - Options.Add(toggleOpt); + if (OptionAttributes.ContainsKey(property)) return; + object newObj = Activator.CreateInstance(type); + IModdedOption result = attribute.CreateOption(property.GetValue(newObj), property); - var setterOriginal = property.GetSetMethod(); - var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); - PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); - /* - var getterOriginal = property.GetGetMethod(); - var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); - PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));*/ + if (result != null) + { + var setterOriginal = property.GetSetMethod(); + var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch)); + PluginSingleton.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch)); - toggleOpt.PropertyInfo = property; + var getterOriginal = property.GetGetMethod(); + var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch)); + PluginSingleton.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch)); - if (!PatchedOptions.ContainsKey(property)) - PatchedOptions.Add(property, toggleOpt); + attribute.HolderOption = result; + result.ParentMod = RegisteredMods[assembly]; - return toggleOpt; + Options.Add(result); + OptionAttributes.Add(property, attribute); + + if (OriginalTypes.ContainsKey(type)) + { + Debug.LogError($"grouping {attribute.Title} with {OriginalTypes[type].GroupName}"); + result.Group = OriginalTypes[type]; + } + } + } + + public static void PropertySetterPatch(MethodBase __originalMethod, object value) + { + ModdedOptionAttribute attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod().Equals(__originalMethod)).Value; + + if (attribute != null) + { + attribute.SetValue(value); + } + } + + public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result) + { + ModdedOptionAttribute attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod().Equals(__originalMethod)).Value; + + if (attribute != null) + { + __result = attribute.GetValue(); + return false; + } + return true; } } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs new file mode 100644 index 0000000..cf10468 --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -0,0 +1,64 @@ +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.GameOptions.OptionTypes +{ + public class ModdedNumberOption : ModdedOption + { + public float Min { get; private set; } + public float Max { get; private set; } + public float Increment { get; private set; } + public NumberSuffixes SuffixType { get; private set; } + public bool ZeroInfinity { get; private set; } + public ModdedNumberOption(string title, float defaultValue, float min, float max, float increment, NumberSuffixes suffixType, bool zeroInfinity, System.Type roleType) : base(title, defaultValue, roleType) + { + Min = min; + Max = max; + Increment = increment; + SuffixType = suffixType; + ZeroInfinity = zeroInfinity; + + Value = Mathf.Clamp(defaultValue, min, max); + } + + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) + { + var numberOption = Object.Instantiate(numberOpt, container); + + numberOption.name = Title; + numberOption.Title = StringName; + numberOption.Value = Value; + numberOption.Increment = Increment; + numberOption.SuffixType = SuffixType; + numberOption.FormatString = "0"; + numberOption.ValidRange = new FloatRange(Min, Max); + numberOption.ZeroIsInfinity = ZeroInfinity; + numberOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + numberOption.OnEnable(); + + OptionBehaviour = numberOption; + + return numberOption; + } + + public override string GetHudStringText() + { + return Title + ": " + Value + Helpers.GetSuffix(SuffixType); + } + + public override float GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) + { + return Mathf.Clamp(optionBehaviour.GetFloat(), Min, Max); + } + + public override void OnValueChanged(float newValue) + { + Value = Mathf.Clamp(newValue, Min, Max); + + if (OptionBehaviour is null) return; + + NumberOption opt = OptionBehaviour as NumberOption; + opt.Value = newValue; + } + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 726290d..a2b4518 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -1,6 +1,7 @@ -using Reactor.Localization.Utilities; +using MiraAPI.Roles; +using Reactor.Localization.Utilities; using System; -using System.Reflection; +using UnityEngine; namespace MiraAPI.GameOptions.OptionTypes { @@ -9,20 +10,26 @@ namespace MiraAPI.GameOptions.OptionTypes public T Value { get; set; } public T DefaultValue { get; set; } public Action ChangedEvent { get; private set; } - - public PropertyInfo PropertyInfo; public string Title { get; } public StringNames StringName { get; } - public Func Hidden { get; set; } - public OptionBehaviour OptionBehaviour { get; protected set; } + public Func Visible { get; set; } + public Type AdvancedRole { get; } + public OptionBehaviour OptionBehaviour { get; set; } + public ModdedOptionGroup Group { get; set; } = null; + public IMiraConfig ParentMod { get; set; } = null; - public ModdedOption(string title, T defaultValue) + public ModdedOption(string title, T defaultValue, Type roleType) { Title = title; DefaultValue = defaultValue; Value = defaultValue; StringName = CustomStringName.CreateAndRegister(Title); - Hidden = () => false; + Visible = () => true; + + if (roleType is not null && roleType.IsAssignableTo(typeof(ICustomRole))) + { + AdvancedRole = roleType; + } } public void ValueChanged(OptionBehaviour optionBehaviour) @@ -39,9 +46,15 @@ namespace MiraAPI.GameOptions.OptionTypes ChangedEvent.Invoke(Value); } - PropertyInfo.SetValue(null, newValue); + OnValueChanged(newValue); } + public abstract void OnValueChanged(T newValue); + public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour); + + public abstract OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container); + + public abstract string GetHudStringText(); } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs new file mode 100644 index 0000000..584fe19 --- /dev/null +++ b/MiraAPI/GameOptions/OptionTypes/ModdedStringOption.cs @@ -0,0 +1,49 @@ +using Reactor.Localization.Utilities; +using System.Linq; +using UnityEngine; + +namespace MiraAPI.GameOptions.OptionTypes +{ + public class ModdedStringOption : ModdedOption + { + public string[] Values { get; private set; } + public ModdedStringOption(string title, int defaultValue, string[] values, System.Type roleType) : base(title, defaultValue, roleType) + { + Values = values; + } + + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) + { + var stringOption = Object.Instantiate(stringOpt, container); + + stringOption.name = Title; + stringOption.Title = StringName; + stringOption.Value = Value; + stringOption.Values = Values.Select(CustomStringName.CreateAndRegister).ToArray(); + stringOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + stringOption.OnEnable(); + + OptionBehaviour = stringOption; + + return stringOption; + } + + public override string GetHudStringText() + { + return Title + ": " + Values[Value]; + } + + public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) + { + return optionBehaviour.GetInt(); + } + + public override void OnValueChanged(int newValue) + { + if (OptionBehaviour is null) return; + + StringOption opt = OptionBehaviour as StringOption; + opt.Value = newValue; + } + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs index 04cad16..1a6edc9 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs @@ -1,14 +1,42 @@ -namespace MiraAPI.GameOptions.OptionTypes +using UnityEngine; + +namespace MiraAPI.GameOptions.OptionTypes { public class ModdedToggleOption : ModdedOption { - public ModdedToggleOption(string title, bool defaultValue = false) : base(title, defaultValue) + public ModdedToggleOption(string title, bool defaultValue, System.Type roleType) : base(title, defaultValue, roleType) { } + public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container) + { + var toggleOption = UnityEngine.Object.Instantiate(toggleOpt, container); + + toggleOption.name = Title; + toggleOption.Title = StringName; + toggleOption.CheckMark.enabled = Value; + toggleOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; + toggleOption.OnEnable(); + OptionBehaviour = toggleOption; + return toggleOption; + } + + public override string GetHudStringText() + { + return Title + ": " + (Value ? "On" : "Off"); + } + public override bool GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour) { return optionBehaviour.GetBool(); } + + public override void OnValueChanged(bool newValue) + { + if (OptionBehaviour is null) return; + + ToggleOption toggleOpt = OptionBehaviour as ToggleOption; + toggleOpt.CheckMark.enabled = newValue; + } } } diff --git a/MiraAPI/GameOptions/UI/ModdedOptionsTab.cs b/MiraAPI/GameOptions/UI/ModdedOptionsTab.cs new file mode 100644 index 0000000..f9a09ef --- /dev/null +++ b/MiraAPI/GameOptions/UI/ModdedOptionsTab.cs @@ -0,0 +1,153 @@ +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 ModdedOptionsTab +{ + public static List CustomTabs = new List(); + public static List CustomScreens = new List(); + + public static GameObject InitializeForMod(IMiraConfig config, GameSettingMenu __instance) + { + var gameBtn = __instance.transform.FindChild("Header/Tabs/GameTab").gameObject; + var roleBtn = __instance.transform.FindChild("Header/Tabs/RoleTab").gameObject; + var screen = CreateNewMenu(config, __instance); + var tab = CreateCustomTab(config, __instance, screen, gameBtn, roleBtn); + + CustomScreens.Add(screen); + CustomTabs.Add(tab); + + SpriteRenderer rend = tab.transform.FindChild("Btn/Tab Background").GetComponent(); + rend.enabled = false; + + UpdateListeners(__instance, gameBtn.GetComponentInChildren(), roleBtn.GetComponentInChildren(), rend); + + var container = screen.transform.FindChild("GameGroup/SliderInner"); + container.DestroyChildren(); + //CreateNewResetButton(__instance, container); + + return container.gameObject; + } + + 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); + + foreach (var screen in CustomScreens) screen.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); + + foreach (var screen in CustomScreens) screen.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(IMiraConfig options, GameSettingMenu __instance, GameObject newSettings, + GameObject gameTab, GameObject roleTab) + { + var newTab = Object.Instantiate(gameTab, gameTab.transform.parent); + newTab.name = "Tab"; + newTab.transform.position = roleTab.transform.position; + newTab.transform.position += new Vector3(CustomTabs.Count + 1, 0, 0); + __instance.Tabs.transform.position -= new Vector3(0.5f, 0, 0); + + var inside = newTab.transform.FindChild("ColorButton"); + inside.name = "Btn"; + + var btn = inside.GetComponentInChildren(); + + btn.OnClick.RemoveAllListeners(); + + btn.OnClick.AddListener((Action)TabAction); + + var spriteRend = inside.GetComponentInChildren(); + // if (options.TabSettings.TabIcon != null) spriteRend.sprite = options.TabSettings.TabIcon.LoadAsset(); + + return newTab; + + void TabAction() + { + __instance.RegularGameSettings.SetActive(false); + __instance.RolesSettings.gameObject.SetActive(false); + + foreach (var screen in CustomScreens) + { + screen.gameObject.SetActive(false); + } + newSettings.gameObject.SetActive(true); + + SpriteRenderer rend = newTab.transform.FindChild("Btn/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; + + foreach (var tab in CustomTabs) + { + tab.transform.FindChild("Btn/Tab Background").gameObject.GetComponent().enabled = false; + } + } + } + + public static GameObject CreateNewMenu(IMiraConfig config, GameSettingMenu __instance) + { + var gameSettings = __instance.RegularGameSettings; + var newSettings = Object.Instantiate(gameSettings, gameSettings.transform.parent); + 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 = $"{config.TabSettings.Title}"; + + return newSettings; + } +} \ No newline at end of file diff --git a/MiraAPI/IMiraConfig.cs b/MiraAPI/IMiraConfig.cs new file mode 100644 index 0000000..adf63a5 --- /dev/null +++ b/MiraAPI/IMiraConfig.cs @@ -0,0 +1,16 @@ +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI +{ + public interface IMiraConfig + { + public ModdedOptionTabSettings TabSettings { get; } + } + + public struct ModdedOptionTabSettings(string title, LoadableAsset tabIcon) + { + public string Title { get; set; } = title; + public LoadableAsset TabIcon { get; set; } = tabIcon; + } +} diff --git a/MiraAPI/MiraAPI.csproj b/MiraAPI/MiraAPI.csproj index 70c9115..052c0f7 100644 --- a/MiraAPI/MiraAPI.csproj +++ b/MiraAPI/MiraAPI.csproj @@ -26,10 +26,6 @@ compile; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/MiraAPI/MiraAPIPlugin.cs b/MiraAPI/MiraAPIPlugin.cs index 4cc3e10..077d721 100644 --- a/MiraAPI/MiraAPIPlugin.cs +++ b/MiraAPI/MiraAPIPlugin.cs @@ -2,7 +2,7 @@ using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.GameModes; -using MiraAPI.GameOptions.Attributes; +using MiraAPI.GameOptions; using MiraAPI.Hud; using MiraAPI.Roles; using Reactor; @@ -27,7 +27,7 @@ public partial class MiraAPIPlugin : BasePlugin Harmony.PatchAll(); RegisterCustomRoleAttribute.Initialize(); - RegisterModdedOptionAttribute.Initialize(); + ModdedOptionsManager.Initialize(); RegisterGameModeAttribute.Register(Assembly.GetCallingAssembly()); RegisterButtonAttribute.Register(Assembly.GetExecutingAssembly()); diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 1f7afaa..f69fbf8 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -1,6 +1,8 @@ using HarmonyLib; using InnerNet; +using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.Patches.Options; using MiraAPI.Roles; using Reactor.Utilities.Extensions; using UnityEngine; @@ -16,10 +18,67 @@ namespace MiraAPI.Patches /// 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 >= ModdedOptionsManager.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; @@ -122,4 +181,4 @@ namespace MiraAPI.Patches } } } -} +} \ 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..167fcf5 --- /dev/null +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -0,0 +1,109 @@ +using HarmonyLib; +using Il2CppSystem; +using MiraAPI.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 ModdedOptionsManager.Options) + { + 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.5f; + __instance.GetComponentInParent().ContentYBounds.max = startOffset + __instance.Children.Count * 0.5f; + + foreach (var option in ModdedOptionsManager.Options.Where(option => option.Group == null)) + { + if (!option.OptionBehaviour) continue; + + option.OptionBehaviour.enabled = option.Visible(); + option.OptionBehaviour.gameObject.SetActive(option.Visible()); + + if (option.Visible()) + { + 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 ModdedOptionsManager.Groups.Where(group => group.AdvancedRole == null)) + { + if (group.Header == null) + { + continue; + } + + group.Header.SetActive(group.GroupVisible()); + + if (group.GroupVisible()) + { + startOffset -= 0.5f; + } + + var position = group.Header.transform.localPosition; + group.Header.transform.localPosition = new Vector3(position.x, startOffset, position.z); + + foreach (var option in ModdedOptionsManager.Options.Where(x => x.Group == group)) + { + if (!option.OptionBehaviour) + { + continue; + } + + var enabled = group.GroupVisible() && option.Visible(); + + option.OptionBehaviour.enabled = enabled; + option.OptionBehaviour.gameObject.SetActive(enabled); + + if (group.GroupVisible() && option.Visible()) + { + 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..04ff2bb --- /dev/null +++ b/MiraAPI/Patches/Options/GameSettingsMenuPatches.cs @@ -0,0 +1,68 @@ +using HarmonyLib; +using MiraAPI.API.GameOptions; +using MiraAPI.GameOptions; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +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) + { + foreach (var tab in ModdedOptionsTab.CustomTabs) tab.gameObject.Destroy(); + foreach (var screen in ModdedOptionsTab.CustomScreens) screen.gameObject.Destroy(); + + ModdedOptionsTab.CustomScreens.Clear(); + ModdedOptionsTab.CustomTabs.Clear(); + + _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(); + + foreach (var pair in ModdedOptionsManager.RegisteredMods) + { + var container = ModdedOptionsTab.InitializeForMod(pair.Value, __instance).transform; + + foreach (var group in ModdedOptionsManager.Groups.Where(group => group.AdvancedRole == null && group.ParentMod == pair.Value)) + { + group.Header = ModdedOptionsTab.CreateHeader(toggleOpt, container, group.GroupName); + CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container, ModdedOptionsManager.Options.Where(x => x.Group == group).ToList()); + } + + CreateOptionsFor(__instance, toggleOpt, numberOpt, stringOpt, container, ModdedOptionsManager.Options.Where(x => x.Group is null).ToList()); + } + + if (!numberOpt || !toggleOpt || !stringOpt) + { + Logger.Error("OPTION PREFABS MISSING"); + } + } + + public static void CreateOptionsFor(GameSettingMenu __instance, ToggleOption togglePrefab, NumberOption numberPrefab, StringOption stringPrefab, Transform container, + List options) + { + foreach (IModdedOption option in options) + { + if (option.AdvancedRole is not null || option.OptionBehaviour) + { + continue; + } + + OptionBehaviour newOption = option.CreateOption(togglePrefab, numberPrefab, stringPrefab, container); + __instance.AllItems = __instance.AllItems.AddItem(newOption.transform).ToArray(); + } + } +} diff --git a/MiraAPI/Patches/Options/HudStringPatch.cs b/MiraAPI/Patches/Options/HudStringPatch.cs new file mode 100644 index 0000000..7daa59d --- /dev/null +++ b/MiraAPI/Patches/Options/HudStringPatch.cs @@ -0,0 +1,118 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using MiraAPI.GameOptions; +using MiraAPI.Roles; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MiraAPI.Patches.Options; + +[HarmonyPatch(typeof(IGameOptionsExtensions), "ToHudString")] +public static class ToHudStringPatch +{ + public static int CurrentPage = 0; + /// + /// Generic method to add options + /// + public static void AddOptions(StringBuilder sb, + List options) + { + foreach (var opt in options) + { + sb.AppendLine(opt.GetHudStringText()); + } + } + + 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 pair in CustomRoleManager.CustomRoles) + { + var customRole = pair.Value as ICustomRole; + if (customRole.IsGhostRole) + { + pair.Value.Role = (RoleTypes)pair.Key; + } + } + + if (CurrentPage != 0) + { + IMiraConfig currentPlugin = ModdedOptionsManager.RegisteredMods.Values.ToArray()[CurrentPage - 1]; + if (currentPlugin == null) + { + CurrentPage = 0; + return; + } + + var sb = new StringBuilder($"{currentPlugin.TabSettings.Title}:\n"); + var groupsWithRoles = ModdedOptionsManager.Groups.Where(group => group.AdvancedRole != null && group.ParentMod == currentPlugin); + var groupsWithoutRoles = ModdedOptionsManager.Groups.Where(group => group.AdvancedRole == null && group.ParentMod == currentPlugin); + + AddOptions(sb, + ModdedOptionsManager.Options.Where(option => option.Group == null && option.ParentMod == currentPlugin && option.Visible()).ToList() + ); + + foreach (var group in groupsWithoutRoles) + { + if (!group.GroupVisible()) + { + continue; + } + + sb.AppendLine($"\n{group.GroupName}"); + AddOptions(sb, ModdedOptionsManager.Options.Where(x => x.Group == group).ToList()); + } + + var customOptionGroups = groupsWithRoles as ModdedOptionGroup[] ?? groupsWithRoles.ToArray(); + if (customOptionGroups.Any()) + { + sb.AppendLine("\nRoles"); + + foreach (var group in customOptionGroups) + { + if (!group.GroupVisible()) + { + continue; + } + + sb.AppendLine($"{group.GroupName}"); + AddOptions(sb, ModdedOptionsManager.Options.Where(x => x.Group == group).ToList()); + sb.Append("\n"); + } + } + + __result = sb.ToString(); + } + + if (CurrentPage == 0) + { + __result = "Normal Options:\n" + __result; + } + __result += $"\nUse TAB to switch pages (Page {CurrentPage}/{ModdedOptionsManager.RegisteredMods.Count})"; + } +} \ No newline at end of file diff --git a/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs b/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs index 0288b6e..068ee13 100644 --- a/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableResourceAsset.cs @@ -1,5 +1,4 @@ -using System; -using UnityEngine; +using UnityEngine; namespace MiraAPI.Utilities.Assets { @@ -12,8 +11,15 @@ namespace MiraAPI.Utilities.Assets return _loadedAsset; } - return _loadedAsset = SpriteTools.LoadSpriteFromPath(resourcesFolder + name); - throw new Exception($"INVALID ASSET: {name}"); + try + { + return _loadedAsset = SpriteTools.LoadSpriteFromPath(resourcesFolder + name); + } + catch + { + Debug.LogError($"Not loading, invalid asset: {name}"); + return null; + } } } }