diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs new file mode 100644 index 0000000..0feb0f0 --- /dev/null +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -0,0 +1,19 @@ +using BepInEx.Configuration; +using MiraAPI.LocalSettings; +using MiraAPI.LocalSettings.Attributes; +using Reactor.Utilities; + +namespace MiraAPI.Example; + +public static class ExampleLocalSetings +{ + public static LocalSettingsTab ModTab { get; } = new LocalSettingsTab("Example Tab"); + + [LocalSettingToggle] + public static ConfigEntry ExampleToggleSetting { get; set; } + + internal static void BindSettings(ConfigFile config) + { + ExampleToggleSetting = config.Bind("General", "Example Toggle", false); + } +} \ No newline at end of file diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index b20ec41..19d23e7 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -21,6 +21,7 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin public ConfigFile GetConfigFile() => Config; public override void Load() { + ExampleLocalSetings.BindSettings(Config); ExampleEventHandlers.Initialize(); Harmony.PatchAll(); } diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs new file mode 100644 index 0000000..3d72d3c --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Base class for all local settings attributes. +/// +[AttributeUsage(AttributeTargets.Property)] +public abstract class LocalSettingAttribute : Attribute +{ +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs new file mode 100644 index 0000000..428acf1 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingToggleAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates a toggle and binds it with the config entry. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalSettingToggleAttribute : LocalSettingAttribute +{ +} diff --git a/MiraAPI/LocalSettings/ILocalSetting.cs b/MiraAPI/LocalSettings/ILocalSetting.cs new file mode 100644 index 0000000..78d3647 --- /dev/null +++ b/MiraAPI/LocalSettings/ILocalSetting.cs @@ -0,0 +1,16 @@ +using BepInEx.Configuration; +using UnityEngine; + +namespace MiraAPI.LocalSettings; + +/// +/// Interface for all local settings +/// +public interface ILocalSetting +{ + string Name { get; } + string Description { get; } + GameObject? Setting { get; } + ConfigEntryBase ConfigEntry { get; } + GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs new file mode 100644 index 0000000..8a6b2dc --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.LocalSettings; + +/// +/// Represents a local settings tab. Gets a button by default +/// +public class LocalSettingsTab +{ + 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; } + + /// + public LocalSettingsTab(string tabName) : this( + tabName, + Color.white, + Palette.AcceptedGreen, + Palette.AcceptedGreen, + createButton: true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// 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, Color tabHoverColor, Color tabActiveColor, LoadableAsset? tabIcon = null, bool createButton = false) + { + TabName = tabName; + TabColor = tabColor; + TabHoverColor = tabHoverColor; + TabActiveColor = tabActiveColor; + TabIcon = tabIcon; + ShouldCreateButton = createButton; + Settings = new(); + } + + public 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 (TabButton != null) + { + TabButton.Content = tab.gameObject; + } + + 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) + { + setting.CreateOption( + toggle, + slider, + tab.transform, + ref contentOffset, + ref contentOrder, + contentIndex == Settings.Count); + contentIndex++; + } + + if (ShouldCreateButton) + { + 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) + { + 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(); + 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); + + 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; + } + + private static string GetShortName(string name) + { + string shortName = string.Empty; + name.Split(' ').Do(x => shortName += x[0]); + return shortName; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs new file mode 100644 index 0000000..a2443dd --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSetingToggle.cs @@ -0,0 +1,117 @@ +using BepInEx.Configuration; +using Reactor.Localization.Utilities; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace MiraAPI.LocalSettings.SettingTypes; + +public class LocalSetingToggle : LocalSettingBase +{ + /// + /// Gets the color of the toggle. + /// + public Color ToggleColor { get; } + + /// + /// Gets the color of the toggle when hovered. + /// + public Color ToggleHoverColor { get; } + + /// + /// Gets the color of the toggle when enabled. + /// + public Color ToggleActiveColor { get; } + + /// + public LocalSetingToggle(LocalSettingsTab tab, ConfigEntryBase configEntry, string name = null, string description = null) : this( + tab, + configEntry, + name, + description, + Color.white, + Palette.AcceptedGreen, + Palette.AcceptedGreen) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The binded config entry. + /// The name of the setting. Defaults to ConfigEntry key. + /// The desription of the setting. No description by default. + /// The color of the toggle. + /// The hover color of the toggle. + /// The active color of the toggle. + public LocalSetingToggle( + LocalSettingsTab tab, + ConfigEntryBase configEntry, + string name, + string description, + Color toggleColor, + Color toggleHoverColor, + Color toggleActiveColor) + : base(tab, configEntry, name, description) + { + ToggleColor = toggleColor; + ToggleHoverColor = toggleHoverColor; + ToggleActiveColor = toggleActiveColor; + } + + public override GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + { + var toggleObject = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = toggleObject.transform.FindChild("Text_TMP").GetComponent(); + var passiveButton = toggleObject.GetComponent(); + var rollover = toggleObject.GetComponent(); + toggleObject.gameObject.SetActive(true); + + if (last && order == 1) + { + // Toggle in the middle + toggleObject.transform.localPosition = new Vector3(0, 1.85f - offset); + } + else + { + toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.35f : 1.28f, 1.85f - offset); + } + + toggleObject.BaseText = CustomStringName.CreateAndRegister(Name); + toggleObject.UpdateText(GetValue()); + toggleObject.name = Name; + toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + passiveButton.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OverColor = ToggleHoverColor; + + passiveButton.OnClick.AddListener((UnityAction)(() => + { + SetValue(!GetValue()); + toggleObject.UpdateText(GetValue()); + toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + })); + passiveButton.OnMouseOver.AddListener((UnityAction)(() => + { + if (!Description.IsNullOrWhiteSpace()) + { + tmp.text = Description; + } + })); + passiveButton.OnMouseOut.AddListener((UnityAction)(() => + { + toggleObject.UpdateText(GetValue()); + toggleObject.Background.color = GetValue() ? ToggleActiveColor : ToggleColor; + })); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return toggleObject.gameObject; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs new file mode 100644 index 0000000..5634ecf --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -0,0 +1,54 @@ +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 ConfigEntryBase ConfigEntry { get; } + + /// + /// Initializes a new instance of the class. + /// + /// 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) + { + ConfigEntry = configEntry; + Name = name ?? ConfigEntry.Definition.Key; + Description = description; + + tab.Settings.Add(this); + } + + /// + /// Creates an instance of the setting. + /// + /// The transform parent of the setting. + /// The created setting. + public abstract GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); + + /// + /// Gets the value of the config entry, casted to T. + /// + /// The value. + public T GetValue() + { + return (T)ConfigEntry.BoxedValue; + } + + /// + /// Sets the value of the config entry + /// + /// The value to set to. + public void SetValue(T value) + { + ConfigEntry.BoxedValue = value; + } +}