diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs new file mode 100644 index 0000000..bac0507 --- /dev/null +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -0,0 +1,53 @@ +using BepInEx.Configuration; +using MiraAPI.LocalSettings; +using MiraAPI.LocalSettings.Attributes; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Example; + +public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) +{ + public override string TabName => "Mira Example"; + protected override bool ShouldCreateLabels => false; + + public override LocalSettingTabAppearance TabAppearance => new() + { + ToggleActiveColor = Color.blue, + ToggleInactiveColor = Color.red, + }; + + [LocalSettingsButton] + public LocalSettingsButton ExampleButton { get; private set; } = new("Example Button", OnExampleButtonClick); + + [LocalToggleSetting] + public ConfigEntry ExampleToggle { get; private set; } = config.Bind("General", "Example Bool", true); + + [LocalToggleSetting] + public ConfigEntry ExampleToggle2 { get; private set; } = config.Bind("General", "Example Bool 2", false); + + [LocalSliderSetting(suffixType: MiraNumberSuffixes.Percent, formatString: "0", roundValue: true)] + public ConfigEntry ExampleSlider { get; private set; } = config.Bind("General", "Slider", 50f); + + [LocalNumberSetting] + public ConfigEntry ExampleNumber { get; private set; } = config.Bind("General", "Example Number", 4f); + + [LocalNumberSetting(min: 0.1f, max: 2.5f, increment: 0.1f, formatString: "0.0", suffixType: MiraNumberSuffixes.Multiplier)] + public ConfigEntry ExampleFloatNumber { get; private set; } = config.Bind("General", "Example Multplier", 1f); + + [LocalEnumSetting] + public ConfigEntry ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries); + + private static void OnExampleButtonClick() + { + LocalSettingsTabSingleton.Instance.Open(); + } +} + +public enum ExampleEnumSetting +{ + Cheeseburger, + Sandwich, + Fries, + ChickenNuggets, +} diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index b20ec41..0aed9c4 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -1,4 +1,5 @@ -using BepInEx; +using System.Linq; +using BepInEx; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; diff --git a/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs new file mode 100644 index 0000000..a4a22e2 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalEnumSettingAttribute.cs @@ -0,0 +1,24 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates an enum setting for the . +/// +/// Optional custom enum names. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalEnumSettingAttribute( + string? name = null, + string? description = null, + string[]? names = null + ) : LocalSettingAttribute(name, description) +{ + /// + internal override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + { + return new LocalEnumSetting(tab, configEntryBase, configEntryBase.SettingType, name, description, names); + } +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs new file mode 100644 index 0000000..febd711 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalNumberSettingAttribute.cs @@ -0,0 +1,33 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates a number setting for the . +/// +/// Minimum range. +/// Maximum range. +/// Increment per use. +/// Suffix for the value. +/// Format string used when formating. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalNumberSettingAttribute( + string? name = null, + string? description = null, + float min = 1, + float max = 5, + float increment = 1, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null + ) : LocalSettingAttribute(name, description) +{ + /// + internal override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + { + return new LocalNumberSetting(tab, configEntryBase, name, description, new FloatRange(min, max), increment, suffixType, formatString); + } +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs new file mode 100644 index 0000000..86dfbc9 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingAttribute.cs @@ -0,0 +1,24 @@ +using System; +using BepInEx.Configuration; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Base class for all local settings attributes. +/// +/// The name of the setting. Defaults to entry key. +/// The description of the setting. Defalts to entry description. +[AttributeUsage(AttributeTargets.Property)] +public abstract class LocalSettingAttribute( + 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. + internal abstract ILocalSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase); +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs new file mode 100644 index 0000000..043a81d --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs @@ -0,0 +1,12 @@ +using System; +using BepInEx.Configuration; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Attribute for local setting buttons. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalSettingsButtonAttribute : Attribute +{ +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs new file mode 100644 index 0000000..4a211d5 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSliderSettingAttribute.cs @@ -0,0 +1,33 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; +using MiraAPI.Utilities; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates a slider setting for the . +/// +/// +/// Minimum range. +/// Maximum range. +/// Should the value be rounded. +/// Suffix for the value. +[AttributeUsage(AttributeTargets.Property)] +public class LocalSliderSettingAttribute( + string? name = null, + float min = 0, + float max = 100, + string? description = null, + bool displayValue = false, + string? formatString = null, + bool roundValue = false, + MiraNumberSuffixes suffixType = MiraNumberSuffixes.None + ) : LocalSettingAttribute(name, description) +{ + /// + internal override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + { + return new LocalSliderSetting(tab, configEntryBase, name, description, new FloatRange(min, max), displayValue, suffixType, formatString, roundValue); + } +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs new file mode 100644 index 0000000..31c9b09 --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalToggleSettingAttribute.cs @@ -0,0 +1,22 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.LocalSettings.SettingTypes; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Creates a toggle setting for the . +/// +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalToggleSettingAttribute( + string? name = null, + string? description = null + ) : LocalSettingAttribute(name, description) +{ + /// + internal override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase) + { + return new LocalToggleSetting(tab, configEntryBase, name, description); + } +} diff --git a/MiraAPI/LocalSettings/ILocalSetting.cs b/MiraAPI/LocalSettings/ILocalSetting.cs new file mode 100644 index 0000000..a94e102 --- /dev/null +++ b/MiraAPI/LocalSettings/ILocalSetting.cs @@ -0,0 +1,42 @@ +using BepInEx.Configuration; +using UnityEngine; + +namespace MiraAPI.LocalSettings; + +/// +/// Interface for all local settings. +/// +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/LocalSettingsButton.cs b/MiraAPI/LocalSettings/LocalSettingsButton.cs new file mode 100644 index 0000000..fa0c0bf --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsButton.cs @@ -0,0 +1,78 @@ +using System; +using MiraAPI.Utilities; +using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings; + +/// +/// Represents a local settings button. +/// +public class LocalSettingsButton(string text, Action onClick) +{ + /// + /// Gets or sets the button text. + /// + public string Text { get; set; } = text; + + /// + /// Gets or sets the button on-click action. + /// + public Action OnClick { get; set; } = onClick; + + /// + /// Gets the tab instance this button belongs to. + /// + public LocalSettingsTab? Tab { get; internal set; } + + internal GameObject CreateButton(ToggleButtonBehaviour toggle, Transform parent, ref float offset, ref int order, bool last) + { + var button = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + var rollover = button.GetComponent(); + tmp.GetComponent().Destroy(); + button.gameObject.SetActive(true); + + var toggleComp = button.GetComponent(); + var background = toggleComp.Background; + button.transform.FindChild("ButtonHighlight")?.gameObject.DestroyImmediate(); + toggleComp.Destroy(); + + if (last && order == 1) + { + // Button in the middle + button.transform.localPosition = new Vector3(0, 1.85f - offset, -7); + } + else + { + button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); + } + + tmp.text = Text; + button.name = Text; + button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OutColor = Tab!.TabAppearance.ButtonColor; + rollover.OverColor = Tab!.TabAppearance.ButtonHoverColor; + rollover.Target = background; + background.color = Tab!.TabAppearance.ButtonColor; + button.OnClick.AddListener((UnityAction)OnClick); + + background.transform.localScale = new Vector3(1, 1.25f, 1); + Helpers.DivideSize(button.gameObject, 1.1f); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return button.gameObject; + } +} 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 new file mode 100644 index 0000000..a04830d --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections.Generic; +using BepInEx.Configuration; +using HarmonyLib; +using MiraAPI.Patches.LocalSettings; +using MiraAPI.Utilities; +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 abstract class LocalSettingsTab(ConfigFile config) +{ + /// + /// Gets the name of the tab. + /// + public abstract string TabName { get; } + + /// + /// Gets the tab appearance. + /// + public virtual LocalSettingTabAppearance TabAppearance { get; } = new(); + + /// + /// Gets a value indicating whether a button should be created for the tab. + /// + public virtual bool ShouldCreateButton => true; + + /// + /// Gets a value indicating whether the tab should have category labels. + /// + protected virtual bool ShouldCreateLabels => true; + + /// + /// Gets the mod's config file. Automatically found in the plugin loader. + /// + public ConfigFile Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); + + /// + /// Gets the tab button instance. + /// + public TabGroup? TabButton { get; internal set; } + + /// + /// Gets the list of settings. + /// + public List Settings { get; } = []; + + /// + /// Gets the list of buttons. + /// + public List Buttons { get; } = []; + + /// + /// Gets the index of this tab. + /// + protected int TabIndex => LocalSettingsManager.Tabs.IndexOf(this) + 10; + + private Scroller? Scroller { get; set; } + + /// + /// Invoked when an option value is changed. + /// + /// The config entry which was changed. + public virtual void OnOptionChanged(ConfigEntryBase configEntry) + { + } + + /// + /// Attempts to open the tab. + /// Override for custom behavior. + /// + public virtual void Open() + { + OptionsMenuPatches.Instance?.OpenTabGroup(TabIndex); + } + + /// + /// Creates the tab and it's content. + /// + /// The instace. + /// The created tab . + public virtual GameObject CreateTab(OptionsMenuBehaviour instance) + { + 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) + { + if (ShouldCreateLabels) + { + 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(true).Do(x => + { + x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + x.sortingOrder = 150; + x.sortingLayerName = "Default"; + }); + + obj.GetComponentsInChildren(true).Do(x => + { + x.sortingOrder = 151; + x.sortingLayerName = "Default"; + }); + + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); + + contentIndex++; + } + + contentOrder = 1; + } + + contentIndex = 1; + contentOrder = 1; + foreach (var button in Buttons) + { + var obj = button.CreateButton( + toggle, + Scroller.Inner, + ref contentOffset, + ref contentOrder, + contentIndex == Buttons.Count); + + obj!.GetComponentsInChildren(true).Do(x => + { + x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + x.sortingOrder = 150; + x.sortingLayerName = "Default"; + }); + + obj.GetComponentsInChildren(true).Do(x => + { + x.sortingOrder = 151; + x.sortingLayerName = "Default"; + }); + + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); + + contentIndex++; + } + + Scroller.SetBounds(new FloatRange(0, (Settings.Count + Buttons.Count) * 0.5f - 5f), new FloatRange(0, 0)); + return tab; + } + + /// + /// Creates the tab button. + /// + /// The instance. + /// The tab index. + /// The current button offset. + /// The created button . + public virtual GameObject 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 = TabAppearance.TabButtonColor; + 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 = TabAppearance.TabButtonHoverColor; + rollover.OutColor = TabAppearance.TabButtonColor; + + SpriteRenderer? tabButtonRend = null; + if (TabAppearance.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.2f, 0.2f, 1); + tabButtonRend.sprite = TabAppearance.TabIcon.LoadAsset(); + tabButtonText.gameObject.SetActive(false); + } + + 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)(() => + { + tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f); + 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)(() => + { + if (!tabButtonObject.Content.gameObject.activeSelf) + { + //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); + } + + tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0); + tabButtonText.maxVisibleCharacters = 4; + tabButtonText.text = $"{GetShortName(TabName)}"; + })); + + return tabButtonObject.gameObject; + } + + 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) + { + var shortName = string.Empty; + name.Split(' ').Do(x => shortName += x[0]); + return shortName; + } +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs new file mode 100644 index 0000000..26fd726 --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs @@ -0,0 +1,91 @@ +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.LocalSettings; + +/// +/// Struct for modyfing tab apparance. +/// Defines how the settings look. +/// +public record struct LocalSettingTabAppearance() +{ + /// + /// Gets or sets main color of the tab. + /// + public Color TabColor { get; set; } = Color.white; + + /// + /// Gets or sets tab button icon. No icon by default. + /// + public LoadableAsset? TabIcon { get; set; } + + /// + /// Gets or sets color of the tab button. + /// + public Color TabButtonColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of the tab button. + /// + public Color TabButtonHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets active color of the tab button. + /// + public Color TabButtonActiveColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets hover color of toggles. + /// + public Color ToggleHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets active color of toggles. + /// + public Color ToggleActiveColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets inactive color of toggles. + /// + public Color ToggleInactiveColor { get; set; } = Color.red; + + /// + /// Gets or sets color of number buttons. + /// + public Color NumberColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of number buttons. + /// + public Color NumberHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets color of sliders. + /// + public Color SliderColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of sliders. + /// + public Color SliderHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets color of enum buttons. + /// + public Color EnumColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of enum buttons. + /// + public Color EnumHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets color of buttons. + /// + public Color ButtonColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of buttons. + /// + public Color ButtonHoverColor { get; set; } = Palette.AcceptedGreen; +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs new file mode 100644 index 0000000..eec81a2 --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsTabSingleton.cs @@ -0,0 +1,19 @@ +using System.Linq; + +namespace MiraAPI.LocalSettings; + +/// +/// 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/LocalEnumSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs new file mode 100644 index 0000000..437eed1 --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalEnumSetting.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using BepInEx.Configuration; +using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings.SettingTypes; + +/// +/// Local setting class for enums. +/// +public class LocalEnumSetting : LocalSettingBase +{ + /// + /// Gets the enum type of the setting. + /// + public Type EnumType { get; } + + /// + /// Gets the enum values. + /// + public string[] Values { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The enum type. + /// The optional values array to replace the enum names. + public LocalEnumSetting( + Type tab, + ConfigEntryBase configEntry, + Type enumType, + string? name = null, + string? description = null, + string[]? values = null) + : base(tab, configEntry, name, description) + { + EnumType = enumType; + Values = values ?? Enum + .GetValues(configEntry.SettingType) + .Cast() + .Select(x => x.ToDisplayString()) + .ToArray(); + } + + /// + public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + { + var button = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + var rollover = button.GetComponent(); + tmp.GetComponent().Destroy(); + button.gameObject.SetActive(true); + + var toggleComp = button.GetComponent(); + var background = toggleComp.Background; + var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); + if (highlight != null) + { + highlight.color = Tab!.TabAppearance.EnumHoverColor; + highlight.gameObject.SetActive(false); + } + toggleComp.Destroy(); + + if (last && order == 1) + { + // Button in the middle + button.transform.localPosition = new Vector3(0, 1.85f - offset, -7); + } + else + { + button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); + } + + tmp.text = GetValueText(); + button.name = Name; + button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OutColor = Tab!.TabAppearance.EnumColor; + rollover.OverColor = Tab!.TabAppearance.EnumHoverColor; + background.color = Tab!.TabAppearance.EnumColor; + + button.OnClick.AddListener((UnityAction)(() => + { + int value = GetValue(); + value++; + if (value >= Values.Length) + { + value = 0; + } + + SetValue(value); + tmp.text = GetValueText(); + })); + button.OnMouseOver.AddListener((UnityAction)(() => + { + if (!Description.IsNullOrWhiteSpace()) + { + tmp.text = Description; + } + highlight?.gameObject.SetActive(true); + })); + button.OnMouseOut.AddListener((UnityAction)(() => + { + tmp.text = GetValueText(); + highlight?.gameObject.SetActive(false); + })); + + Helpers.DivideSize(button.gameObject, 1.1f); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return button.gameObject; + } + + /// + protected override string GetValueText() + { + return $"{Name}: {Values[GetValue()]}"; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs new file mode 100644 index 0000000..fb86fdc --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalNumberSetting.cs @@ -0,0 +1,145 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings.SettingTypes; + +/// +/// Local setting class for numbers. +/// +public class LocalNumberSetting : LocalSettingBase +{ + /// + /// Gets the range of the button. + /// + public FloatRange NumberRange { get; } + + /// + /// Gets the increment of the value when button is pressed. + /// + public float Increment { get; } + + /// + /// Gets a format for the text to use to format the number. + /// + public string FormatString { get; } + + /// + /// Gets the suffix for the number value. + /// + public MiraNumberSuffixes SuffixType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The value range. + /// The increment per click. + /// The suffix used for formating. + /// The format string used for formating. + public LocalNumberSetting( + Type tab, + ConfigEntryBase configEntry, + string? name = null, + string? description = null, + FloatRange? numberRange = null, + float? increment = null, + MiraNumberSuffixes? suffixType = null, + string? formatString = null) + : base(tab, configEntry, name, description) + { + SuffixType = suffixType ?? MiraNumberSuffixes.None; + NumberRange = numberRange ?? new FloatRange(1, 5); + Increment = increment ?? 1; + FormatString = formatString ?? "0"; + } + + /// + public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + { + var button = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + var rollover = button.GetComponent(); + tmp.GetComponent().Destroy(); + button.gameObject.SetActive(true); + + var toggleComp = button.GetComponent(); + var background = toggleComp.Background; + var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent(); + if (highlight != null) + { + highlight.color = Tab!.TabAppearance.NumberHoverColor; + highlight.gameObject.SetActive(false); + } + toggleComp.Destroy(); + + if (last && order == 1) + { + // Button in the middle + button.transform.localPosition = new Vector3(0, 1.85f - offset, -7); + } + else + { + button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); + } + + tmp.text = GetValueText(); + button.name = Name; + button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OutColor = Tab!.TabAppearance.NumberColor; + rollover.OverColor = Tab!.TabAppearance.NumberHoverColor; + background.color = Tab!.TabAppearance.NumberColor; + + button.OnClick.AddListener((UnityAction)(() => + { + float value = GetValue(); + value += Increment; + if (value > NumberRange.max) + { + value = NumberRange.min; + } + + SetValue(value); + tmp.text = GetValueText(); + })); + button.OnMouseOver.AddListener((UnityAction)(() => + { + if (!Description.IsNullOrWhiteSpace()) + { + tmp.text = Description; + } + highlight?.gameObject.SetActive(true); + })); + button.OnMouseOut.AddListener((UnityAction)(() => + { + tmp.text = GetValueText(); + highlight?.gameObject.SetActive(false); + })); + + Helpers.DivideSize(button.gameObject, 1.1f); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return button.gameObject; + } + + /// + protected override string GetValueText() + { + var value = GetValue(); + var formated = Helpers.FormatValue(value, SuffixType, FormatString); + return $"{Name}: {formated}"; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs new file mode 100644 index 0000000..d144ce4 --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using BepInEx.Configuration; +using UnityEngine; + +namespace MiraAPI.LocalSettings.SettingTypes; + +/// +public abstract class LocalSettingBase : ILocalSetting +{ + /// + public string Name { get; } + + /// + public string Description { get; } + + /// + public GameObject? Setting { get; } = null!; + + /// + public ConfigEntryBase ConfigEntry { get; } + + internal LocalSettingsTab? Tab => LocalSettingsManager.Tabs.FirstOrDefault(x => x.Settings.Contains(this)); + + /// + /// Initializes a new instance of the class. + /// + /// The tab to create the setting in. + /// The config entry. + /// The name of the setting. + /// The description of the setting. + protected LocalSettingBase(Type tab, ConfigEntryBase configEntry, string? name = null, string? description = null) + { + ConfigEntry = configEntry; + Name = name ?? ConfigEntry.Definition.Key; + Description = description ?? ConfigEntry.Description.Description; + LocalSettingsManager.TypeToTab[tab].Settings.Add(this); + } + + /// + /// Creates an instance of the setting. + /// + /// The created setting. + public abstract GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last); + + /// + /// Returns the formated string to use in the text of the setting. + /// + /// The value text. + protected virtual string GetValueText() + { + return string.Empty; + } + + /// + /// Gets the value of the config entry, casted to the setting type. + /// + /// The value. + public virtual T GetValue() + { + return (T)ConfigEntry.BoxedValue; + } + + /// + /// Sets the value of the config entry. + /// + /// The value to set to. + public virtual void SetValue(T value) + { + ConfigEntry.BoxedValue = value; + Tab?.OnOptionChanged(ConfigEntry); + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs new file mode 100644 index 0000000..ae6f500 --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSliderSetting.cs @@ -0,0 +1,120 @@ +using System; +using BepInEx.Configuration; +using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings.SettingTypes; + +/// +/// Local setting class for sliders. +/// +public class LocalSliderSetting : LocalSettingBase +{ + /// + /// Gets the range of the slider. + /// + public FloatRange SliderRange { get; } + + /// + /// Gets a value indicating whether the value should be displayed next to name. + /// + public bool DisplayValue { get; } + + /// + /// Gets a format for the text to use to format the number. + /// + public string FormatString { get; } + + /// + /// Gets a value indicating whether the value should be rounded. + /// + public bool RoundValue { get; } + + /// + /// Gets the suffix for the number value. + /// + public MiraNumberSuffixes SuffixType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The value range. + /// The suffix used for formating. + /// The format string used for formating. + /// Should the value be rounded. + public LocalSliderSetting( + Type tab, + ConfigEntryBase configEntry, + string? name = null, + string? description = null, + FloatRange? sliderRange = null, + bool displayValue = false, + MiraNumberSuffixes? suffixType = null, + string? formatString = null, + bool roundValue = false) + : base(tab, configEntry, name, description) + { + SliderRange = sliderRange ?? new FloatRange(0, 100); + DisplayValue = displayValue; + SuffixType = suffixType ?? MiraNumberSuffixes.None; + FormatString = formatString ?? "0.0"; + RoundValue = roundValue; + } + + /// + public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last) + { + var newSlider = Object.Instantiate(slider, parent).GetComponent(); + var rollover = newSlider.GetComponent(); + newSlider.Title = newSlider.transform.FindChild("Text_TMP").GetComponent(); // Why the hell slider has a title property that is not even assigned??? + newSlider.Title.GetComponent().Destroy(); + newSlider.gameObject.SetActive(true); + newSlider.Bar.color = Tab!.TabAppearance.SliderColor; + + if (order == 2) + offset += 0.5f; + + newSlider.Bar.transform.localPosition = new Vector3(2.85f, 0, 0); + newSlider.transform.localPosition = new Vector3(-2.12f, 1.85f - offset, -7); + newSlider.name = Name; + newSlider.Range = new FloatRange(-1.5f, 1.5f); + newSlider.SetValue(Mathf.InverseLerp(SliderRange.min, SliderRange.max, GetValue())); + rollover.OutColor = Tab!.TabAppearance.SliderColor; + rollover.OverColor = Tab!.TabAppearance.SliderHoverColor; + newSlider.Title.transform.localPosition = new Vector3(0.5f, 0, -1f); + newSlider.Title.horizontalAlignment = DisplayValue ? HorizontalAlignmentOptions.Left : HorizontalAlignmentOptions.Center; + newSlider.Title.text = GetValueText(); + + newSlider.OnValueChange.AddListener((UnityAction)(() => + { + SetValue(RoundValue + ? Mathf.Round(Mathf.Lerp(SliderRange.min, SliderRange.max, newSlider.Value)) + : Mathf.Lerp(SliderRange.min, SliderRange.max, newSlider.Value)); + + newSlider.Title.text = GetValueText(); + })); + + order = 1; + offset += 0.5f; + return newSlider.gameObject; + } + + /// + protected override string GetValueText() + { + if (DisplayValue) + { + var value = GetValue(); + var formated = Helpers.FormatValue(value, SuffixType, FormatString); + var maxFormated = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString); + return $"{Name}: {formated} / {maxFormated}"; + } + + return $"{Name}"; + } +} diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs new file mode 100644 index 0000000..54cd8ee --- /dev/null +++ b/MiraAPI/LocalSettings/SettingTypes/LocalToggleSetting.cs @@ -0,0 +1,88 @@ +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; + +/// +/// Local setting class for toggles. +/// +public class LocalToggleSetting : LocalSettingBase +{ + /// + /// Initializes a new instance of the class. + /// + /// + public LocalToggleSetting( + Type tab, + ConfigEntryBase configEntry, + string? name = null, + string? description = null + ) : base(tab, configEntry, name, description) + { + } + + /// + 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, -7); + } + else + { + toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); + } + + toggleObject.BaseText = CustomStringName.CreateAndRegister(Name); + toggleObject.UpdateText(GetValue()); + toggleObject.name = Name; + toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; + passiveButton.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OverColor = Tab!.TabAppearance.ToggleHoverColor; + + passiveButton.OnClick.AddListener((UnityAction)(() => + { + SetValue(!GetValue()); + toggleObject.UpdateText(GetValue()); + toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; + })); + passiveButton.OnMouseOver.AddListener((UnityAction)(() => + { + if (!Description.IsNullOrWhiteSpace()) + { + tmp.text = Description; + } + })); + passiveButton.OnMouseOut.AddListener((UnityAction)(() => + { + toggleObject.UpdateText(GetValue()); + toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor; + })); + + Helpers.DivideSize(toggleObject.gameObject, 1.1f); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return toggleObject.gameObject; + } +} 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/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs new file mode 100644 index 0000000..3014411 --- /dev/null +++ b/MiraAPI/MiraApiSettings.cs @@ -0,0 +1,28 @@ +using BepInEx.Configuration; +using MiraAPI.LocalSettings; +using MiraAPI.LocalSettings.Attributes; +using MiraAPI.Utilities.Assets; + +namespace MiraAPI; + +/// +/// Mira API Config File Handler +/// +public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) +{ + /// + public override string TabName => "Mira API"; + + /// + public override LocalSettingTabAppearance TabAppearance => new() + { + TabButtonHoverColor = MiraApiPlugin.MiraColor, + TabIcon = MiraAssets.SettingsIcon, + }; + + /// + /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers. + /// + [LocalToggleSetting] + 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..ef812a5 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.LocalSettings; 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..2982f67 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using HarmonyLib; using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Patches.Roles; +using MiraAPI.LocalSettings; using MiraAPI.Utilities.Assets; -using Reactor.Utilities; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using TMPro; @@ -98,7 +96,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..574070f --- /dev/null +++ b/MiraAPI/Patches/LocalSettings/OptionsMenuPatches.cs @@ -0,0 +1,232 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using MiraAPI.LocalSettings; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; + +namespace MiraAPI.Patches.LocalSettings; + +[HarmonyPatch(typeof(OptionsMenuBehaviour))] +public static class OptionsMenuPatches +{ + internal static OptionsMenuBehaviour? Instance { get; private set; } + internal static BoxCollider2D MaskCollider; + private static SpriteRenderer? background; + + private static int currentPage = 1; + private static Dictionary> tabButtons = new(); + + /// + /// Creates the tabs and their content + /// + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Start))] + public static void StartPostfix(OptionsMenuBehaviour __instance) + { + Instance = __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); + } + + background = __instance.Background; + + 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; + + currentPage = 1; + tabButtons.Clear(); + float yOffset = 0; + int i = 0; + int tabIdx = 0; + int page = 1; + foreach (var settings in LocalSettingsManager.Tabs) + { + var tab = settings.CreateTab(__instance); + if (settings.ShouldCreateButton) + { + var button = settings.CreateTabButton(__instance, ref tabIdx, ref yOffset); + button.GetComponent().Content = tab; + button.SetActive(page == currentPage); + + if (!tabButtons.TryGetValue(page, out var list)) + { + tabButtons.Add(page, new()); + list = tabButtons[page]; + } + list.Add(button); + yOffset += 0.6f; + tabIdx++; + i++; + } + if (i > 7) + { + i = 0; + yOffset = 0; + page++; + } + } + + void ChangePage(int increase) + { + currentPage += increase; + if (currentPage > tabButtons.Count) + { + currentPage = 1; + } + if (currentPage < 1) + { + currentPage = tabButtons.Count; + } + } + + // Create page buttons + // TODO: In-game menu has no close button. I tried making one from scratch but failed. Add one later plz + if (tabButtons.Count <= 1) + { + return; + } + + var nextButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent(); + nextButton.transform.localPosition = new Vector3(3.2f, 2.6f, -10); + nextButton.transform.localScale = new Vector2(0.25f, 0.25f); + var nextRend = nextButton.GetComponent(); + nextRend.sprite = MiraAssets.NextButton.LoadAsset(); + nextButton.gameObject.GetComponent().DestroyImmediate(); + + var nextRollover = nextButton.gameObject.AddComponent(); + nextRollover.Target = nextRend; + nextRollover.OverColor = Palette.AcceptedGreen; + + nextButton.OnClick = new Button.ButtonClickedEvent(); + nextButton.OnClick.AddListener( + (UnityAction)(() => + { + ChangePage(1); + UpdatePages(); + })); + + var backButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent(); + backButton.transform.localPosition = new Vector3(2.9f, 2.6f, -10); + backButton.transform.localScale = new Vector2(0.25f, 0.25f); + var backRend = backButton.GetComponent(); + backRend.sprite = MiraAssets.NextButton.LoadAsset(); + backRend.flipX = true; + backButton.gameObject.GetComponent().DestroyImmediate(); + + var backRollover = backButton.gameObject.AddComponent(); + backRollover.Target = backRend; + backRollover.OverColor = Palette.AcceptedGreen; + + backButton.OnClick = new Button.ButtonClickedEvent(); + backButton.OnClick.AddListener( + (UnityAction)(() => + { + ChangePage(-1); + UpdatePages(); + })); + } + + /// + /// 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 + + // Tabs with index 10 and above are the mod settings tabs + if (index >= 10) + { + CustomOpen(LocalSettingsManager.Tabs[index - 10]); + return false; + } + + return true; + } + + /// + /// Makes the custom tabs close when opening the options menu and resets pages. + /// + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Open))] + public static void OpenPostfix(OptionsMenuBehaviour __instance) + { + LocalSettingsManager.Tabs.ForEach(CustomClose); + currentPage = 1; + UpdatePages(); + } + + private static void UpdatePages() + { + foreach (var pages in tabButtons) + { + pages.Value? + .Where(x => x != null) + .Do( + x => x?.SetActive( + pages.Key == currentPage)); + } + } + + private static void CustomOpen(LocalSettingsTab tab) + { + if (tab.TabButton?.Button) + tab.TabButton!.Button.color = tab.TabAppearance.TabButtonHoverColor; + + if (tab.TabButton?.Rollover) + tab.TabButton!.Rollover.OutColor = tab.TabAppearance.TabButtonHoverColor; + + if (tab.TabButton?.Content) + tab.TabButton!.Content.SetActive(true); + + if (background != null) + background.color = tab.TabAppearance.TabColor; + } + private static void CustomClose(LocalSettingsTab tab) + { + if (tab.TabButton?.Button) + tab.TabButton!.Button.color = tab.TabAppearance.TabButtonColor; + + if (tab.TabButton?.Rollover) + tab.TabButton!.Rollover.OutColor = tab.TabAppearance.TabButtonColor; + + if (tab.TabButton?.Content) + tab.TabButton!.Content.SetActive(false); + + if (background != null) + background.color = Color.white; + } +} diff --git a/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs b/MiraAPI/Patches/LocalSettings/ToggleBehaviourPatch.cs new file mode 100644 index 0000000..9a8321d --- /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. + /// Skill issue + /// + [HarmonyPrefix] + [HarmonyPatch(nameof(ToggleButtonBehaviour.UpdateText))] + public static bool UpdateTextPrefix(ToggleButtonBehaviour __instance, bool on) + { + var color = on ? new Color(0f, 1f, 0.16470589f, 1f) : Color.white; + __instance.onState = on; + __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 34e2033..a74b02c 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using MiraAPI.Colors; using MiraAPI.Events; @@ -12,12 +13,15 @@ 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; using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; +using Rewired; namespace MiraAPI.PluginLoading; @@ -91,6 +95,11 @@ public sealed class MiraPluginManager continue; } + if (RegisterLocalTabs(type, plugin)) + { + continue; + } + if (RegisterRole(type, info, out var role)) { roles.Add(role); @@ -137,6 +146,7 @@ public sealed class MiraPluginManager }; RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton.Instance); + RegisterLocalTabs(typeof(MiraApiSettings), PluginSingleton.Instance); } /// @@ -308,6 +318,77 @@ public sealed class MiraPluginManager return false; } + private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo) + { + try + { + if (!type.IsAssignableTo(typeof(LocalSettingsTab))) + { + return false; + } + + if (!LocalSettingsManager.RegisterTab(type, pluginInfo)) + { + return false; + } + + foreach (var property in type.GetProperties()) + { + if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance) + { + continue; + } + + if (property.GetCustomAttribute() != null && + property.PropertyType.IsAssignableTo(typeof(LocalSettingsButton))) + { + var button = property.GetValue(tabInstance) as LocalSettingsButton; + button!.Tab = tabInstance; + tabInstance.Buttons.Add(button); + continue; + } + + if (!typeof(ConfigEntryBase).IsAssignableFrom(property.PropertyType)) + { + continue; + } + + if (property.GetMethod?.IsStatic == true) + { + Logger.Error($"Option property {property.Name} in {type.Name} must not be static."); + continue; + } + + if (property.GetValue(tabInstance) is not ConfigEntryBase configEntry) + { + Logger.Error($"Option property {property.Name} in {type.Name} has to be a config entry."); + continue; + } + + var attribute = property.GetCustomAttribute(); + if (attribute == null) + { + continue; + } + + attribute.CreateSetting(type, configEntry); + } + + 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 options for {type.Name}: {e.ToString()}"); + } + + return false; + } + private static void RegisterKeybinds(Type type, BasePlugin source) { try 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 0da66ae..ddbf354 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 7731aac..6a00751 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,7 +1,10 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using MiraAPI.Keybinds; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; @@ -34,6 +37,66 @@ 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. + /// + /// The action ID. + /// The keyboard key code. + public static KeyboardKeyCode GetKeybindByActionId(int actionId) + { + var player = ReInput.players.GetPlayer(0); + return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false) + .keyboardKeyCode; + } + /// /// Determines whether a given probability check succeeds. /// @@ -412,5 +475,43 @@ public static class Helpers return new string(Enumerable.Repeat(chars, length) .Select(s => s[UnityEngine.Random.RandomRangeInt(0, s.Length)]).ToArray()); } -} + /// + /// Finds and returns an unused KeyCode that is not equal to the excluded key. + /// + /// The KeyCode to skip during the search. + /// + /// The first available KeyCode not currently used by any registered keybind, + /// or KeyCode.None if none are available. + /// + public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude) + { + foreach (KeyboardKeyCode key in Enum.GetValues(typeof(KeyboardKeyCode))) + { + if (key == exclude) continue; + bool used = KeybindManager.GetEntries().Exists(e => e.Key == key); + if (!used) return key; + } + return KeyboardKeyCode.None; + } + + /// + /// Returns the formated value using the specified suffix and format string. + /// + /// The value to format. + /// The suffix to add. + /// The format string to use to format. + /// The formated value. + public static string FormatValue(float value, MiraNumberSuffixes suffix = MiraNumberSuffixes.None, string formatString = "0.0") + { + return suffix switch + { + MiraNumberSuffixes.None => value.ToString(formatString, NumberFormatInfo.InvariantInfo), + MiraNumberSuffixes.Multiplier => value.ToString(formatString, NumberFormatInfo.InvariantInfo) + "x", + MiraNumberSuffixes.Percent => value.ToString(formatString, NumberFormatInfo.InvariantInfo) + "%", + _ => TranslationController.Instance.GetString( + StringNames.GameSecondsAbbrev, + (Il2CppSystem.Object[])[value.ToString(formatString, CultureInfo.InvariantCulture)]), + }; + } +}