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 0000000..6f0da71 Binary files /dev/null and b/MiraAPI/Resources/Settings.png differ 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. ///