From b4e6e448f2b024f3505838698d285a1d8a31edec Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sun, 31 Aug 2025 18:04:52 -0400 Subject: [PATCH] Major work/rewrite on local settings --- MiraAPI.Example/ExampleLocalSetings.cs | 35 +- MiraAPI.Example/ExamplePlugin.cs | 2 - .../Attributes/LocalSettingAttribute.cs | 9 +- .../Attributes/LocalSettingToggleAttribute.cs | 12 +- MiraAPI/LocalSettings/ILocalSetting.cs | 26 ++ MiraAPI/LocalSettings/LocalSettingsManager.cs | 42 +++ MiraAPI/LocalSettings/LocalSettingsTab.cs | 309 +++++++++++------- .../LocalSettingsTabSingleton.cs | 20 ++ .../SettingTypes/LocalSettingBase.cs | 16 +- .../SettingTypes/LocalSettingToggle.cs | 11 +- MiraAPI/MiraApiPlugin.cs | 5 - .../{MiraApiConfig.cs => MiraApiSettings.cs} | 12 +- MiraAPI/Modifiers/ModifierComponent.cs | 3 +- .../ModifierDisplayComponent.cs | 3 +- .../LocalSettings/OptionsMenuPatches.cs | 116 +++++++ .../LocalSettings/ToggleBehaviourPatch.cs | 37 +++ .../Patches/Options/GamePresetsTabPatches.cs | 21 +- MiraAPI/PluginLoading/MiraPluginInfo.cs | 1 + MiraAPI/PluginLoading/MiraPluginManager.cs | 47 ++- MiraAPI/Resources/Settings.png | Bin 0 -> 9598 bytes MiraAPI/Utilities/Assets/MiraAssets.cs | 5 + MiraAPI/Utilities/Helpers.cs | 51 +++ 22 files changed, 604 insertions(+), 179 deletions(-) create mode 100644 MiraAPI/LocalSettings/LocalSettingsManager.cs create mode 100644 MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs rename MiraAPI/{MiraApiConfig.cs => MiraApiSettings.cs} (62%) create mode 100644 MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs create mode 100644 MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs create mode 100644 MiraAPI/Resources/Settings.png diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index c682617..fb07869 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -5,10 +5,37 @@ using Reactor.Utilities; namespace MiraAPI.Example; -public static class ExampleLocalSetings +public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) { - public static LocalSettingsTab ModTab { get; } = new("Example Tab"); + public override string TabName => "Mira Example"; - [LocalSetting] - public static ConfigEntry ExampleToggleSetting { get; private set; } + [LocalSettingToggle] + public ConfigEntry PlayStartupSound { get; private set; } = config.Bind("Audio", "Play Startup Sound", true); + + [LocalSettingToggle] + public ConfigEntry MuteInBackground { get; private set; } = config.Bind("Audio", "Mute In Background", false); + + [LocalSettingToggle] + public ConfigEntry AutoSaveEnabled { get; private set; } = config.Bind("General", "Auto Save Enabled", true); + + [LocalSettingToggle] + public ConfigEntry EnableCloudSync { get; private set; } = config.Bind("General", "Enable Cloud Sync", false); + + [LocalSettingToggle] + public ConfigEntry ShowAdvancedTooltips { get; private set; } = config.Bind("UI", "Show Advanced Tooltips", true); + + [LocalSettingToggle] + public ConfigEntry CompactUI { get; private set; } = config.Bind("UI", "Compact UI Layout", false); + + [LocalSettingToggle] + public ConfigEntry EnableControllerSupport { get; private set; } = config.Bind("Input", "Enable Controller Support", true); + + [LocalSettingToggle] + public ConfigEntry EnableVibration { get; private set; } = config.Bind("Input", "Enable Controller Vibration", true); + + [LocalSettingToggle] + public ConfigEntry ShowPlayerNames { get; private set; } = config.Bind("Gameplay", "Show Player Names", true); + + [LocalSettingToggle] + public ConfigEntry EnableHardMode { get; private set; } = config.Bind("Gameplay", "Enable Hard Mode", false); } diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index 3819347..0aed9c4 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -24,7 +24,5 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin { ExampleEventHandlers.Initialize(); Harmony.PatchAll(); - - Config.Keys. } } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs index 6735d4e..1b101c5 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -1,22 +1,21 @@ using System; using BepInEx.Configuration; -using UnityEngine; namespace MiraAPI.LocalSettings.Attributes; /// /// Base class for all local settings attributes. /// -/// The local setting type the attribute handles. [AttributeUsage(AttributeTargets.Property)] public abstract class LocalSettingAttribute( - LocalSettingsTab tab, string? name = null, string? description = null) : Attribute { /// /// Returns the created setting object. /// + /// Gets the tab where the setting is located. + /// Gets the config entry the setting is attached to. /// The created class. - protected abstract GameObject CreateSetting(); -} \ No newline at end of file + internal abstract ILocalSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase); +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs index 7a8cd34..7b44f7f 100644 --- a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs @@ -10,17 +10,13 @@ namespace MiraAPI.LocalSettings.Attributes; /// [AttributeUsage(AttributeTargets.Property)] public class LocalSettingToggleAttribute( - LocalSettingsTab tab, string? name = null, - string? description = null, - Color? toggleColor = null, - Color? toggleHoverColor = null, - Color? toggleActiveColor = null - ) : LocalSettingAttribute(tab, name, description) + string? description = null + ) : LocalSettingAttribute(name, description) { /// - protected override LocalSettingToggle CreateSetting(ConfigEntryBase configEntry) + internal override LocalSettingToggle CreateSetting(Type tab, ConfigEntryBase configEntryBase) { - return new LocalSettingToggle(tab, configEntry, name, description, toggleColor, toggleHoverColor, toggleActiveColor); + return new LocalSettingToggle(tab, configEntryBase, name, description); } } diff --git a/MiraAPI/LocalSettings/ILocalSetting.cs b/MiraAPI/LocalSettings/ILocalSetting.cs index 78d3647..e6649bb 100644 --- a/MiraAPI/LocalSettings/ILocalSetting.cs +++ b/MiraAPI/LocalSettings/ILocalSetting.cs @@ -8,9 +8,35 @@ namespace MiraAPI.LocalSettings; /// public interface ILocalSetting { + /// + /// Gets the preferred name of a setting. + /// string Name { get; } + + /// + /// Gets the description of a setting. + /// string Description { get; } + + /// + /// Gets the setting's gameobject. + /// GameObject? Setting { get; } + + /// + /// Gets the setting's config entry. + /// ConfigEntryBase ConfigEntry { get; } + + /// + /// Used to create the setting + /// + /// Toggle template. + /// Slider template. + /// The parent of the setting. + /// The offset. + /// The order. + /// Whether it's the last on the row. + /// The created setting. GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); } diff --git a/MiraAPI/LocalSettings/LocalSettingsManager.cs b/MiraAPI/LocalSettings/LocalSettingsManager.cs new file mode 100644 index 0000000..3b75d05 --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using BepInEx.Unity.IL2CPP; +using MiraAPI.GameOptions; +using Reactor.Utilities; + +namespace MiraAPI.LocalSettings; + +/// +/// Manages local settings. +/// +public static class LocalSettingsManager +{ + internal static readonly Dictionary TypeToTab = []; + internal static readonly List Tabs = []; + + internal static bool RegisterTab(Type type, BasePlugin pluginInfo) + { + if (Activator.CreateInstance(type, pluginInfo.Config) is not LocalSettingsTab tab) + { + return false; + } + + if (TypeToTab.ContainsKey(type)) + { + Logger.Error($"Local settings tab {type.Name} already exists."); + return false; + } + + Tabs.Add(tab); + TypeToTab.Add(type, tab); + + typeof(LocalSettingsTabSingleton<>).MakeGenericType(type) +#pragma warning disable S3011 + .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! +#pragma warning restore S3011 + .SetValue(null, tab); + + return true; + } +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index e956589..a7e2ea7 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -1,154 +1,231 @@ -using System.Collections.Generic; -using System.Linq; +using System; +using System.Collections.Generic; +using BepInEx.Configuration; using HarmonyLib; +using MiraAPI.Patches.LocalSettings; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using Object = UnityEngine.Object; namespace MiraAPI.LocalSettings; /// /// Represents a local settings tab. Gets a button by default /// -public class LocalSettingsTab +public abstract class LocalSettingsTab(ConfigFile config) { - public string TabName { get; } - public Color TabColor { get; } - public Color TabHoverColor { get; } - public Color TabActiveColor { get; } - public LoadableAsset? TabIcon { get; } - - public TabGroup? TabButton { get; protected set; } - private bool ShouldCreateButton { get; set; } - - public List Settings { get; } + /// + /// Gets the name of the tab + /// + public abstract string TabName { get; } /// - /// Initializes a new instance of the class. + /// Gets the tab button icon. /// - /// The name of the tab. - /// The color of the tab. - /// The hover color of the tab. - /// The active color of the tab. - /// The tab icon. Defaults to no icon. - /// Should the tab button be created. Without it the tab needs to be opened manually. - public LocalSettingsTab(string tabName, Color? tabColor = null, Color? tabHoverColor = null, Color? tabActiveColor = null, LoadableAsset? tabIcon = null, bool createButton = true) - { - TabName = tabName; - TabColor = tabColor ?? Color.white; - TabHoverColor = tabHoverColor ?? Palette.AcceptedGreen; - TabActiveColor = tabActiveColor ?? Palette.AcceptedGreen; - TabIcon = tabIcon; - ShouldCreateButton = createButton; - Settings = new(); - } + public virtual LoadableAsset? TabIcon => null; + + /// + /// Gets the color of the tab button. + /// + public virtual Color TabColor => Color.white; + + /// + /// Gets the color of the tab button when hovered. + /// + public Color TabHoverColor => Palette.AcceptedGreen; + + /// + /// Gets the color of the tab button when active. + /// + public virtual Color TabActiveColor => Palette.AcceptedGreen; + + /// + /// Gets a value indicating whether a button should be created for the tab. + /// + protected virtual bool ShouldCreateButton => true; + + /// + /// Gets the mod's config file. Automatically found in the plugin loader. + /// + public ConfigFile Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); + + internal TabGroup? TabButton { get; set; } + internal List Settings { get; } = []; + private Scroller? Scroller { get; set; } internal GameObject? CreateTab(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) { - var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); - tab.name = $"{TabName}Tab"; - tab.transform.DestroyChildren(); - tab.gameObject.SetActive(false); + if (ShouldCreateButton) + { + CreateTabButton(instance, ref tabIdx, ref offset); + } - if (TabButton != null) + var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); + tab.name = $"{TabName}Tab"; + tab.transform.DestroyChildren(); + tab.gameObject.SetActive(false); + + if (Scroller == null) + { + Scroller = Helpers.CreateScroller(tab.transform, OptionsMenuPatches.MaskCollider); + } + + if (TabButton != null) + { + TabButton.Content = tab.gameObject; + } + + var generalLabel = instance.transform.FindChild("GeneralTab").FindChild("ControlGroup") + .FindChild("ControlText_TMP").gameObject; + var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent(); + var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent(); + + Dictionary> entriesByGroup = new(); + foreach (var entry in Settings) + { + var group = entry.ConfigEntry.Definition.Section; + if (!entriesByGroup.ContainsKey(group)) + entriesByGroup.Add(group, []); + + entriesByGroup[group].Add(entry); + } + + float contentOffset = 0; + var contentOrder = 1; + var contentIndex = 1; + + foreach (var pair in entriesByGroup) + { + CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset); + + foreach (var setting in pair.Value) + { + var obj = setting.CreateOption( + toggle, + slider, + Scroller.Inner, + ref contentOffset, + ref contentOrder, + contentIndex == pair.Value.Count); + + obj!.GetComponentsInChildren().Do(x => + { + x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + x.sortingOrder = 150; + x.sortingLayerName = "Default"; + }); + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); + + contentIndex++; + } + + contentOrder = 1; + } + + Scroller.SetBounds(new FloatRange(0, Settings.Count * 0.5f - 5f), new FloatRange(0, 0)); + return tab; + } + + private void CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + { + var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); + tabButtonObject.name = $"{TabName} Button"; + tabButtonObject.transform.localPosition = new Vector3(2.4f, 2.1f - offset, 5.5f); + tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1); + tabButtonObject.Button.color = TabColor; + TabButton = tabButtonObject; + + var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent(); + tabButtonText.GetComponent().Destroy(); // i hate text translators + tabButtonText.transform.localPosition = new Vector3(0.078f, 0, 0); + tabButtonText.transform.localScale = new Vector3(0.9f, 0.9f); + tabButtonText.alignment = TextAlignmentOptions.Right; + tabButtonText.text = $"{GetShortName(TabName)}"; + + var tabButton = tabButtonObject.GetComponent(); + var rollover = tabButtonObject.Rollover; + rollover.OverColor = TabColor; + rollover.OutColor = TabColor; + + SpriteRenderer? tabButtonRend = null; + if (TabIcon != null) { - TabButton.Content = tab.gameObject; + tabButtonRend = new GameObject("sprite").AddComponent(); + tabButtonRend.gameObject.layer = 5; // ui layer + tabButtonRend.transform.SetParent(tabButtonObject.transform); + tabButtonRend.transform.localPosition = new Vector3(0.5f, 0, -2); + tabButtonRend.transform.localScale = new Vector3(0.2f, 0.2f, 1); + tabButtonRend.sprite = TabIcon.LoadAsset(); + tabButtonText.gameObject.SetActive(false); } - var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent(); - var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent(); - - float contentOffset = 0; - var contentOrder = 1; - var contentIndex = 1; - foreach (var setting in Settings) + var tabIndex = tabIdx; // Local copies of the tab variables so we can use them in the lambdas + var tabOffset = offset; + tabButton.OnClick.AddListener((UnityAction)(() => { instance.OpenTabGroup(tabIndex + 10); })); + tabButton.OnMouseOver.AddListener((UnityAction)(() => { - setting.CreateOption( - toggle, - slider, - tab.transform, - ref contentOffset, - ref contentOrder, - contentIndex == Settings.Count); - contentIndex++; - } - - if (ShouldCreateButton) + tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f); + tabButtonObject.Button.color = TabHoverColor; + tabButtonObject.Rollover.OutColor = TabHoverColor; + tabButtonText.gameObject.SetActive(true); + tabButtonText.transform.localPosition = new Vector3(-0.024f, 0, 0); + tabButtonText.maxVisibleCharacters = int.MaxValue; + tabButtonText.text = TabName; + tabButtonText.alignment = TextAlignmentOptions.Left; + })); + tabButton.OnMouseOut.AddListener((UnityAction)(() => { - var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); - tabButtonObject.name = $"{TabName} Button"; - tabButtonObject.transform.localPosition = new Vector3(2.4f, 2.1f - offset, 5.5f); - tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1); - tabButtonObject.Button.color = TabColor; - TabButton = tabButtonObject; - - var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent(); - tabButtonText.GetComponent().Destroy(); // i hate text translators - tabButtonText.transform.localPosition = new Vector3(0.078f, 0, 0); - tabButtonText.transform.localScale = new Vector3(0.9f, 0.9f); - tabButtonText.alignment = TextAlignmentOptions.Right; - tabButtonText.text = $"{GetShortName(TabName)}"; - - var tabButton = tabButtonObject.GetComponent(); - var rollover = tabButtonObject.Rollover; - rollover.OverColor = TabColor; - rollover.OutColor = TabColor; - - SpriteRenderer tabButtonRend = null; - if (TabIcon != null) + if (!tabButtonObject.Content.gameObject.activeSelf) { - tabButtonRend = new GameObject("sprite").AddComponent(); - tabButtonRend.gameObject.layer = 5; // ui layer - tabButtonRend.transform.SetParent(tabButtonObject.transform); - tabButtonRend.transform.localPosition = new Vector3(0.5f, 0, -2); - tabButtonRend.transform.localScale = new Vector3(0.5f, 0.5f, 1); - tabButtonRend.sprite = TabIcon.LoadAsset(); + tabButtonObject.Button.color = TabColor; + tabButtonObject.Rollover.OutColor = TabColor; + } + + tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f); + tabButtonText.alignment = TextAlignmentOptions.Right; + if (tabButtonRend != null) + { + tabButtonRend.gameObject.SetActive(true); tabButtonText.gameObject.SetActive(false); } - int tabIndex = tabIdx; // Local copies of the tab variables so we can use them in the lambdas - float tabOffset = offset; - tabButton.OnClick.AddListener((UnityAction)(() => - { - instance.OpenTabGroup(tabIndex + 10); - })); - tabButton.OnMouseOver.AddListener((UnityAction)(() => - { - tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f); + tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0); + tabButtonText.maxVisibleCharacters = 4; + tabButtonText.text = $"{GetShortName(TabName)}"; + })); - if (tabButtonRend != null) - { - tabButtonRend.gameObject.SetActive(false); - } - tabButtonText.gameObject.SetActive(true); - tabButtonText.transform.localPosition = new Vector3(0.045f, 0, 0); - tabButtonText.maxVisibleCharacters = int.MaxValue; - tabButtonText.text = TabName; - })); - tabButton.OnMouseOut.AddListener((UnityAction)(() => - { - tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f); - - if (tabButtonRend != null) - { - tabButtonRend.gameObject.SetActive(true); - tabButtonText.gameObject.SetActive(false); - } - tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0); - tabButtonText.maxVisibleCharacters = 4; - tabButtonText.text = $"{GetShortName(TabName)}"; - })); - } - - return tab; + offset += 0.6f; + tabIdx++; } + private static void CreateLabel(GameObject template, Transform parent, string text, ref float offset) + { + var label = Object.Instantiate(template, parent); + label.transform.localPosition = new Vector3(0, 1.85f - offset); + label.GetComponent().Destroy(); + label.name = text; + + var tmp = label.GetComponent(); + tmp.text = $"{text}"; + tmp.alignment = TextAlignmentOptions.Center; + + var meshRend = label.GetComponent(); + meshRend.sortingLayerName = "Default"; + meshRend.sortingOrder = 151; + + offset += 0.5f; + } + + private static string GetShortName(string name) { - string shortName = string.Empty; + var shortName = string.Empty; name.Split(' ').Do(x => shortName += x[0]); return shortName; } } + diff --git a/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs new file mode 100644 index 0000000..1994235 --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs @@ -0,0 +1,20 @@ +using System.Linq; +using MiraAPI.LocalSettings; + +namespace MiraAPI.GameOptions; + +/// +/// Singleton for local settings tabs. +/// +/// The settings tab type. +public static class LocalSettingsTabSingleton where T : LocalSettingsTab +{ + private static T? _instance; + + /// + /// Gets the instance of the setting tab. + /// +#pragma warning disable CA1000 + public static T Instance => _instance ??= LocalSettingsManager.Tabs.OfType().Single(); +#pragma warning restore CA1000 +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index e93131a..e1ebe10 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -1,15 +1,23 @@ -using BepInEx.Configuration; +using System; +using BepInEx.Configuration; using TMPro; using UnityEngine; namespace MiraAPI.LocalSettings.SettingTypes; +/// public abstract class LocalSettingBase : ILocalSetting { + /// public string Name { get; } + + /// public string Description { get; } - public GameObject? Setting { get; } + /// + public GameObject? Setting { get; } = null!; + + /// public ConfigEntryBase ConfigEntry { get; } /// @@ -19,13 +27,13 @@ public abstract class LocalSettingBase : ILocalSetting /// The config entry. /// The name of the setting. /// The description of the setting. - protected LocalSettingBase(LocalSettingsTab tab, ConfigEntryBase configEntry, string? name = null, string? description = null) + protected LocalSettingBase(Type tab, ConfigEntryBase configEntry, string? name = null, string? description = null) { ConfigEntry = configEntry; Name = name ?? ConfigEntry.Definition.Key; Description = description ?? ConfigEntry.Description.Description; - tab.Settings.Add(this); + LocalSettingsManager.TypeToTab[tab].Settings.Add(this); } /// diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs index 00babc1..fa36333 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingToggle.cs @@ -1,8 +1,11 @@ -using BepInEx.Configuration; +using System; +using BepInEx.Configuration; +using MiraAPI.Utilities; using Reactor.Localization.Utilities; using TMPro; using UnityEngine; using UnityEngine.Events; +using Object = UnityEngine.Object; namespace MiraAPI.LocalSettings.SettingTypes; @@ -37,7 +40,7 @@ public class LocalSettingToggle : LocalSettingBase /// The hover color of the toggle. /// The active color of the toggle. public LocalSettingToggle( - LocalSettingsTab tab, + Type tab, ConfigEntryBase configEntry, string? name = null, string? description = null, @@ -66,7 +69,7 @@ public class LocalSettingToggle : LocalSettingBase } else { - toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.35f : 1.28f, 1.85f - offset); + toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset); } toggleObject.BaseText = CustomStringName.CreateAndRegister(Name); @@ -95,6 +98,8 @@ public class LocalSettingToggle : LocalSettingBase toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; })); + Helpers.DivideSize(toggleObject.gameObject, 1.1f); + order++; if (order > 2 && !last) { diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index aa7f3c6..8654cf6 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -30,10 +30,6 @@ public partial class MiraApiPlugin : BasePlugin /// public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255); - /// - /// Gets the Mira Api Config Handler. - /// - public MiraApiConfig? MiraConfig { get; private set; } private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); @@ -44,7 +40,6 @@ public partial class MiraApiPlugin : BasePlugin ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow); - MiraConfig = new MiraApiConfig(Config); PluginManager = new MiraPluginManager(); PluginManager.Initialize(); } diff --git a/MiraAPI/MiraApiConfig.cs b/MiraAPI/MiraApiSettings.cs similarity index 62% rename from MiraAPI/MiraApiConfig.cs rename to MiraAPI/MiraApiSettings.cs index 10c9776..e5fe303 100644 --- a/MiraAPI/MiraApiConfig.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -2,7 +2,10 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.LocalSettings; +using MiraAPI.LocalSettings.Attributes; using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; @@ -14,10 +17,17 @@ namespace MiraAPI; /// /// Mira API Config File Handler /// -public class MiraApiConfig(ConfigFile config) +public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) { + /// + public override string TabName => "Mira API"; + + /// + public override LoadableAsset? TabIcon => MiraAssets.SettingsIcon; + /// /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. /// + [LocalSettingToggle] public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false); } diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 4f556f5..f4003c9 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameOptions; using MiraAPI.Modifiers.ModifierDisplay; using MiraAPI.Modifiers.Types; using MiraAPI.Patches.Roles; @@ -129,7 +130,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) { var taskPanelOpen = HudManager.Instance.TaskPanel.open; var roleTabOpen = HudManagerPatches.RoleTab != null && HudManagerPatches.RoleTab.open; - var leftSideHud = PluginSingleton.Instance.MiraConfig!.ModifiersHudLeftSide.Value; + var leftSideHud = LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value; if (leftSideHud && (taskPanelOpen || roleTabOpen)) { diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs index 0836935..0cbb8c3 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.Attributes; +using MiraAPI.GameOptions; using MiraAPI.Patches.Roles; using MiraAPI.Utilities.Assets; using Reactor.Utilities; @@ -98,7 +99,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) IsOpen = false; _children.gameObject.SetActive(false); - if (PluginSingleton.Instance.MiraConfig!.ModifiersHudLeftSide.Value) + if (LocalSettingsTabSingleton.Instance.ModifiersHudLeftSide.Value) { var aspect = GetComponent(); _toggleButton.transform.localPosition = new Vector3(-1f, 2.7f, 0f); diff --git a/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs new file mode 100644 index 0000000..0b4ad9c --- /dev/null +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using MiraAPI.LocalSettings; +using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.Patches.LocalSettings; + +[HarmonyPatch(typeof(OptionsMenuBehaviour))] +public static class OptionsMenuPatches +{ + internal static BoxCollider2D MaskCollider; + /// + /// Creates the tabs and their content + /// + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Start))] + public static void StartPostfix(OptionsMenuBehaviour __instance) + { + // Fix for tabs not being clickable in the main menu + if (!AmongUsClient.Instance.IsInGame) + { + __instance.Background.GetComponent().enabled = false; + __instance.transform.FindChild("Tint").SetLocalZ(5.6f); + } + + var maskObj = new GameObject + { + layer = 5, + name = "SpriteMask", + }; + maskObj.transform.SetParent(__instance.transform); + maskObj.transform.localPosition = new Vector3(0, -0.3f, 0); + maskObj.transform.localScale = new Vector3(500, 120, 1); + + var blank = Sprite.Create( + Texture2D.whiteTexture, + new Rect(0, 0, Texture2D.whiteTexture.width, Texture2D.whiteTexture.height), + new Vector2(0.5f, 0.5f)); + + var mask = maskObj.AddComponent(); + mask.sprite = blank; + mask.isCustomRangeActive = true; + mask.sortingLayerName = "Default"; + mask.backSortingOrder = 100; + mask.frontSortingOrder = 500; + + MaskCollider = maskObj.AddComponent(); + MaskCollider.size = new Vector2(0.01f, 0.1f); + MaskCollider.isTrigger = true; + MaskCollider.enabled = true; + + float yOffset = 0; + var i = 0; + foreach (var settings in LocalSettingsManager.Tabs) + { + settings.CreateTab(__instance, ref i, ref yOffset); + } + } + + /// + /// Modifies how opening tabs is handled for the custom ones to work + /// + [HarmonyPrefix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.OpenTabGroup))] + public static bool OpenTabGroupPrefix(OptionsMenuBehaviour __instance, ref int index) + { + __instance.Tabs.ToList().ForEach(x => x.Close()); // Close all vanilla tabs + LocalSettingsManager.Tabs.ForEach(CustomClose); // Close all mod tabs + + if (index >= 10) // Tabs with index 10 and above are the mod settings tabs + { + CustomOpen(LocalSettingsManager.Tabs[index - 10]); + return false; + } + + return true; + } + + /// + /// Makes the custom tabs close when opening the options menu + /// + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Open))] + public static void OpenPostfix(OptionsMenuBehaviour __instance) + { + LocalSettingsManager.Tabs.ForEach(CustomClose); + } + + private static void CustomOpen(LocalSettingsTab tab) + { + if (tab.TabButton?.Button) + tab.TabButton!.Button.color = tab.TabHoverColor; + + if (tab.TabButton?.Rollover) + tab.TabButton!.Rollover.OutColor = tab.TabHoverColor; + + if (tab.TabButton?.Content) + tab.TabButton!.Content.SetActive(true); + } + private static void CustomClose(LocalSettingsTab tab) + { + if (tab.TabButton?.Button) + tab.TabButton!.Button.color = tab.TabColor; + + if (tab.TabButton?.Rollover) + tab.TabButton!.Rollover.OutColor = tab.TabColor; + + if (tab.TabButton?.Content) + tab.TabButton!.Content.SetActive(false); + } +} diff --git a/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs new file mode 100644 index 0000000..5022d1c --- /dev/null +++ b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs @@ -0,0 +1,37 @@ +using HarmonyLib; +using UnityEngine; + +namespace MiraAPI.Patches.LocalSettings; + +[HarmonyPatch(typeof(ToggleButtonBehaviour))] +public static class ToggleBehaviourPatch +{ + /// + /// ResetText was inlined ofc + /// + [HarmonyPrefix] + [HarmonyPatch(nameof(ToggleButtonBehaviour.UpdateText))] + public static bool ResetTextPrefix(ToggleButtonBehaviour __instance, bool on) + { + __instance.onState = on; + var color = on ? new Color(0f, 1f, 0.16470589f, 1f) : Color.white; + __instance.Background.color = color; + + // Used to mask the text + __instance.Text.richText = true; + __instance.Text.text = $"" + + $"{DestroyableSingleton.Instance.GetString(__instance.BaseText)}: " + + $"{DestroyableSingleton.Instance.GetString(__instance.onState ? StringNames.SettingsOn : StringNames.SettingsOff)}"; + + __instance.Text.sortingOrder = 151; + __instance.Text.renderer.sortingLayerName = "Default"; + __instance.Text.renderer.sortingOrder = 151; + + if (__instance.Rollover) + { + __instance.Rollover.ChangeOutColor(color); + } + + return false; + } +} diff --git a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs index 2b4f77d..ed80bb1 100644 --- a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs +++ b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs @@ -6,6 +6,7 @@ using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Presets; using MiraAPI.Roles; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -28,22 +29,6 @@ internal static class GamePresetsTabPatches private static readonly object Lock = new(); - public static void DivideSize(GameObject obj, float amount) - { - foreach (var collider in obj.GetComponentsInChildren(true)) - { - if (collider.TryCast() is { } col) - { - col.size = new Vector2(col.size.x / amount, col.size.y); - } - } - - foreach (var rend in obj.GetComponentsInChildren(true)) - { - rend.size = new Vector2(rend.size.x / amount, rend.size.y); - } - } - [HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnEnable))] public static class GamePresetsOnEnablePatch { @@ -207,7 +192,7 @@ internal static class GamePresetsTabPatches saveText.transform.parent.localPosition.y, saveText.transform.parent.localPosition.z); - DivideSize(saveButton.gameObject, 2f); + Helpers.DivideSize(saveButton.gameObject, 2f); saveButton.OnClick = new Button.ButtonClickedEvent(); saveButton.OnClick.AddListener( @@ -276,7 +261,7 @@ internal static class GamePresetsTabPatches var iconSpriteRend = icon.AddComponent(); iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset(); - DivideSize(refreshButton.gameObject, 2f); + Helpers.DivideSize(refreshButton.gameObject, 2f); refreshButton.buttonText.gameObject.Destroy(); refreshButton.OnClick = new Button.ButtonClickedEvent(); diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 4a454a6..891c3ba 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -5,6 +5,7 @@ using BepInEx.Configuration; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Presets; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index aa6dfcd..98b8db5 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -13,6 +13,7 @@ using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; using MiraAPI.Keybinds; +using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Modifiers; using MiraAPI.Presets; @@ -95,6 +96,11 @@ public sealed class MiraPluginManager continue; } + if (RegisterLocalTabs(type, plugin)) + { + continue; + } + if (RegisterRole(type, info, out var role)) { roles.Add(role); @@ -111,7 +117,6 @@ public sealed class MiraPluginManager continue; } - RegisterLocalSettings(type); RegisterColorClasses(type); } @@ -150,6 +155,8 @@ public sealed class MiraPluginManager Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {(KeyboardKeyCode)button.DefaultKeybind}."); } }; + + RegisterLocalTabs(typeof(MiraApiSettings), PluginSingleton.Instance); } /// @@ -321,45 +328,63 @@ public sealed class MiraPluginManager return false; } - private static void RegisterLocalSettings(Type type) + private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo) { try { - if (!type.IsStatic()) + if (!type.IsAssignableTo(typeof(LocalSettingsTab))) { - Logger.Error($"The type {type.Name} has to be static for local settings."); - return; + return false; + } + + if (!LocalSettingsManager.RegisterTab(type, pluginInfo)) + { + return false; } foreach (var property in type.GetProperties()) { - if (property.PropertyType.IsAssignableTo(typeof(ConfigEntryBase))) + if (!typeof(ConfigEntryBase).IsAssignableFrom(property.PropertyType)) { continue; } - if (property.GetValue(null) is not ConfigEntryBase configEntry) + if (property.GetMethod?.IsStatic == true) + { + Logger.Error($"Option property {property.Name} in {type.Name} must not be static."); + continue; + } + + if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance) { continue; } - var attribute = property.GetCustomAttribute(); + if (property.GetValue(tabInstance) is not ConfigEntryBase configEntry) + { + continue; + } + + var attribute = property.GetCustomAttribute(); if (attribute == null) { continue; } - attribute.Create(configEntry); + attribute.CreateSetting(type, configEntry); } - foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute() != null)) + foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute() != null)) { Logger.Error($"{field.Name} is a field, not a property. Use properties for local settings."); } + + return true; } catch (Exception e) { - Logger.Error($"Failed to register local settings from {type.Name}: {e}"); + Logger.Error($"Failed to register options for {type.Name}: {e.ToString()}"); } + return false; } } diff --git a/MiraAPI/Resources/Settings.png b/MiraAPI/Resources/Settings.png new file mode 100644 index 0000000000000000000000000000000000000000..6f0da713c35a5771e03c581ac0520a5f91629211 GIT binary patch literal 9598 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 148c65a..3f50a79 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -69,6 +69,11 @@ public static class MiraAssets /// Gets the Next Button sprite. /// public static LoadableResourceAsset NextButton { get; } = new("MiraAPI.Resources.NextButton.png"); + + /// + /// Gets the Mira settings icon.. + /// + public static LoadableResourceAsset SettingsIcon { get; } = new("MiraAPI.Resources.Settings.png"); /// /// Gets the highlighted Next Button sprite. diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 6c0bf4c..06ddc76 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -3,8 +3,10 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem.IO; using MiraAPI.Keybinds; +using MiraAPI.Patches.LocalSettings; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; @@ -26,6 +28,7 @@ public static class Helpers { return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x => x.Object)]; } + internal static GameObject CreateKeybindIcon(GameObject button, KeyboardKeyCode keyCode, Vector3 localPos) { var keybindIcon = Object.Instantiate(HudManager.Instance.AbilityButton.usesRemainingSprite.gameObject, button.transform); @@ -36,6 +39,54 @@ public static class Helpers return keybindIcon; } + /// + /// Creates a draggable scroller. Add items into the scroller.Inner transform. This does not automatically position children. + /// + /// Where the scroller should be parented to. + /// The collider of the scroller. Used to drag. + /// The created scroller object. + public static Scroller CreateScroller(Transform parent, BoxCollider2D hitBoxCollider) + { + var scrollObj = new GameObject("Scroller"); + scrollObj.transform.SetParent(parent); + scrollObj.transform.localScale = new Vector3(1, 1, 1); + + var inner = new GameObject("Inner"); + inner.transform.SetParent(scrollObj.transform); + inner.transform.localScale = new Vector3(1, 1, 1); + inner.transform.localPosition = new Vector3(0, 0, 2); + + var scroller = scrollObj.AddComponent(); + scroller.allowX = false; + scroller.allowY = true; + scroller.DragScrollSpeed = 1f; + scroller.Colliders = new Il2CppReferenceArray([hitBoxCollider]); + scroller.Inner = inner.transform; + + return scroller; + } + + /// + /// Divides a button/etc by a certain amount by resizing colliders and renderer sizes. + /// + /// The object you want to divide. + /// How much you want to divide by. + public static void DivideSize(GameObject obj, float amount) + { + foreach (var collider in obj.GetComponentsInChildren(true)) + { + if (collider.TryCast() is { } col) + { + col.size = new Vector2(col.size.x / amount, col.size.y); + } + } + + foreach (var rend in obj.GetComponentsInChildren(true)) + { + rend.size = new Vector2(rend.size.x / amount, rend.size.y); + } + } + /// /// Gets the keybind for an action with ReInput. ///