diff --git a/PathfinderAPI/Event/Options/CustomOptionsLoadEvent.cs b/PathfinderAPI/Event/Options/CustomOptionsLoadEvent.cs new file mode 100644 index 00000000..2def3a90 --- /dev/null +++ b/PathfinderAPI/Event/Options/CustomOptionsLoadEvent.cs @@ -0,0 +1,6 @@ +namespace Pathfinder.Event.Options; + +public class CustomOptionsLoadEvent : PathfinderEvent +{ + public CustomOptionsLoadEvent() { } +} \ No newline at end of file diff --git a/PathfinderAPI/Meta/Load/HacknetPluginExtensions.cs b/PathfinderAPI/Meta/Load/HacknetPluginExtensions.cs index ff5949ca..4dba1d90 100644 --- a/PathfinderAPI/Meta/Load/HacknetPluginExtensions.cs +++ b/PathfinderAPI/Meta/Load/HacknetPluginExtensions.cs @@ -6,13 +6,13 @@ public static class HacknetPluginExtensions { public static string GetOptionsTag(this HacknetPlugin plugin) { - if(!OptionsTabAttribute.pluginToOptionsTag.TryGetValue(plugin, out var tag)) + if(!OptionsTabAttribute.pluginToOptTabAttribute.TryGetValue(plugin, out var attr)) return null; - return tag; + return attr.TabName; } public static bool HasOptionsTag(this HacknetPlugin plugin) { - return OptionsTabAttribute.pluginToOptionsTag.ContainsKey(plugin); + return OptionsTabAttribute.pluginToOptTabAttribute.ContainsKey(plugin); } } \ No newline at end of file diff --git a/PathfinderAPI/Meta/Load/OptionAttribute.cs b/PathfinderAPI/Meta/Load/OptionAttribute.cs index 3c594577..66a0d23a 100644 --- a/PathfinderAPI/Meta/Load/OptionAttribute.cs +++ b/PathfinderAPI/Meta/Load/OptionAttribute.cs @@ -7,48 +7,55 @@ namespace Pathfinder.Meta.Load; [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class OptionAttribute : BaseAttribute { - public string Tag { get; set; } + [Obsolete("Use TabName")] + public string Tag { get => TabName; set => TabName = value; } + public string TabName { get; set; } + public string TabId { get; set; } - public OptionAttribute(string tag = null) + public OptionAttribute(string tag = null, string tabId = null) { - this.Tag = tag; + TabName = tag; + TabId = tabId; } public OptionAttribute(Type pluginType) { - this.Tag = pluginType.GetCustomAttribute()?.Tag; + var tabAttr = pluginType.GetCustomAttribute(); + TabName = tabAttr.TabName; + TabId = tabAttr.TabId; } protected internal override void CallOn(HacknetPlugin plugin, MemberInfo targettedInfo) { - if(Tag == null) + if(TabName == null) { - Tag = plugin.GetOptionsTag(); - if(Tag == null) + if(!OptionsTabAttribute.pluginToOptTabAttribute.TryGetValue(plugin, out var tab)) throw new InvalidOperationException($"Could not find Pathfinder.Meta.Load.OptionsTabAttribute for {targettedInfo.DeclaringType.FullName}"); + TabName = tab.TabName; + TabId = tab.TabId; } if(targettedInfo.DeclaringType != plugin.GetType()) throw new InvalidOperationException($"Pathfinder.Meta.Load.OptionAttribute is only valid in a class derived from BepInEx.Hacknet.HacknetPlugin"); - Option option = null; + IPluginOption option = null; switch(targettedInfo) { case PropertyInfo propertyInfo: - if(!propertyInfo.PropertyType.IsSubclassOf(typeof(Option))) - throw new InvalidOperationException($"Property {propertyInfo.Name}'s type does not derive from Pathfinder.Options.Option"); - option = (Option)(propertyInfo.GetGetMethod()?.Invoke(plugin, null)); + if(!typeof(IPluginOption).IsAssignableFrom(propertyInfo.PropertyType)) + throw new InvalidOperationException($"Property {propertyInfo.Name}'s type does not derive from Pathfinder.Options.IPluginOption"); + option = (IPluginOption)(propertyInfo.GetGetMethod()?.Invoke(plugin, null)); break; case FieldInfo fieldInfo: - if(!fieldInfo.FieldType.IsSubclassOf(typeof(Option))) - throw new InvalidOperationException($"Field {fieldInfo.Name}'s type does not derive from Pathfinder.Options.Option"); - option = (Option)fieldInfo.GetValue(plugin); + if(!typeof(IPluginOption).IsAssignableFrom(fieldInfo.FieldType)) + throw new InvalidOperationException($"Field {fieldInfo.Name}'s type does not derive from Pathfinder.Options.IPluginOption"); + option = (IPluginOption)fieldInfo.GetValue(plugin); break; } if(option == null) - throw new InvalidOperationException($"Option not set to a default value, Option members should be set before HacknetPlugin.Load() is called"); + throw new InvalidOperationException($"IPluginOption not set to a default value, IPluginOption members should be set before HacknetPlugin.Load() is called"); - OptionsManager.AddOption(Tag, option); + OptionsManager.GetOrRegisterTab(plugin, TabName, TabId).AddOption(option); } } \ No newline at end of file diff --git a/PathfinderAPI/Meta/Load/OptionsTabAttribute.cs b/PathfinderAPI/Meta/Load/OptionsTabAttribute.cs index e941e0e9..aac27212 100644 --- a/PathfinderAPI/Meta/Load/OptionsTabAttribute.cs +++ b/PathfinderAPI/Meta/Load/OptionsTabAttribute.cs @@ -6,17 +6,21 @@ namespace Pathfinder.Meta.Load; [AttributeUsage(AttributeTargets.Class)] public class OptionsTabAttribute : BaseAttribute { - internal static readonly Dictionary pluginToOptionsTag = new Dictionary(); + internal static readonly Dictionary pluginToOptTabAttribute = new Dictionary(); - public string Tag { get; } + [Obsolete("Use TabName")] + public string Tag { get => TabName; set => TabName = value; } + public string TabName { get; set; } + public string TabId { get; set; } - public OptionsTabAttribute(string tag) + public OptionsTabAttribute(string tag, string tabId = null) { - this.Tag = tag; + TabName = tag; + TabId = tabId; } protected internal override void CallOn(HacknetPlugin plugin, MemberInfo targettedInfo) { - pluginToOptionsTag.Add(plugin, Tag); + pluginToOptTabAttribute.Add(plugin, this); } } \ No newline at end of file diff --git a/PathfinderAPI/MiscPatches.cs b/PathfinderAPI/MiscPatches.cs index 1d58d7cc..b5df43e5 100644 --- a/PathfinderAPI/MiscPatches.cs +++ b/PathfinderAPI/MiscPatches.cs @@ -51,7 +51,7 @@ private static void NoSteamErrorMessageIL(ILContext il) c.RemoveRange(3); c.Emit(OpCodes.Ldsfld, AccessTools.Field(typeof(PathfinderOptions), nameof(PathfinderOptions.DisableSteamCloudError))); - c.Emit(OpCodes.Ldfld, AccessTools.Field(typeof(OptionCheckbox), nameof(OptionCheckbox.Value))); + c.Emit(OpCodes.Callvirt, AccessTools.Method(typeof(PluginCheckbox), $"get_{nameof(PluginCheckbox.Value)}")); c.GotoNext(MoveType.Before, x => x.MatchLdstr(out _)); c.Next.Operand = "Steam Cloud saving disabled by Pathfinder"; diff --git a/PathfinderAPI/Options/BasePluginOption.cs b/PathfinderAPI/Options/BasePluginOption.cs new file mode 100644 index 00000000..8890c5ad --- /dev/null +++ b/PathfinderAPI/Options/BasePluginOption.cs @@ -0,0 +1,135 @@ +using BepInEx.Configuration; +using Pathfinder.GUI; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Hacknet; + +namespace Pathfinder.Options; + +public interface IPluginOption +{ + PluginOptionTab Tab { get; set; } + public ConfigEntryBase ConfigEntry { get; } + string Id { get; } + Rectangle Rectangle { get; set; } + Vector2 Size { get; } + bool TrySetOffset(Vector2 offset); + void LoadContent(); + void OnDraw(GameTime gameTime); +} + +public abstract class BasePluginOption : IPluginOption +{ + public PluginOptionTab Tab { get; set; } + public virtual ConfigEntryBase ConfigEntry => TypedConfigEntry; + public virtual ConfigEntry TypedConfigEntry { get; protected set; } + + public Rectangle Rectangle { get; set; } + public int HacknetGuiId { get; private set; } + + public virtual ValueT Value + { + get + { + if(TypedConfigEntry == null) + { + if(Tab.Plugin.Config.TryGetEntry(Tab.Id, Id, out var entry)) + TypedConfigEntry = entry; + else + TypedConfigEntry = Tab.Plugin.Config.Bind(Tab.Id, Id, DefaultValue, ConfigDescription ?? ""); + } + return TypedConfigEntry.Value; + } + set + { + if(TypedConfigEntry == null) + TypedConfigEntry = Tab.Plugin.Config.Bind(Tab.Id, Id, DefaultValue, ConfigDescription ?? ""); + TypedConfigEntry.Value = value; + } + } + public virtual ValueT DefaultValue { get; set; } = default; + public virtual string HeaderText { get; protected set; } + public virtual string DescriptionText { get; protected set; } + public virtual string ConfigDescription { get; protected set; } + public virtual string Id { get; } + + public Color HeaderColor { get; set; } = Color.White; + public Color DescriptionColor { get; set; } = Color.White; + + public SpriteFont HeaderFont { get; set; } + public SpriteFont DescriptionFont { get; set; } + + public Vector2 HeaderTextSize => HeaderFont.MeasureString(HeaderText); + public Vector2 DescriptionTextSize => DescriptionFont.MeasureString(DescriptionText); + + public virtual Vector2 Position => new Vector2(Rectangle.X + Offset.X, Rectangle.Y + Offset.Y); + public virtual Vector2 Offset { get; set; } + + public Vector2 Size + { + get + { + var minsize = MinSize; + return new Vector2(Math.Max(Rectangle.Width, minsize.X), Math.Max(Rectangle.Height, minsize.Y)); + } + } + public virtual Vector2 MinSize + { + get + { + var headerSize = HeaderTextSize; + var descSize = DescriptionTextSize; + return new Vector2(Math.Max(headerSize.X, descSize.X), headerSize.Y + descSize.Y); + } + } + + protected static string MakeIdFrom(string name, string id) + => id ?? string.Concat(name.Where(c => !char.IsWhiteSpace(c) && c != '=')); + + protected BasePluginOption(string headerText, string descriptionText = null, ValueT defaultValue = default, string configDesc = null, string id = null) + { + HeaderText = headerText; + DescriptionText = descriptionText; + DefaultValue = defaultValue; + ConfigDescription = configDesc; + Id = MakeIdFrom(headerText, id); + } + + public bool TrySetOffset(Vector2 offset) + { + Offset = offset; + return Offset == offset; + } + + public bool TrySetHeaderText(string text) + { + HeaderText = text; + return HeaderText == text; + } + + public bool TrySetDescriptionText(string text) + { + DescriptionText = text; + return DescriptionText == text; + } + + public bool TrySetConfigDescription(string desc) + { + ConfigDescription = desc; + return ConfigDescription == desc; + } + + public virtual void LoadContent() + { + HacknetGuiId = PFButton.GetNextID(); + HeaderFont ??= GuiData.font; + DescriptionFont ??= GuiData.smallfont; + } + + public abstract void OnDraw(GameTime gameTime); + + protected void DrawString(Vector2 pos, string text, Color? color = null, SpriteFont font = null) + { + Tab.Batch.DrawString(font ?? GuiData.font, text, pos, color ?? Color.White); + } +} \ No newline at end of file diff --git a/PathfinderAPI/Options/Options.cs b/PathfinderAPI/Options/Options.cs index 95c6d630..68b3ce88 100644 --- a/PathfinderAPI/Options/Options.cs +++ b/PathfinderAPI/Options/Options.cs @@ -1,9 +1,12 @@ +#pragma warning disable 618 + using Hacknet.Gui; using Microsoft.Xna.Framework; using Pathfinder.GUI; namespace Pathfinder.Options; +[Obsolete("Use BasePluginOption")] public abstract class Option { public string Name; @@ -22,6 +25,7 @@ public Option(string name, string description="") public abstract void Draw(int x, int y); } +[Obsolete("Use PluginCheckbox")] public class OptionCheckbox : Option { public bool Value; diff --git a/PathfinderAPI/Options/OptionsManager.cs b/PathfinderAPI/Options/OptionsManager.cs index 871861d5..e1a4f193 100644 --- a/PathfinderAPI/Options/OptionsManager.cs +++ b/PathfinderAPI/Options/OptionsManager.cs @@ -1,13 +1,135 @@ -using Pathfinder.GUI; +using BepInEx.Configuration; +using HarmonyLib; +using Hacknet; +using BepInEx.Hacknet; +using System.Collections.ObjectModel; +using BepInEx; namespace Pathfinder.Options; +[HarmonyPatch] public static class OptionsManager { + [Obsolete("Use PluginTabs")] public readonly static Dictionary Tabs = new Dictionary(); + private readonly static Dictionary _PluginTabs = new Dictionary(); + private static ReadOnlyDictionary _readonlyPluginTabs; + public static ReadOnlyDictionary PluginTabs => _readonlyPluginTabs ??= new ReadOnlyDictionary(_PluginTabs); - static OptionsManager() { } + public const string PluginOptionTabIdPostfix = "Options"; + public static (string Name, string Id) MakeTabDataFrom(HacknetPlugin plugin, string tabName) + { + var metadata = MetadataHelper.GetMetadata(plugin); + var pair = HacknetChainloader.Instance.Plugins.First(dataPair => dataPair.Value.Metadata.GUID == metadata.GUID); + return (tabName ?? pair.Value.Metadata.Name, pair.Value.Metadata.GUID+PluginOptionTabIdPostfix); + } + + public static string GetIdFrom(HacknetPlugin plugin, string name, string id = null) + => id ?? (MakeTabDataFrom(plugin, name).Id); + + public static bool TryGetTab(string tabId, out TabT tab) + where TabT : PluginOptionTab + { + tab = null; + if(_PluginTabs.TryGetValue(tabId, out var possibleTab)) + tab = possibleTab as TabT; + return tab != null; + } + + public static PluginOptionTab GetTab(string tabId, bool shouldThrow = false) + where TabT : PluginOptionTab + { + if(!TryGetTab(tabId, out TabT tab)) + return tab; + if(shouldThrow) + throw new KeyNotFoundException($"The given key '{tabId}' was not present in the dictionary."); + return null; + } + + public static PluginOptionTab RegisterTab(HacknetPlugin plugin = null, string tabName = null, string tabId = null) + { + var pair = MakeTabDataFrom(plugin, tabName); + tabName ??= pair.Name; + tabId ??= pair.Id; + if(GetTab(GetIdFrom(plugin, tabName, tabId)) != null) + ThrowDuplicateIdAttempt(tabId); + return RegisterTab(plugin, new PluginOptionTab(plugin, tabName, tabId)); + } + + public static TabT RegisterTab(HacknetPlugin plugin, TabT tab) + where TabT : PluginOptionTab + { + if(GetTab(tab.Id) != null) + ThrowDuplicateIdAttempt(tab.Id); + tab.Plugin = plugin; + _PluginTabs[tab.Id] = tab; + return tab; + } + + public static TabT RegisterTab(HacknetPlugin plugin) + where TabT : PluginOptionTab, new() + => RegisterTab(plugin, new TabT()); + + public static PluginOptionTab GetOrRegisterTab(HacknetPlugin plugin, string tabName = null) + { + var pair = MakeTabDataFrom(plugin, tabName); + if(!TryGetTab(GetIdFrom(plugin, pair.Name, pair.Id), out PluginOptionTab tab)) + tab = RegisterTab(plugin, pair.Name, pair.Id); + return tab; + } + + public static PluginOptionTab GetOrRegisterTab(HacknetPlugin plugin, string tabName, string tabId = null) + { + if(!TryGetTab(GetIdFrom(plugin, tabName, tabId), out PluginOptionTab tab)) + tab = RegisterTab(plugin, tabName, tabId); + return tab; + } + + public static TabT GetOrRegisterTab(HacknetPlugin plugin, string tabName, string tabId, Func genFunc) + where TabT : PluginOptionTab + { + if(!TryGetTab(GetIdFrom(plugin, tabName, tabId), out TabT tab)) + tab = RegisterTab(plugin, genFunc()); + return tab; + } + + public static TabT GetOrRegisterTab(HacknetPlugin plugin, Func genFunc) + where TabT : PluginOptionTab + { + var pair = MakeTabDataFrom(plugin, null); + return GetOrRegisterTab(plugin, pair.Name, pair.Id, genFunc); + } + + public static T RegisterOption(HacknetPlugin plugin, string tabName, string tabId, T option) + where T : IPluginOption + { + GetOrRegisterTab(plugin, tabName, tabId).AddOption(option); + return option; + } + + public static T RegisterOption(HacknetPlugin plugin, string tabName, T option) + where T : IPluginOption + { + return RegisterOption(plugin, tabName, null, option); + } + + public static T RegisterOption(HacknetPlugin plugin, string tabName, string tabId = null) + where T : IPluginOption, new() + { + return RegisterOption(plugin, tabName, tabId, (T)Activator.CreateInstance(typeof(T))); + } + + public static void OnConfigSave(string tabId, ConfigFile config) + { + _PluginTabs.GetValueSafe(tabId)?.Save(); + } + + public static void OnConfigLoad(string tabId, ConfigFile config) + { + _PluginTabs.GetValueSafe(tabId)?.Load(); + } + [Obsolete("Use RegisterOption or PluginOptionTab.AddOption")] public static void AddOption(string tag, Option opt) { if (!Tabs.TryGetValue(tag, out var tab)) { @@ -16,17 +138,16 @@ public static void AddOption(string tag, Option opt) } tab.Options.Add(opt); } -} - -public class OptionsTab -{ - public string Name; - public List