From 00289f4af8c0a6a3f867a546b82b3c8f3f5a3986 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 14 Jul 2024 21:19:21 -0500 Subject: [PATCH 01/20] Initial data feed and dash screen test --- ResoniteModLoader/DashScreenInjector.cs | 61 ++++++++++++++++ ResoniteModLoader/HarmonyWorker.cs | 1 + ResoniteModLoader/ModConfigurationDataFeed.cs | 69 +++++++++++++++++++ ResoniteModLoader/ModLoaderConfiguration.cs | 2 + ResoniteModLoader/Properties/AssemblyInfo.cs | 2 +- ResoniteModLoader/ResoniteModLoader.csproj | 13 +++- 6 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 ResoniteModLoader/DashScreenInjector.cs create mode 100644 ResoniteModLoader/ModConfigurationDataFeed.cs diff --git a/ResoniteModLoader/DashScreenInjector.cs b/ResoniteModLoader/DashScreenInjector.cs new file mode 100644 index 0000000..11aaa28 --- /dev/null +++ b/ResoniteModLoader/DashScreenInjector.cs @@ -0,0 +1,61 @@ +using FrooxEngine; +using HarmonyLib; + +namespace ResoniteModLoader; + +internal sealed class DashScreenInjector +{ + internal static RadiantDashScreen? InjectedScreen; + + internal static void PatchScreenManager(Harmony harmony) + { + MethodInfo SetupDefaultMethod = AccessTools.DeclaredMethod(typeof(UserspaceScreensManager), "SetupDefaults"); + MethodInfo TryInjectScreenMethod = AccessTools.DeclaredMethod(typeof(DashScreenInjector), nameof(TryInjectScreen)); + harmony.Patch(SetupDefaultMethod, postfix: new HarmonyMethod(TryInjectScreen)); + } + + internal static async void TryInjectScreen(UserspaceScreensManager __instance) + { + if (ModLoaderConfiguration.Get().NoDashScreen) + { + Logger.DebugInternal("Dash screen will not be injected due to configuration file"); + return; + } + if (__instance.World != Userspace.UserspaceWorld) + { + Logger.WarnInternal("Dash screen will not be injected because we're somehow not in userspace (WTF?)"); // it stands for What the Froox :> + return; + } + if (InjectedScreen is not null && !InjectedScreen.IsRemoved) + { + Logger.WarnInternal("Dash screen will not be injected again because it already exists"); + } + + RadiantDash dash = __instance.Slot.GetComponentInParents(); + InjectedScreen = dash.AttachScreen("Mods", RadiantUI_Constants.Hero.RED, OfficialAssets.Graphics.Icons.General.BoxClosed); // Replace with RML icon later + + Slot screenSlot = InjectedScreen.Slot; + screenSlot.OrderOffset = 128; + screenSlot.PersistentSelf = false; + + SingleFeedView view = screenSlot.AttachComponent(); + ModConfigurationDataFeed feed = screenSlot.AttachComponent(); + + Slot templates = screenSlot.AddSlot("Template"); + templates.ActiveSelf = false; + + if (await templates.LoadObjectAsync(__instance.Cloud.Platform.GetSpawnObjectUri("SettingsItemMappers"), skipHolder: true)) + { + DataFeedItemMapper itemMapper = templates.FindChild("ItemsMapper").GetComponent(); + view.ItemsManager.TemplateMapper.Target = itemMapper; + view.ItemsManager.ContainerRoot.Target = InjectedScreen.ScreenRoot; + } + else + { + Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, aborting."); + InjectedScreen.Slot.Destroy(); + } + + view.Feed.Target = feed; + } +} diff --git a/ResoniteModLoader/HarmonyWorker.cs b/ResoniteModLoader/HarmonyWorker.cs index c19cacb..989aed1 100644 --- a/ResoniteModLoader/HarmonyWorker.cs +++ b/ResoniteModLoader/HarmonyWorker.cs @@ -9,5 +9,6 @@ internal static void LoadModsAndHideModAssemblies(HashSet initialAssem ModLoader.LoadMods(); ModConfiguration.RegisterShutdownHook(harmony); AssemblyHider.PatchResonite(harmony, initialAssemblies); + DashScreenInjector.PatchScreenManager(harmony); } } diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs new file mode 100644 index 0000000..06338bd --- /dev/null +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Elements.Core; +using FrooxEngine; +using SkyFrost.Base; + +namespace ResoniteModLoader; + +[GloballyRegistered] +[Category(new string[] { "Userspace" })] +public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed, IWorldElement +{ + public override bool UserspaceOnly => true; + + public bool SupportsBackgroundQuerying => true; + + public async IAsyncEnumerable Enumerate(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData) { + { + DataFeedLabel modLoaderVersion = new DataFeedLabel(); + modLoaderVersion.InitBase("ResoniteModLoder.Version", null, null, $"ResoniteModLoader version {ModLoader.VERSION}"); + yield return modLoaderVersion; + + DataFeedLabel modLoaderLoadedModCount = new DataFeedLabel(); + modLoaderLoadedModCount.InitBase("ResoniteModLoder.LoadedModCount", null, null, $"{ModLoader.Mods().Count()} mods loaded"); + yield return modLoaderLoadedModCount; + } + + foreach (ResoniteModBase mod in ModLoader.Mods()) + { + DataFeedGroup modDataFeedGroup = new DataFeedGroup(); + modDataFeedGroup.InitBase(mod.Name + ".Group", null, null, mod.Name); + yield return modDataFeedGroup; + + DataFeedLabel authorDataFeedLabel = new DataFeedLabel(); + authorDataFeedLabel.InitBase(mod.Name + ".Author", null, null, $"Author: {mod.Author}"); + yield return authorDataFeedLabel; + + DataFeedLabel versionDataFeedLabel = new DataFeedLabel(); + versionDataFeedLabel.InitBase(mod.Name + ".Version", null, null, $"Version: {mod.Version}"); + yield return versionDataFeedLabel; + + DataFeedLabel isLoadedDataFeedLabel = new DataFeedLabel(); + isLoadedDataFeedLabel.InitBase(mod.Name + ".IsLoaded", null, null, $"IsLoaded: {mod.FinishedLoading}"); + yield return isLoadedDataFeedLabel; + } + } + + public void ListenToUpdates(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler, object viewData) { + Logger.DebugInternal($"ModConfigurationDataFeed.ListenToUpdates called, handler: {handler}"); + } + + public LocaleString PathSegmentName(string segment, int depth) { + return $"{segment} ({depth})"; + } + + public object RegisterViewData() { + Logger.DebugInternal("ModConfigurationDataFeed.RegisterViewData called"); + return null; + } + + public void UnregisterListener(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler) { + Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterListener called, handler: {handler}"); + } + + public void UnregisterViewData(object data) { + Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterViewData called, object: {data}"); + } +} diff --git a/ResoniteModLoader/ModLoaderConfiguration.cs b/ResoniteModLoader/ModLoaderConfiguration.cs index a1ba099..4919323 100644 --- a/ResoniteModLoader/ModLoaderConfiguration.cs +++ b/ResoniteModLoader/ModLoaderConfiguration.cs @@ -20,6 +20,7 @@ internal static ModLoaderConfiguration Get() { { "logconflicts", (value) => _configuration.LogConflicts = bool.Parse(value) }, //{ "hidemodtypes", (value) => _configuration.HideModTypes = bool.Parse(value) }, //{ "hidelatetypes", (value) => _configuration.HideLateTypes = bool.Parse(value) } + { "nodashscreen", (value) => _configuration.NoDashScreen = bool.Parse(value) }, }; // .NET's ConfigurationManager is some hot trash to the point where I'm just done with it. @@ -70,5 +71,6 @@ private static string GetAssemblyDirectory() { public bool LogConflicts { get; private set; } = true; //public bool HideModTypes { get; private set; } = true; //public bool HideLateTypes { get; private set; } = true; + public bool NoDashScreen { get; private set; } = false; #pragma warning restore CA1805 } diff --git a/ResoniteModLoader/Properties/AssemblyInfo.cs b/ResoniteModLoader/Properties/AssemblyInfo.cs index 043611b..5e5ac30 100644 --- a/ResoniteModLoader/Properties/AssemblyInfo.cs +++ b/ResoniteModLoader/Properties/AssemblyInfo.cs @@ -18,4 +18,4 @@ [module: Description("FROOXENGINE_WEAVED")] //Mark as DataModelAssembly for the Plugin loading system to load this assembly -[assembly: DataModelAssembly(DataModelAssemblyType.Optional)] +[assembly: DataModelAssembly(DataModelAssemblyType.UserspaceCore)] diff --git a/ResoniteModLoader/ResoniteModLoader.csproj b/ResoniteModLoader/ResoniteModLoader.csproj index ac00e16..9637108 100644 --- a/ResoniteModLoader/ResoniteModLoader.csproj +++ b/ResoniteModLoader/ResoniteModLoader.csproj @@ -34,6 +34,14 @@ $(ResonitePath)Resonite_Data\Managed\FrooxEngine.dll False + + $(ResonitePath)Resonite_Data\Managed\SkyFrost.Base.dll + False + + + $(ResonitePath)Resonite_Data\Managed\SkyFrost.Base.Models.dll + False + $(ResonitePath)Resonite_Data\Managed\Newtonsoft.Json.dll False @@ -43,9 +51,12 @@ False - R:\SteamLibrary\steamapps\common\Resonite\Resonite_Data\Managed\UnityEngine.CoreModule.dll + $(ResonitePath)Resonite_Data\Managed\UnityEngine.CoreModule.dll False + + $(ResonitePath)Resonite_Data\Managed\Microsoft.Bcl.AsyncInterfaces.dll + From 746c49300dad715731a68abe0d7d14a4b0c102f2 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 14 Jul 2024 21:53:04 -0500 Subject: [PATCH 02/20] Fix compiler async issues --- ResoniteModLoader/DashScreenInjector.cs | 13 ++++++++++--- ResoniteModLoader/ModConfigurationDataFeed.cs | 2 +- ResoniteModLoader/ResoniteModLoader.csproj | 4 +--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ResoniteModLoader/DashScreenInjector.cs b/ResoniteModLoader/DashScreenInjector.cs index 11aaa28..6935ec9 100644 --- a/ResoniteModLoader/DashScreenInjector.cs +++ b/ResoniteModLoader/DashScreenInjector.cs @@ -9,9 +9,12 @@ internal sealed class DashScreenInjector internal static void PatchScreenManager(Harmony harmony) { - MethodInfo SetupDefaultMethod = AccessTools.DeclaredMethod(typeof(UserspaceScreensManager), "SetupDefaults"); - MethodInfo TryInjectScreenMethod = AccessTools.DeclaredMethod(typeof(DashScreenInjector), nameof(TryInjectScreen)); - harmony.Patch(SetupDefaultMethod, postfix: new HarmonyMethod(TryInjectScreen)); + MethodInfo setupDefaultMethod = AccessTools.DeclaredMethod(typeof(UserspaceScreensManager), "SetupDefaults"); + MethodInfo onLoadingMethod = AccessTools.DeclaredMethod(typeof(UserspaceScreensManager), "OnLoading"); + MethodInfo tryInjectScreenMethod = AccessTools.DeclaredMethod(typeof(DashScreenInjector), nameof(TryInjectScreen)); + harmony.Patch(setupDefaultMethod, postfix: new HarmonyMethod(tryInjectScreenMethod)); + harmony.Patch(onLoadingMethod, postfix: new HarmonyMethod(tryInjectScreenMethod)); + Logger.DebugInternal("UserspaceScreensManager patched"); } internal static async void TryInjectScreen(UserspaceScreensManager __instance) @@ -31,6 +34,8 @@ internal static async void TryInjectScreen(UserspaceScreensManager __instance) Logger.WarnInternal("Dash screen will not be injected again because it already exists"); } + Logger.DebugInternal("Injecting dash screen"); + RadiantDash dash = __instance.Slot.GetComponentInParents(); InjectedScreen = dash.AttachScreen("Mods", RadiantUI_Constants.Hero.RED, OfficialAssets.Graphics.Icons.General.BoxClosed); // Replace with RML icon later @@ -57,5 +62,7 @@ internal static async void TryInjectScreen(UserspaceScreensManager __instance) } view.Feed.Target = feed; + + Logger.DebugInternal("Dash screen should be injected!"); } } diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index 06338bd..5c12c5a 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Elements.Core; diff --git a/ResoniteModLoader/ResoniteModLoader.csproj b/ResoniteModLoader/ResoniteModLoader.csproj index 9637108..69d6372 100644 --- a/ResoniteModLoader/ResoniteModLoader.csproj +++ b/ResoniteModLoader/ResoniteModLoader.csproj @@ -26,6 +26,7 @@ + $(ResonitePath)Resonite_Data\Managed\Elements.Core.dll False @@ -54,9 +55,6 @@ $(ResonitePath)Resonite_Data\Managed\UnityEngine.CoreModule.dll False - - $(ResonitePath)Resonite_Data\Managed\Microsoft.Bcl.AsyncInterfaces.dll - From ec8e7d956f54d3ee217738f9b05c69f2c92ad06e Mon Sep 17 00:00:00 2001 From: David Date: Sun, 14 Jul 2024 21:54:23 -0500 Subject: [PATCH 03/20] Revert "remove Unsafe/HideModTypes/HideLateTypes modloader config options" This reverts commit 1b060a11ee10fa1e4c3459bb280db6fb2186d6dc. --- ResoniteModLoader/AssemblyHider.cs | 12 ++++++------ ResoniteModLoader/ModLoaderConfiguration.cs | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ResoniteModLoader/AssemblyHider.cs b/ResoniteModLoader/AssemblyHider.cs index 6f4b33e..0df5595 100644 --- a/ResoniteModLoader/AssemblyHider.cs +++ b/ResoniteModLoader/AssemblyHider.cs @@ -48,7 +48,7 @@ internal static class AssemblyHider { /// Our RML harmony instance /// Assemblies that were loaded when RML first started internal static void PatchResonite(Harmony harmony, HashSet initialAssemblies) { - //if (ModLoaderConfiguration.Get().HideModTypes) { + if (ModLoaderConfiguration.Get().HideModTypes) { // initialize the static assembly sets that our patches will need later resoniteAssemblies = GetResoniteAssemblies(initialAssemblies); modAssemblies = GetModAssemblies(resoniteAssemblies); @@ -68,7 +68,7 @@ internal static void PatchResonite(Harmony harmony, HashSet initialAss MethodInfo getAssembliesTarget = AccessTools.DeclaredMethod(typeof(AppDomain), nameof(AppDomain.GetAssemblies), Array.Empty()); MethodInfo getAssembliesPatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(GetAssembliesPostfix)); harmony.Patch(getAssembliesTarget, postfix: new HarmonyMethod(getAssembliesPatch)); - //} + } } private static HashSet GetResoniteAssemblies(HashSet initialAssemblies) { @@ -116,13 +116,13 @@ private static bool IsModAssembly(Assembly assembly, string typeOrAssembly, stri // this implies someone late-loaded an assembly after RML, and it was later used in-game // this is super weird, and probably shouldn't ever happen... but if it does, I want to know about it. // since this is an edge case users may want to handle in different ways, the HideLateTypes rml config option allows them to choose. - //bool hideLate = true;// ModLoaderConfiguration.Get().HideLateTypes; - /*if (log) { + bool hideLate = ModLoaderConfiguration.Get().HideLateTypes; + if (log) { Logger.WarnInternal($"The \"{name}\" {typeOrAssembly} does not appear to part of Resonite or a mod. It is unclear whether it should be hidden or not. Due to the HideLateTypes config option being {hideLate} it will be {(hideLate ? "Hidden" : "Shown")}"); - }*/ + } // if forceShowLate == true, then this function will always return `false` for late-loaded types // if forceShowLate == false, then this function will return `true` when hideLate == true - return !forceShowLate; + return hideLate && !forceShowLate; } } } diff --git a/ResoniteModLoader/ModLoaderConfiguration.cs b/ResoniteModLoader/ModLoaderConfiguration.cs index 4919323..9e51e02 100644 --- a/ResoniteModLoader/ModLoaderConfiguration.cs +++ b/ResoniteModLoader/ModLoaderConfiguration.cs @@ -12,14 +12,14 @@ internal static ModLoaderConfiguration Get() { _configuration = new ModLoaderConfiguration(); Dictionary> keyActions = new() { - //{ "unsafe", (value) => _configuration.Unsafe = bool.Parse(value) }, + { "unsafe", (value) => _configuration.Unsafe = bool.Parse(value) }, { "debug", (value) => _configuration.Debug = bool.Parse(value) }, { "hidevisuals", (value) => _configuration.HideVisuals = bool.Parse(value) }, { "nomods", (value) => _configuration.NoMods = bool.Parse(value) }, { "advertiseversion", (value) => _configuration.AdvertiseVersion = bool.Parse(value) }, { "logconflicts", (value) => _configuration.LogConflicts = bool.Parse(value) }, - //{ "hidemodtypes", (value) => _configuration.HideModTypes = bool.Parse(value) }, - //{ "hidelatetypes", (value) => _configuration.HideLateTypes = bool.Parse(value) } + { "hidemodtypes", (value) => _configuration.HideModTypes = bool.Parse(value) }, + { "hidelatetypes", (value) => _configuration.HideLateTypes = bool.Parse(value) }, { "nodashscreen", (value) => _configuration.NoDashScreen = bool.Parse(value) }, }; @@ -63,14 +63,14 @@ private static string GetAssemblyDirectory() { } #pragma warning disable CA1805 - //public bool Unsafe { get; private set; } = false; + public bool Unsafe { get; private set; } = false; public bool Debug { get; private set; } = false; public bool NoMods { get; private set; } = false; public bool HideVisuals { get; private set; } = false; public bool AdvertiseVersion { get; private set; } = false; public bool LogConflicts { get; private set; } = true; - //public bool HideModTypes { get; private set; } = true; - //public bool HideLateTypes { get; private set; } = true; + public bool HideModTypes { get; private set; } = true; + public bool HideLateTypes { get; private set; } = true; public bool NoDashScreen { get; private set; } = false; #pragma warning restore CA1805 } From 33ef2ab49252d3233a11f76d919ac61f1cb55f13 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 16 Jul 2024 03:11:08 -0500 Subject: [PATCH 04/20] It sort of works? --- ResoniteModLoader/DashScreenInjector.cs | 43 ++++-- ResoniteModLoader/ModConfigurationDataFeed.cs | 133 ++++++++++++++---- 2 files changed, 138 insertions(+), 38 deletions(-) diff --git a/ResoniteModLoader/DashScreenInjector.cs b/ResoniteModLoader/DashScreenInjector.cs index 6935ec9..8bed761 100644 --- a/ResoniteModLoader/DashScreenInjector.cs +++ b/ResoniteModLoader/DashScreenInjector.cs @@ -1,4 +1,5 @@ using FrooxEngine; +using FrooxEngine.UIX; using HarmonyLib; namespace ResoniteModLoader; @@ -19,7 +20,9 @@ internal static void PatchScreenManager(Harmony harmony) internal static async void TryInjectScreen(UserspaceScreensManager __instance) { - if (ModLoaderConfiguration.Get().NoDashScreen) + ModLoaderConfiguration config = ModLoaderConfiguration.Get(); + + if (config.NoDashScreen) { Logger.DebugInternal("Dash screen will not be injected due to configuration file"); return; @@ -37,28 +40,44 @@ internal static async void TryInjectScreen(UserspaceScreensManager __instance) Logger.DebugInternal("Injecting dash screen"); RadiantDash dash = __instance.Slot.GetComponentInParents(); - InjectedScreen = dash.AttachScreen("Mods", RadiantUI_Constants.Hero.RED, OfficialAssets.Graphics.Icons.General.BoxClosed); // Replace with RML icon later + InjectedScreen = dash.AttachScreen("Mods", RadiantUI_Constants.MidLight.ORANGE, OfficialAssets.Graphics.Icons.General.BoxClosed); // Replace with RML icon later - Slot screenSlot = InjectedScreen.Slot; - screenSlot.OrderOffset = 128; - screenSlot.PersistentSelf = false; + InjectedScreen.Slot.OrderOffset = 128; + InjectedScreen.Slot.PersistentSelf = false; - SingleFeedView view = screenSlot.AttachComponent(); - ModConfigurationDataFeed feed = screenSlot.AttachComponent(); + SingleFeedView view = InjectedScreen.ScreenRoot.AttachComponent(); + ModConfigurationDataFeed feed = InjectedScreen.ScreenRoot.AttachComponent(); - Slot templates = screenSlot.AddSlot("Template"); + Slot templates = InjectedScreen.ScreenRoot.AddSlot("Template"); templates.ActiveSelf = false; - if (await templates.LoadObjectAsync(__instance.Cloud.Platform.GetSpawnObjectUri("SettingsItemMappers"), skipHolder: true)) + if (await templates.LoadObjectAsync(__instance.Cloud.Platform.GetSpawnObjectUri("Settings"), skipHolder: true)) + { + // we do a little bit of thievery + RootCategoryView rootCategoryView = templates.GetComponentInChildren(); + rootCategoryView.Slot.GetComponentInChildren().Path.Target = view.Path; + rootCategoryView.CategoryManager.ContainerRoot.Target.ActiveSelf = false; + rootCategoryView.Slot.Children.First().Parent = InjectedScreen.ScreenCanvas.Slot; + view.ItemsManager.TemplateMapper.Target = rootCategoryView.ItemsManager.TemplateMapper.Target; + view.ItemsManager.ContainerRoot.Target = rootCategoryView.ItemsManager.ContainerRoot.Target; + rootCategoryView.Destroy(); + } + else if (config.Debug) { - DataFeedItemMapper itemMapper = templates.FindChild("ItemsMapper").GetComponent(); + Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, falling back to template."); + DataFeedItemMapper itemMapper = templates.AttachComponent(); + Canvas tempCanvas = templates.AttachComponent(); // Needed for next method to work + itemMapper.SetupTemplate(); + tempCanvas.Destroy(); view.ItemsManager.TemplateMapper.Target = itemMapper; - view.ItemsManager.ContainerRoot.Target = InjectedScreen.ScreenRoot; + view.ItemsManager.ContainerRoot.Target = InjectedScreen.ScreenCanvas.Slot; + InjectedScreen.ScreenCanvas.Slot.AttachComponent(); // just for debugging } else { - Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, aborting."); + Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, aborting and cleaning up."); InjectedScreen.Slot.Destroy(); + return; } view.Feed.Target = feed; diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index 5c12c5a..90974fd 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Elements.Core; @@ -7,43 +7,46 @@ namespace ResoniteModLoader; -[GloballyRegistered] [Category(new string[] { "Userspace" })] public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed, IWorldElement { + #pragma warning disable CS1591 public override bool UserspaceOnly => true; public bool SupportsBackgroundQuerying => true; public async IAsyncEnumerable Enumerate(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData) { + switch (path.Count) { - DataFeedLabel modLoaderVersion = new DataFeedLabel(); - modLoaderVersion.InitBase("ResoniteModLoder.Version", null, null, $"ResoniteModLoader version {ModLoader.VERSION}"); - yield return modLoaderVersion; + case 0: { + DataFeedLabel modLoaderVersion = new DataFeedLabel(); + modLoaderVersion.InitBase( + itemKey: "ResoniteModLoder.Version", + label: $"ResoniteModLoader version {ModLoader.VERSION}", + path: null, + groupingParameters: null + ); + yield return modLoaderVersion; - DataFeedLabel modLoaderLoadedModCount = new DataFeedLabel(); - modLoaderLoadedModCount.InitBase("ResoniteModLoder.LoadedModCount", null, null, $"{ModLoader.Mods().Count()} mods loaded"); - yield return modLoaderLoadedModCount; - } - - foreach (ResoniteModBase mod in ModLoader.Mods()) - { - DataFeedGroup modDataFeedGroup = new DataFeedGroup(); - modDataFeedGroup.InitBase(mod.Name + ".Group", null, null, mod.Name); - yield return modDataFeedGroup; + DataFeedIndicator modLoaderLoadedModCount = new DataFeedIndicator(); + modLoaderLoadedModCount.InitBase( + itemKey: "ResoniteModLoder.LoadedModCount", + label: "Loaded mods:", + path: null, + groupingParameters: null + ); + modLoaderLoadedModCount.InitSetupValue((count) => count.Value = ModLoader.Mods().Count()); + yield return modLoaderLoadedModCount; - DataFeedLabel authorDataFeedLabel = new DataFeedLabel(); - authorDataFeedLabel.InitBase(mod.Name + ".Author", null, null, $"Author: {mod.Author}"); - yield return authorDataFeedLabel; + foreach (ResoniteModBase mod in ModLoader.Mods()) yield return GenerateModFeedGroup(mod); + } + break; + case 2: { - DataFeedLabel versionDataFeedLabel = new DataFeedLabel(); - versionDataFeedLabel.InitBase(mod.Name + ".Version", null, null, $"Version: {mod.Version}"); - yield return versionDataFeedLabel; - - DataFeedLabel isLoadedDataFeedLabel = new DataFeedLabel(); - isLoadedDataFeedLabel.InitBase(mod.Name + ".IsLoaded", null, null, $"IsLoaded: {mod.FinishedLoading}"); - yield return isLoadedDataFeedLabel; + } + break; } + } public void ListenToUpdates(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler, object viewData) { @@ -56,7 +59,7 @@ public LocaleString PathSegmentName(string segment, int depth) { public object RegisterViewData() { Logger.DebugInternal("ModConfigurationDataFeed.RegisterViewData called"); - return null; + return null!; } public void UnregisterListener(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler) { @@ -66,4 +69,82 @@ public void UnregisterListener(IReadOnlyList path, IReadOnlyList public void UnregisterViewData(object data) { Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterViewData called, object: {data}"); } + #pragma warning restore CS1591 + + private static DataFeedGroup GenerateModFeedGroup(ResoniteModBase mod) { + DataFeedGroup modFeedGroup = new DataFeedGroup(); + List groupChildren = new List(); // Could this be a pool list instead? + string key = mod.ModAssembly!.Sha256; + + DataFeedIndicator modAuthorIndicator = new DataFeedIndicator(); + modAuthorIndicator.InitBase( + itemKey: key + ".Author", + label: "Author", + path: null, + groupingParameters: null + ); + modAuthorIndicator.InitSetupValue((str) => str.Value = mod.Author); + groupChildren.Add(modAuthorIndicator); + + DataFeedIndicator modVersionIndicator = new DataFeedIndicator(); + modVersionIndicator.InitBase( + itemKey: key + ".Version", + label: "Version", + path: null, + groupingParameters: null + ); + modVersionIndicator.InitSetupValue((str) => str.Value = mod.Version); + groupChildren.Add(modVersionIndicator); + + DataFeedIndicator modAssemblyIndicator = new DataFeedIndicator(); + modAssemblyIndicator.InitBase( + itemKey: key + ".Assembly", + label: "File", + path: null, + groupingParameters: null + ); + modAssemblyIndicator.InitSetupValue((str) => str.Value = Path.GetFileName(mod.ModAssembly!.File)); + groupChildren.Add(modAssemblyIndicator); + + if (Uri.TryCreate(mod.Link, UriKind.Absolute, out var uri)) + { + DataFeedAction modOpenLinkAction = new DataFeedAction(); + modOpenLinkAction.InitBase( + itemKey: key + ".OpenLinkAction", + label: $"Open mod link ({uri.Host})", + path: null, + groupingParameters: null + ); + modOpenLinkAction.InitAction(delegate { + Userspace.UserspaceWorld.RunSynchronously(delegate { + Slot slot = Userspace.UserspaceWorld.AddSlot("Hyperlink"); + slot.PositionInFrontOfUser(float3.Backward); + slot.AttachComponent().Setup(uri, "Outgoing hyperlink"); + }); + }); + groupChildren.Add(modOpenLinkAction); + } + + if (mod.ModConfiguration is not null) + { + DataFeedCategory modConfigurationCategory = new DataFeedCategory(); + modConfigurationCategory.InitBase( + itemKey: key + ".ConfigurationCategory", + label: "Mod configuration", + path: new string[] {key, "Configuration"}, + groupingParameters: null + ); + groupChildren.Add(modConfigurationCategory); + } + + modFeedGroup.InitBase( + itemKey: key + ".Group", + label: mod.Name, + path: null, + groupingParameters: null, + subitems: groupChildren + ); + + return modFeedGroup; + } } From 37f6943544b19fe2f8779365ba098842d818a3fb Mon Sep 17 00:00:00 2001 From: David Date: Wed, 17 Jul 2024 16:09:30 -0500 Subject: [PATCH 05/20] It works better --- ResoniteModLoader/DashScreenInjector.cs | 31 ++- ResoniteModLoader/ModConfigurationDataFeed.cs | 208 ++++++++++++------ ResoniteModLoader/ResoniteModLoader.csproj | 2 +- 3 files changed, 160 insertions(+), 81 deletions(-) diff --git a/ResoniteModLoader/DashScreenInjector.cs b/ResoniteModLoader/DashScreenInjector.cs index 8bed761..d8bce3e 100644 --- a/ResoniteModLoader/DashScreenInjector.cs +++ b/ResoniteModLoader/DashScreenInjector.cs @@ -4,12 +4,10 @@ namespace ResoniteModLoader; -internal sealed class DashScreenInjector -{ +internal sealed class DashScreenInjector { internal static RadiantDashScreen? InjectedScreen; - internal static void PatchScreenManager(Harmony harmony) - { + internal static void PatchScreenManager(Harmony harmony) { MethodInfo setupDefaultMethod = AccessTools.DeclaredMethod(typeof(UserspaceScreensManager), "SetupDefaults"); MethodInfo onLoadingMethod = AccessTools.DeclaredMethod(typeof(UserspaceScreensManager), "OnLoading"); MethodInfo tryInjectScreenMethod = AccessTools.DeclaredMethod(typeof(DashScreenInjector), nameof(TryInjectScreen)); @@ -18,23 +16,20 @@ internal static void PatchScreenManager(Harmony harmony) Logger.DebugInternal("UserspaceScreensManager patched"); } - internal static async void TryInjectScreen(UserspaceScreensManager __instance) - { + internal static async void TryInjectScreen(UserspaceScreensManager __instance) { ModLoaderConfiguration config = ModLoaderConfiguration.Get(); - if (config.NoDashScreen) - { + if (config.NoDashScreen) { Logger.DebugInternal("Dash screen will not be injected due to configuration file"); return; } - if (__instance.World != Userspace.UserspaceWorld) - { + if (__instance.World != Userspace.UserspaceWorld) { Logger.WarnInternal("Dash screen will not be injected because we're somehow not in userspace (WTF?)"); // it stands for What the Froox :> return; } - if (InjectedScreen is not null && !InjectedScreen.IsRemoved) - { + if (InjectedScreen is not null && !InjectedScreen.IsRemoved) { Logger.WarnInternal("Dash screen will not be injected again because it already exists"); + return; } Logger.DebugInternal("Injecting dash screen"); @@ -51,8 +46,7 @@ internal static async void TryInjectScreen(UserspaceScreensManager __instance) Slot templates = InjectedScreen.ScreenRoot.AddSlot("Template"); templates.ActiveSelf = false; - if (await templates.LoadObjectAsync(__instance.Cloud.Platform.GetSpawnObjectUri("Settings"), skipHolder: true)) - { + if (await templates.LoadObjectAsync(__instance.Cloud.Platform.GetSpawnObjectUri("Settings"), skipHolder: true)) { // we do a little bit of thievery RootCategoryView rootCategoryView = templates.GetComponentInChildren(); rootCategoryView.Slot.GetComponentInChildren().Path.Target = view.Path; @@ -61,9 +55,9 @@ internal static async void TryInjectScreen(UserspaceScreensManager __instance) view.ItemsManager.TemplateMapper.Target = rootCategoryView.ItemsManager.TemplateMapper.Target; view.ItemsManager.ContainerRoot.Target = rootCategoryView.ItemsManager.ContainerRoot.Target; rootCategoryView.Destroy(); + templates.GetComponentInChildren().NameConverter.Target = view.PathSegmentName; } - else if (config.Debug) - { + else if (config.Debug) { Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, falling back to template."); DataFeedItemMapper itemMapper = templates.AttachComponent(); Canvas tempCanvas = templates.AttachComponent(); // Needed for next method to work @@ -73,14 +67,15 @@ internal static async void TryInjectScreen(UserspaceScreensManager __instance) view.ItemsManager.ContainerRoot.Target = InjectedScreen.ScreenCanvas.Slot; InjectedScreen.ScreenCanvas.Slot.AttachComponent(); // just for debugging } - else - { + else { Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, aborting and cleaning up."); InjectedScreen.Slot.Destroy(); return; } + InjectedScreen.ScreenCanvas.Slot.AttachComponent().Tint.Value = UserspaceRadiantDash.DEFAULT_BACKGROUND; view.Feed.Target = feed; + view.SetCategoryPath(["ResoniteModLoader"]); Logger.DebugInternal("Dash screen should be injected!"); } diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index 90974fd..aacf2da 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Elements.Core; using FrooxEngine; @@ -7,50 +8,93 @@ namespace ResoniteModLoader; -[Category(new string[] { "Userspace" })] -public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed, IWorldElement -{ - #pragma warning disable CS1591 +/// +/// A custom data feed that can be used to show information about loaded mods, and alter their configuration. Path must start with "ResoniteModLoder" +/// +[Category(["Userspace"])] +public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed, IWorldElement { +#pragma warning disable CS1591 public override bool UserspaceOnly => true; public bool SupportsBackgroundQuerying => true; public async IAsyncEnumerable Enumerate(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData) { - switch (path.Count) - { + switch (path.Count) { case 0: { - DataFeedLabel modLoaderVersion = new DataFeedLabel(); - modLoaderVersion.InitBase( - itemKey: "ResoniteModLoder.Version", - label: $"ResoniteModLoader version {ModLoader.VERSION}", - path: null, - groupingParameters: null - ); - yield return modLoaderVersion; - - DataFeedIndicator modLoaderLoadedModCount = new DataFeedIndicator(); - modLoaderLoadedModCount.InitBase( - itemKey: "ResoniteModLoder.LoadedModCount", - label: "Loaded mods:", - path: null, - groupingParameters: null - ); - modLoaderLoadedModCount.InitSetupValue((count) => count.Value = ModLoader.Mods().Count()); - yield return modLoaderLoadedModCount; - - foreach (ResoniteModBase mod in ModLoader.Mods()) yield return GenerateModFeedGroup(mod); - } - break; - case 2: { + DataFeedCategory modLoaderCategory = new DataFeedCategory(); + modLoaderCategory.InitBase( + itemKey: "ResoniteModLoader", + label: $"Open ResoniteModLoader category", + path: null, + groupingParameters: null + ); + yield return modLoaderCategory; + } + yield break; + + case 1: { + if (path[0] != "ResoniteModLoader") yield break; + + DataFeedLabel modLoaderVersion = new DataFeedLabel(); + modLoaderVersion.InitBase( + itemKey: "ResoniteModLoder.Version", + label: $"ResoniteModLoader version {ModLoader.VERSION}", + path: null, + groupingParameters: null + ); + yield return modLoaderVersion; + + DataFeedIndicator modLoaderLoadedModCount = new DataFeedIndicator(); // Todo: Make DataFeedIndicator template + modLoaderLoadedModCount.InitBase( + itemKey: "ResoniteModLoder.LoadedModCount", + label: "Loaded mods:", + path: null, + groupingParameters: null + ); + modLoaderLoadedModCount.InitSetupValue((count) => count.Value = ModLoader.Mods().Count().ToString()); + yield return modLoaderLoadedModCount; + + foreach (ResoniteModBase mod in ModLoader.Mods()) + if (string.IsNullOrEmpty(searchPhrase) || mod.Name.ToLowerInvariant().Contains(searchPhrase.ToLowerInvariant())) + yield return GenerateModInfoGroup(mod); + } + yield break; - } - break; + case 2: { + if (path[0] != "ResoniteModLoader") yield break; + string key = path[1]; + ResoniteModBase mod = ModFromKey(key); + yield return GenerateModInfoGroup(mod, true); + // GenerateModLogFeed + // GenerateModExceptionFeed + } + yield break; + + case 3: { + if (path[0] != "ResoniteModLoader") yield break; + string key = path[1]; + ResoniteModBase mod = ModFromKey(key); + switch (path[2].ToLowerInvariant()) { + case "configuration": { + + } + yield break; + case "logs": { + + } + yield break; + case "exceptions": { + + } + yield break; + } + } + yield break; } - } public void ListenToUpdates(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler, object viewData) { - Logger.DebugInternal($"ModConfigurationDataFeed.ListenToUpdates called, handler: {handler}"); + Logger.DebugInternal($"ModConfigurationDataFeed.ListenToUpdates called, handler: {handler}\n{Environment.StackTrace}"); } public LocaleString PathSegmentName(string segment, int depth) { @@ -58,23 +102,48 @@ public LocaleString PathSegmentName(string segment, int depth) { } public object RegisterViewData() { - Logger.DebugInternal("ModConfigurationDataFeed.RegisterViewData called"); + Logger.DebugInternal($"ModConfigurationDataFeed.RegisterViewData called\n{Environment.StackTrace}"); return null!; } public void UnregisterListener(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler) { - Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterListener called, handler: {handler}"); + Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterListener called, handler: {handler}\n{Environment.StackTrace}"); } public void UnregisterViewData(object data) { - Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterViewData called, object: {data}"); + Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterViewData called, object: {data}\n{Environment.StackTrace}"); + } +#pragma warning restore CS1591 + + public static string KeyFromMod(ResoniteModBase mod) => mod.ModAssembly!.Sha256; + + public static ResoniteModBase? ModFromKey(string key) => ModLoader.Mods().First((mod) => KeyFromMod(mod) == key); + + [SyncMethod(typeof(Action), [])] + public static void OpenURI(Uri uri) { + Userspace.UserspaceWorld.RunSynchronously(delegate { + Slot slot = Userspace.UserspaceWorld.AddSlot("Hyperlink"); + slot.PositionInFrontOfUser(float3.Backward); + slot.AttachComponent().Setup(uri, "Outgoing hyperlink"); + }); } - #pragma warning restore CS1591 - private static DataFeedGroup GenerateModFeedGroup(ResoniteModBase mod) { + private static DataFeedGroup GenerateModInfoGroup(ResoniteModBase mod, bool standalone = false) { DataFeedGroup modFeedGroup = new DataFeedGroup(); List groupChildren = new List(); // Could this be a pool list instead? - string key = mod.ModAssembly!.Sha256; + string key = KeyFromMod(mod); + + if (standalone) { + DataFeedIndicator modNameIndicator = new DataFeedIndicator(); + modNameIndicator.InitBase( + itemKey: key + ".Name", + label: "Name", + path: null, + groupingParameters: null + ); + modNameIndicator.InitSetupValue((str) => str.Value = mod.Name); + groupChildren.Add(modNameIndicator); + } DataFeedIndicator modAuthorIndicator = new DataFeedIndicator(); modAuthorIndicator.InitBase( @@ -96,50 +165,57 @@ private static DataFeedGroup GenerateModFeedGroup(ResoniteModBase mod) { modVersionIndicator.InitSetupValue((str) => str.Value = mod.Version); groupChildren.Add(modVersionIndicator); - DataFeedIndicator modAssemblyIndicator = new DataFeedIndicator(); - modAssemblyIndicator.InitBase( - itemKey: key + ".Assembly", - label: "File", - path: null, - groupingParameters: null - ); - modAssemblyIndicator.InitSetupValue((str) => str.Value = Path.GetFileName(mod.ModAssembly!.File)); - groupChildren.Add(modAssemblyIndicator); + if (standalone) { + DataFeedIndicator modAssemblyFileIndicator = new DataFeedIndicator(); + modAssemblyFileIndicator.InitBase( + itemKey: key + ".AssemblyFile", + label: "Assembly file", + path: null, + groupingParameters: null + ); + modAssemblyFileIndicator.InitSetupValue((str) => str.Value = Path.GetFileName(mod.ModAssembly!.File)); + groupChildren.Add(modAssemblyFileIndicator); + + DataFeedIndicator modAssemblyHashIndicator = new DataFeedIndicator(); + modAssemblyHashIndicator.InitBase( + itemKey: key + ".AssemblyHash", + label: "Assembly hash", + path: null, + groupingParameters: null + ); + modAssemblyHashIndicator.InitSetupValue((str) => str.Value = mod.ModAssembly!.Sha256); + groupChildren.Add(modAssemblyHashIndicator); + + // TODO: Add initialization time recording + } - if (Uri.TryCreate(mod.Link, UriKind.Absolute, out var uri)) - { - DataFeedAction modOpenLinkAction = new DataFeedAction(); + if (Uri.TryCreate(mod.Link, UriKind.Absolute, out var uri)) { + DataFeedValueAction modOpenLinkAction = new DataFeedValueAction(); modOpenLinkAction.InitBase( itemKey: key + ".OpenLinkAction", label: $"Open mod link ({uri.Host})", path: null, groupingParameters: null ); - modOpenLinkAction.InitAction(delegate { - Userspace.UserspaceWorld.RunSynchronously(delegate { - Slot slot = Userspace.UserspaceWorld.AddSlot("Hyperlink"); - slot.PositionInFrontOfUser(float3.Backward); - slot.AttachComponent().Setup(uri, "Outgoing hyperlink"); - }); - }); + modOpenLinkAction.InitAction((action) => action.Target = OpenURI, uri); groupChildren.Add(modOpenLinkAction); } - if (mod.ModConfiguration is not null) - { + if (mod.ModConfiguration is not null) { DataFeedCategory modConfigurationCategory = new DataFeedCategory(); modConfigurationCategory.InitBase( itemKey: key + ".ConfigurationCategory", label: "Mod configuration", - path: new string[] {key, "Configuration"}, + path: [key, "Configuration"], groupingParameters: null ); + modConfigurationCategory.SetOverrideSubpath(standalone ? ["Configuration"] : [key, "Configuration"]); groupChildren.Add(modConfigurationCategory); } modFeedGroup.InitBase( itemKey: key + ".Group", - label: mod.Name, + label: standalone ? "Mod info" : mod.Name, path: null, groupingParameters: null, subitems: groupChildren @@ -147,4 +223,12 @@ private static DataFeedGroup GenerateModFeedGroup(ResoniteModBase mod) { return modFeedGroup; } + + // private static DataFeedGroup GenerateModLogFeed(ResoniteModBase mod, int last = -1) { + + // } + + // private static DataFeedGroup GenerateModExceptionFeed(ResoniteModBase mod, int last = -1) { + + // } } diff --git a/ResoniteModLoader/ResoniteModLoader.csproj b/ResoniteModLoader/ResoniteModLoader.csproj index 69d6372..a5c349b 100644 --- a/ResoniteModLoader/ResoniteModLoader.csproj +++ b/ResoniteModLoader/ResoniteModLoader.csproj @@ -5,7 +5,7 @@ false net472 512 - 10.0 + 12 enable true True From 87d185903327dd9ca9bc97c968979346e5cb8e49 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jul 2024 12:38:15 -0500 Subject: [PATCH 06/20] Prepare log querying and refactor with new FeedBuilder utility class --- ResoniteModLoader/Logger.cs | 26 +-- ResoniteModLoader/ModConfigurationDataFeed.cs | 200 +++++++---------- ResoniteModLoader/Properties/AssemblyInfo.cs | 2 +- ResoniteModLoader/ResoniteModLoader.csproj | 4 + ResoniteModLoader/Utility/FeedBuilder.cs | 203 ++++++++++++++++++ 5 files changed, 298 insertions(+), 137 deletions(-) create mode 100644 ResoniteModLoader/Utility/FeedBuilder.cs diff --git a/ResoniteModLoader/Logger.cs b/ResoniteModLoader/Logger.cs index 051c7c2..b7caef6 100644 --- a/ResoniteModLoader/Logger.cs +++ b/ResoniteModLoader/Logger.cs @@ -8,6 +8,10 @@ internal sealed class Logger { // logged for null objects internal const string NULL_STRING = "null"; + internal enum LogType { DEBUG, INFO, WARN, ERROR } + + internal static readonly List<(ResoniteModBase?, LogType, string, StackTrace)> LogBuffer = new(); + internal static bool IsDebugEnabled() { return ModLoaderConfiguration.Get().Debug; } @@ -52,21 +56,24 @@ internal static void DebugListExternal(object[] messages) { internal static void ErrorExternal(object message) => LogInternal(LogType.ERROR, message, SourceFromStackTrace(new(1))); internal static void ErrorListExternal(object[] messages) => LogListInternal(LogType.ERROR, messages, SourceFromStackTrace(new(1))); - private static void LogInternal(string logTypePrefix, object message, string? source = null) { + private static void LogInternal(LogType logType, object message, string? source = null) { message ??= NULL_STRING; + string logTypePrefix = LogTypeTag(logType); if (source == null) { UniLog.Log($"{logTypePrefix}[ResoniteModLoader] {message}"); - } else { + } + else { UniLog.Log($"{logTypePrefix}[ResoniteModLoader/{source}] {message}"); } } - private static void LogListInternal(string logTypePrefix, object[] messages, string? source) { + private static void LogListInternal(LogType logType, object[] messages, string? source) { if (messages == null) { - LogInternal(logTypePrefix, NULL_STRING, source); - } else { + LogInternal(logType, NULL_STRING, source); + } + else { foreach (object element in messages) { - LogInternal(logTypePrefix, element.ToString(), source); + LogInternal(logType, element.ToString(), source); } } } @@ -76,10 +83,5 @@ private static void LogListInternal(string logTypePrefix, object[] messages, str return Util.ExecutingMod(stackTrace)?.Name; } - private static class LogType { - internal const string DEBUG = "[DEBUG]"; - internal const string INFO = "[INFO] "; - internal const string WARN = "[WARN] "; - internal const string ERROR = "[ERROR]"; - } + private static string LogTypeTag(LogType logType) => $"[{Enum.GetName(typeof(LogType), logType)}]"; } diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index aacf2da..a55a623 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -17,64 +17,60 @@ public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed public override bool UserspaceOnly => true; public bool SupportsBackgroundQuerying => true; +#pragma warning restore CS1591 +#pragma warning disable CS8618, CA1051 // FrooxEngine weaver will take care of these + /// + /// Show mod configuration keys marked as internal. Default: False. + /// + public readonly Sync IncludeInternalConfigItems; + + /// + /// Enable or disable the use of custom configuration feeds. Default: True. + /// + public readonly Sync UseModDefinedEnumerate; +#pragma warning restore CS8618, CA1051 +#pragma warning disable CS1591 + protected override void OnAttach() { + base.OnAttach(); + IncludeInternalConfigItems.Value = false; + UseModDefinedEnumerate.Value = true; + } public async IAsyncEnumerable Enumerate(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData) { switch (path.Count) { case 0: { - DataFeedCategory modLoaderCategory = new DataFeedCategory(); - modLoaderCategory.InitBase( - itemKey: "ResoniteModLoader", - label: $"Open ResoniteModLoader category", - path: null, - groupingParameters: null - ); - yield return modLoaderCategory; + yield return FeedBuilder.Category("ResoniteModLoader", "Open ResoniteModLoader category"); } yield break; case 1: { if (path[0] != "ResoniteModLoader") yield break; - DataFeedLabel modLoaderVersion = new DataFeedLabel(); - modLoaderVersion.InitBase( - itemKey: "ResoniteModLoder.Version", - label: $"ResoniteModLoader version {ModLoader.VERSION}", - path: null, - groupingParameters: null - ); - yield return modLoaderVersion; - - DataFeedIndicator modLoaderLoadedModCount = new DataFeedIndicator(); // Todo: Make DataFeedIndicator template - modLoaderLoadedModCount.InitBase( - itemKey: "ResoniteModLoder.LoadedModCount", - label: "Loaded mods:", - path: null, - groupingParameters: null - ); - modLoaderLoadedModCount.InitSetupValue((count) => count.Value = ModLoader.Mods().Count().ToString()); - yield return modLoaderLoadedModCount; + yield return FeedBuilder.Label("ResoniteModLoder.Version", $"ResoniteModLoader version {ModLoader.VERSION}"); + yield return FeedBuilder.StringIndicator("ResoniteModLoder.LoadedModCount", "Loaded mods:", ModLoader.Mods().Count()); + List groupChildren = Pool.BorrowList(); foreach (ResoniteModBase mod in ModLoader.Mods()) - if (string.IsNullOrEmpty(searchPhrase) || mod.Name.ToLowerInvariant().Contains(searchPhrase.ToLowerInvariant())) - yield return GenerateModInfoGroup(mod); + if (string.IsNullOrEmpty(searchPhrase) || mod.Name.IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) >= 0) + yield return GenerateModInfoGroup(mod, false, groupChildren); + Pool.Return(ref groupChildren); } yield break; case 2: { - if (path[0] != "ResoniteModLoader") yield break; - string key = path[1]; - ResoniteModBase mod = ModFromKey(key); - yield return GenerateModInfoGroup(mod, true); + if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; + + List groupChildren = Pool.BorrowList(); + yield return GenerateModInfoGroup(mod, true, groupChildren); + Pool.Return(ref groupChildren); // GenerateModLogFeed // GenerateModExceptionFeed } yield break; case 3: { - if (path[0] != "ResoniteModLoader") yield break; - string key = path[1]; - ResoniteModBase mod = ModFromKey(key); - switch (path[2].ToLowerInvariant()) { + if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; + switch (path[2].ToLower()) { case "configuration": { } @@ -98,7 +94,10 @@ public void ListenToUpdates(IReadOnlyList path, IReadOnlyList gr } public LocaleString PathSegmentName(string segment, int depth) { - return $"{segment} ({depth})"; + return depth switch { + 2 => ModFromKey(segment)?.Name ?? "INVALID", + _ => segment + }; } public object RegisterViewData() { @@ -114,11 +113,38 @@ public void UnregisterViewData(object data) { Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterViewData called, object: {data}\n{Environment.StackTrace}"); } #pragma warning restore CS1591 - + /// + /// Returns a unique key that can be used to reference this mod. + /// + /// The mod to return a key from. + /// A unique key representing the mod. + /// + /// public static string KeyFromMod(ResoniteModBase mod) => mod.ModAssembly!.Sha256; + /// + /// Returns the mod that corresponds to a unique key. + /// + /// A unique key from . + /// The mod that corresponds with the unique key, or null if one couldn't be found. + /// public static ResoniteModBase? ModFromKey(string key) => ModLoader.Mods().First((mod) => KeyFromMod(mod) == key); + /// + /// Tries to get the mod that corresponds to a unique key. + /// + /// A unique key from . + /// Set if a matching mod is found. + /// True if a matching mod is found, false otherwise. + public static bool TryModFromKey(string key, out ResoniteModBase mod) { + mod = ModFromKey(key)!; + return mod is not null; + } + + /// + /// Spawns the prompt for a user to open a hyperlink. + /// + /// The URI that the user will be prompted to open. [SyncMethod(typeof(Action), [])] public static void OpenURI(Uri uri) { Userspace.UserspaceWorld.RunSynchronously(delegate { @@ -128,100 +154,26 @@ public static void OpenURI(Uri uri) { }); } - private static DataFeedGroup GenerateModInfoGroup(ResoniteModBase mod, bool standalone = false) { - DataFeedGroup modFeedGroup = new DataFeedGroup(); - List groupChildren = new List(); // Could this be a pool list instead? + private static DataFeedGroup GenerateModInfoGroup(ResoniteModBase mod, bool standalone = false, List groupChildren = null!) { + DataFeedGroup modFeedGroup = new(); + groupChildren = groupChildren ?? new(); + groupChildren.Clear(); string key = KeyFromMod(mod); - if (standalone) { - DataFeedIndicator modNameIndicator = new DataFeedIndicator(); - modNameIndicator.InitBase( - itemKey: key + ".Name", - label: "Name", - path: null, - groupingParameters: null - ); - modNameIndicator.InitSetupValue((str) => str.Value = mod.Name); - groupChildren.Add(modNameIndicator); - } - - DataFeedIndicator modAuthorIndicator = new DataFeedIndicator(); - modAuthorIndicator.InitBase( - itemKey: key + ".Author", - label: "Author", - path: null, - groupingParameters: null - ); - modAuthorIndicator.InitSetupValue((str) => str.Value = mod.Author); - groupChildren.Add(modAuthorIndicator); - - DataFeedIndicator modVersionIndicator = new DataFeedIndicator(); - modVersionIndicator.InitBase( - itemKey: key + ".Version", - label: "Version", - path: null, - groupingParameters: null - ); - modVersionIndicator.InitSetupValue((str) => str.Value = mod.Version); - groupChildren.Add(modVersionIndicator); + if (standalone) groupChildren.Add(FeedBuilder.Indicator(key + ".Name", "Name", mod.Name)); + groupChildren.Add(FeedBuilder.Indicator(key + ".Author", "Author", mod.Author)); + groupChildren.Add(FeedBuilder.Indicator(key + ".Version", "Version", mod.Version)); if (standalone) { - DataFeedIndicator modAssemblyFileIndicator = new DataFeedIndicator(); - modAssemblyFileIndicator.InitBase( - itemKey: key + ".AssemblyFile", - label: "Assembly file", - path: null, - groupingParameters: null - ); - modAssemblyFileIndicator.InitSetupValue((str) => str.Value = Path.GetFileName(mod.ModAssembly!.File)); - groupChildren.Add(modAssemblyFileIndicator); - - DataFeedIndicator modAssemblyHashIndicator = new DataFeedIndicator(); - modAssemblyHashIndicator.InitBase( - itemKey: key + ".AssemblyHash", - label: "Assembly hash", - path: null, - groupingParameters: null - ); - modAssemblyHashIndicator.InitSetupValue((str) => str.Value = mod.ModAssembly!.Sha256); - groupChildren.Add(modAssemblyHashIndicator); - + groupChildren.Add(FeedBuilder.Indicator(key + ".AssemblyFile", "Assembly file", Path.GetFileName(mod.ModAssembly!.File))); + groupChildren.Add(FeedBuilder.Indicator(key + ".AssemblyHash", "Assembly hash", mod.ModAssembly!.Sha256)); // TODO: Add initialization time recording } - if (Uri.TryCreate(mod.Link, UriKind.Absolute, out var uri)) { - DataFeedValueAction modOpenLinkAction = new DataFeedValueAction(); - modOpenLinkAction.InitBase( - itemKey: key + ".OpenLinkAction", - label: $"Open mod link ({uri.Host})", - path: null, - groupingParameters: null - ); - modOpenLinkAction.InitAction((action) => action.Target = OpenURI, uri); - groupChildren.Add(modOpenLinkAction); - } - - if (mod.ModConfiguration is not null) { - DataFeedCategory modConfigurationCategory = new DataFeedCategory(); - modConfigurationCategory.InitBase( - itemKey: key + ".ConfigurationCategory", - label: "Mod configuration", - path: [key, "Configuration"], - groupingParameters: null - ); - modConfigurationCategory.SetOverrideSubpath(standalone ? ["Configuration"] : [key, "Configuration"]); - groupChildren.Add(modConfigurationCategory); - } - - modFeedGroup.InitBase( - itemKey: key + ".Group", - label: standalone ? "Mod info" : mod.Name, - path: null, - groupingParameters: null, - subitems: groupChildren - ); + if (Uri.TryCreate(mod.Link, UriKind.Absolute, out var uri)) groupChildren.Add(FeedBuilder.ValueAction(key + ".OpenLinkAction", $"Open mod link ({uri.Host})", (action) => action.Target = OpenURI, uri)); + if (mod.GetConfiguration() is not null) groupChildren.Add(FeedBuilder.Category(key + ".ConfigurationCategory", "Mod configuration", standalone ? ["Configuration"] : [key, "Configuration"])); - return modFeedGroup; + return FeedBuilder.Group(key + ".Group", standalone ? "Mod info" : mod.Name, groupChildren); } // private static DataFeedGroup GenerateModLogFeed(ResoniteModBase mod, int last = -1) { diff --git a/ResoniteModLoader/Properties/AssemblyInfo.cs b/ResoniteModLoader/Properties/AssemblyInfo.cs index 5e5ac30..a0976a7 100644 --- a/ResoniteModLoader/Properties/AssemblyInfo.cs +++ b/ResoniteModLoader/Properties/AssemblyInfo.cs @@ -15,7 +15,7 @@ // Prevent FrooxEngine.Weaver from modifying this assembly, as it doesn't need anything done to it // This keeps Weaver from overwriting AssemblyVersionAttribute -[module: Description("FROOXENGINE_WEAVED")] +// [module: Description("FROOXENGINE_WEAVED")] //Mark as DataModelAssembly for the Plugin loading system to load this assembly [assembly: DataModelAssembly(DataModelAssemblyType.UserspaceCore)] diff --git a/ResoniteModLoader/ResoniteModLoader.csproj b/ResoniteModLoader/ResoniteModLoader.csproj index a5c349b..fcf97a8 100644 --- a/ResoniteModLoader/ResoniteModLoader.csproj +++ b/ResoniteModLoader/ResoniteModLoader.csproj @@ -31,6 +31,10 @@ $(ResonitePath)Resonite_Data\Managed\Elements.Core.dll False + + $(ResonitePath)Resonite_Data\Managed\Elements.Quantity.dll + False + $(ResonitePath)Resonite_Data\Managed\FrooxEngine.dll False diff --git a/ResoniteModLoader/Utility/FeedBuilder.cs b/ResoniteModLoader/Utility/FeedBuilder.cs new file mode 100644 index 0000000..a66ee98 --- /dev/null +++ b/ResoniteModLoader/Utility/FeedBuilder.cs @@ -0,0 +1,203 @@ +using Elements.Core; +using Elements.Quantity; +using FrooxEngine; + +namespace ResoniteModLoader; + +public sealed class FeedBuilder { + public static T Item(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : DataFeedItem { + T item = Activator.CreateInstance(); + item.InitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); + return item; + } + + // CONFLICT AA + public static DataFeedCategory Category(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + // CONFLICT AB + public static DataFeedCategory Category(string itemKey, LocaleString label, string[] subpath, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Category(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainSetOverrideSubpath(subpath); + + public static DataFeedGroup Group(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGroup Group(string itemKey, LocaleString label, IReadOnlyList subitems, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, object customEntity = null) + => Group(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGrid Grid(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGrid Grid(string itemKey, LocaleString label, IReadOnlyList subitems, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, object customEntity = null) + => Grid(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEntity Entity(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEntity Entity(string itemKey, LocaleString label, E entity, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Entity(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitEntity(entity); + + public static DataFeedAction Action(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedAction Action(string itemKey, LocaleString label, Action> setupAction, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Action(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction); + + public static DataFeedAction Action(string itemKey, LocaleString label, Action> setupAction, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Action(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction).ChainInitHighlight(setupHighlight); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, Action>> setupAction, Action> setupValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, setupValue); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, Action>> setupAction, T value, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, value); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, Action>> setupAction, Action> setupValue, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, setupValue).ChainInitHighlight(setupHighlight); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, Action>> setupAction, T value, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, value).ChainInitHighlight(setupHighlight); + + public static DataFeedSelection Selection(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, colorX color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, color color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, Action> setup, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup, format); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, T value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value, format); + public static DataFeedIndicator StringIndicator(string itemKey, LocaleString label, object value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value.ToString(), format); + +} + +public static class DataFeedItemChaining { + public static DataFeedItem ChainInitBase(this DataFeedItem item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) { + item.InitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); + return item; + } + + public static DataFeedItem ChainInitBase(this DataFeedItem item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, LocaleString description, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) { + item.InitBase(itemKey, path, groupingParameters, label, description, icon, setupVisible, setupEnabled, subitems, customEntity); + return item; + } + + public static DataFeedItem ChainInitVisible(this DataFeedItem item, Action> setupVisible) { + item.InitVisible(setupVisible); + return item; + } + + public static DataFeedItem ChainInitEnabled(this DataFeedItem item, Action> setupEnabled) { + item.InitEnabled(setupEnabled); + return item; + } + + public static DataFeedItem ChainInitDescription(this DataFeedItem item, LocaleString description) { + item.InitDescription(description); + return item; + } + + public static DataFeedItem ChainInitSorting(this DataFeedItem item, long order) { + item.InitSorting(order); + return item; + } + + public static DataFeedItem ChainInitSorting(this DataFeedItem item, Func orderGetter) { + item.InitSorting(orderGetter); + return item; + } + + public static DataFeedCategory ChainSetOverrideSubpath(this DataFeedCategory item, params string[] subpath) { + item.SetOverrideSubpath(subpath); + return item; + } + + public static DataFeedEntity ChainInitEntity(this DataFeedEntity item, E entity) { + item.InitEntity(entity); + return item; + } + + public static DataFeedValueElement ChainInitSetupValue(this DataFeedValueElement item, Action> setup) { + item.InitSetupValue(setup); + return item; + } + + public static DataFeedValueElement ChainInitFormatting(this DataFeedValueElement item, Action> setupFormatting) { + item.InitFormatting(setupFormatting); + return item; + } + + public static DataFeedValueElement ChainInitFormatting(this DataFeedValueElement item, string formatting) { + item.InitFormatting(formatting); + return item; + } + + public static DataFeedOrderedItem ChainInitSetup(this DataFeedOrderedItem item, Action> orderValue, Action> setupIsFirst, Action> setupIsLast, Action> setupMoveUp, Action> setupMoveDown, Action> setupMakeFirst, Action> setupMakeLast, LocaleString moveUpLabel = default, LocaleString moveDownLabel = default, LocaleString makeFirstLabel = default, LocaleString makeLastLabel = default) where T : IComparable { + item.InitSetup(orderValue, setupIsFirst, setupIsLast, setupMoveUp, setupMoveDown, setupMakeFirst, setupMakeLast, moveUpLabel, moveDownLabel, makeFirstLabel, makeLastLabel); + return item; + } + + public static DataFeedClampedValueField ChainInitSetup(this DataFeedClampedValueField item, Action> value, Action> min, Action> max) { + item.InitSetup(value, min, max); + return item; + } + public static DataFeedClampedValueField ChainInitSetup(this DataFeedClampedValueField item, Action> value, T min, T max) { + item.InitSetup(value, min, max); + return item; + } + + public static DataFeedQuantityField ChainInitUnitConfiguration(this DataFeedQuantityField item, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null) where Q : unmanaged, IQuantity { + item.InitUnitConfiguration(defaultConfig, imperialConfig); + return item; + } + + public static DataFeedSlider ChainInitSlider(this DataFeedSlider item, Action> setupReferenceValue) { + item.InitSlider(setupReferenceValue); + return item; + } + + public static DataFeedAction ChainInitAction(this DataFeedAction item, Action> setupAction) { + item.InitAction(setupAction); + return item; + } + + public static DataFeedAction ChainInitHighlight(this DataFeedAction item, Action> setupHighlight) { + item.InitHighlight(setupHighlight); + return item; + } + + public static DataFeedValueAction ChainInitAction(this DataFeedValueAction item, Action>> setupAction, Action> setupValue) { + item.InitAction(setupAction, setupValue); + return item; + } + + public static DataFeedValueAction ChainInitAction(this DataFeedValueAction item, Action>> setupAction, T value) { + item.InitAction(setupAction, value); + return item; + } + + public static DataFeedValueAction ChainInitHighlight(this DataFeedValueAction item, Action> setupHighlight) { + item.InitHighlight(setupHighlight); + return item; + } + + public static DataFeedIndicator ChainInitSetupValue(this DataFeedIndicator item, Action> setup, string format = null) { + item.InitSetupValue(setup, format); + return item; + } +} From f8a336b67877b602ef5572fcb38e19ebe2ab6f80 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jul 2024 15:00:08 -0500 Subject: [PATCH 07/20] Add value fields and descriptions to FeedBuilder --- ResoniteModLoader/Utility/FeedBuilder.cs | 346 +++++++++++++++++++++-- 1 file changed, 324 insertions(+), 22 deletions(-) diff --git a/ResoniteModLoader/Utility/FeedBuilder.cs b/ResoniteModLoader/Utility/FeedBuilder.cs index a66ee98..4884994 100644 --- a/ResoniteModLoader/Utility/FeedBuilder.cs +++ b/ResoniteModLoader/Utility/FeedBuilder.cs @@ -4,18 +4,15 @@ namespace ResoniteModLoader; -public sealed class FeedBuilder { - public static T Item(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : DataFeedItem { - T item = Activator.CreateInstance(); - item.InitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); - return item; - } +public static class FeedBuilder { + public static T Item(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : DataFeedItem + => Activator.CreateInstance().ChainInitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); - // CONFLICT AA + // CONFLICT AB public static DataFeedCategory Category(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); - // CONFLICT AB + // CONFLICT BA public static DataFeedCategory Category(string itemKey, LocaleString label, string[] subpath, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) => Category(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainSetOverrideSubpath(subpath); @@ -68,10 +65,10 @@ public static DataFeedLabel Label(string itemKey, LocaleString label, IReadOnlyL => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); public static DataFeedLabel Label(string itemKey, LocaleString label, colorX color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) - => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); public static DataFeedLabel Label(string itemKey, LocaleString label, color color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) - => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); public static DataFeedIndicator Indicator(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); @@ -84,40 +81,345 @@ public static DataFeedIndicator Indicator(string itemKey, LocaleString lab public static DataFeedIndicator StringIndicator(string itemKey, LocaleString label, object value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) => Indicator(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value.ToString(), format); + public static DataFeedToggle Toggle(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedToggle Toggle(string itemKey, LocaleString label, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Toggle(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, Func orderGetter, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => OrderedItem(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSorting(orderGetter); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, long order, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => OrderedItem(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSorting(order); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, Action> setup, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, Action> setup, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, T>(formatting); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, Action> setup, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, E>(setupFormatting); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, Action> setup, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, E>(formatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, T min, T max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, T min, T max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitUnitConfiguration(defaultConfig, imperialConfig); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, Action> value, T min, T max, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitUnitConfiguration(defaultConfig, imperialConfig); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, Action> setupFormatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, Action> setupFormatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); + + // With description + + public static T Item(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : DataFeedItem + => Activator.CreateInstance().ChainInitBase(itemKey, path, groupingParameters, label, description, icon, setupVisible, setupEnabled, subitems, customEntity); + + // CONFLICT AB + public static DataFeedCategory Category(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + // CONFLICT BA + public static DataFeedCategory Category(string itemKey, LocaleString label, LocaleString description, string[] subpath, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Category(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainSetOverrideSubpath(subpath); + + public static DataFeedGroup Group(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGroup Group(string itemKey, LocaleString label, LocaleString description, IReadOnlyList subitems, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, object customEntity = null) + => Group(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGrid Grid(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGrid Grid(string itemKey, LocaleString label, LocaleString description, IReadOnlyList subitems, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, object customEntity = null) + => Grid(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEntity Entity(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEntity Entity(string itemKey, LocaleString label, LocaleString description, E entity, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Entity(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitEntity(entity); + + public static DataFeedAction Action(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedAction Action(string itemKey, LocaleString label, LocaleString description, Action> setupAction, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Action(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction); + + public static DataFeedAction Action(string itemKey, LocaleString label, LocaleString description, Action> setupAction, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Action(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction).ChainInitHighlight(setupHighlight); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, Action>> setupAction, Action> setupValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, setupValue); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, Action>> setupAction, T value, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, value); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, Action>> setupAction, Action> setupValue, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, setupValue).ChainInitHighlight(setupHighlight); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, Action>> setupAction, T value, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, value).ChainInitHighlight(setupHighlight); + + public static DataFeedSelection Selection(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleString description, colorX color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleString description, color color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, LocaleString description, Action> setup, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup, format); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, LocaleString description, T value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value, format); + public static DataFeedIndicator StringIndicator(string itemKey, LocaleString label, LocaleString description, object value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value.ToString(), format); + + public static DataFeedToggle Toggle(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedToggle Toggle(string itemKey, LocaleString label, LocaleString description, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Toggle(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, LocaleString description, Func orderGetter, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => OrderedItem(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSorting(orderGetter); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, LocaleString description, long order, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => OrderedItem(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSorting(order); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, LocaleString description, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, LocaleString description, Action> setup, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, LocaleString description, Action> setup, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, T>(formatting); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, LocaleString description, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, LocaleString description, Action> setup, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, E>(setupFormatting); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, LocaleString description, Action> setup, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, E>(formatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitUnitConfiguration(defaultConfig, imperialConfig); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitUnitConfiguration(defaultConfig, imperialConfig); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, Action> setupFormatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, Action> setupFormatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); } public static class DataFeedItemChaining { - public static DataFeedItem ChainInitBase(this DataFeedItem item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) { + public static I ChainInitBase(this I item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where I : DataFeedItem { item.InitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); return item; } - public static DataFeedItem ChainInitBase(this DataFeedItem item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, LocaleString description, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) { + public static I ChainInitBase(this I item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, LocaleString description, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where I : DataFeedItem { item.InitBase(itemKey, path, groupingParameters, label, description, icon, setupVisible, setupEnabled, subitems, customEntity); return item; } - public static DataFeedItem ChainInitVisible(this DataFeedItem item, Action> setupVisible) { + public static I ChainInitVisible(this I item, Action> setupVisible) where I : DataFeedItem { item.InitVisible(setupVisible); return item; } - public static DataFeedItem ChainInitEnabled(this DataFeedItem item, Action> setupEnabled) { + public static I ChainInitEnabled(this I item, Action> setupEnabled) where I : DataFeedItem { item.InitEnabled(setupEnabled); return item; } - public static DataFeedItem ChainInitDescription(this DataFeedItem item, LocaleString description) { + public static I ChainInitDescription(this I item, LocaleString description) where I : DataFeedItem { item.InitDescription(description); return item; } - public static DataFeedItem ChainInitSorting(this DataFeedItem item, long order) { + public static I ChainInitSorting(this I item, long order) where I : DataFeedItem { item.InitSorting(order); return item; } - public static DataFeedItem ChainInitSorting(this DataFeedItem item, Func orderGetter) { + public static I ChainInitSorting(this I item, Func orderGetter) where I : DataFeedItem { item.InitSorting(orderGetter); return item; } @@ -132,17 +434,17 @@ public static DataFeedEntity ChainInitEntity(this DataFeedEntity item, return item; } - public static DataFeedValueElement ChainInitSetupValue(this DataFeedValueElement item, Action> setup) { + public static I ChainInitSetupValue(this I item, Action> setup) where I : DataFeedValueElement { item.InitSetupValue(setup); return item; } - public static DataFeedValueElement ChainInitFormatting(this DataFeedValueElement item, Action> setupFormatting) { + public static I ChainInitFormatting(this I item, Action> setupFormatting) where I : DataFeedValueElement { item.InitFormatting(setupFormatting); return item; } - public static DataFeedValueElement ChainInitFormatting(this DataFeedValueElement item, string formatting) { + public static I ChainInitFormatting(this I item, string formatting) where I : DataFeedValueElement { item.InitFormatting(formatting); return item; } @@ -152,11 +454,11 @@ public static DataFeedOrderedItem ChainInitSetup(this DataFeedOrderedItem< return item; } - public static DataFeedClampedValueField ChainInitSetup(this DataFeedClampedValueField item, Action> value, Action> min, Action> max) { + public static I ChainInitSetup(this I item, Action> value, Action> min, Action> max) where I : DataFeedClampedValueField { item.InitSetup(value, min, max); return item; } - public static DataFeedClampedValueField ChainInitSetup(this DataFeedClampedValueField item, Action> value, T min, T max) { + public static I ChainInitSetup(this I item, Action> value, T min, T max) where I : DataFeedClampedValueField { item.InitSetup(value, min, max); return item; } From 2d250bc5a09d1e8855303a7e091d49058fd84012 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 21 Jul 2024 04:52:36 -0500 Subject: [PATCH 08/20] A bunch of progress --- ResoniteModLoader/ModConfigurationDataFeed.cs | 189 +++++++++++++----- .../ModConfigurationFeedBuilder.cs | 74 +++++++ .../ModConfigurationValueSync.cs | 44 ++++ ResoniteModLoader/ResoniteMod.cs | 8 + ResoniteModLoader/ResoniteModBase.cs | 18 ++ ResoniteModLoader/Utility/FeedBuilder.cs | 58 +++++- 6 files changed, 335 insertions(+), 56 deletions(-) create mode 100644 ResoniteModLoader/ModConfigurationFeedBuilder.cs create mode 100644 ResoniteModLoader/ModConfigurationValueSync.cs diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index a55a623..c69466a 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -1,17 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Elements.Core; +using Elements.Core; using FrooxEngine; -using SkyFrost.Base; +using System.Collections; namespace ResoniteModLoader; /// /// A custom data feed that can be used to show information about loaded mods, and alter their configuration. Path must start with "ResoniteModLoder" /// -[Category(["Userspace"])] +[Category(["ResoniteModLoder"])] public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed, IWorldElement { #pragma warning disable CS1591 public override bool UserspaceOnly => true; @@ -20,23 +16,32 @@ public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed #pragma warning restore CS1591 #pragma warning disable CS8618, CA1051 // FrooxEngine weaver will take care of these /// - /// Show mod configuration keys marked as internal. Default: False. + /// Show mod configuration keys marked as internal. /// public readonly Sync IncludeInternalConfigItems; /// - /// Enable or disable the use of custom configuration feeds. Default: True. + /// Enable or disable the use of custom configuration feeds. /// - public readonly Sync UseModDefinedEnumerate; + public readonly Sync IgnoreModDefinedEnumerate; + + /// + /// Set to true if this feed is being used in a RootCategoryView. + /// + public readonly Sync UsingRootCategoryView; #pragma warning restore CS8618, CA1051 #pragma warning disable CS1591 - protected override void OnAttach() { - base.OnAttach(); - IncludeInternalConfigItems.Value = false; - UseModDefinedEnumerate.Value = true; - } - public async IAsyncEnumerable Enumerate(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData) { + if (UsingRootCategoryView.Value) { + if (path.Count == 0) { + foreach (ResoniteModBase mod in ModLoader.Mods()) + yield return FeedBuilder.Category(KeyFromMod(mod), mod.Name); + yield break; + } + + path = path.Prepend("ResoniteModLoader").ToList().AsReadOnly(); + } + switch (path.Count) { case 0: { yield return FeedBuilder.Category("ResoniteModLoader", "Open ResoniteModLoader category"); @@ -46,25 +51,33 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path case 1: { if (path[0] != "ResoniteModLoader") yield break; - yield return FeedBuilder.Label("ResoniteModLoder.Version", $"ResoniteModLoader version {ModLoader.VERSION}"); - yield return FeedBuilder.StringIndicator("ResoniteModLoder.LoadedModCount", "Loaded mods:", ModLoader.Mods().Count()); + if (string.IsNullOrEmpty(searchPhrase)) { + yield return FeedBuilder.Group("ResoniteModLoder", "RML", [ + FeedBuilder.Label("ResoniteModLoder.Version", $"ResoniteModLoader version {ModLoader.VERSION}"), + FeedBuilder.StringIndicator("ResoniteModLoder.LoadedModCount", "Loaded mods:", ModLoader.Mods().Count()) + ]); + List modCategories = new(); + foreach (ResoniteModBase mod in ModLoader.Mods()) + modCategories.Add(FeedBuilder.Category(KeyFromMod(mod), mod.Name)); - List groupChildren = Pool.BorrowList(); - foreach (ResoniteModBase mod in ModLoader.Mods()) - if (string.IsNullOrEmpty(searchPhrase) || mod.Name.IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) >= 0) - yield return GenerateModInfoGroup(mod, false, groupChildren); - Pool.Return(ref groupChildren); + yield return FeedBuilder.Grid("Mods", "Mods", modCategories); + } + else { + // yield return FeedBuilder.Label("SearchResults", "Search results"); + foreach (ResoniteModBase mod in ModLoader.Mods().Where((mod) => mod.Name.IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) >= 0)) + yield return mod.GenerateModInfoGroup(); + } } yield break; case 2: { if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; - - List groupChildren = Pool.BorrowList(); - yield return GenerateModInfoGroup(mod, true, groupChildren); - Pool.Return(ref groupChildren); - // GenerateModLogFeed - // GenerateModExceptionFeed + yield return mod.GenerateModInfoGroup(true); + string key = KeyFromMod(mod); + IReadOnlyList latestLogs = mod.GenerateModLogFeed(5).Append(FeedBuilder.Category("Logs", "View full log")).ToList().AsReadOnly(); + yield return FeedBuilder.Group(key + ".Logs", "Recent mod logs", latestLogs); + IReadOnlyList latestException = mod.GenerateModExceptionFeed(1).Append(FeedBuilder.Category("Exceptions", "View all exceptions")).ToList().AsReadOnly(); + yield return FeedBuilder.Group(key + ".Exceptions", "Latest mod exception", latestException); } yield break; @@ -72,17 +85,47 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; switch (path[2].ToLower()) { case "configuration": { - + if (IgnoreModDefinedEnumerate.Value) { + foreach (DataFeedItem item in mod.GenerateModConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value)) + yield return item; + } + else { + await foreach (DataFeedItem item in mod.BuildConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value)) + yield return item; + } } yield break; case "logs": { - + foreach (DataFeedLabel item in mod.GenerateModLogFeed()) + yield return item; } yield break; case "exceptions": { - + foreach (DataFeedLabel item in mod.GenerateModExceptionFeed()) + yield return item; } yield break; + default: { + // Reserved for future use - mods defining their own subfeeds + } + yield break; + } + } + case > 3: { + if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; + if (path[2].ToLower() == "configuration") { + if (IgnoreModDefinedEnumerate.Value) { + foreach (DataFeedItem item in mod.GenerateModConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value)) + yield return item; + } + else { + await foreach (DataFeedItem item in mod.BuildConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value)) + yield return item; + } + yield break; + } + else { + // Reserved for future use - mods defining their own subfeeds } } yield break; @@ -96,6 +139,7 @@ public void ListenToUpdates(IReadOnlyList path, IReadOnlyList gr public LocaleString PathSegmentName(string segment, int depth) { return depth switch { 2 => ModFromKey(segment)?.Name ?? "INVALID", + 3 => segment.Capitalize(), _ => segment }; } @@ -120,7 +164,7 @@ public void UnregisterViewData(object data) { /// A unique key representing the mod. /// /// - public static string KeyFromMod(ResoniteModBase mod) => mod.ModAssembly!.Sha256; + public static string KeyFromMod(ResoniteModBase mod) => Path.GetFileNameWithoutExtension(mod.ModAssembly!.File); /// /// Returns the mod that corresponds to a unique key. @@ -140,25 +184,19 @@ public static bool TryModFromKey(string key, out ResoniteModBase mod) { mod = ModFromKey(key)!; return mod is not null; } +} +public static class ModConfigurationDataFeedExtensions { /// - /// Spawns the prompt for a user to open a hyperlink. + /// Generates a DataFeedGroup that displays basic information about a mod. /// - /// The URI that the user will be prompted to open. - [SyncMethod(typeof(Action), [])] - public static void OpenURI(Uri uri) { - Userspace.UserspaceWorld.RunSynchronously(delegate { - Slot slot = Userspace.UserspaceWorld.AddSlot("Hyperlink"); - slot.PositionInFrontOfUser(float3.Backward); - slot.AttachComponent().Setup(uri, "Outgoing hyperlink"); - }); - } - - private static DataFeedGroup GenerateModInfoGroup(ResoniteModBase mod, bool standalone = false, List groupChildren = null!) { + /// The target mod + /// Set to true if this group will be displayed on its own page + /// + public static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool standalone = false) { DataFeedGroup modFeedGroup = new(); - groupChildren = groupChildren ?? new(); - groupChildren.Clear(); - string key = KeyFromMod(mod); + List groupChildren = new(); + string key = ModConfigurationDataFeed.KeyFromMod(mod); if (standalone) groupChildren.Add(FeedBuilder.Indicator(key + ".Name", "Name", mod.Name)); groupChildren.Add(FeedBuilder.Indicator(key + ".Author", "Author", mod.Author)); @@ -176,11 +214,58 @@ private static DataFeedGroup GenerateModInfoGroup(ResoniteModBase mod, bool stan return FeedBuilder.Group(key + ".Group", standalone ? "Mod info" : mod.Name, groupChildren); } - // private static DataFeedGroup GenerateModLogFeed(ResoniteModBase mod, int last = -1) { + public static IEnumerable GenerateModConfigurationFeed(this ResoniteModBase mod, IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { + if (!mod.TryGetConfiguration(out ModConfiguration config) || !config.ConfigurationItemDefinitions.Any()) { + yield return FeedBuilder.Label("NoConfig", "This mod does not define any configuration keys.", color.Red); + yield break; + } + + ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out ModConfigurationFeedBuilder builder); + builder = builder ?? new ModConfigurationFeedBuilder(config); + IEnumerable items; - // } + if (path.Any()) { + ModConfigurationKey key = config.ConfigurationItemDefinitions.First((config) => config.Name == path[0]); + if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; + MethodInfo genericEnumerablePage = typeof(ModConfigurationFeedBuilder).GetMethod(nameof(ModConfigurationFeedBuilder.OrderedPage)).MakeGenericMethod(key.ValueType()); + items = (IEnumerable)genericEnumerablePage.Invoke(builder, [key]); + } + else { + items = builder.RootPage(searchPhrase, includeInternal); + } + foreach (DataFeedItem item in items) + yield return item; + } - // private static DataFeedGroup GenerateModExceptionFeed(ResoniteModBase mod, int last = -1) { + private static DataFeedItem AsFeedItem(this string text, int index, bool copyable = true) { + if (copyable) + return FeedBuilder.ValueAction(index.ToString(), text, (action) => action.Target = CopyText, text); + else + return FeedBuilder.Label(index.ToString(), text); + } + + public static IEnumerable GenerateModLogFeed(this ResoniteModBase mod, int last = -1, bool copyable = true) { + yield return "Not implemented".AsFeedItem(0, copyable); + } - // } + public static IEnumerable GenerateModExceptionFeed(this ResoniteModBase mod, int last = -1, bool copyable = true) { + yield return "Not implemented".AsFeedItem(0, copyable); + } + + /// + /// Spawns the prompt for a user to open a hyperlink. + /// + /// The URI that the user will be prompted to open. + [SyncMethod(typeof(Action), [])] + public static void OpenURI(Uri uri) { + Slot slot = Userspace.UserspaceWorld.AddSlot("Hyperlink"); + slot.PositionInFrontOfUser(float3.Backward); + slot.AttachComponent().Setup(uri, "Outgoing hyperlink"); + } + + [SyncMethod(typeof(Action), [])] + public static void CopyText(string text) { + Userspace.UserspaceWorld.InputInterface.Clipboard.SetText(text); + NotificationMessage.SpawnTextMessage("Copied line.", colorX.White); + } } diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs new file mode 100644 index 0000000..d43dcf4 --- /dev/null +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -0,0 +1,74 @@ +using Elements.Core; +using FrooxEngine; +using HarmonyLib; +using System.Collections; + +namespace ResoniteModLoader; + +public class ModConfigurationFeedBuilder { + + private readonly ModConfiguration Config; + + private readonly Dictionary KeyFields = new(); + + public readonly static Dictionary CachedBuilders = new(); + + private static bool HasAutoRegisterAttribute(FieldInfo field) => field.GetCustomAttribute() is not null; + + private static bool HasRangeAttribute(FieldInfo field, out RangeAttribute attribute) { + attribute = field.GetCustomAttribute(); + return attribute is not null; + } + + private void AssertChildKey(ModConfigurationKey key) { + if (!Config.IsKeyDefined(key)) + throw new InvalidOperationException($"Mod key ({key}) is not owned by {Config.Owner.Name}'s config"); + } + + public ModConfigurationFeedBuilder(ModConfiguration config) { + Config = config; + IEnumerable autoConfigKeys = config.Owner.GetType().GetDeclaredFields().Where(HasAutoRegisterAttribute); + foreach (FieldInfo field in autoConfigKeys) { + ModConfigurationKey key = (ModConfigurationKey)field.GetValue(field.IsStatic ? null : config.Owner); + KeyFields[key] = field; + } + CachedBuilders[config] = this; + } + + public IEnumerable RootPage(string searchPhrase = "", bool includeInternal = false) { + foreach (ModConfigurationKey key in Config.ConfigurationItemDefinitions) { + if (key.InternalAccessOnly && !includeInternal) continue; + if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; + yield return GenerateDataFeedItem(key); + } + } + + public IEnumerable> OrderedPage(ModConfigurationKey key) { + AssertChildKey(key); + if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; + var value = (IEnumerable)Config.GetValue(key); + int i = 0; + foreach (object item in value) + yield return FeedBuilder.OrderedItem(key.Name + i, key.Name, item.ToString(), i++); + } + + public DataFeedValueField GenerateDataFeedField(ModConfigurationKey key) { + AssertChildKey(key); + return FeedBuilder.ValueField(key.Name, key.Name, key.Description, (field) => field.SyncWithModConfiguration(Config, key)); + } + + public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { + AssertChildKey(key); + Type valueType = key.ValueType(); + if (valueType == typeof(dummy)) + return FeedBuilder.Label(key.Name, key.Description ?? key.Name); + else if (valueType == typeof(bool)) + return FeedBuilder.Toggle(key.Name, key.Name, key.Description, (field) => field.SyncWithModConfiguration(Config, key)); + else if (valueType == typeof(float) && HasRangeAttribute(KeyFields[key], out RangeAttribute range)) + return FeedBuilder.Slider(key.Name, key.Name, key.Description, (field) => field.SyncWithModConfiguration(Config, key), range.Min, range.Max, range.TextFormat); + else if (valueType != typeof(string) && valueType != typeof(Uri) && typeof(IEnumerable).IsAssignableFrom(valueType)) + return FeedBuilder.Category(key.Name, key.Name, key.Description); + else + return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedField)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); + } +} diff --git a/ResoniteModLoader/ModConfigurationValueSync.cs b/ResoniteModLoader/ModConfigurationValueSync.cs new file mode 100644 index 0000000..63f74c3 --- /dev/null +++ b/ResoniteModLoader/ModConfigurationValueSync.cs @@ -0,0 +1,44 @@ +using Elements.Core; +using FrooxEngine; + +namespace ResoniteModLoader; + +[Category(["ResoniteModLoder"])] +public class ModConfigurationValueSync : Component { +#pragma warning disable CS1591 + public override bool UserspaceOnly => true; +#pragma warning restore CS1591 +#pragma warning disable CS8618, CA1051 + public readonly Sync DefiningModAssembly; + + public readonly Sync ConfigurationKeyName; + + public readonly Sync DefinitionFound; + + public readonly FieldDrive TargetField; +#pragma warning restore CS8618, CA1051 + private ResoniteModBase _mappedMod; + + private ModConfiguration _mappedConfig; + + private ModConfigurationKey _mappedKey; + + public void LoadConfigKey(ModConfiguration config, ModConfigurationKey key) { + + _mappedMod = config.Owner; + _mappedConfig = config; + _mappedKey = key; + DefiningModAssembly.Value = Path.GetFileNameWithoutExtension(config.Owner.ModAssembly!.File); + ConfigurationKeyName.Value = key.Name; + } +} + +public static class ModConfigurationValueSyncExtensions { + public static ModConfigurationValueSync SyncWithModConfiguration(this IField field, ModConfiguration config, ModConfigurationKey key) { + ModConfigurationValueSync driver = field.FindNearestParent().AttachComponent>(); + driver.LoadConfigKey(config, key); + driver.TargetField.Target = field; + + return driver; + } +} diff --git a/ResoniteModLoader/ResoniteMod.cs b/ResoniteModLoader/ResoniteMod.cs index fb1d6ee..05f70ec 100644 --- a/ResoniteModLoader/ResoniteMod.cs +++ b/ResoniteModLoader/ResoniteMod.cs @@ -1,3 +1,5 @@ +using FrooxEngine; + namespace ResoniteModLoader; /// @@ -99,4 +101,10 @@ public virtual void DefineConfiguration(ModConfigurationDefinitionBuilder builde public virtual IncompatibleConfigurationHandlingOption HandleIncompatibleConfigurationVersions(Version serializedVersion, Version definedVersion) { return IncompatibleConfigurationHandlingOption.ERROR; } + + /// + public override async IAsyncEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { + foreach (DataFeedItem item in this.GenerateModConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal)) + yield return item; + } } diff --git a/ResoniteModLoader/ResoniteModBase.cs b/ResoniteModLoader/ResoniteModBase.cs index a3f0302..e08834a 100644 --- a/ResoniteModLoader/ResoniteModBase.cs +++ b/ResoniteModLoader/ResoniteModBase.cs @@ -1,3 +1,5 @@ +using FrooxEngine; + namespace ResoniteModLoader; /// @@ -46,5 +48,21 @@ public abstract class ResoniteModBase { return ModConfiguration; } + public bool TryGetConfiguration(out ModConfiguration configuration) { + configuration = ModConfiguration!; + return configuration is not null; + } + + /// + /// Define a custom configuration DataFeed for this mod. + /// + /// Starts empty at the root of the configuration category, allows sub-categories to be used. + /// Passed-through from 's Enumerate call. + /// A phrase by which configuration items should be filtered. Passed-through from 's Enumerate call + /// Passed-through from 's Enumerate call. + /// Indicates whether the user has requested that internal configuration keys are included in the returned feed. + /// DataFeedItem's to be directly returned by the calling . + public abstract IAsyncEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false); + internal bool FinishedLoading { get; set; } } diff --git a/ResoniteModLoader/Utility/FeedBuilder.cs b/ResoniteModLoader/Utility/FeedBuilder.cs index 4884994..01a989a 100644 --- a/ResoniteModLoader/Utility/FeedBuilder.cs +++ b/ResoniteModLoader/Utility/FeedBuilder.cs @@ -1,10 +1,15 @@ using Elements.Core; using Elements.Quantity; using FrooxEngine; +using HarmonyLib; namespace ResoniteModLoader; +/// +/// Utility class to easily generate DataFeedItem's. +/// public static class FeedBuilder { +#pragma warning disable CS8625, CS1591, CA1715 public static T Item(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : DataFeedItem => Activator.CreateInstance().ChainInitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); @@ -200,11 +205,11 @@ public static DataFeedSlider Slider(string itemKey, LocaleString label, Ac public static T Item(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : DataFeedItem => Activator.CreateInstance().ChainInitBase(itemKey, path, groupingParameters, label, description, icon, setupVisible, setupEnabled, subitems, customEntity); - // CONFLICT AB + // CONFLICT CD public static DataFeedCategory Category(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); - // CONFLICT BA + // CONFLICT DC public static DataFeedCategory Category(string itemKey, LocaleString label, LocaleString description, string[] subpath, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) => Category(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainSetOverrideSubpath(subpath); @@ -257,10 +262,10 @@ public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleStri => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleString description, colorX color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) - => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + => Label(itemKey, $"{label}", description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleString description, color color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) - => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + => Label(itemKey, $"{label}", description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); public static DataFeedIndicator Indicator(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); @@ -386,120 +391,165 @@ public static DataFeedSlider Slider(string itemKey, LocaleString label, Lo public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); +#pragma warning restore CS8625, CS1591, CA1715 } +/// +/// Extends all DataFeedItem's "Init" methods so they can be called in a chain (methods return original item). +/// public static class DataFeedItemChaining { +#pragma warning disable CS8625, CA1715 + /// Mapped to InitBase public static I ChainInitBase(this I item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where I : DataFeedItem { item.InitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); return item; } + /// Mapped to InitBase public static I ChainInitBase(this I item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, LocaleString description, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where I : DataFeedItem { item.InitBase(itemKey, path, groupingParameters, label, description, icon, setupVisible, setupEnabled, subitems, customEntity); return item; } + /// Mapped to InitVisible public static I ChainInitVisible(this I item, Action> setupVisible) where I : DataFeedItem { item.InitVisible(setupVisible); return item; } + /// Mapped to InitEnabled public static I ChainInitEnabled(this I item, Action> setupEnabled) where I : DataFeedItem { item.InitEnabled(setupEnabled); return item; } + /// Mapped to InitDescription public static I ChainInitDescription(this I item, LocaleString description) where I : DataFeedItem { item.InitDescription(description); return item; } + /// Mapped to InitSorting public static I ChainInitSorting(this I item, long order) where I : DataFeedItem { item.InitSorting(order); return item; } + /// Mapped to InitSorting public static I ChainInitSorting(this I item, Func orderGetter) where I : DataFeedItem { item.InitSorting(orderGetter); return item; } + /// Mapped to SetOverrideSubpath public static DataFeedCategory ChainSetOverrideSubpath(this DataFeedCategory item, params string[] subpath) { item.SetOverrideSubpath(subpath); return item; } + /// Mapped to InitEntity public static DataFeedEntity ChainInitEntity(this DataFeedEntity item, E entity) { item.InitEntity(entity); return item; } + /// Mapped to InitSetupValue public static I ChainInitSetupValue(this I item, Action> setup) where I : DataFeedValueElement { item.InitSetupValue(setup); return item; } + /// Mapped to InitFormatting public static I ChainInitFormatting(this I item, Action> setupFormatting) where I : DataFeedValueElement { item.InitFormatting(setupFormatting); return item; } + /// Mapped to InitFormatting public static I ChainInitFormatting(this I item, string formatting) where I : DataFeedValueElement { item.InitFormatting(formatting); return item; } + /// Mapped to InitSetup public static DataFeedOrderedItem ChainInitSetup(this DataFeedOrderedItem item, Action> orderValue, Action> setupIsFirst, Action> setupIsLast, Action> setupMoveUp, Action> setupMoveDown, Action> setupMakeFirst, Action> setupMakeLast, LocaleString moveUpLabel = default, LocaleString moveDownLabel = default, LocaleString makeFirstLabel = default, LocaleString makeLastLabel = default) where T : IComparable { item.InitSetup(orderValue, setupIsFirst, setupIsLast, setupMoveUp, setupMoveDown, setupMakeFirst, setupMakeLast, moveUpLabel, moveDownLabel, makeFirstLabel, makeLastLabel); return item; } + /// Mapped to InitSetup public static I ChainInitSetup(this I item, Action> value, Action> min, Action> max) where I : DataFeedClampedValueField { item.InitSetup(value, min, max); return item; } + /// Mapped to InitSetup public static I ChainInitSetup(this I item, Action> value, T min, T max) where I : DataFeedClampedValueField { item.InitSetup(value, min, max); return item; } + /// Mapped to InitUnitConfiguration public static DataFeedQuantityField ChainInitUnitConfiguration(this DataFeedQuantityField item, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null) where Q : unmanaged, IQuantity { item.InitUnitConfiguration(defaultConfig, imperialConfig); return item; } + /// Mapped to InitSlider public static DataFeedSlider ChainInitSlider(this DataFeedSlider item, Action> setupReferenceValue) { item.InitSlider(setupReferenceValue); return item; } + /// Mapped to InitAction public static DataFeedAction ChainInitAction(this DataFeedAction item, Action> setupAction) { item.InitAction(setupAction); return item; } + /// Mapped to InitHighlight public static DataFeedAction ChainInitHighlight(this DataFeedAction item, Action> setupHighlight) { item.InitHighlight(setupHighlight); return item; } + /// Mapped to InitAction public static DataFeedValueAction ChainInitAction(this DataFeedValueAction item, Action>> setupAction, Action> setupValue) { item.InitAction(setupAction, setupValue); return item; } + /// Mapped to InitAction public static DataFeedValueAction ChainInitAction(this DataFeedValueAction item, Action>> setupAction, T value) { item.InitAction(setupAction, value); return item; } + /// Mapped to InitHighlight public static DataFeedValueAction ChainInitHighlight(this DataFeedValueAction item, Action> setupHighlight) { item.InitHighlight(setupHighlight); return item; } + /// Mapped to InitSetupValue public static DataFeedIndicator ChainInitSetupValue(this DataFeedIndicator item, Action> setup, string format = null) { item.InitSetupValue(setup, format); return item; } + + private static MethodInfo SubItemsSetter = AccessTools.PropertySetter(typeof(DataFeedItem), nameof(DataFeedItem.SubItems)); + + public static I Subitem(this I item, params DataFeedItem[] subitem) where I : DataFeedItem { + if (item.SubItems is null) + SubItemsSetter.Invoke(item, [subitem]); + else + SubItemsSetter.Invoke(item, [item.SubItems.Concat(subitem).ToArray()]); + return item; + } + + public static I ClearSubitems(this I item, params DataFeedItem[] subitem) where I : DataFeedItem { + if (item.SubItems is not null && item.SubItems.Any()) + SubItemsSetter.Invoke(item, [null]); + return item; + } +#pragma warning restore CS8625, CA1715 } From edae026431c7dd2fd1291b697398473cecc36bc6 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 21 Jul 2024 06:18:29 -0500 Subject: [PATCH 09/20] Generate enum fields separately --- .../AutoRegisterConfigKeyAttribute.cs | 21 ++++++++++++++++++- ResoniteModLoader/ModConfigurationDataFeed.cs | 4 ++-- .../ModConfigurationFeedBuilder.cs | 15 +++++++++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs index bafd0eb..f4ae495 100644 --- a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs +++ b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs @@ -4,4 +4,23 @@ namespace ResoniteModLoader; /// deriving from to be automatically included in that mod's configuration. /// [AttributeUsage(AttributeTargets.Field)] -public sealed class AutoRegisterConfigKeyAttribute : Attribute { } +public sealed class AutoRegisterConfigKeyAttribute : Attribute { + public readonly string GroupName; + + // public readonly IReadOnlyList Subpath; + + public AutoRegisterConfigKeyAttribute() { } + + public AutoRegisterConfigKeyAttribute(string groupName) { + GroupName = groupName; + } + + // public AutoRegisterConfigKeyAttribute(params string[] subpath) { + // Subpath = subpath; + // } + + // public AutoRegisterConfigKeyAttribute(string groupName, params string[] subpath) { + // GroupName = groupName; + // Subpath = subpath; + // } +} diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index c69466a..da325c2 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -38,8 +38,8 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path yield return FeedBuilder.Category(KeyFromMod(mod), mod.Name); yield break; } - - path = path.Prepend("ResoniteModLoader").ToList().AsReadOnly(); + else if (path[0] != "ResoniteModLoader") + path = path.Prepend("ResoniteModLoader").ToList().AsReadOnly(); } switch (path.Count) { diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs index d43dcf4..9bf0272 100644 --- a/ResoniteModLoader/ModConfigurationFeedBuilder.cs +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -54,7 +54,12 @@ public IEnumerable> OrderedPage(ModConfigurationKey key public DataFeedValueField GenerateDataFeedField(ModConfigurationKey key) { AssertChildKey(key); - return FeedBuilder.ValueField(key.Name, key.Name, key.Description, (field) => field.SyncWithModConfiguration(Config, key)); + return FeedBuilder.ValueField(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); + } + + public DataFeedEnum GenerateDataFeedEnum(ModConfigurationKey key) where T : Enum { + AssertChildKey(key); + return FeedBuilder.Enum(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); } public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { @@ -63,11 +68,13 @@ public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { if (valueType == typeof(dummy)) return FeedBuilder.Label(key.Name, key.Description ?? key.Name); else if (valueType == typeof(bool)) - return FeedBuilder.Toggle(key.Name, key.Name, key.Description, (field) => field.SyncWithModConfiguration(Config, key)); + return FeedBuilder.Toggle(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); else if (valueType == typeof(float) && HasRangeAttribute(KeyFields[key], out RangeAttribute range)) - return FeedBuilder.Slider(key.Name, key.Name, key.Description, (field) => field.SyncWithModConfiguration(Config, key), range.Min, range.Max, range.TextFormat); + return FeedBuilder.Slider(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key), range.Min, range.Max, range.TextFormat); else if (valueType != typeof(string) && valueType != typeof(Uri) && typeof(IEnumerable).IsAssignableFrom(valueType)) - return FeedBuilder.Category(key.Name, key.Name, key.Description); + return FeedBuilder.Category(key.Name, key.Description ?? key.Name); + else if (valueType.InheritsFrom(typeof(Enum))) + return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedEnum)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); else return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedField)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); } From d5ad61a1ce9b1fcd5b91f3334c36d9464ea452c6 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 22 Jul 2024 00:32:19 -0500 Subject: [PATCH 10/20] Logger revamp and ConfigurationFeedBuilder grouping/subcategories --- .../AutoRegisterConfigKeyAttribute.cs | 20 +-- ResoniteModLoader/Logger.cs | 118 +++++++++++++---- ResoniteModLoader/ModConfigurationDataFeed.cs | 60 +++++---- .../ModConfigurationFeedBuilder.cs | 120 ++++++++++++++++-- ResoniteModLoader/ResoniteMod.cs | 25 +++- ResoniteModLoader/ResoniteModBase.cs | 2 +- ResoniteModLoader/Util.cs | 9 +- ResoniteModLoader/Utility/FeedBuilder.cs | 2 +- 8 files changed, 273 insertions(+), 83 deletions(-) diff --git a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs index f4ae495..96ae100 100644 --- a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs +++ b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs @@ -5,22 +5,22 @@ namespace ResoniteModLoader; /// [AttributeUsage(AttributeTargets.Field)] public sealed class AutoRegisterConfigKeyAttribute : Attribute { - public readonly string GroupName; + public readonly string Group; - // public readonly IReadOnlyList Subpath; + public readonly IReadOnlyList Path; public AutoRegisterConfigKeyAttribute() { } public AutoRegisterConfigKeyAttribute(string groupName) { - GroupName = groupName; + Group = groupName; } - // public AutoRegisterConfigKeyAttribute(params string[] subpath) { - // Subpath = subpath; - // } + public AutoRegisterConfigKeyAttribute(string[] subpath) { + Path = subpath; + } - // public AutoRegisterConfigKeyAttribute(string groupName, params string[] subpath) { - // GroupName = groupName; - // Subpath = subpath; - // } + public AutoRegisterConfigKeyAttribute(string[] subpath, string groupName) { + Group = groupName; + Path = subpath; + } } diff --git a/ResoniteModLoader/Logger.cs b/ResoniteModLoader/Logger.cs index b7caef6..0924c52 100644 --- a/ResoniteModLoader/Logger.cs +++ b/ResoniteModLoader/Logger.cs @@ -4,18 +4,86 @@ namespace ResoniteModLoader; -internal sealed class Logger { +public sealed class Logger { // logged for null objects internal const string NULL_STRING = "null"; - internal enum LogType { DEBUG, INFO, WARN, ERROR } + public enum LogType { TRACE, DEBUG, INFO, WARN, ERROR } - internal static readonly List<(ResoniteModBase?, LogType, string, StackTrace)> LogBuffer = new(); + public readonly struct LogMessage { + public LogMessage(DateTime time, ResoniteModBase? mod, LogType level, string message, StackTrace trace) { + Time = time; + Mod = mod; + Level = level; + Message = message; + Trace = trace; + } + + public DateTime Time { get; } + public ResoniteModBase? Mod { get; } + public LogType Level { get; } + public string Message { get; } + public StackTrace Trace { get; } + + public override string ToString() => $"({Mod?.Name ?? "ResoniteModLoader"} @ {Time}) {LogTypeTag(Level)} {Message}"; + } + + public readonly struct LogException { + public LogException(DateTime time, Assembly? assembly, Exception exception) { + Time = time; + Assembly = assembly; + Exception = exception; + } + + public DateTime Time { get; } + public Assembly? Assembly { get; } + public Exception Exception { get; } + + public override string ToString() => $"({Time}) [{Assembly?.FullName} ?? Unknown assembly] {Exception.Message}\n{Exception.StackTrace}"; + } + + private static List _logBuffer = new(); + + public static IReadOnlyList Logs => _logBuffer.AsReadOnly(); + + private static List _exceptionBuffer = new(); + + public static IReadOnlyList Exceptions => _exceptionBuffer.AsReadOnly(); internal static bool IsDebugEnabled() { return ModLoaderConfiguration.Get().Debug; } + internal static void TraceFuncInternal(Func messageProducer) { + if (IsDebugEnabled()) { + LogInternal(LogType.TRACE, messageProducer(), null, true); + } + } + + internal static void TraceFuncExternal(Func messageProducer) { + if (IsDebugEnabled()) { + LogInternal(LogType.TRACE, messageProducer(), new(1), true); + } + } + + internal static void TraceInternal(string message) { + if (IsDebugEnabled()) { + LogInternal(LogType.TRACE, message, null, true); + } + } + + internal static void TraceExternal(object message) { + if (IsDebugEnabled()) { + LogInternal(LogType.TRACE, message, new(1), true); + } + } + + internal static void TraceListExternal(object[] messages) { + if (IsDebugEnabled()) { + LogListInternal(LogType.TRACE, messages, new(1), true); + } + } + internal static void DebugFuncInternal(Func messageProducer) { if (IsDebugEnabled()) { LogInternal(LogType.DEBUG, messageProducer()); @@ -24,7 +92,7 @@ internal static void DebugFuncInternal(Func messageProducer) { internal static void DebugFuncExternal(Func messageProducer) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, messageProducer(), SourceFromStackTrace(new(1))); + LogInternal(LogType.DEBUG, messageProducer(), new(1)); } } @@ -36,52 +104,56 @@ internal static void DebugInternal(string message) { internal static void DebugExternal(object message) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, message, SourceFromStackTrace(new(1))); + LogInternal(LogType.DEBUG, message, new(1)); } } internal static void DebugListExternal(object[] messages) { if (IsDebugEnabled()) { - LogListInternal(LogType.DEBUG, messages, SourceFromStackTrace(new(1))); + LogListInternal(LogType.DEBUG, messages, new(1)); } } internal static void MsgInternal(string message) => LogInternal(LogType.INFO, message); - internal static void MsgExternal(object message) => LogInternal(LogType.INFO, message, SourceFromStackTrace(new(1))); - internal static void MsgListExternal(object[] messages) => LogListInternal(LogType.INFO, messages, SourceFromStackTrace(new(1))); + internal static void MsgExternal(object message) => LogInternal(LogType.INFO, message, new(1)); + internal static void MsgListExternal(object[] messages) => LogListInternal(LogType.INFO, messages, new(1)); internal static void WarnInternal(string message) => LogInternal(LogType.WARN, message); - internal static void WarnExternal(object message) => LogInternal(LogType.WARN, message, SourceFromStackTrace(new(1))); - internal static void WarnListExternal(object[] messages) => LogListInternal(LogType.WARN, messages, SourceFromStackTrace(new(1))); + internal static void WarnExternal(object message) => LogInternal(LogType.WARN, message, new(1)); + internal static void WarnListExternal(object[] messages) => LogListInternal(LogType.WARN, messages, new(1)); internal static void ErrorInternal(string message) => LogInternal(LogType.ERROR, message); - internal static void ErrorExternal(object message) => LogInternal(LogType.ERROR, message, SourceFromStackTrace(new(1))); - internal static void ErrorListExternal(object[] messages) => LogListInternal(LogType.ERROR, messages, SourceFromStackTrace(new(1))); + internal static void ErrorExternal(object message) => LogInternal(LogType.ERROR, message, new(1)); + internal static void ErrorListExternal(object[] messages) => LogListInternal(LogType.ERROR, messages, new(1)); - private static void LogInternal(LogType logType, object message, string? source = null) { + private static void LogInternal(LogType logType, object message, StackTrace? stackTrace = null, bool includeTrace = false) { message ??= NULL_STRING; + stackTrace = stackTrace ?? new(1); + ResoniteMod? source = Util.ExecutingMod(stackTrace); string logTypePrefix = LogTypeTag(logType); + _logBuffer.Add(new LogMessage(DateTime.Now, source, logType, message.ToString(), stackTrace)); if (source == null) { - UniLog.Log($"{logTypePrefix}[ResoniteModLoader] {message}"); + UniLog.Log($"{logTypePrefix}[ResoniteModLoader] {message}", includeTrace); } else { - UniLog.Log($"{logTypePrefix}[ResoniteModLoader/{source}] {message}"); + UniLog.Log($"{logTypePrefix}[ResoniteModLoader/{source.Name}] {message}", includeTrace); } } - private static void LogListInternal(LogType logType, object[] messages, string? source) { + private static void LogListInternal(LogType logType, object[] messages, StackTrace? stackTrace, bool includeTrace = false) { if (messages == null) { - LogInternal(logType, NULL_STRING, source); + LogInternal(logType, NULL_STRING, stackTrace, includeTrace); } else { foreach (object element in messages) { - LogInternal(logType, element.ToString(), source); + LogInternal(logType, element.ToString(), stackTrace, includeTrace); } } } - private static string? SourceFromStackTrace(StackTrace stackTrace) { - // MsgExternal() and Msg() are above us in the stack - return Util.ExecutingMod(stackTrace)?.Name; - } - private static string LogTypeTag(LogType logType) => $"[{Enum.GetName(typeof(LogType), logType)}]"; } + +public static class LoggerExtensions { + public static IEnumerable Logs(this ResoniteModBase mod) => Logger.Logs.Where((line) => line.Mod == mod); + + public static IEnumerable Exceptions(this ResoniteModBase mod) => Logger.Exceptions.Where((line) => line.Assembly == mod.ModAssembly!.Assembly); +} diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index da325c2..4f731e1 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -24,24 +24,9 @@ public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed /// Enable or disable the use of custom configuration feeds. /// public readonly Sync IgnoreModDefinedEnumerate; - - /// - /// Set to true if this feed is being used in a RootCategoryView. - /// - public readonly Sync UsingRootCategoryView; #pragma warning restore CS8618, CA1051 #pragma warning disable CS1591 public async IAsyncEnumerable Enumerate(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData) { - if (UsingRootCategoryView.Value) { - if (path.Count == 0) { - foreach (ResoniteModBase mod in ModLoader.Mods()) - yield return FeedBuilder.Category(KeyFromMod(mod), mod.Name); - yield break; - } - else if (path[0] != "ResoniteModLoader") - path = path.Prepend("ResoniteModLoader").ToList().AsReadOnly(); - } - switch (path.Count) { case 0: { yield return FeedBuilder.Category("ResoniteModLoader", "Open ResoniteModLoader category"); @@ -74,10 +59,14 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; yield return mod.GenerateModInfoGroup(true); string key = KeyFromMod(mod); - IReadOnlyList latestLogs = mod.GenerateModLogFeed(5).Append(FeedBuilder.Category("Logs", "View full log")).ToList().AsReadOnly(); - yield return FeedBuilder.Group(key + ".Logs", "Recent mod logs", latestLogs); - IReadOnlyList latestException = mod.GenerateModExceptionFeed(1).Append(FeedBuilder.Category("Exceptions", "View all exceptions")).ToList().AsReadOnly(); - yield return FeedBuilder.Group(key + ".Exceptions", "Latest mod exception", latestException); + if (mod.Logs().Any()) { + IReadOnlyList latestLogs = mod.GenerateModLogFeed(5, false).Append(FeedBuilder.Category("Logs", "View full log")).ToList().AsReadOnly(); + yield return FeedBuilder.Group(key + ".Logs", "Recent mod logs", latestLogs); + } + if (mod.Exceptions().Any()) { + IReadOnlyList latestException = mod.GenerateModExceptionFeed(1, false).Append(FeedBuilder.Category("Exceptions", "View all exceptions")).ToList().AsReadOnly(); + yield return FeedBuilder.Group(key + ".Exceptions", "Latest mod exception", latestException); + } } yield break; @@ -224,15 +213,16 @@ public static IEnumerable GenerateModConfigurationFeed(this Resoni builder = builder ?? new ModConfigurationFeedBuilder(config); IEnumerable items; - if (path.Any()) { - ModConfigurationKey key = config.ConfigurationItemDefinitions.First((config) => config.Name == path[0]); - if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; - MethodInfo genericEnumerablePage = typeof(ModConfigurationFeedBuilder).GetMethod(nameof(ModConfigurationFeedBuilder.OrderedPage)).MakeGenericMethod(key.ValueType()); - items = (IEnumerable)genericEnumerablePage.Invoke(builder, [key]); - } - else { - items = builder.RootPage(searchPhrase, includeInternal); - } + // if (path.Any()) { + // ModConfigurationKey key = config.ConfigurationItemDefinitions.First((config) => config.Name == path[0]); + // if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; + // MethodInfo genericEnumerablePage = typeof(ModConfigurationFeedBuilder).GetMethod(nameof(ModConfigurationFeedBuilder.OrderedPage)).MakeGenericMethod(key.ValueType()); + // items = (IEnumerable)genericEnumerablePage.Invoke(builder, [key]); + // } + // else { + // items = builder.Page(searchPhrase, includeInternal); + // } + items = builder.GeneratePage(path.ToArray(), searchPhrase, includeInternal); foreach (DataFeedItem item in items) yield return item; } @@ -245,11 +235,19 @@ private static DataFeedItem AsFeedItem(this string text, int index, bool copyabl } public static IEnumerable GenerateModLogFeed(this ResoniteModBase mod, int last = -1, bool copyable = true) { - yield return "Not implemented".AsFeedItem(0, copyable); + last = last < 0 ? int.MaxValue : last; + List modLogs = mod.Logs().ToList(); + modLogs.Reverse(); + foreach (Logger.LogMessage line in modLogs.GetRange(0, Math.Min(modLogs.Count, last))) + yield return line.ToString().AsFeedItem(line.Time.GetHashCode(), copyable); } public static IEnumerable GenerateModExceptionFeed(this ResoniteModBase mod, int last = -1, bool copyable = true) { - yield return "Not implemented".AsFeedItem(0, copyable); + last = last < 0 ? int.MaxValue : last; + List modExceptions = mod.Exceptions().ToList(); + modExceptions.Reverse(); + foreach (Logger.LogException line in modExceptions.GetRange(0, Math.Min(modExceptions.Count, last))) + yield return line.ToString().AsFeedItem(line.Time.GetHashCode(), copyable); } /// @@ -266,6 +264,6 @@ public static void OpenURI(Uri uri) { [SyncMethod(typeof(Action), [])] public static void CopyText(string text) { Userspace.UserspaceWorld.InputInterface.Clipboard.SetText(text); - NotificationMessage.SpawnTextMessage("Copied line.", colorX.White); + NotificationMessage.SpawnTextMessage("Copied line", colorX.White); } } diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs index 9bf0272..642fd38 100644 --- a/ResoniteModLoader/ModConfigurationFeedBuilder.cs +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -11,11 +11,22 @@ public class ModConfigurationFeedBuilder { private readonly Dictionary KeyFields = new(); + private readonly Dictionary> KeyGrouping = new(); + + private readonly Dictionary> KeyCategories = new(); + public readonly static Dictionary CachedBuilders = new(); private static bool HasAutoRegisterAttribute(FieldInfo field) => field.GetCustomAttribute() is not null; - private static bool HasRangeAttribute(FieldInfo field, out RangeAttribute attribute) { + private static bool TryGetAutoRegisterAttribute(FieldInfo field, out AutoRegisterConfigKeyAttribute attribute) { + attribute = field.GetCustomAttribute(); + return attribute is not null; + } + + private static bool HasRangeAttribute(FieldInfo field) => field.GetCustomAttribute() is not null; + + private static bool TryGetRangeAttribute(FieldInfo field, out RangeAttribute attribute) { attribute = field.GetCustomAttribute(); return attribute is not null; } @@ -25,25 +36,74 @@ private void AssertChildKey(ModConfigurationKey key) { throw new InvalidOperationException($"Mod key ({key}) is not owned by {Config.Owner.Name}'s config"); } + private static bool AStartsWithB(T[] A, T[] B) => string.Join("\t", A).StartsWith(string.Join("\t", B), StringComparison.InvariantCultureIgnoreCase); + public ModConfigurationFeedBuilder(ModConfiguration config) { Config = config; IEnumerable autoConfigKeys = config.Owner.GetType().GetDeclaredFields().Where(HasAutoRegisterAttribute); + HashSet groupedKeys = new(); + HashSet categorizedKeys = new(); foreach (FieldInfo field in autoConfigKeys) { ModConfigurationKey key = (ModConfigurationKey)field.GetValue(field.IsStatic ? null : config.Owner); + if (key is null) continue; // dunno why this would happen KeyFields[key] = field; + AutoRegisterConfigKeyAttribute attribute = field.GetCustomAttribute(); + if (attribute.Group is string groupName) { + if (!KeyGrouping.ContainsKey(groupName)) + KeyGrouping[groupName] = new(); + KeyGrouping[groupName].Add(key); + groupedKeys.Add(key); + } + if (attribute.Path is string[] categoryPath) { + if (!KeyCategories.ContainsKey(categoryPath)) + KeyCategories[categoryPath] = new(); + KeyCategories[categoryPath].Add(key); + categorizedKeys.Add(key); + } + } + foreach (ModConfigurationKey key in config.ConfigurationItemDefinitions) { + if (groupedKeys.Any() && !groupedKeys.Contains(key)) { + if (!KeyGrouping.ContainsKey("Uncategorized")) + KeyGrouping["Uncategorized"] = new(); + KeyGrouping["Uncategorized"].Add(key); + } + if (!categorizedKeys.Contains(key)) { + if (!KeyCategories.ContainsKey([])) + KeyCategories[[]] = new(); + KeyCategories[[]].Add(key); + } } CachedBuilders[config] = this; } - public IEnumerable RootPage(string searchPhrase = "", bool includeInternal = false) { - foreach (ModConfigurationKey key in Config.ConfigurationItemDefinitions) { - if (key.InternalAccessOnly && !includeInternal) continue; - if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; - yield return GenerateDataFeedItem(key); + public IEnumerable GeneratePage(string[] path, string searchPhrase = "", bool includeInternal = false) { + path = path ?? []; + DataFeedGrid? subcategories = GenerateSubcategoryButtons(path); + if (subcategories is not null) yield return subcategories; + Logger.DebugInternal($"KeyCategories[{string.Join(", ", path)}].Contains"); + IEnumerable filteredItems = string.IsNullOrEmpty(searchPhrase) ? Config.ConfigurationItemDefinitions.Where(KeyCategories[path].Contains) : Config.ConfigurationItemDefinitions; + if (KeyGrouping.Any()) { + foreach (string group in KeyGrouping.Keys) { + DataFeedGroup container = FeedBuilder.Group(group, group); + foreach (ModConfigurationKey key in filteredItems.Where(KeyGrouping[group].Contains)) { + if (key.InternalAccessOnly && !includeInternal) continue; + if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; + container.Subitem(GenerateDataFeedItem(key)); + } + if (container.SubItems is not null && container.SubItems.Any()) yield return container; + } + } + else { + foreach (ModConfigurationKey key in filteredItems) { + if (key.InternalAccessOnly && !includeInternal) continue; + if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; + yield return GenerateDataFeedItem(key); + } } + yield return GenerateSaveControlButtons(); } - public IEnumerable> OrderedPage(ModConfigurationKey key) { + public IEnumerable> OrderedItem(ModConfigurationKey key) { AssertChildKey(key); if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; var value = (IEnumerable)Config.GetValue(key); @@ -54,7 +114,12 @@ public IEnumerable> OrderedPage(ModConfigurationKey key public DataFeedValueField GenerateDataFeedField(ModConfigurationKey key) { AssertChildKey(key); - return FeedBuilder.ValueField(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); + if (typeof(T) == typeof(bool)) + return (DataFeedValueField)(object)FeedBuilder.Toggle(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); + else if (typeof(T).IsAssignableFrom(typeof(float)) && KeyFields.TryGetValue(key, out FieldInfo field) && TryGetRangeAttribute(field, out RangeAttribute range) && range.Min is T min && range.Max is T max) + return FeedBuilder.Slider(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key), min, max, range.TextFormat); + else + return FeedBuilder.ValueField(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); } public DataFeedEnum GenerateDataFeedEnum(ModConfigurationKey key) where T : Enum { @@ -67,10 +132,6 @@ public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { Type valueType = key.ValueType(); if (valueType == typeof(dummy)) return FeedBuilder.Label(key.Name, key.Description ?? key.Name); - else if (valueType == typeof(bool)) - return FeedBuilder.Toggle(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); - else if (valueType == typeof(float) && HasRangeAttribute(KeyFields[key], out RangeAttribute range)) - return FeedBuilder.Slider(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key), range.Min, range.Max, range.TextFormat); else if (valueType != typeof(string) && valueType != typeof(Uri) && typeof(IEnumerable).IsAssignableFrom(valueType)) return FeedBuilder.Category(key.Name, key.Description ?? key.Name); else if (valueType.InheritsFrom(typeof(Enum))) @@ -78,4 +139,39 @@ public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { else return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedField)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); } + + public DataFeedGrid? GenerateSubcategoryButtons(string[] currentPath) { + if (!KeyCategories.Any()) return null; + IEnumerable subCategories = KeyCategories.Keys.Where((subPath) => subPath.Length == currentPath.Length + 1 && AStartsWithB(subPath, currentPath)); + if (subCategories is null || !subCategories.Any()) return null; + DataFeedGrid container = FeedBuilder.Grid("SubcategoryButtonsGrid", ""); + foreach (string[] subCategory in subCategories) + container.Subitem(FeedBuilder.Category(subCategory.Last(), subCategory.Last() + " >")); + return container; + } + + public DataFeedGrid GenerateSaveControlButtons() { + string configName = Path.GetFileNameWithoutExtension(Config.Owner.ModAssembly!.File); + DataFeedGrid container = FeedBuilder.Grid("SaveControlButtonsGrid", "", [ + FeedBuilder.ValueAction("Save", "Save changes", (action) => action.Target = SaveConfig, configName), + FeedBuilder.ValueAction("Discard", "Discard changes", (action) => action.Target = DiscardConfig, configName), + FeedBuilder.ValueAction("Reset", "Reset all options", (action) => action.Target = ResetConfig, configName) + ]); + return container; + } + + [SyncMethod(typeof(Action), [])] + public static void SaveConfig(string configName) { + + } + + [SyncMethod(typeof(Action), [])] + public static void DiscardConfig(string configName) { + + } + + [SyncMethod(typeof(Action), [])] + public static void ResetConfig(string configName) { + + } } diff --git a/ResoniteModLoader/ResoniteMod.cs b/ResoniteModLoader/ResoniteMod.cs index 05f70ec..b55e3c7 100644 --- a/ResoniteModLoader/ResoniteMod.cs +++ b/ResoniteModLoader/ResoniteMod.cs @@ -13,7 +13,28 @@ public abstract class ResoniteMod : ResoniteModBase { public static bool IsDebugEnabled() => Logger.IsDebugEnabled(); /// - /// Logs an object as a line in the log based on the value produced by the given function if debug logging is enabled.. + /// Logs an object as a line in the log with a stack trace based on the value produced by the given function if debug logging is enabled. + /// + /// This is more efficient than passing an or a directly, + /// as it won't be generated if debug logging is disabled. + /// + /// The function generating the object to log. + public static void TraceFunc(Func messageProducer) => Logger.TraceFuncExternal(messageProducer); + + /// + /// Logs the given object as a line in the log if debug logging is enabled. + /// + /// The object to log. + public static void Trace(object message) => Logger.TraceExternal(message); + + /// + /// Logs the given objects as lines in the log with a stack trace if debug logging is enabled. + /// + /// The objects to log. + public static void Trace(params object[] messages) => Logger.TraceListExternal(messages); + + /// + /// Logs an object as a line in the log with a stack trace based on the value produced by the given function if debug logging is enabled. /// /// This is more efficient than passing an or a directly, /// as it won't be generated if debug logging is disabled. @@ -103,7 +124,7 @@ public virtual IncompatibleConfigurationHandlingOption HandleIncompatibleConfigu } /// - public override async IAsyncEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { + internal override async IAsyncEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { foreach (DataFeedItem item in this.GenerateModConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal)) yield return item; } diff --git a/ResoniteModLoader/ResoniteModBase.cs b/ResoniteModLoader/ResoniteModBase.cs index e08834a..8d30757 100644 --- a/ResoniteModLoader/ResoniteModBase.cs +++ b/ResoniteModLoader/ResoniteModBase.cs @@ -62,7 +62,7 @@ public bool TryGetConfiguration(out ModConfiguration configuration) { /// Passed-through from 's Enumerate call. /// Indicates whether the user has requested that internal configuration keys are included in the returned feed. /// DataFeedItem's to be directly returned by the calling . - public abstract IAsyncEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false); + internal abstract IAsyncEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false); internal bool FinishedLoading { get; set; } } diff --git a/ResoniteModLoader/Util.cs b/ResoniteModLoader/Util.cs index e03b3e1..6a7af85 100644 --- a/ResoniteModLoader/Util.cs +++ b/ResoniteModLoader/Util.cs @@ -81,7 +81,8 @@ internal static bool CanBeNull(Type t) { internal static IEnumerable GetLoadableTypes(this Assembly assembly, Predicate predicate) { try { return assembly.GetTypes().Where(type => CheckType(type, predicate)); - } catch (ReflectionTypeLoadException e) { + } + catch (ReflectionTypeLoadException e) { return e.Types.Where(type => CheckType(type, predicate)); } } @@ -95,14 +96,16 @@ private static bool CheckType(Type type, Predicate predicate) { try { string _name = type.Name; - } catch (Exception e) { + } + catch (Exception e) { Logger.DebugFuncInternal(() => $"Could not read the name for a type: {e}"); return false; } try { return predicate(type); - } catch (Exception e) { + } + catch (Exception e) { Logger.DebugFuncInternal(() => $"Could not load type \"{type}\": {e}"); return false; } diff --git a/ResoniteModLoader/Utility/FeedBuilder.cs b/ResoniteModLoader/Utility/FeedBuilder.cs index 01a989a..d400c71 100644 --- a/ResoniteModLoader/Utility/FeedBuilder.cs +++ b/ResoniteModLoader/Utility/FeedBuilder.cs @@ -397,7 +397,7 @@ public static DataFeedSlider Slider(string itemKey, LocaleString label, Lo /// /// Extends all DataFeedItem's "Init" methods so they can be called in a chain (methods return original item). /// -public static class DataFeedItemChaining { +public static class FeedBuilderExtensions { #pragma warning disable CS8625, CA1715 /// Mapped to InitBase public static I ChainInitBase(this I item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where I : DataFeedItem { From e1bc47828d548ec8ed54cbf920d6dcaf4e4d34c5 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 23 Jul 2024 01:18:27 -0500 Subject: [PATCH 11/20] Pro tip: don't try to use an array as a dictionary key! --- .editorconfig | 23 ++-- .../AutoRegisterConfigKeyAttribute.cs | 2 +- ResoniteModLoader/Logger.cs | 6 +- ResoniteModLoader/ModConfigurationDataFeed.cs | 116 +++++++++++------- .../ModConfigurationFeedBuilder.cs | 68 +++++++--- ResoniteModLoader/ResoniteMod.cs | 4 +- ResoniteModLoader/ResoniteModBase.cs | 4 +- ResoniteModLoader/Utility/FeedBuilder.cs | 18 +-- 8 files changed, 155 insertions(+), 86 deletions(-) diff --git a/.editorconfig b/.editorconfig index 243b28e..cd4e246 100644 --- a/.editorconfig +++ b/.editorconfig @@ -60,34 +60,33 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion diff --git a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs index 96ae100..72b0653 100644 --- a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs +++ b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs @@ -7,7 +7,7 @@ namespace ResoniteModLoader; public sealed class AutoRegisterConfigKeyAttribute : Attribute { public readonly string Group; - public readonly IReadOnlyList Path; + public readonly string[] Path; public AutoRegisterConfigKeyAttribute() { } diff --git a/ResoniteModLoader/Logger.cs b/ResoniteModLoader/Logger.cs index 0924c52..9bc8113 100644 --- a/ResoniteModLoader/Logger.cs +++ b/ResoniteModLoader/Logger.cs @@ -129,7 +129,7 @@ private static void LogInternal(LogType logType, object message, StackTrace? sta stackTrace = stackTrace ?? new(1); ResoniteMod? source = Util.ExecutingMod(stackTrace); string logTypePrefix = LogTypeTag(logType); - _logBuffer.Add(new LogMessage(DateTime.Now, source, logType, message.ToString(), stackTrace)); + _logBuffer.Add(new(DateTime.Now, source, logType, message.ToString(), stackTrace)); if (source == null) { UniLog.Log($"{logTypePrefix}[ResoniteModLoader] {message}", includeTrace); } @@ -149,6 +149,10 @@ private static void LogListInternal(LogType logType, object[] messages, StackTra } } + internal static void ProcessException(Exception exception, Assembly? assembly = null) { + _exceptionBuffer.Add(new(DateTime.Now, assembly, exception)); + } + private static string LogTypeTag(LogType logType) => $"[{Enum.GetName(typeof(LogType), logType)}]"; } diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index 4f731e1..04a299b 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -41,6 +41,7 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path FeedBuilder.Label("ResoniteModLoder.Version", $"ResoniteModLoader version {ModLoader.VERSION}"), FeedBuilder.StringIndicator("ResoniteModLoder.LoadedModCount", "Loaded mods:", ModLoader.Mods().Count()) ]); + List modCategories = new(); foreach (ResoniteModBase mod in ModLoader.Mods()) modCategories.Add(FeedBuilder.Category(KeyFromMod(mod), mod.Name)); @@ -48,23 +49,30 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path yield return FeedBuilder.Grid("Mods", "Mods", modCategories); } else { - // yield return FeedBuilder.Label("SearchResults", "Search results"); - foreach (ResoniteModBase mod in ModLoader.Mods().Where((mod) => mod.Name.IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) >= 0)) - yield return mod.GenerateModInfoGroup(); + IEnumerable filteredMods = ModLoader.Mods().Where((mod) => mod.Name.IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) >= 0); + yield return FeedBuilder.Label("SearchResults", filteredMods.Any() ? $"Search results: {filteredMods.Count()} mods found." : "No results!"); + + foreach (ResoniteModBase mod in filteredMods) + yield return mod.GenerateModInfoGroup(false); } } yield break; case 2: { if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; - yield return mod.GenerateModInfoGroup(true); + string key = KeyFromMod(mod); - if (mod.Logs().Any()) { - IReadOnlyList latestLogs = mod.GenerateModLogFeed(5, false).Append(FeedBuilder.Category("Logs", "View full log")).ToList().AsReadOnly(); + yield return mod.GenerateModInfoGroup(true); + + IEnumerable modLogs = mod.Logs(); + if (modLogs.Any()) { + IReadOnlyList latestLogs = mod.GenerateModLogFeed(5, false).Append(FeedBuilder.Category("Logs", $"View full log ({modLogs.Count()})")).ToList().AsReadOnly(); yield return FeedBuilder.Group(key + ".Logs", "Recent mod logs", latestLogs); } - if (mod.Exceptions().Any()) { - IReadOnlyList latestException = mod.GenerateModExceptionFeed(1, false).Append(FeedBuilder.Category("Exceptions", "View all exceptions")).ToList().AsReadOnly(); + + IEnumerable modExceptions = mod.Exceptions(); + if (modExceptions.Any()) { + IReadOnlyList latestException = mod.GenerateModExceptionFeed(1, false).Append(FeedBuilder.Category("Exceptions", $"View all exceptions ({modExceptions.Count()})")).ToList().AsReadOnly(); yield return FeedBuilder.Group(key + ".Exceptions", "Latest mod exception", latestException); } } @@ -72,28 +80,26 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path case 3: { if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; + switch (path[2].ToLower()) { case "configuration": { - if (IgnoreModDefinedEnumerate.Value) { - foreach (DataFeedItem item in mod.GenerateModConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value)) - yield return item; - } - else { - await foreach (DataFeedItem item in mod.BuildConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value)) - yield return item; - } + foreach (DataFeedItem item in mod.GenerateModConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value, IgnoreModDefinedEnumerate.Value)) + yield return item; } yield break; + case "logs": { - foreach (DataFeedLabel item in mod.GenerateModLogFeed()) + foreach (DataFeedItem item in mod.GenerateModLogFeed(-1, true, searchPhrase)) yield return item; } yield break; + case "exceptions": { - foreach (DataFeedLabel item in mod.GenerateModExceptionFeed()) + foreach (DataFeedItem item in mod.GenerateModExceptionFeed(-1, true, searchPhrase)) yield return item; } yield break; + default: { // Reserved for future use - mods defining their own subfeeds } @@ -102,15 +108,11 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path } case > 3: { if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; + if (path[2].ToLower() == "configuration") { - if (IgnoreModDefinedEnumerate.Value) { - foreach (DataFeedItem item in mod.GenerateModConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value)) - yield return item; - } - else { - await foreach (DataFeedItem item in mod.BuildConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value)) - yield return item; - } + foreach (DataFeedItem item in mod.GenerateModConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value, IgnoreModDefinedEnumerate.Value)) + yield return item; + yield break; } else { @@ -175,14 +177,14 @@ public static bool TryModFromKey(string key, out ResoniteModBase mod) { } } -public static class ModConfigurationDataFeedExtensions { +internal static class ModConfigurationDataFeedExtensions { /// /// Generates a DataFeedGroup that displays basic information about a mod. /// /// The target mod /// Set to true if this group will be displayed on its own page /// - public static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool standalone = false) { + public static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool standalone) { DataFeedGroup modFeedGroup = new(); List groupChildren = new(); string key = ModConfigurationDataFeed.KeyFromMod(mod); @@ -200,10 +202,20 @@ public static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool if (Uri.TryCreate(mod.Link, UriKind.Absolute, out var uri)) groupChildren.Add(FeedBuilder.ValueAction(key + ".OpenLinkAction", $"Open mod link ({uri.Host})", (action) => action.Target = OpenURI, uri)); if (mod.GetConfiguration() is not null) groupChildren.Add(FeedBuilder.Category(key + ".ConfigurationCategory", "Mod configuration", standalone ? ["Configuration"] : [key, "Configuration"])); + if (!standalone) { + IEnumerable modLogs = mod.Logs(); + IEnumerable modExceptions = mod.Exceptions(); + if (modLogs.Any()) groupChildren.Add(FeedBuilder.Category(key + ".LogsCategory", $"Mod logs ({modLogs.Count()})", [key, "Logs"])); + if (modExceptions.Any()) groupChildren.Add(FeedBuilder.Category(key + ".ExceptionsCategory", $"Mod exceptions ({modExceptions.Count()})", [key, "Exceptions"])); + } + return FeedBuilder.Group(key + ".Group", standalone ? "Mod info" : mod.Name, groupChildren); } - public static IEnumerable GenerateModConfigurationFeed(this ResoniteModBase mod, IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { + public static IEnumerable GenerateModConfigurationFeed(this ResoniteModBase mod, IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false, bool forceDefaultBuilder = false) { + if (path.FirstOrDefault() == "ResoniteModLoader") + Logger.WarnInternal("Call to GenerateModConfigurationFeed may include full DataFeed path, if so expect broken behavior."); + if (!mod.TryGetConfiguration(out ModConfiguration config) || !config.ConfigurationItemDefinitions.Any()) { yield return FeedBuilder.Label("NoConfig", "This mod does not define any configuration keys.", color.Red); yield break; @@ -213,16 +225,21 @@ public static IEnumerable GenerateModConfigurationFeed(this Resoni builder = builder ?? new ModConfigurationFeedBuilder(config); IEnumerable items; - // if (path.Any()) { - // ModConfigurationKey key = config.ConfigurationItemDefinitions.First((config) => config.Name == path[0]); - // if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; - // MethodInfo genericEnumerablePage = typeof(ModConfigurationFeedBuilder).GetMethod(nameof(ModConfigurationFeedBuilder.OrderedPage)).MakeGenericMethod(key.ValueType()); - // items = (IEnumerable)genericEnumerablePage.Invoke(builder, [key]); - // } - // else { - // items = builder.Page(searchPhrase, includeInternal); - // } - items = builder.GeneratePage(path.ToArray(), searchPhrase, includeInternal); + if (!forceDefaultBuilder) { + try { + items = mod.BuildConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, includeInternal); + } + catch (Exception ex) { + Logger.ProcessException(ex, mod.ModAssembly!.Assembly); + Logger.ErrorInternal($"Exception was thrown while running {mod.Name}'s BuildConfigurationFeed method"); + items = builder.GeneratePage(path.Skip(3).ToArray(), searchPhrase, includeInternal); + items = items.Prepend(FeedBuilder.Label("BuildConfigurationFeedException", "Encountered error while building custom configuration feed!", color.Red)); + } + } + else { + items = builder.GeneratePage(path.Skip(3).ToArray(), searchPhrase, includeInternal); + } + foreach (DataFeedItem item in items) yield return item; } @@ -234,19 +251,24 @@ private static DataFeedItem AsFeedItem(this string text, int index, bool copyabl return FeedBuilder.Label(index.ToString(), text); } - public static IEnumerable GenerateModLogFeed(this ResoniteModBase mod, int last = -1, bool copyable = true) { - last = last < 0 ? int.MaxValue : last; + public static IEnumerable GenerateModLogFeed(this ResoniteModBase mod, int last = -1, bool copyable = true, string? filter = null) { List modLogs = mod.Logs().ToList(); - modLogs.Reverse(); - foreach (Logger.LogMessage line in modLogs.GetRange(0, Math.Min(modLogs.Count, last))) + last = last < 0 ? int.MaxValue : last; + last = Math.Min(modLogs.Count, last); + modLogs = modLogs.GetRange(modLogs.Count - last, last); + if (!string.IsNullOrEmpty(filter)) + modLogs = modLogs.Where((line) => line.Message.IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0).ToList(); + foreach (Logger.LogMessage line in modLogs) yield return line.ToString().AsFeedItem(line.Time.GetHashCode(), copyable); } - public static IEnumerable GenerateModExceptionFeed(this ResoniteModBase mod, int last = -1, bool copyable = true) { - last = last < 0 ? int.MaxValue : last; + public static IEnumerable GenerateModExceptionFeed(this ResoniteModBase mod, int last = -1, bool copyable = true, string? filter = null) { List modExceptions = mod.Exceptions().ToList(); - modExceptions.Reverse(); - foreach (Logger.LogException line in modExceptions.GetRange(0, Math.Min(modExceptions.Count, last))) + last = last < 0 ? int.MaxValue : last; + last = Math.Min(modExceptions.Count, last); + if (!string.IsNullOrEmpty(filter)) + modExceptions = modExceptions.Where((line) => line.Exception.ToString().IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0).ToList(); + foreach (Logger.LogException line in modExceptions) yield return line.ToString().AsFeedItem(line.Time.GetHashCode(), copyable); } diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs index 642fd38..cdaa583 100644 --- a/ResoniteModLoader/ModConfigurationFeedBuilder.cs +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -13,7 +13,7 @@ public class ModConfigurationFeedBuilder { private readonly Dictionary> KeyGrouping = new(); - private readonly Dictionary> KeyCategories = new(); + private readonly Dictionary> KeyCategories = new(new StringArrayEqualityComparer()); public readonly static Dictionary CachedBuilders = new(); @@ -36,7 +36,14 @@ private void AssertChildKey(ModConfigurationKey key) { throw new InvalidOperationException($"Mod key ({key}) is not owned by {Config.Owner.Name}'s config"); } - private static bool AStartsWithB(T[] A, T[] B) => string.Join("\t", A).StartsWith(string.Join("\t", B), StringComparison.InvariantCultureIgnoreCase); + private static bool IsFirstChild(string[] x, string[] y) { + Logger.DebugInternal($"Is [({x.Length})[{string.Join(", ", x)}]] a first child to [({y.Length})[{string.Join(", ", y)}]]?"); + if (x.Length != y.Length + 1) return false; + for (int i = 0; i < y.Length; i++) + if (x[i] != y[i]) return false; + Logger.DebugInternal("You are the father!"); + return true; + } public ModConfigurationFeedBuilder(ModConfiguration config) { Config = config; @@ -74,13 +81,23 @@ public ModConfigurationFeedBuilder(ModConfiguration config) { } } CachedBuilders[config] = this; + if (Logger.IsDebugEnabled()) { + Logger.DebugInternal("--- ModConfigurationFeedBuilder instantiated ---"); + Logger.DebugInternal($"Config owner: {config.Owner.Name}"); + Logger.DebugInternal($"Total keys: {config.ConfigurationItemDefinitions.Count}"); + Logger.DebugInternal($"AutoRegistered keys: {autoConfigKeys.Count()}, Grouped: {groupedKeys.Count}, Categorized: {categorizedKeys.Count}"); + Logger.DebugInternal($"Key groups ({KeyGrouping.Keys.Count}): [{string.Join(", ", KeyGrouping.Keys)}]"); + List categories = new(); + KeyCategories.Keys.Do((key) => categories.Add($"[{string.Join(", ", key)}]")); + Logger.DebugInternal($"Key categories ({KeyCategories.Keys.Count}): {string.Join(", ", categories)}"); + } } public IEnumerable GeneratePage(string[] path, string searchPhrase = "", bool includeInternal = false) { + Logger.DebugInternal($"KeyCategories[({path.Length})[{string.Join(", ", path)}]].Contains"); path = path ?? []; DataFeedGrid? subcategories = GenerateSubcategoryButtons(path); if (subcategories is not null) yield return subcategories; - Logger.DebugInternal($"KeyCategories[{string.Join(", ", path)}].Contains"); IEnumerable filteredItems = string.IsNullOrEmpty(searchPhrase) ? Config.ConfigurationItemDefinitions.Where(KeyCategories[path].Contains) : Config.ConfigurationItemDefinitions; if (KeyGrouping.Any()) { foreach (string group in KeyGrouping.Keys) { @@ -88,9 +105,9 @@ public IEnumerable GeneratePage(string[] path, string searchPhrase foreach (ModConfigurationKey key in filteredItems.Where(KeyGrouping[group].Contains)) { if (key.InternalAccessOnly && !includeInternal) continue; if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; - container.Subitem(GenerateDataFeedItem(key)); + container.AddSubitem(GenerateDataFeedItem(key)); } - if (container.SubItems is not null && container.SubItems.Any()) yield return container; + if (container.SubItems?.Any() ?? false) yield return container; } } else { @@ -114,26 +131,29 @@ public IEnumerable> OrderedItem(ModConfigurationKey key public DataFeedValueField GenerateDataFeedField(ModConfigurationKey key) { AssertChildKey(key); - if (typeof(T) == typeof(bool)) - return (DataFeedValueField)(object)FeedBuilder.Toggle(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); - else if (typeof(T).IsAssignableFrom(typeof(float)) && KeyFields.TryGetValue(key, out FieldInfo field) && TryGetRangeAttribute(field, out RangeAttribute range) && range.Min is T min && range.Max is T max) - return FeedBuilder.Slider(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key), min, max, range.TextFormat); + string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; + if (typeof(T).IsAssignableFrom(typeof(float)) && KeyFields.TryGetValue(key, out FieldInfo field) && TryGetRangeAttribute(field, out RangeAttribute range) && range.Min is T min && range.Max is T max) + return FeedBuilder.Slider(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key), min, max, range.TextFormat); else - return FeedBuilder.ValueField(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); + return FeedBuilder.ValueField(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); } public DataFeedEnum GenerateDataFeedEnum(ModConfigurationKey key) where T : Enum { AssertChildKey(key); - return FeedBuilder.Enum(key.Name, key.Description ?? key.Name, (field) => field.SyncWithModConfiguration(Config, key)); + string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; + return FeedBuilder.Enum(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); } public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { AssertChildKey(key); + string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; Type valueType = key.ValueType(); if (valueType == typeof(dummy)) - return FeedBuilder.Label(key.Name, key.Description ?? key.Name); + return FeedBuilder.Label(key.Name, label); + else if (valueType == typeof(bool)) + return FeedBuilder.Toggle(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); else if (valueType != typeof(string) && valueType != typeof(Uri) && typeof(IEnumerable).IsAssignableFrom(valueType)) - return FeedBuilder.Category(key.Name, key.Description ?? key.Name); + return FeedBuilder.Category(key.Name, label); else if (valueType.InheritsFrom(typeof(Enum))) return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedEnum)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); else @@ -142,11 +162,11 @@ public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { public DataFeedGrid? GenerateSubcategoryButtons(string[] currentPath) { if (!KeyCategories.Any()) return null; - IEnumerable subCategories = KeyCategories.Keys.Where((subPath) => subPath.Length == currentPath.Length + 1 && AStartsWithB(subPath, currentPath)); + IEnumerable subCategories = KeyCategories.Keys.Where((subPath) => subPath.Length == currentPath.Length + 1 && IsFirstChild(subPath, currentPath)); if (subCategories is null || !subCategories.Any()) return null; DataFeedGrid container = FeedBuilder.Grid("SubcategoryButtonsGrid", ""); foreach (string[] subCategory in subCategories) - container.Subitem(FeedBuilder.Category(subCategory.Last(), subCategory.Last() + " >")); + container.AddSubitem(FeedBuilder.Category(subCategory.Last(), subCategory.Last() + " >")); return container; } @@ -175,3 +195,21 @@ public static void ResetConfig(string configName) { } } + +internal class StringArrayEqualityComparer : EqualityComparer { + public override bool Equals(string[] x, string[] y) { + Logger.DebugInternal($"Comparing [({x.Length})[{string.Join(", ", x)}]] to [({y.Length})[{string.Join(", ", y)}]]"); + if (x.Length != y.Length) return false; + for (int i = 0; i < x.Length; i++) + if (x[i] != y[i]) return false; + Logger.DebugInternal("Values equal"); + return true; + } + + public override int GetHashCode(string[] obj) { + int hashCode = 699494; + foreach (string item in obj) + hashCode += item.GetHashCode() - (item.Length + 621) ^ 8; + return hashCode; + } +} diff --git a/ResoniteModLoader/ResoniteMod.cs b/ResoniteModLoader/ResoniteMod.cs index b55e3c7..70354f4 100644 --- a/ResoniteModLoader/ResoniteMod.cs +++ b/ResoniteModLoader/ResoniteMod.cs @@ -124,8 +124,8 @@ public virtual IncompatibleConfigurationHandlingOption HandleIncompatibleConfigu } /// - internal override async IAsyncEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { - foreach (DataFeedItem item in this.GenerateModConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal)) + internal override IEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { + foreach (DataFeedItem item in this.GenerateModConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal, true)) yield return item; } } diff --git a/ResoniteModLoader/ResoniteModBase.cs b/ResoniteModLoader/ResoniteModBase.cs index 8d30757..81f8b12 100644 --- a/ResoniteModLoader/ResoniteModBase.cs +++ b/ResoniteModLoader/ResoniteModBase.cs @@ -62,7 +62,9 @@ public bool TryGetConfiguration(out ModConfiguration configuration) { /// Passed-through from 's Enumerate call. /// Indicates whether the user has requested that internal configuration keys are included in the returned feed. /// DataFeedItem's to be directly returned by the calling . - internal abstract IAsyncEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false); + internal abstract IEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false); + + // Why would anyone need an async config? They depend on Microsoft.Bcl.AsyncInterfaces too internal bool FinishedLoading { get; set; } } diff --git a/ResoniteModLoader/Utility/FeedBuilder.cs b/ResoniteModLoader/Utility/FeedBuilder.cs index d400c71..a0c8b1f 100644 --- a/ResoniteModLoader/Utility/FeedBuilder.cs +++ b/ResoniteModLoader/Utility/FeedBuilder.cs @@ -536,19 +536,23 @@ public static DataFeedIndicator ChainInitSetupValue(this DataFeedIndicator return item; } - private static MethodInfo SubItemsSetter = AccessTools.PropertySetter(typeof(DataFeedItem), nameof(DataFeedItem.SubItems)); + private static PropertyInfo SubItemsSetter = typeof(DataFeedItem).GetProperty(nameof(DataFeedItem.SubItems)); - public static I Subitem(this I item, params DataFeedItem[] subitem) where I : DataFeedItem { + public static I AddSubitem(this I item, params DataFeedItem[] subitem) where I : DataFeedItem { if (item.SubItems is null) - SubItemsSetter.Invoke(item, [subitem]); + SubItemsSetter.SetValue(item, subitem.ToList().AsReadOnly(), null); else - SubItemsSetter.Invoke(item, [item.SubItems.Concat(subitem).ToArray()]); + SubItemsSetter.SetValue(item, item.SubItems.Concat(subitem).ToList().AsReadOnly(), null); return item; } - public static I ClearSubitems(this I item, params DataFeedItem[] subitem) where I : DataFeedItem { - if (item.SubItems is not null && item.SubItems.Any()) - SubItemsSetter.Invoke(item, [null]); + public static I ReplaceSubitems(this I item, params DataFeedItem[] subitem) where I : DataFeedItem { + SubItemsSetter.SetValue(item, subitem.ToList().AsReadOnly(), null); + return item; + } + + public static I ClearSubitems(this I item) where I : DataFeedItem { + SubItemsSetter.SetValue(item, null, null); return item; } #pragma warning restore CS8625, CA1715 From 82a88874e544d93c08b46e0ecfbe50adaac88b77 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 23 Jul 2024 02:51:32 -0500 Subject: [PATCH 12/20] Remove subcategories from ModConfigurationFeedBuilder --- .../AutoRegisterConfigKeyAttribute.cs | 17 +--- .../ModConfigurationFeedBuilder.cs | 85 ++++--------------- 2 files changed, 19 insertions(+), 83 deletions(-) diff --git a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs index 72b0653..e508268 100644 --- a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs +++ b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs @@ -5,22 +5,11 @@ namespace ResoniteModLoader; /// [AttributeUsage(AttributeTargets.Field)] public sealed class AutoRegisterConfigKeyAttribute : Attribute { - public readonly string Group; - - public readonly string[] Path; + public readonly string? Group; public AutoRegisterConfigKeyAttribute() { } - public AutoRegisterConfigKeyAttribute(string groupName) { - Group = groupName; - } - - public AutoRegisterConfigKeyAttribute(string[] subpath) { - Path = subpath; - } - - public AutoRegisterConfigKeyAttribute(string[] subpath, string groupName) { - Group = groupName; - Path = subpath; + public AutoRegisterConfigKeyAttribute(string group) { + Group = group; } } diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs index cdaa583..a368abd 100644 --- a/ResoniteModLoader/ModConfigurationFeedBuilder.cs +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -6,6 +6,7 @@ namespace ResoniteModLoader; public class ModConfigurationFeedBuilder { + public readonly static Dictionary CachedBuilders = new(); private readonly ModConfiguration Config; @@ -13,10 +14,6 @@ public class ModConfigurationFeedBuilder { private readonly Dictionary> KeyGrouping = new(); - private readonly Dictionary> KeyCategories = new(new StringArrayEqualityComparer()); - - public readonly static Dictionary CachedBuilders = new(); - private static bool HasAutoRegisterAttribute(FieldInfo field) => field.GetCustomAttribute() is not null; private static bool TryGetAutoRegisterAttribute(FieldInfo field, out AutoRegisterConfigKeyAttribute attribute) { @@ -36,24 +33,16 @@ private void AssertChildKey(ModConfigurationKey key) { throw new InvalidOperationException($"Mod key ({key}) is not owned by {Config.Owner.Name}'s config"); } - private static bool IsFirstChild(string[] x, string[] y) { - Logger.DebugInternal($"Is [({x.Length})[{string.Join(", ", x)}]] a first child to [({y.Length})[{string.Join(", ", y)}]]?"); - if (x.Length != y.Length + 1) return false; - for (int i = 0; i < y.Length; i++) - if (x[i] != y[i]) return false; - Logger.DebugInternal("You are the father!"); - return true; - } - public ModConfigurationFeedBuilder(ModConfiguration config) { Config = config; IEnumerable autoConfigKeys = config.Owner.GetType().GetDeclaredFields().Where(HasAutoRegisterAttribute); HashSet groupedKeys = new(); - HashSet categorizedKeys = new(); + foreach (FieldInfo field in autoConfigKeys) { ModConfigurationKey key = (ModConfigurationKey)field.GetValue(field.IsStatic ? null : config.Owner); if (key is null) continue; // dunno why this would happen KeyFields[key] = field; + AutoRegisterConfigKeyAttribute attribute = field.GetCustomAttribute(); if (attribute.Group is string groupName) { if (!KeyGrouping.ContainsKey(groupName)) @@ -61,48 +50,33 @@ public ModConfigurationFeedBuilder(ModConfiguration config) { KeyGrouping[groupName].Add(key); groupedKeys.Add(key); } - if (attribute.Path is string[] categoryPath) { - if (!KeyCategories.ContainsKey(categoryPath)) - KeyCategories[categoryPath] = new(); - KeyCategories[categoryPath].Add(key); - categorizedKeys.Add(key); - } } + foreach (ModConfigurationKey key in config.ConfigurationItemDefinitions) { if (groupedKeys.Any() && !groupedKeys.Contains(key)) { if (!KeyGrouping.ContainsKey("Uncategorized")) KeyGrouping["Uncategorized"] = new(); KeyGrouping["Uncategorized"].Add(key); } - if (!categorizedKeys.Contains(key)) { - if (!KeyCategories.ContainsKey([])) - KeyCategories[[]] = new(); - KeyCategories[[]].Add(key); - } } + CachedBuilders[config] = this; + if (Logger.IsDebugEnabled()) { Logger.DebugInternal("--- ModConfigurationFeedBuilder instantiated ---"); Logger.DebugInternal($"Config owner: {config.Owner.Name}"); Logger.DebugInternal($"Total keys: {config.ConfigurationItemDefinitions.Count}"); - Logger.DebugInternal($"AutoRegistered keys: {autoConfigKeys.Count()}, Grouped: {groupedKeys.Count}, Categorized: {categorizedKeys.Count}"); + Logger.DebugInternal($"AutoRegistered keys: {autoConfigKeys.Count()}, Grouped: {groupedKeys.Count}"); Logger.DebugInternal($"Key groups ({KeyGrouping.Keys.Count}): [{string.Join(", ", KeyGrouping.Keys)}]"); - List categories = new(); - KeyCategories.Keys.Do((key) => categories.Add($"[{string.Join(", ", key)}]")); - Logger.DebugInternal($"Key categories ({KeyCategories.Keys.Count}): {string.Join(", ", categories)}"); } } - public IEnumerable GeneratePage(string[] path, string searchPhrase = "", bool includeInternal = false) { - Logger.DebugInternal($"KeyCategories[({path.Length})[{string.Join(", ", path)}]].Contains"); - path = path ?? []; - DataFeedGrid? subcategories = GenerateSubcategoryButtons(path); - if (subcategories is not null) yield return subcategories; - IEnumerable filteredItems = string.IsNullOrEmpty(searchPhrase) ? Config.ConfigurationItemDefinitions.Where(KeyCategories[path].Contains) : Config.ConfigurationItemDefinitions; + public IEnumerable RootPage(string searchPhrase = "", bool includeInternal = false) { + if (KeyGrouping.Any()) { foreach (string group in KeyGrouping.Keys) { DataFeedGroup container = FeedBuilder.Group(group, group); - foreach (ModConfigurationKey key in filteredItems.Where(KeyGrouping[group].Contains)) { + foreach (ModConfigurationKey key in Config.ConfigurationItemDefinitions.Where(KeyGrouping[group].Contains)) { if (key.InternalAccessOnly && !includeInternal) continue; if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; container.AddSubitem(GenerateDataFeedItem(key)); @@ -111,16 +85,17 @@ public IEnumerable GeneratePage(string[] path, string searchPhrase } } else { - foreach (ModConfigurationKey key in filteredItems) { + foreach (ModConfigurationKey key in Config.ConfigurationItemDefinitions) { if (key.InternalAccessOnly && !includeInternal) continue; if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; yield return GenerateDataFeedItem(key); } } + yield return GenerateSaveControlButtons(); } - public IEnumerable> OrderedItem(ModConfigurationKey key) { + public IEnumerable> ListPage(ModConfigurationKey key) { AssertChildKey(key); if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; var value = (IEnumerable)Config.GetValue(key); @@ -160,16 +135,6 @@ public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedField)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); } - public DataFeedGrid? GenerateSubcategoryButtons(string[] currentPath) { - if (!KeyCategories.Any()) return null; - IEnumerable subCategories = KeyCategories.Keys.Where((subPath) => subPath.Length == currentPath.Length + 1 && IsFirstChild(subPath, currentPath)); - if (subCategories is null || !subCategories.Any()) return null; - DataFeedGrid container = FeedBuilder.Grid("SubcategoryButtonsGrid", ""); - foreach (string[] subCategory in subCategories) - container.AddSubitem(FeedBuilder.Category(subCategory.Last(), subCategory.Last() + " >")); - return container; - } - public DataFeedGrid GenerateSaveControlButtons() { string configName = Path.GetFileNameWithoutExtension(Config.Owner.ModAssembly!.File); DataFeedGrid container = FeedBuilder.Grid("SaveControlButtonsGrid", "", [ @@ -181,35 +146,17 @@ public DataFeedGrid GenerateSaveControlButtons() { } [SyncMethod(typeof(Action), [])] - public static void SaveConfig(string configName) { + private static void SaveConfig(string configName) { } [SyncMethod(typeof(Action), [])] - public static void DiscardConfig(string configName) { + private static void DiscardConfig(string configName) { } [SyncMethod(typeof(Action), [])] - public static void ResetConfig(string configName) { - - } -} - -internal class StringArrayEqualityComparer : EqualityComparer { - public override bool Equals(string[] x, string[] y) { - Logger.DebugInternal($"Comparing [({x.Length})[{string.Join(", ", x)}]] to [({y.Length})[{string.Join(", ", y)}]]"); - if (x.Length != y.Length) return false; - for (int i = 0; i < x.Length; i++) - if (x[i] != y[i]) return false; - Logger.DebugInternal("Values equal"); - return true; - } + private static void ResetConfig(string configName) { - public override int GetHashCode(string[] obj) { - int hashCode = 699494; - foreach (string item in obj) - hashCode += item.GetHashCode() - (item.Length + 621) ^ 8; - return hashCode; } } From 36cd4d229629437a52df3e4353d44f84c0af53b3 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 4 Aug 2024 19:16:23 -0500 Subject: [PATCH 13/20] Refactor logger for exception handling --- ResoniteModLoader/Logger.cs | 120 ++++++++++++++++++++++++------------ 1 file changed, 79 insertions(+), 41 deletions(-) diff --git a/ResoniteModLoader/Logger.cs b/ResoniteModLoader/Logger.cs index 9bc8113..f4222d9 100644 --- a/ResoniteModLoader/Logger.cs +++ b/ResoniteModLoader/Logger.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Diagnostics; using Elements.Core; @@ -5,14 +6,11 @@ namespace ResoniteModLoader; public sealed class Logger { - // logged for null objects - internal const string NULL_STRING = "null"; - - public enum LogType { TRACE, DEBUG, INFO, WARN, ERROR } + public enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR } public readonly struct LogMessage { - public LogMessage(DateTime time, ResoniteModBase? mod, LogType level, string message, StackTrace trace) { - Time = time; + internal LogMessage(ResoniteModBase? mod, LogLevel level, string message, StackTrace? trace = null) { + Time = DateTime.Now; Mod = mod; Level = level; Message = message; @@ -21,27 +19,47 @@ public LogMessage(DateTime time, ResoniteModBase? mod, LogType level, string mes public DateTime Time { get; } public ResoniteModBase? Mod { get; } - public LogType Level { get; } + public LogLevel Level { get; } public string Message { get; } - public StackTrace Trace { get; } + public StackTrace? Trace { get; } public override string ToString() => $"({Mod?.Name ?? "ResoniteModLoader"} @ {Time}) {LogTypeTag(Level)} {Message}"; } public readonly struct LogException { - public LogException(DateTime time, Assembly? assembly, Exception exception) { - Time = time; - Assembly = assembly; + internal LogException(Exception exception) { + Time = DateTime.Now; + Exception = exception; + } + + internal LogException(Exception exception, Assembly? assembly) { + Time = DateTime.Now; Exception = exception; + Source = (assembly, null); + } + + internal LogException(Exception exception, ResoniteModBase? mod) { + Time = DateTime.Now; + Exception = exception; + Source = (mod?.ModAssembly?.Assembly, mod); + } + + internal LogException(Exception exception, Assembly? assembly, ResoniteModBase? mod) { + Time = DateTime.Now; + Exception = exception; + Source = (assembly, mod); } public DateTime Time { get; } - public Assembly? Assembly { get; } public Exception Exception { get; } + public (Assembly? Assembly, ResoniteModBase? Mod)? Source { get; } - public override string ToString() => $"({Time}) [{Assembly?.FullName} ?? Unknown assembly] {Exception.Message}\n{Exception.StackTrace}"; + public override string ToString() => $"({Time}) [{Source?.Assembly?.FullName} ?? Unknown assembly] {Exception.Message}\n{Exception.StackTrace}"; } + // logged for null objects + internal const string NULL_STRING = "null"; + private static List _logBuffer = new(); public static IReadOnlyList Logs => _logBuffer.AsReadOnly(); @@ -56,80 +74,80 @@ internal static bool IsDebugEnabled() { internal static void TraceFuncInternal(Func messageProducer) { if (IsDebugEnabled()) { - LogInternal(LogType.TRACE, messageProducer(), null, true); + LogInternal(LogLevel.TRACE, messageProducer(), null, true); } } internal static void TraceFuncExternal(Func messageProducer) { if (IsDebugEnabled()) { - LogInternal(LogType.TRACE, messageProducer(), new(1), true); + LogInternal(LogLevel.TRACE, messageProducer(), new(1), true); } } internal static void TraceInternal(string message) { if (IsDebugEnabled()) { - LogInternal(LogType.TRACE, message, null, true); + LogInternal(LogLevel.TRACE, message, null, true); } } internal static void TraceExternal(object message) { if (IsDebugEnabled()) { - LogInternal(LogType.TRACE, message, new(1), true); + LogInternal(LogLevel.TRACE, message, new(1), true); } } internal static void TraceListExternal(object[] messages) { if (IsDebugEnabled()) { - LogListInternal(LogType.TRACE, messages, new(1), true); + LogListInternal(LogLevel.TRACE, messages, new(1), true); } } internal static void DebugFuncInternal(Func messageProducer) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, messageProducer()); + LogInternal(LogLevel.DEBUG, messageProducer()); } } internal static void DebugFuncExternal(Func messageProducer) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, messageProducer(), new(1)); + LogInternal(LogLevel.DEBUG, messageProducer(), new(1)); } } internal static void DebugInternal(string message) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, message); + LogInternal(LogLevel.DEBUG, message); } } internal static void DebugExternal(object message) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, message, new(1)); + LogInternal(LogLevel.DEBUG, message, new(1)); } } internal static void DebugListExternal(object[] messages) { if (IsDebugEnabled()) { - LogListInternal(LogType.DEBUG, messages, new(1)); + LogListInternal(LogLevel.DEBUG, messages, new(1)); } } - internal static void MsgInternal(string message) => LogInternal(LogType.INFO, message); - internal static void MsgExternal(object message) => LogInternal(LogType.INFO, message, new(1)); - internal static void MsgListExternal(object[] messages) => LogListInternal(LogType.INFO, messages, new(1)); - internal static void WarnInternal(string message) => LogInternal(LogType.WARN, message); - internal static void WarnExternal(object message) => LogInternal(LogType.WARN, message, new(1)); - internal static void WarnListExternal(object[] messages) => LogListInternal(LogType.WARN, messages, new(1)); - internal static void ErrorInternal(string message) => LogInternal(LogType.ERROR, message); - internal static void ErrorExternal(object message) => LogInternal(LogType.ERROR, message, new(1)); - internal static void ErrorListExternal(object[] messages) => LogListInternal(LogType.ERROR, messages, new(1)); + internal static void MsgInternal(string message) => LogInternal(LogLevel.INFO, message); + internal static void MsgExternal(object message) => LogInternal(LogLevel.INFO, message, new(1)); + internal static void MsgListExternal(object[] messages) => LogListInternal(LogLevel.INFO, messages, new(1)); + internal static void WarnInternal(string message) => LogInternal(LogLevel.WARN, message); + internal static void WarnExternal(object message) => LogInternal(LogLevel.WARN, message, new(1)); + internal static void WarnListExternal(object[] messages) => LogListInternal(LogLevel.WARN, messages, new(1)); + internal static void ErrorInternal(string message) => LogInternal(LogLevel.ERROR, message); + internal static void ErrorExternal(object message) => LogInternal(LogLevel.ERROR, message, new(1)); + internal static void ErrorListExternal(object[] messages) => LogListInternal(LogLevel.ERROR, messages, new(1)); - private static void LogInternal(LogType logType, object message, StackTrace? stackTrace = null, bool includeTrace = false) { + private static void LogInternal(LogLevel logType, object message, StackTrace? stackTrace = null, bool includeTrace = false) { message ??= NULL_STRING; stackTrace = stackTrace ?? new(1); ResoniteMod? source = Util.ExecutingMod(stackTrace); string logTypePrefix = LogTypeTag(logType); - _logBuffer.Add(new(DateTime.Now, source, logType, message.ToString(), stackTrace)); + _logBuffer.Add(new(source, logType, message.ToString(), stackTrace)); if (source == null) { UniLog.Log($"{logTypePrefix}[ResoniteModLoader] {message}", includeTrace); } @@ -138,7 +156,7 @@ private static void LogInternal(LogType logType, object message, StackTrace? sta } } - private static void LogListInternal(LogType logType, object[] messages, StackTrace? stackTrace, bool includeTrace = false) { + private static void LogListInternal(LogLevel logType, object[] messages, StackTrace? stackTrace, bool includeTrace = false) { if (messages == null) { LogInternal(logType, NULL_STRING, stackTrace, includeTrace); } @@ -149,15 +167,35 @@ private static void LogListInternal(LogType logType, object[] messages, StackTra } } - internal static void ProcessException(Exception exception, Assembly? assembly = null) { - _exceptionBuffer.Add(new(DateTime.Now, assembly, exception)); + internal static void ProcessException(Exception exception, Assembly? assembly = null, ResoniteModBase? mod = null) => _exceptionBuffer.Add(new(exception, assembly)); + + private static string LogTypeTag(LogLevel logType) => $"[{Enum.GetName(typeof(LogLevel), logType)}]"; + + internal static void RegisterExceptionHook() { + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionProcessor; + DebugInternal("Unhandled exception hook registered"); + } + + internal static void UnregisterExceptionHook() { + AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionProcessor; + DebugInternal("Unhandled exception hook unregistered"); } - private static string LogTypeTag(LogType logType) => $"[{Enum.GetName(typeof(LogType), logType)}]"; + private static void UnhandledExceptionProcessor(object sender, UnhandledExceptionEventArgs args) { + Exception exception = (Exception)args.ExceptionObject; + StackTrace trace = new StackTrace(exception); + ResoniteModBase? mod = Util.ExecutingMod(trace); + Assembly assembly = Assembly.GetAssembly(sender.GetType()); + // this should handle most uncaught cases in RML and mods + if (mod is not null || assembly == Assembly.GetExecutingAssembly()) { + if (IsDebugEnabled()) ErrorInternal($"Caught unhandled exception, {exception.Message}. Attributed to {mod?.Name ?? "No mod"} / {assembly.FullName}"); + ProcessException(exception, assembly, mod); + } + } } -public static class LoggerExtensions { - public static IEnumerable Logs(this ResoniteModBase mod) => Logger.Logs.Where((line) => line.Mod == mod); +internal static class LoggerExtensions { + internal static IEnumerable Logs(this ResoniteModBase mod) => Logger.Logs.Where((line) => line.Mod == mod); - public static IEnumerable Exceptions(this ResoniteModBase mod) => Logger.Exceptions.Where((line) => line.Assembly == mod.ModAssembly!.Assembly); + internal static IEnumerable Exceptions(this ResoniteModBase mod) => Logger.Exceptions.Where((line) => line.Source?.Mod == mod); } From b0f7149a4e872e3f116029ffb0c685b13b483335 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 4 Aug 2024 19:17:55 -0500 Subject: [PATCH 14/20] Implement initialization time recording, Display mod logs/exceptions --- ResoniteModLoader/DebugInfo.cs | 2 + ResoniteModLoader/ExecutionHook.cs | 7 +++ ResoniteModLoader/ModConfigurationDataFeed.cs | 47 +++++++++---------- ResoniteModLoader/ModLoader.cs | 8 ++++ ResoniteModLoader/ResoniteModBase.cs | 3 ++ 5 files changed, 43 insertions(+), 24 deletions(-) diff --git a/ResoniteModLoader/DebugInfo.cs b/ResoniteModLoader/DebugInfo.cs index db7ec8d..b86b90f 100644 --- a/ResoniteModLoader/DebugInfo.cs +++ b/ResoniteModLoader/DebugInfo.cs @@ -3,6 +3,8 @@ namespace ResoniteModLoader; internal static class DebugInfo { + internal static TimeSpan InitializationTime; + internal static void Log() { Logger.MsgInternal($"ResoniteModLoader v{ModLoader.VERSION} starting up!{(ModLoaderConfiguration.Get().Debug ? " Debug logs will be shown." : "")}"); Logger.DebugFuncInternal(() => $"Launched with args: {string.Join(" ", Environment.GetCommandLineArgs())}"); diff --git a/ResoniteModLoader/ExecutionHook.cs b/ResoniteModLoader/ExecutionHook.cs index 3757107..44cb137 100644 --- a/ResoniteModLoader/ExecutionHook.cs +++ b/ResoniteModLoader/ExecutionHook.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using FrooxEngine; namespace ResoniteModLoader; @@ -18,6 +19,8 @@ private static DummyConnector InstantiateConnector() { static ExecutionHook() { Logger.DebugInternal($"Start of ExecutionHook"); try { + Stopwatch timer = Stopwatch.StartNew(); + Logger.RegisterExceptionHook(); BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic; var byName = (Dictionary)typeof(GlobalTypeRegistry).GetField("_byName", flags).GetValue(null); @@ -39,8 +42,12 @@ static ExecutionHook() { LoadProgressIndicator.SetCustom("Initializing"); DebugInfo.Log(); HarmonyWorker.LoadModsAndHideModAssemblies(initialAssemblies); + timer.Stop(); LoadProgressIndicator.SetCustom("Loaded"); + Logger.MsgInternal($"Initialization & mod loading completed in {timer.ElapsedMilliseconds}ms."); + DebugInfo.InitializationTime = timer.Elapsed; } catch (Exception e) { + Logger.UnregisterExceptionHook(); // it's important that this doesn't send exceptions back to Resonite Logger.ErrorInternal($"Exception in execution hook!\n{e}"); } diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index 04a299b..1e841c8 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -39,7 +39,8 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path if (string.IsNullOrEmpty(searchPhrase)) { yield return FeedBuilder.Group("ResoniteModLoder", "RML", [ FeedBuilder.Label("ResoniteModLoder.Version", $"ResoniteModLoader version {ModLoader.VERSION}"), - FeedBuilder.StringIndicator("ResoniteModLoder.LoadedModCount", "Loaded mods:", ModLoader.Mods().Count()) + FeedBuilder.StringIndicator("ResoniteModLoder.LoadedModCount", "Loaded mods", ModLoader.Mods().Count()), + FeedBuilder.StringIndicator("ResoniteModLoder.InitializationTime", "Startup time", DebugInfo.InitializationTime.Milliseconds + "ms") ]); List modCategories = new(); @@ -155,7 +156,7 @@ public void UnregisterViewData(object data) { /// A unique key representing the mod. /// /// - public static string KeyFromMod(ResoniteModBase mod) => Path.GetFileNameWithoutExtension(mod.ModAssembly!.File); + internal static string KeyFromMod(ResoniteModBase mod) => Path.GetFileNameWithoutExtension(mod.ModAssembly!.File); /// /// Returns the mod that corresponds to a unique key. @@ -163,7 +164,7 @@ public void UnregisterViewData(object data) { /// A unique key from . /// The mod that corresponds with the unique key, or null if one couldn't be found. /// - public static ResoniteModBase? ModFromKey(string key) => ModLoader.Mods().First((mod) => KeyFromMod(mod) == key); + internal static ResoniteModBase? ModFromKey(string key) => ModLoader.Mods().First((mod) => KeyFromMod(mod) == key); /// /// Tries to get the mod that corresponds to a unique key. @@ -171,7 +172,7 @@ public void UnregisterViewData(object data) { /// A unique key from . /// Set if a matching mod is found. /// True if a matching mod is found, false otherwise. - public static bool TryModFromKey(string key, out ResoniteModBase mod) { + internal static bool TryModFromKey(string key, out ResoniteModBase mod) { mod = ModFromKey(key)!; return mod is not null; } @@ -184,7 +185,7 @@ internal static class ModConfigurationDataFeedExtensions { /// The target mod /// Set to true if this group will be displayed on its own page /// - public static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool standalone) { + internal static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool standalone) { DataFeedGroup modFeedGroup = new(); List groupChildren = new(); string key = ModConfigurationDataFeed.KeyFromMod(mod); @@ -194,9 +195,9 @@ public static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool groupChildren.Add(FeedBuilder.Indicator(key + ".Version", "Version", mod.Version)); if (standalone) { + groupChildren.Add(FeedBuilder.StringIndicator(key + ".InitializationTime", "Startup impact", mod.InitializationTime.Milliseconds + "ms")); groupChildren.Add(FeedBuilder.Indicator(key + ".AssemblyFile", "Assembly file", Path.GetFileName(mod.ModAssembly!.File))); groupChildren.Add(FeedBuilder.Indicator(key + ".AssemblyHash", "Assembly hash", mod.ModAssembly!.Sha256)); - // TODO: Add initialization time recording } if (Uri.TryCreate(mod.Link, UriKind.Absolute, out var uri)) groupChildren.Add(FeedBuilder.ValueAction(key + ".OpenLinkAction", $"Open mod link ({uri.Host})", (action) => action.Target = OpenURI, uri)); @@ -212,7 +213,7 @@ public static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool return FeedBuilder.Group(key + ".Group", standalone ? "Mod info" : mod.Name, groupChildren); } - public static IEnumerable GenerateModConfigurationFeed(this ResoniteModBase mod, IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false, bool forceDefaultBuilder = false) { + internal static IEnumerable GenerateModConfigurationFeed(this ResoniteModBase mod, IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false, bool forceDefaultBuilder = false) { if (path.FirstOrDefault() == "ResoniteModLoader") Logger.WarnInternal("Call to GenerateModConfigurationFeed may include full DataFeed path, if so expect broken behavior."); @@ -221,27 +222,29 @@ public static IEnumerable GenerateModConfigurationFeed(this Resoni yield break; } - ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out ModConfigurationFeedBuilder builder); - builder = builder ?? new ModConfigurationFeedBuilder(config); - IEnumerable items; + List items = new(); if (!forceDefaultBuilder) { try { - items = mod.BuildConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, includeInternal); + items = mod.BuildConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal).ToList(); } catch (Exception ex) { Logger.ProcessException(ex, mod.ModAssembly!.Assembly); Logger.ErrorInternal($"Exception was thrown while running {mod.Name}'s BuildConfigurationFeed method"); - items = builder.GeneratePage(path.Skip(3).ToArray(), searchPhrase, includeInternal); - items = items.Prepend(FeedBuilder.Label("BuildConfigurationFeedException", "Encountered error while building custom configuration feed!", color.Red)); } } - else { - items = builder.GeneratePage(path.Skip(3).ToArray(), searchPhrase, includeInternal); + + if (!items.Any()) { + ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); + builder = builder ?? new ModConfigurationFeedBuilder(config); + items = builder.RootPage(searchPhrase, includeInternal).ToList(); } - foreach (DataFeedItem item in items) + Logger.DebugInternal($"GenerateModConfigurationFeed output for {mod.Name} @ {string.Join("/", path)}"); + foreach (DataFeedItem item in items) { + Logger.DebugInternal($"\t{item.GetType().Name} : {item.ItemKey}"); yield return item; + } } private static DataFeedItem AsFeedItem(this string text, int index, bool copyable = true) { @@ -251,7 +254,7 @@ private static DataFeedItem AsFeedItem(this string text, int index, bool copyabl return FeedBuilder.Label(index.ToString(), text); } - public static IEnumerable GenerateModLogFeed(this ResoniteModBase mod, int last = -1, bool copyable = true, string? filter = null) { + internal static IEnumerable GenerateModLogFeed(this ResoniteModBase mod, int last = -1, bool copyable = true, string? filter = null) { List modLogs = mod.Logs().ToList(); last = last < 0 ? int.MaxValue : last; last = Math.Min(modLogs.Count, last); @@ -262,7 +265,7 @@ public static IEnumerable GenerateModLogFeed(this ResoniteModBase yield return line.ToString().AsFeedItem(line.Time.GetHashCode(), copyable); } - public static IEnumerable GenerateModExceptionFeed(this ResoniteModBase mod, int last = -1, bool copyable = true, string? filter = null) { + internal static IEnumerable GenerateModExceptionFeed(this ResoniteModBase mod, int last = -1, bool copyable = true, string? filter = null) { List modExceptions = mod.Exceptions().ToList(); last = last < 0 ? int.MaxValue : last; last = Math.Min(modExceptions.Count, last); @@ -272,19 +275,15 @@ public static IEnumerable GenerateModExceptionFeed(this ResoniteMo yield return line.ToString().AsFeedItem(line.Time.GetHashCode(), copyable); } - /// - /// Spawns the prompt for a user to open a hyperlink. - /// - /// The URI that the user will be prompted to open. [SyncMethod(typeof(Action), [])] - public static void OpenURI(Uri uri) { + private static void OpenURI(Uri uri) { Slot slot = Userspace.UserspaceWorld.AddSlot("Hyperlink"); slot.PositionInFrontOfUser(float3.Backward); slot.AttachComponent().Setup(uri, "Outgoing hyperlink"); } [SyncMethod(typeof(Action), [])] - public static void CopyText(string text) { + private static void CopyText(string text) { Userspace.UserspaceWorld.InputInterface.Clipboard.SetText(text); NotificationMessage.SpawnTextMessage("Copied line", colorX.White); } diff --git a/ResoniteModLoader/ModLoader.cs b/ResoniteModLoader/ModLoader.cs index 179dc38..2a70c07 100644 --- a/ResoniteModLoader/ModLoader.cs +++ b/ResoniteModLoader/ModLoader.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using HarmonyLib; namespace ResoniteModLoader; @@ -16,6 +17,7 @@ public sealed class ModLoader { internal static readonly Dictionary AssemblyLookupMap = new(); // used for logging private static readonly Dictionary ModNameLookupMap = new(); // used for duplicate mod checking + private static readonly Stopwatch InitTimer = new(); // used to measure mod hooking duration /// /// Returns true if ResoniteModLoader was loaded by a headless @@ -189,9 +191,15 @@ private static void HookMod(ResoniteMod mod) { LoadProgressIndicator.SetCustom($"Starting mod [{mod.Name}/{mod.Version}]"); Logger.DebugFuncInternal(() => $"calling OnEngineInit() for [{mod.Name}/{mod.Version}]"); try { + InitTimer.Start(); mod.OnEngineInit(); } catch (Exception e) { Logger.ErrorInternal($"Mod {mod.Name} from {mod.ModAssembly?.File ?? "Unknown Assembly"} threw error from OnEngineInit():\n{e}"); } + finally { + InitTimer.Stop(); + mod.InitializationTime = InitTimer.Elapsed; + InitTimer.Reset(); + } } } diff --git a/ResoniteModLoader/ResoniteModBase.cs b/ResoniteModLoader/ResoniteModBase.cs index 81f8b12..ca2c1fb 100644 --- a/ResoniteModLoader/ResoniteModBase.cs +++ b/ResoniteModLoader/ResoniteModBase.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using FrooxEngine; namespace ResoniteModLoader; @@ -26,6 +27,8 @@ public abstract class ResoniteModBase { /// public virtual string? Link { get; } + public TimeSpan InitializationTime { get; internal set; } + /// /// A reference to the AssemblyFile that this mod was loaded from. /// The reference is set once the mod is successfully loaded, and is null before that. From 390f682a0db153032496c250bffa40a3b019f5aa Mon Sep 17 00:00:00 2001 From: David Date: Sun, 4 Aug 2024 19:18:22 -0500 Subject: [PATCH 15/20] ModConfigurationValueSync finally works, praise the sun --- .../ModConfigurationFeedBuilder.cs | 20 +++-- .../ModConfigurationValueSync.cs | 79 +++++++++++++++++-- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs index a368abd..d82e9bc 100644 --- a/ResoniteModLoader/ModConfigurationFeedBuilder.cs +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -52,12 +52,13 @@ public ModConfigurationFeedBuilder(ModConfiguration config) { } } - foreach (ModConfigurationKey key in config.ConfigurationItemDefinitions) { - if (groupedKeys.Any() && !groupedKeys.Contains(key)) { - if (!KeyGrouping.ContainsKey("Uncategorized")) - KeyGrouping["Uncategorized"] = new(); - KeyGrouping["Uncategorized"].Add(key); - } + if (groupedKeys.Any()) { + if (!KeyGrouping.ContainsKey("Uncategorized")) + KeyGrouping["Uncategorized"] = new(); + + foreach (ModConfigurationKey key in config.ConfigurationItemDefinitions) + if (!groupedKeys.Contains(key)) + KeyGrouping["Uncategorized"].Add(key); } CachedBuilders[config] = this; @@ -160,3 +161,10 @@ private static void ResetConfig(string configName) { } } + +public static class ModConfigurationFeedBuilderExtensions { + public static ModConfigurationFeedBuilder ConfigurationFeedBuilder(this ModConfiguration config) { + ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); + return builder ?? new ModConfigurationFeedBuilder(config); + } +} diff --git a/ResoniteModLoader/ModConfigurationValueSync.cs b/ResoniteModLoader/ModConfigurationValueSync.cs index 63f74c3..0840a6c 100644 --- a/ResoniteModLoader/ModConfigurationValueSync.cs +++ b/ResoniteModLoader/ModConfigurationValueSync.cs @@ -13,28 +13,97 @@ public class ModConfigurationValueSync : Component { public readonly Sync ConfigurationKeyName; - public readonly Sync DefinitionFound; + public readonly RawOutput DefinitionFound; public readonly FieldDrive TargetField; #pragma warning restore CS8618, CA1051 - private ResoniteModBase _mappedMod; + private ResoniteModBase? _mappedMod; - private ModConfiguration _mappedConfig; + private ModConfiguration? _mappedConfig; - private ModConfigurationKey _mappedKey; + private ModConfigurationKey? _mappedKey; - public void LoadConfigKey(ModConfiguration config, ModConfigurationKey key) { + private bool _definitionFound; + + protected override void OnAwake() { + base.OnAwake(); + TargetField.SetupValueSetHook((IField field, T value) => { + if (_mappedKey is not null) { + if (_mappedKey.Validate(value)) { + TargetField.Target.Value = value; + _mappedConfig!.Set(_mappedKey, value); + } + } + }); + } + + protected override void OnChanges() { + base.OnChanges(); + Unregister(); + if (MapModConfigKey()) + Register(); + DefinitionFound.Value = _definitionFound; + } + + protected override void OnDispose() { + Unregister(); + base.OnDispose(); + } + + protected override void OnStart() { + base.OnStart(); + if (MapModConfigKey()) + Register(); + } + private bool MapModConfigKey() { + if (string.IsNullOrEmpty(DefiningModAssembly.Value) || string.IsNullOrEmpty(ConfigurationKeyName.Value)) + return false; + try { + _mappedMod = ModLoader.Mods().Single((mod) => Path.GetFileNameWithoutExtension(mod.ModAssembly?.File) == DefiningModAssembly.Value); + _mappedConfig = _mappedMod?.GetConfiguration(); + _mappedKey = _mappedConfig?.ConfigurationItemDefinitions.Single((key) => key.Name == ConfigurationKeyName.Value); + if (_mappedMod is null || _mappedConfig is null || _mappedKey is null) + return false; + return _mappedKey.ValueType() == typeof(T); + } + catch (Exception) { + return false; + } + } + + private void Register() { + ConfigValueChanged(_mappedConfig.GetValue(_mappedKey)); + _mappedKey!.OnChanged += ConfigValueChanged; + _definitionFound = true; + } + + private void Unregister() { + _mappedKey!.OnChanged -= ConfigValueChanged; + _mappedMod = null; + _mappedConfig = null; + _mappedKey = null; + _definitionFound = false; + } + + private void ConfigValueChanged(object? value) { + if (TargetField.IsLinkValid) + TargetField.Target.Value = (T)value ?? default; + } + + public void LoadConfigKey(ModConfiguration config, ModConfigurationKey key) { _mappedMod = config.Owner; _mappedConfig = config; _mappedKey = key; DefiningModAssembly.Value = Path.GetFileNameWithoutExtension(config.Owner.ModAssembly!.File); ConfigurationKeyName.Value = key.Name; + Register(); } } public static class ModConfigurationValueSyncExtensions { public static ModConfigurationValueSync SyncWithModConfiguration(this IField field, ModConfiguration config, ModConfigurationKey key) { + Logger.DebugInternal($"Syncing field with [{key}] from {config.Owner.Name}"); ModConfigurationValueSync driver = field.FindNearestParent().AttachComponent>(); driver.LoadConfigKey(config, key); driver.TargetField.Target = field; From bf6df6146a955062ca27ab78b8611cc609b04431 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 5 Aug 2024 21:30:34 -0500 Subject: [PATCH 16/20] Reapply "remove Unsafe/HideModTypes/HideLateTypes modloader config options" This reverts commit ec8e7d956f54d3ee217738f9b05c69f2c92ad06e. --- ResoniteModLoader/AssemblyHider.cs | 12 ++++++------ ResoniteModLoader/ModLoaderConfiguration.cs | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ResoniteModLoader/AssemblyHider.cs b/ResoniteModLoader/AssemblyHider.cs index 0df5595..6f4b33e 100644 --- a/ResoniteModLoader/AssemblyHider.cs +++ b/ResoniteModLoader/AssemblyHider.cs @@ -48,7 +48,7 @@ internal static class AssemblyHider { /// Our RML harmony instance /// Assemblies that were loaded when RML first started internal static void PatchResonite(Harmony harmony, HashSet initialAssemblies) { - if (ModLoaderConfiguration.Get().HideModTypes) { + //if (ModLoaderConfiguration.Get().HideModTypes) { // initialize the static assembly sets that our patches will need later resoniteAssemblies = GetResoniteAssemblies(initialAssemblies); modAssemblies = GetModAssemblies(resoniteAssemblies); @@ -68,7 +68,7 @@ internal static void PatchResonite(Harmony harmony, HashSet initialAss MethodInfo getAssembliesTarget = AccessTools.DeclaredMethod(typeof(AppDomain), nameof(AppDomain.GetAssemblies), Array.Empty()); MethodInfo getAssembliesPatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(GetAssembliesPostfix)); harmony.Patch(getAssembliesTarget, postfix: new HarmonyMethod(getAssembliesPatch)); - } + //} } private static HashSet GetResoniteAssemblies(HashSet initialAssemblies) { @@ -116,13 +116,13 @@ private static bool IsModAssembly(Assembly assembly, string typeOrAssembly, stri // this implies someone late-loaded an assembly after RML, and it was later used in-game // this is super weird, and probably shouldn't ever happen... but if it does, I want to know about it. // since this is an edge case users may want to handle in different ways, the HideLateTypes rml config option allows them to choose. - bool hideLate = ModLoaderConfiguration.Get().HideLateTypes; - if (log) { + //bool hideLate = true;// ModLoaderConfiguration.Get().HideLateTypes; + /*if (log) { Logger.WarnInternal($"The \"{name}\" {typeOrAssembly} does not appear to part of Resonite or a mod. It is unclear whether it should be hidden or not. Due to the HideLateTypes config option being {hideLate} it will be {(hideLate ? "Hidden" : "Shown")}"); - } + }*/ // if forceShowLate == true, then this function will always return `false` for late-loaded types // if forceShowLate == false, then this function will return `true` when hideLate == true - return hideLate && !forceShowLate; + return !forceShowLate; } } } diff --git a/ResoniteModLoader/ModLoaderConfiguration.cs b/ResoniteModLoader/ModLoaderConfiguration.cs index 9e51e02..4919323 100644 --- a/ResoniteModLoader/ModLoaderConfiguration.cs +++ b/ResoniteModLoader/ModLoaderConfiguration.cs @@ -12,14 +12,14 @@ internal static ModLoaderConfiguration Get() { _configuration = new ModLoaderConfiguration(); Dictionary> keyActions = new() { - { "unsafe", (value) => _configuration.Unsafe = bool.Parse(value) }, + //{ "unsafe", (value) => _configuration.Unsafe = bool.Parse(value) }, { "debug", (value) => _configuration.Debug = bool.Parse(value) }, { "hidevisuals", (value) => _configuration.HideVisuals = bool.Parse(value) }, { "nomods", (value) => _configuration.NoMods = bool.Parse(value) }, { "advertiseversion", (value) => _configuration.AdvertiseVersion = bool.Parse(value) }, { "logconflicts", (value) => _configuration.LogConflicts = bool.Parse(value) }, - { "hidemodtypes", (value) => _configuration.HideModTypes = bool.Parse(value) }, - { "hidelatetypes", (value) => _configuration.HideLateTypes = bool.Parse(value) }, + //{ "hidemodtypes", (value) => _configuration.HideModTypes = bool.Parse(value) }, + //{ "hidelatetypes", (value) => _configuration.HideLateTypes = bool.Parse(value) } { "nodashscreen", (value) => _configuration.NoDashScreen = bool.Parse(value) }, }; @@ -63,14 +63,14 @@ private static string GetAssemblyDirectory() { } #pragma warning disable CA1805 - public bool Unsafe { get; private set; } = false; + //public bool Unsafe { get; private set; } = false; public bool Debug { get; private set; } = false; public bool NoMods { get; private set; } = false; public bool HideVisuals { get; private set; } = false; public bool AdvertiseVersion { get; private set; } = false; public bool LogConflicts { get; private set; } = true; - public bool HideModTypes { get; private set; } = true; - public bool HideLateTypes { get; private set; } = true; + //public bool HideModTypes { get; private set; } = true; + //public bool HideLateTypes { get; private set; } = true; public bool NoDashScreen { get; private set; } = false; #pragma warning restore CA1805 } From 5261a4b197c454499b871e428af455573e76dabc Mon Sep 17 00:00:00 2001 From: David Date: Mon, 5 Aug 2024 21:53:37 -0500 Subject: [PATCH 17/20] Properly exclude RML's assembly from getting hidden so data model items can be added --- ResoniteModLoader/AssemblyHider.cs | 77 ++++++++++----------- ResoniteModLoader/ModLoaderConfiguration.cs | 6 -- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/ResoniteModLoader/AssemblyHider.cs b/ResoniteModLoader/AssemblyHider.cs index 6f4b33e..68352dd 100644 --- a/ResoniteModLoader/AssemblyHider.cs +++ b/ResoniteModLoader/AssemblyHider.cs @@ -48,27 +48,25 @@ internal static class AssemblyHider { /// Our RML harmony instance /// Assemblies that were loaded when RML first started internal static void PatchResonite(Harmony harmony, HashSet initialAssemblies) { - //if (ModLoaderConfiguration.Get().HideModTypes) { - // initialize the static assembly sets that our patches will need later - resoniteAssemblies = GetResoniteAssemblies(initialAssemblies); - modAssemblies = GetModAssemblies(resoniteAssemblies); - dotNetAssemblies = resoniteAssemblies.Where(LooksLikeDotNetAssembly).ToHashSet(); - - // TypeHelper.FindType explicitly does a type search - MethodInfo findTypeTarget = AccessTools.DeclaredMethod(typeof(TypeHelper), nameof(TypeHelper.FindType), new Type[] { typeof(string) }); - MethodInfo findTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(FindTypePostfix)); - harmony.Patch(findTypeTarget, postfix: new HarmonyMethod(findTypePatch)); - - // ReflectionExtensions.IsValidGenericType checks a type for validity, and if it returns `true` it reveals that the type exists - MethodInfo isValidGenericTypeTarget = AccessTools.DeclaredMethod(typeof(ReflectionExtensions), nameof(ReflectionExtensions.IsValidGenericType), new Type[] { typeof(Type), typeof(bool) }); - MethodInfo isValidGenericTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(IsValidTypePostfix)); - harmony.Patch(isValidGenericTypeTarget, postfix: new HarmonyMethod(isValidGenericTypePatch)); - - // FrooxEngine likes to enumerate all types in all assemblies, which is prone to issues (such as crashing FrooxCode if a type isn't loadable) - MethodInfo getAssembliesTarget = AccessTools.DeclaredMethod(typeof(AppDomain), nameof(AppDomain.GetAssemblies), Array.Empty()); - MethodInfo getAssembliesPatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(GetAssembliesPostfix)); - harmony.Patch(getAssembliesTarget, postfix: new HarmonyMethod(getAssembliesPatch)); - //} + // initialize the static assembly sets that our patches will need later + resoniteAssemblies = GetResoniteAssemblies(initialAssemblies); + modAssemblies = GetModAssemblies(resoniteAssemblies); + dotNetAssemblies = resoniteAssemblies.Where(LooksLikeDotNetAssembly).ToHashSet(); + + // TypeHelper.FindType explicitly does a type search + MethodInfo findTypeTarget = AccessTools.DeclaredMethod(typeof(TypeHelper), nameof(TypeHelper.FindType), new Type[] { typeof(string) }); + MethodInfo findTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(FindTypePostfix)); + harmony.Patch(findTypeTarget, postfix: new HarmonyMethod(findTypePatch)); + + // ReflectionExtensions.IsValidGenericType checks a type for validity, and if it returns `true` it reveals that the type exists + MethodInfo isValidGenericTypeTarget = AccessTools.DeclaredMethod(typeof(ReflectionExtensions), nameof(ReflectionExtensions.IsValidGenericType), new Type[] { typeof(Type), typeof(bool) }); + MethodInfo isValidGenericTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(IsValidTypePostfix)); + harmony.Patch(isValidGenericTypeTarget, postfix: new HarmonyMethod(isValidGenericTypePatch)); + + // FrooxEngine likes to enumerate all types in all assemblies, which is prone to issues (such as crashing FrooxCode if a type isn't loadable) + MethodInfo getAssembliesTarget = AccessTools.DeclaredMethod(typeof(AppDomain), nameof(AppDomain.GetAssemblies), Array.Empty()); + MethodInfo getAssembliesPatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(GetAssembliesPostfix)); + harmony.Patch(getAssembliesTarget, postfix: new HarmonyMethod(getAssembliesPatch)); } private static HashSet GetResoniteAssemblies(HashSet initialAssemblies) { @@ -88,6 +86,9 @@ private static HashSet GetModAssemblies(HashSet resoniteAsse // remove assemblies that we know to have come with Resonite assemblies.ExceptWith(resoniteAssemblies); + // remove ourselves because we technically aren't a mod + assemblies.Remove(Assembly.GetExecutingAssembly()); + // what's left are assemblies that magically appeared during the mod loading process. So mods and their dependencies. return assemblies; } @@ -104,26 +105,24 @@ private static HashSet GetModAssemblies(HashSet resoniteAsse private static bool IsModAssembly(Assembly assembly, string typeOrAssembly, string name, bool log, bool forceShowLate) { if (resoniteAssemblies!.Contains(assembly)) { return false; // The assembly belongs to Resonite and shouldn't be hidden + } else if (modAssemblies!.Contains(assembly)) { + // The assembly belongs to a mod and should be hidden + if (log) { + Logger.DebugFuncInternal(() => $"Hid {typeOrAssembly} \"{name}\" from Resonite"); + } + return true; + } else if (assembly == Assembly.GetExecutingAssembly()) { + // we don't want the data feed components getting hidden + return false; } else { - if (modAssemblies!.Contains(assembly)) { - // The assembly belongs to a mod and should be hidden - if (log) { - Logger.DebugFuncInternal(() => $"Hid {typeOrAssembly} \"{name}\" from Resonite"); - } - return true; - } else { - // an assembly was in neither resoniteAssemblies nor modAssemblies - // this implies someone late-loaded an assembly after RML, and it was later used in-game - // this is super weird, and probably shouldn't ever happen... but if it does, I want to know about it. - // since this is an edge case users may want to handle in different ways, the HideLateTypes rml config option allows them to choose. - //bool hideLate = true;// ModLoaderConfiguration.Get().HideLateTypes; - /*if (log) { - Logger.WarnInternal($"The \"{name}\" {typeOrAssembly} does not appear to part of Resonite or a mod. It is unclear whether it should be hidden or not. Due to the HideLateTypes config option being {hideLate} it will be {(hideLate ? "Hidden" : "Shown")}"); - }*/ - // if forceShowLate == true, then this function will always return `false` for late-loaded types - // if forceShowLate == false, then this function will return `true` when hideLate == true - return !forceShowLate; + // an assembly was in neither resoniteAssemblies nor modAssemblies + // this implies someone late-loaded an assembly after RML, and it was later used in-game + // this is super weird, and probably shouldn't ever happen... but if it does, I want to know about it. + if (log) { + Logger.WarnInternal($"The \"{name}\" {typeOrAssembly} does not appear to part of Resonite or a mod. It is unclear whether it should be hidden or not. forceShowLate is {forceShowLate}, so it will be {(forceShowLate ? "Shown" : "Hidden")}"); } + // if forceShowLate == true, then this function will always return `false` for late-loaded types + return !forceShowLate; } } diff --git a/ResoniteModLoader/ModLoaderConfiguration.cs b/ResoniteModLoader/ModLoaderConfiguration.cs index 4919323..a906e0a 100644 --- a/ResoniteModLoader/ModLoaderConfiguration.cs +++ b/ResoniteModLoader/ModLoaderConfiguration.cs @@ -12,14 +12,11 @@ internal static ModLoaderConfiguration Get() { _configuration = new ModLoaderConfiguration(); Dictionary> keyActions = new() { - //{ "unsafe", (value) => _configuration.Unsafe = bool.Parse(value) }, { "debug", (value) => _configuration.Debug = bool.Parse(value) }, { "hidevisuals", (value) => _configuration.HideVisuals = bool.Parse(value) }, { "nomods", (value) => _configuration.NoMods = bool.Parse(value) }, { "advertiseversion", (value) => _configuration.AdvertiseVersion = bool.Parse(value) }, { "logconflicts", (value) => _configuration.LogConflicts = bool.Parse(value) }, - //{ "hidemodtypes", (value) => _configuration.HideModTypes = bool.Parse(value) }, - //{ "hidelatetypes", (value) => _configuration.HideLateTypes = bool.Parse(value) } { "nodashscreen", (value) => _configuration.NoDashScreen = bool.Parse(value) }, }; @@ -63,14 +60,11 @@ private static string GetAssemblyDirectory() { } #pragma warning disable CA1805 - //public bool Unsafe { get; private set; } = false; public bool Debug { get; private set; } = false; public bool NoMods { get; private set; } = false; public bool HideVisuals { get; private set; } = false; public bool AdvertiseVersion { get; private set; } = false; public bool LogConflicts { get; private set; } = true; - //public bool HideModTypes { get; private set; } = true; - //public bool HideLateTypes { get; private set; } = true; public bool NoDashScreen { get; private set; } = false; #pragma warning restore CA1805 } From df7a3a9ccaf261cf8db864bfeb0570ce85f550f3 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 12 Aug 2024 21:32:54 -0500 Subject: [PATCH 18/20] Document all the things --- .../AutoRegisterConfigKeyAttribute.cs | 17 ++- ResoniteModLoader/DashScreenInjector.cs | 2 +- ResoniteModLoader/Logger.cs | 122 +++++++++++++++--- ResoniteModLoader/ModConfigurationDataFeed.cs | 36 +++--- .../ModConfigurationFeedBuilder.cs | 84 +++++++++++- .../ModConfigurationValueSync.cs | 52 +++++++- ResoniteModLoader/ResoniteMod.cs | 2 +- ResoniteModLoader/ResoniteModBase.cs | 14 +- ResoniteModLoader/Utility/FeedBuilder.cs | 10 +- 9 files changed, 280 insertions(+), 59 deletions(-) diff --git a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs index e508268..fd0e5ad 100644 --- a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs +++ b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs @@ -5,11 +5,24 @@ namespace ResoniteModLoader; /// [AttributeUsage(AttributeTargets.Field)] public sealed class AutoRegisterConfigKeyAttribute : Attribute { - public readonly string? Group; + /// + /// Defines a group that this configuration key belongs to, used by default configuration feed builder. + /// + public string? Group => _group; + + private readonly string? _group; + + /// + /// Flag this field to be automatically registered as a configuration key for this mod that is not grouped with any other keys. + /// public AutoRegisterConfigKeyAttribute() { } + /// + /// Flag this field to be automatically registered as a configuration key for this mod that is part of a group. + /// + /// The name of the group this configuration key belongs to. public AutoRegisterConfigKeyAttribute(string group) { - Group = group; + _group = group; } } diff --git a/ResoniteModLoader/DashScreenInjector.cs b/ResoniteModLoader/DashScreenInjector.cs index d8bce3e..abb19a2 100644 --- a/ResoniteModLoader/DashScreenInjector.cs +++ b/ResoniteModLoader/DashScreenInjector.cs @@ -35,7 +35,7 @@ internal static async void TryInjectScreen(UserspaceScreensManager __instance) { Logger.DebugInternal("Injecting dash screen"); RadiantDash dash = __instance.Slot.GetComponentInParents(); - InjectedScreen = dash.AttachScreen("Mods", RadiantUI_Constants.MidLight.ORANGE, OfficialAssets.Graphics.Icons.General.BoxClosed); // Replace with RML icon later + InjectedScreen = dash.AttachScreen("Mods", RadiantUI_Constants.Neutrals.LIGHT, OfficialAssets.Graphics.Icons.Dash.Tools); InjectedScreen.Slot.OrderOffset = 128; InjectedScreen.Slot.PersistentSelf = false; diff --git a/ResoniteModLoader/Logger.cs b/ResoniteModLoader/Logger.cs index f4222d9..d79ecd2 100644 --- a/ResoniteModLoader/Logger.cs +++ b/ResoniteModLoader/Logger.cs @@ -5,11 +5,23 @@ namespace ResoniteModLoader; +/// +/// General class that manages all RML-related log/exception processing. +/// Use the inherited methods from the class instead of UniLog or calling these methods directly! +/// Generally, you will only use this class to read RML/other mod logs, or directly pass exceptions to RML with . +/// public sealed class Logger { + + /// + /// Represents the severity level of a log message. + /// public enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR } - public readonly struct LogMessage { - internal LogMessage(ResoniteModBase? mod, LogLevel level, string message, StackTrace? trace = null) { + /// + /// Represents a single log entry. + /// + public readonly struct MessageItem { + internal MessageItem(ResoniteModBase? mod, LogLevel level, string message, StackTrace? trace = null) { Time = DateTime.Now; Mod = mod; Level = level; @@ -18,55 +30,92 @@ internal LogMessage(ResoniteModBase? mod, LogLevel level, string message, StackT } public DateTime Time { get; } + + /// + /// The mod that created this log entry, or RML if null. + /// public ResoniteModBase? Mod { get; } public LogLevel Level { get; } public string Message { get; } + + /// + /// A stack trace relating to the log entry, if recorded. + /// public StackTrace? Trace { get; } + /// public override string ToString() => $"({Mod?.Name ?? "ResoniteModLoader"} @ {Time}) {LogTypeTag(Level)} {Message}"; } - public readonly struct LogException { - internal LogException(Exception exception) { + /// + /// Represents an exception that was caught or passed for logging. + /// + public readonly struct ExceptionItem { + internal ExceptionItem(System.Exception exception) { Time = DateTime.Now; Exception = exception; } - internal LogException(Exception exception, Assembly? assembly) { + internal ExceptionItem(System.Exception exception, Assembly? assembly) { Time = DateTime.Now; Exception = exception; Source = (assembly, null); } - internal LogException(Exception exception, ResoniteModBase? mod) { + internal ExceptionItem(System.Exception exception, ResoniteModBase? mod) { Time = DateTime.Now; Exception = exception; Source = (mod?.ModAssembly?.Assembly, mod); } - internal LogException(Exception exception, Assembly? assembly, ResoniteModBase? mod) { + internal ExceptionItem(System.Exception exception, Assembly? assembly, ResoniteModBase? mod) { Time = DateTime.Now; Exception = exception; Source = (assembly, mod); } public DateTime Time { get; } - public Exception Exception { get; } + public System.Exception Exception { get; } + + /// + /// The (possible) source of the exception. Note the assembly and mod may be unrelated if both set! + /// public (Assembly? Assembly, ResoniteModBase? Mod)? Source { get; } + /// public override string ToString() => $"({Time}) [{Source?.Assembly?.FullName} ?? Unknown assembly] {Exception.Message}\n{Exception.StackTrace}"; } // logged for null objects internal const string NULL_STRING = "null"; - private static List _logBuffer = new(); + private static List _logBuffer = new(); + + /// + /// Stores all logs posted by mods and RML itself. + /// + public static IReadOnlyList Logs => _logBuffer.AsReadOnly(); + + private static List _exceptionBuffer = new(); - public static IReadOnlyList Logs => _logBuffer.AsReadOnly(); + /// + /// Stores all exceptions caught by RML or passed by mods for logging. + /// + public static IReadOnlyList Exceptions => _exceptionBuffer.AsReadOnly(); - private static List _exceptionBuffer = new(); + public delegate void MessageHandler(MessageItem message); - public static IReadOnlyList Exceptions => _exceptionBuffer.AsReadOnly(); + /// + /// Fired whenever a message is logged. + /// + public static event MessageHandler? OnMessagePosted; + + public delegate void ExceptionHandler(ExceptionItem exception); + + /// + /// Fired whenever an exception is caught by RML or passed by a mod. + /// + public static event ExceptionHandler? OnExceptionPosted; internal static bool IsDebugEnabled() { return ModLoaderConfiguration.Get().Debug; @@ -144,10 +193,12 @@ internal static void DebugListExternal(object[] messages) { private static void LogInternal(LogLevel logType, object message, StackTrace? stackTrace = null, bool includeTrace = false) { message ??= NULL_STRING; - stackTrace = stackTrace ?? new(1); + stackTrace ??= new(1); ResoniteMod? source = Util.ExecutingMod(stackTrace); string logTypePrefix = LogTypeTag(logType); - _logBuffer.Add(new(source, logType, message.ToString(), stackTrace)); + MessageItem item = new(source, logType, message.ToString(), stackTrace); + _logBuffer.Add(item); + OnMessagePosted?.SafeInvoke(item); if (source == null) { UniLog.Log($"{logTypePrefix}[ResoniteModLoader] {message}", includeTrace); } @@ -167,7 +218,25 @@ private static void LogListInternal(LogLevel logType, object[] messages, StackTr } } - internal static void ProcessException(Exception exception, Assembly? assembly = null, ResoniteModBase? mod = null) => _exceptionBuffer.Add(new(exception, assembly)); + /// + /// Use to pass a caught exception to RML for logging purposes. + /// Note that calling this will not automatically produce an error message, unless debug is enabled in RML's config. + /// + /// The exception to be recorded + /// The assembly responsible for causing the exception, if known + /// The mod where the exception occurred, if known + public static void ProcessException(System.Exception exception, Assembly? assembly = null, ResoniteModBase? mod = null) { + ExceptionItem item = new(exception, assembly, mod); + _exceptionBuffer.Add(item); + OnExceptionPosted?.SafeInvoke(item); + if (IsDebugEnabled()) { + string? attribution = null; + attribution ??= mod?.Name; + attribution ??= assembly?.FullName; + attribution ??= "unknown mod/assembly"; + LogInternal(LogLevel.ERROR, $"DEBUG EXCEPTION [{attribution}]: {exception.Message}", new StackTrace(exception), true); + } + } private static string LogTypeTag(LogLevel logType) => $"[{Enum.GetName(typeof(LogLevel), logType)}]"; @@ -182,7 +251,7 @@ internal static void UnregisterExceptionHook() { } private static void UnhandledExceptionProcessor(object sender, UnhandledExceptionEventArgs args) { - Exception exception = (Exception)args.ExceptionObject; + System.Exception exception = (System.Exception)args.ExceptionObject; StackTrace trace = new StackTrace(exception); ResoniteModBase? mod = Util.ExecutingMod(trace); Assembly assembly = Assembly.GetAssembly(sender.GetType()); @@ -194,8 +263,21 @@ private static void UnhandledExceptionProcessor(object sender, UnhandledExceptio } } -internal static class LoggerExtensions { - internal static IEnumerable Logs(this ResoniteModBase mod) => Logger.Logs.Where((line) => line.Mod == mod); - - internal static IEnumerable Exceptions(this ResoniteModBase mod) => Logger.Exceptions.Where((line) => line.Source?.Mod == mod); +/// +/// Extension methods to filter logs/exceptions from a single mod. +/// +public static class LoggerExtensions { + /// + /// Gets messages that were logged by this mod. + /// + /// The mod to filter messages from + /// Any messages logged by this mod. + public static IEnumerable Logs(this ResoniteModBase mod) => Logger.Logs.Where((line) => line.Mod == mod); + + /// + /// Gets exceptions that are related to this mod. + /// + /// The mod to filter exceptions on + /// Any exceptions related to this mod. + public static IEnumerable Exceptions(this ResoniteModBase mod) => Logger.Exceptions.Where((line) => line.Source?.Mod == mod); } diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index 1e841c8..6ce0859 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -5,9 +5,9 @@ namespace ResoniteModLoader; /// -/// A custom data feed that can be used to show information about loaded mods, and alter their configuration. Path must start with "ResoniteModLoder" +/// A custom data feed that can be used to show information about loaded mods, and alter their configuration. Path must start with "ResoniteModLoader" /// -[Category(["ResoniteModLoder"])] +[Category(["ResoniteModLoader"])] public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed, IWorldElement { #pragma warning disable CS1591 public override bool UserspaceOnly => true; @@ -37,10 +37,10 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path if (path[0] != "ResoniteModLoader") yield break; if (string.IsNullOrEmpty(searchPhrase)) { - yield return FeedBuilder.Group("ResoniteModLoder", "RML", [ - FeedBuilder.Label("ResoniteModLoder.Version", $"ResoniteModLoader version {ModLoader.VERSION}"), - FeedBuilder.StringIndicator("ResoniteModLoder.LoadedModCount", "Loaded mods", ModLoader.Mods().Count()), - FeedBuilder.StringIndicator("ResoniteModLoder.InitializationTime", "Startup time", DebugInfo.InitializationTime.Milliseconds + "ms") + yield return FeedBuilder.Group("ResoniteModLoader", "RML", [ + FeedBuilder.Label("ResoniteModLoader.Version", $"ResoniteModLoader version {ModLoader.VERSION}"), + FeedBuilder.StringIndicator("ResoniteModLoader.LoadedModCount", "Loaded mods", ModLoader.Mods().Count()), + FeedBuilder.StringIndicator("ResoniteModLoader.InitializationTime", "Startup time", DebugInfo.InitializationTime.Milliseconds + "ms") ]); List modCategories = new(); @@ -65,13 +65,13 @@ public async IAsyncEnumerable Enumerate(IReadOnlyList path string key = KeyFromMod(mod); yield return mod.GenerateModInfoGroup(true); - IEnumerable modLogs = mod.Logs(); + IEnumerable modLogs = mod.Logs(); if (modLogs.Any()) { IReadOnlyList latestLogs = mod.GenerateModLogFeed(5, false).Append(FeedBuilder.Category("Logs", $"View full log ({modLogs.Count()})")).ToList().AsReadOnly(); yield return FeedBuilder.Group(key + ".Logs", "Recent mod logs", latestLogs); } - IEnumerable modExceptions = mod.Exceptions(); + IEnumerable modExceptions = mod.Exceptions(); if (modExceptions.Any()) { IReadOnlyList latestException = mod.GenerateModExceptionFeed(1, false).Append(FeedBuilder.Category("Exceptions", $"View all exceptions ({modExceptions.Count()})")).ToList().AsReadOnly(); yield return FeedBuilder.Group(key + ".Exceptions", "Latest mod exception", latestException); @@ -171,7 +171,7 @@ public void UnregisterViewData(object data) { /// /// A unique key from . /// Set if a matching mod is found. - /// True if a matching mod is found, false otherwise. + /// true if a matching mod is found, false otherwise. internal static bool TryModFromKey(string key, out ResoniteModBase mod) { mod = ModFromKey(key)!; return mod is not null; @@ -183,8 +183,8 @@ internal static class ModConfigurationDataFeedExtensions { /// Generates a DataFeedGroup that displays basic information about a mod. /// /// The target mod - /// Set to true if this group will be displayed on its own page - /// + /// Set to true if this group will be displayed on its own page + /// A group containing indicators for the mod's info, as well as categories to view its config/logs/exceptions. internal static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool standalone) { DataFeedGroup modFeedGroup = new(); List groupChildren = new(); @@ -204,8 +204,8 @@ internal static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, boo if (mod.GetConfiguration() is not null) groupChildren.Add(FeedBuilder.Category(key + ".ConfigurationCategory", "Mod configuration", standalone ? ["Configuration"] : [key, "Configuration"])); if (!standalone) { - IEnumerable modLogs = mod.Logs(); - IEnumerable modExceptions = mod.Exceptions(); + IEnumerable modLogs = mod.Logs(); + IEnumerable modExceptions = mod.Exceptions(); if (modLogs.Any()) groupChildren.Add(FeedBuilder.Category(key + ".LogsCategory", $"Mod logs ({modLogs.Count()})", [key, "Logs"])); if (modExceptions.Any()) groupChildren.Add(FeedBuilder.Category(key + ".ExceptionsCategory", $"Mod exceptions ({modExceptions.Count()})", [key, "Exceptions"])); } @@ -236,7 +236,7 @@ internal static IEnumerable GenerateModConfigurationFeed(this Reso if (!items.Any()) { ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); - builder = builder ?? new ModConfigurationFeedBuilder(config); + builder ??= new ModConfigurationFeedBuilder(config); items = builder.RootPage(searchPhrase, includeInternal).ToList(); } @@ -255,23 +255,23 @@ private static DataFeedItem AsFeedItem(this string text, int index, bool copyabl } internal static IEnumerable GenerateModLogFeed(this ResoniteModBase mod, int last = -1, bool copyable = true, string? filter = null) { - List modLogs = mod.Logs().ToList(); + List modLogs = mod.Logs().ToList(); last = last < 0 ? int.MaxValue : last; last = Math.Min(modLogs.Count, last); modLogs = modLogs.GetRange(modLogs.Count - last, last); if (!string.IsNullOrEmpty(filter)) modLogs = modLogs.Where((line) => line.Message.IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0).ToList(); - foreach (Logger.LogMessage line in modLogs) + foreach (Logger.MessageItem line in modLogs) yield return line.ToString().AsFeedItem(line.Time.GetHashCode(), copyable); } internal static IEnumerable GenerateModExceptionFeed(this ResoniteModBase mod, int last = -1, bool copyable = true, string? filter = null) { - List modExceptions = mod.Exceptions().ToList(); + List modExceptions = mod.Exceptions().ToList(); last = last < 0 ? int.MaxValue : last; last = Math.Min(modExceptions.Count, last); if (!string.IsNullOrEmpty(filter)) modExceptions = modExceptions.Where((line) => line.Exception.ToString().IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0).ToList(); - foreach (Logger.LogException line in modExceptions) + foreach (Logger.ExceptionItem line in modExceptions) yield return line.ToString().AsFeedItem(line.Time.GetHashCode(), copyable); } diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs index d82e9bc..113a440 100644 --- a/ResoniteModLoader/ModConfigurationFeedBuilder.cs +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -5,7 +5,20 @@ namespace ResoniteModLoader; +/// +/// A utility class that aids in the creation of mod configuration feeds. +/// public class ModConfigurationFeedBuilder { + /// + /// A cache of , indexed by the they belong to. + /// New builders are automatically added to this cache upon instantiation, so you should try to get a cached builder before creating a new one. + /// + /// + /// + /// ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); + /// builder ??= new ModConfigurationFeedBuilder(config); + /// + /// public readonly static Dictionary CachedBuilders = new(); private readonly ModConfiguration Config; @@ -33,6 +46,16 @@ private void AssertChildKey(ModConfigurationKey key) { throw new InvalidOperationException($"Mod key ({key}) is not owned by {Config.Owner.Name}'s config"); } + private static void AssertMatchingType(ModConfigurationKey key) { + if (key.ValueType() != typeof(T)) + throw new InvalidOperationException($"Type of mod key ({key}) does not match field type {typeof(T)}"); + } + + /// + /// Instantiates and caches a new builder for a specific . + /// Check if a cached builder exists in before creating a new one! + /// + /// The mod configuration this builder will generate items for public ModConfigurationFeedBuilder(ModConfiguration config) { Config = config; IEnumerable autoConfigKeys = config.Owner.GetType().GetDeclaredFields().Where(HasAutoRegisterAttribute); @@ -72,15 +95,20 @@ public ModConfigurationFeedBuilder(ModConfiguration config) { } } + /// + /// Generates a root config page containing all defined config keys. + /// + /// If set, only show keys whose name or description contains this string + /// If true, also generate items for config keys marked as internal + /// Feed items for all defined config keys, plus buttons to save, discard, and reset the config. public IEnumerable RootPage(string searchPhrase = "", bool includeInternal = false) { - if (KeyGrouping.Any()) { foreach (string group in KeyGrouping.Keys) { DataFeedGroup container = FeedBuilder.Group(group, group); foreach (ModConfigurationKey key in Config.ConfigurationItemDefinitions.Where(KeyGrouping[group].Contains)) { if (key.InternalAccessOnly && !includeInternal) continue; if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; - container.AddSubitem(GenerateDataFeedItem(key)); + container.AddSubitems(GenerateDataFeedItem(key)); } if (container.SubItems?.Any() ?? false) yield return container; } @@ -96,30 +124,64 @@ public IEnumerable RootPage(string searchPhrase = "", bool include yield return GenerateSaveControlButtons(); } - public IEnumerable> ListPage(ModConfigurationKey key) { + /// + /// (NOT YET IMPLEMENTED) Generates a subpage for an indexed/enumerable config key. + /// ie. arrays, lists, dictionaries, sets. + /// + /// A key with an enumerable type + /// If true, items may only be reordered, not added/removed. + /// A ordered feed item for each element in the key's value, plus a group of buttons to add/remove items if set. + private IEnumerable> EnumerablePage(ModConfigurationKey key, bool reorderOnly = false) { AssertChildKey(key); if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; var value = (IEnumerable)Config.GetValue(key); int i = 0; foreach (object item in value) yield return FeedBuilder.OrderedItem(key.Name + i, key.Name, item.ToString(), i++); + if (reorderOnly) yield break; + // Group that contains input field plus buttons to prepend/append, and remove first/last item } + // these generate methods need to be cleaned up and more strongly typed + // todo: Make all these methods use generic keys + + /// + /// Generates a slider for the defining key if it is a float has a range attribute, otherwise generates a generic value field. + /// + /// The value type of the supplied key + /// The key to generate the item from + /// A DataFeedSlider if possible, otherwise a DataFeedValueField. + /// public DataFeedValueField GenerateDataFeedField(ModConfigurationKey key) { AssertChildKey(key); + AssertMatchingType(key); string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; if (typeof(T).IsAssignableFrom(typeof(float)) && KeyFields.TryGetValue(key, out FieldInfo field) && TryGetRangeAttribute(field, out RangeAttribute range) && range.Min is T min && range.Max is T max) return FeedBuilder.Slider(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key), min, max, range.TextFormat); + // If range attribute wasn't limited to floats, we could also make ClampedValueField's else return FeedBuilder.ValueField(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); } - public DataFeedEnum GenerateDataFeedEnum(ModConfigurationKey key) where T : Enum { + /// + /// Generates an enum field for a specific configuration key. + /// + /// The enum type of the supplied key + /// The key to generate the item from + /// A physical mango if it is opposite day. + /// + public DataFeedEnum GenerateDataFeedEnum(ModConfigurationKey key) where E : Enum { AssertChildKey(key); + AssertMatchingType(key); string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; - return FeedBuilder.Enum(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); + return FeedBuilder.Enum(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); } + /// + /// Generates the appropriate DataFeedItem for any config key type. + /// + /// The key to generate the item from + /// Automatically picks the best item type for the config key type. public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { AssertChildKey(key); string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; @@ -136,6 +198,10 @@ public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedField)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); } + /// + /// Generates buttons to save/discard changes, or reset all config keys to their defaults. + /// + /// A group with the aforementioned options. public DataFeedGrid GenerateSaveControlButtons() { string configName = Path.GetFileNameWithoutExtension(Config.Owner.ModAssembly!.File); DataFeedGrid container = FeedBuilder.Grid("SaveControlButtonsGrid", "", [ @@ -162,7 +228,15 @@ private static void ResetConfig(string configName) { } } +/// +/// Extentions that work with 's +/// public static class ModConfigurationFeedBuilderExtensions { + /// + /// Returns a cached , or creates a new one. + /// + /// The the builder belongs to + /// A cached or new builder. public static ModConfigurationFeedBuilder ConfigurationFeedBuilder(this ModConfiguration config) { ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); return builder ?? new ModConfigurationFeedBuilder(config); diff --git a/ResoniteModLoader/ModConfigurationValueSync.cs b/ResoniteModLoader/ModConfigurationValueSync.cs index 0840a6c..bb642c4 100644 --- a/ResoniteModLoader/ModConfigurationValueSync.cs +++ b/ResoniteModLoader/ModConfigurationValueSync.cs @@ -3,7 +3,11 @@ namespace ResoniteModLoader; -[Category(["ResoniteModLoder"])] +/// +/// Bi-directionally syncs a field with a specific mod configuration key. +/// +/// The mod configuration key type +[Category(["ResoniteModLoader"])] public class ModConfigurationValueSync : Component { #pragma warning disable CS1591 public override bool UserspaceOnly => true; @@ -24,7 +28,7 @@ public class ModConfigurationValueSync : Component { private ModConfigurationKey? _mappedKey; private bool _definitionFound; - +#pragma warning disable CS1591 protected override void OnAwake() { base.OnAwake(); TargetField.SetupValueSetHook((IField field, T value) => { @@ -55,7 +59,11 @@ protected override void OnStart() { if (MapModConfigKey()) Register(); } - +#pragma warning restore CS1591 + /// + /// Attempts to match the supplied and fields to a mod config and key + /// + /// Success private bool MapModConfigKey() { if (string.IsNullOrEmpty(DefiningModAssembly.Value) || string.IsNullOrEmpty(ConfigurationKeyName.Value)) return false; @@ -72,14 +80,21 @@ private bool MapModConfigKey() { } } + /// + /// Call AFTER mapping has been confirmed to begin syncing the target field + /// private void Register() { ConfigValueChanged(_mappedConfig.GetValue(_mappedKey)); _mappedKey!.OnChanged += ConfigValueChanged; _definitionFound = true; } + /// + /// Stop syncing, call whenever any field has changed to make sure the rug isn't pulled out from under us. + /// private void Unregister() { - _mappedKey!.OnChanged -= ConfigValueChanged; + if (_mappedKey is not null) + _mappedKey.OnChanged -= ConfigValueChanged; _mappedMod = null; _mappedConfig = null; _mappedKey = null; @@ -91,7 +106,15 @@ private void ConfigValueChanged(object? value) { TargetField.Target.Value = (T)value ?? default; } - public void LoadConfigKey(ModConfiguration config, ModConfigurationKey key) { + /// + /// Sets the and fields to match the supplied config and key. + /// + /// The configuration the key belongs to + /// Any key with a matching type + public void LoadConfigKey(ModConfiguration config, ModConfigurationKey key) { + if (!config.IsKeyDefined(key)) + throw new InvalidOperationException($"Mod key ({key}) is not owned by {config.Owner.Name}'s config"); + _mappedMod = config.Owner; _mappedConfig = config; _mappedKey = key; @@ -101,11 +124,28 @@ public void LoadConfigKey(ModConfiguration config, ModConfigurationKey key) { } } +/// +/// Utilities methods that attaches to stuff. +/// public static class ModConfigurationValueSyncExtensions { + /// + /// Syncs a target IField with a mod configuration key. + /// + /// The field and key type + /// The field to bi-directionally sync + /// The configuration the key belongs to + /// Any key with a matching type + /// A new component that was attached to the same slot as the field. + /// Thrown if key doesn't belong to config, or is of wrong type public static ModConfigurationValueSync SyncWithModConfiguration(this IField field, ModConfiguration config, ModConfigurationKey key) { + if (!config.IsKeyDefined(key)) + throw new InvalidOperationException($"Mod key ({key}) is not owned by {config.Owner.Name}'s config"); + if (key.ValueType() != typeof(T)) + throw new InvalidOperationException($"Type of mod key ({key}) does not match field type {typeof(T)}"); + Logger.DebugInternal($"Syncing field with [{key}] from {config.Owner.Name}"); ModConfigurationValueSync driver = field.FindNearestParent().AttachComponent>(); - driver.LoadConfigKey(config, key); + driver.LoadConfigKey(config, key as ModConfigurationKey); driver.TargetField.Target = field; return driver; diff --git a/ResoniteModLoader/ResoniteMod.cs b/ResoniteModLoader/ResoniteMod.cs index 70354f4..b3e21b5 100644 --- a/ResoniteModLoader/ResoniteMod.cs +++ b/ResoniteModLoader/ResoniteMod.cs @@ -124,7 +124,7 @@ public virtual IncompatibleConfigurationHandlingOption HandleIncompatibleConfigu } /// - internal override IEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { + protected internal override IEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { foreach (DataFeedItem item in this.GenerateModConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal, true)) yield return item; } diff --git a/ResoniteModLoader/ResoniteModBase.cs b/ResoniteModLoader/ResoniteModBase.cs index ca2c1fb..3b2da9b 100644 --- a/ResoniteModLoader/ResoniteModBase.cs +++ b/ResoniteModLoader/ResoniteModBase.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Runtime.Remoting.Messaging; using FrooxEngine; namespace ResoniteModLoader; @@ -51,11 +52,22 @@ public abstract class ResoniteModBase { return ModConfiguration; } + /// + /// Returns whether or not this mod has a configuration, and set an out param if it does. + /// + /// The variable that is set to this mods configuration if it has one + /// true if the out param was set, false if the mod has no configuration. public bool TryGetConfiguration(out ModConfiguration configuration) { configuration = ModConfiguration!; return configuration is not null; } + /// + /// Checks if this mod has defined a configuration. + /// + /// true if there is a config, false if there is not. + public bool HasConfiguration() => ModConfiguration is not null; + /// /// Define a custom configuration DataFeed for this mod. /// @@ -65,7 +77,7 @@ public bool TryGetConfiguration(out ModConfiguration configuration) { /// Passed-through from 's Enumerate call. /// Indicates whether the user has requested that internal configuration keys are included in the returned feed. /// DataFeedItem's to be directly returned by the calling . - internal abstract IEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false); + protected internal abstract IEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false); // Why would anyone need an async config? They depend on Microsoft.Bcl.AsyncInterfaces too diff --git a/ResoniteModLoader/Utility/FeedBuilder.cs b/ResoniteModLoader/Utility/FeedBuilder.cs index a0c8b1f..8e10f2f 100644 --- a/ResoniteModLoader/Utility/FeedBuilder.cs +++ b/ResoniteModLoader/Utility/FeedBuilder.cs @@ -538,16 +538,16 @@ public static DataFeedIndicator ChainInitSetupValue(this DataFeedIndicator private static PropertyInfo SubItemsSetter = typeof(DataFeedItem).GetProperty(nameof(DataFeedItem.SubItems)); - public static I AddSubitem(this I item, params DataFeedItem[] subitem) where I : DataFeedItem { + public static I AddSubitems(this I item, params DataFeedItem[] subitems) where I : DataFeedItem { if (item.SubItems is null) - SubItemsSetter.SetValue(item, subitem.ToList().AsReadOnly(), null); + SubItemsSetter.SetValue(item, subitems.ToList().AsReadOnly(), null); else - SubItemsSetter.SetValue(item, item.SubItems.Concat(subitem).ToList().AsReadOnly(), null); + SubItemsSetter.SetValue(item, item.SubItems.Concat(subitems).ToList().AsReadOnly(), null); return item; } - public static I ReplaceSubitems(this I item, params DataFeedItem[] subitem) where I : DataFeedItem { - SubItemsSetter.SetValue(item, subitem.ToList().AsReadOnly(), null); + public static I ReplaceSubitems(this I item, params DataFeedItem[] subitems) where I : DataFeedItem { + SubItemsSetter.SetValue(item, subitems.ToList().AsReadOnly(), null); return item; } From 36ef92e652481f3276c1843586ae07f47567a224 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Aug 2024 02:06:43 -0500 Subject: [PATCH 19/20] ConfigurationFeedBuilder improvements --- ResoniteModLoader/ModConfiguration.cs | 5 ++ .../ModConfigurationFeedBuilder.cs | 75 ++++++++++++++++--- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/ResoniteModLoader/ModConfiguration.cs b/ResoniteModLoader/ModConfiguration.cs index 3f8442a..8ae4066 100644 --- a/ResoniteModLoader/ModConfiguration.cs +++ b/ResoniteModLoader/ModConfiguration.cs @@ -134,6 +134,9 @@ public class ModConfiguration : IModConfigurationDefinition { // The naughty list is global, while the actual debouncing is per-configuration. private static HashSet naughtySavers = new HashSet(); + // maps configs by their filename sans extensions + internal static Dictionary configNameMap = new Dictionary(); + // used to keep track of the debouncers for this configuration. private Dictionary> saveActionForCallee = new(); @@ -160,6 +163,8 @@ private static JsonSerializer CreateJsonSerializer() { private ModConfiguration(ModConfigurationDefinition definition) { Definition = definition; + configNameMap[Path.GetFileNameWithoutExtension(Owner.ModAssembly!.File)] = this; + // thank goodness ModAssembly is set literally right before this is created } internal static void EnsureDirectoryExists() { diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs index 113a440..a45b2a2 100644 --- a/ResoniteModLoader/ModConfigurationFeedBuilder.cs +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -51,6 +51,20 @@ private static void AssertMatchingType(ModConfigurationKey key) { throw new InvalidOperationException($"Type of mod key ({key}) does not match field type {typeof(T)}"); } + private string GetKeyLabel(ModConfigurationKey key) + => (key.InternalAccessOnly ? "[INTERNAL] " : "") + + (PreferDescriptionLabels ? (key.Description ?? key.Name) : key.Name); + + private string GetKeyDescription(ModConfigurationKey key) + => PreferDescriptionLabels ? $"Key name: {key.Name}" : (key.Description ?? "(No description)"); + + /// + /// If true, configuration key descriptions will be used as the DataFeedItem's label if they exist. + /// If false, the configuration key name will be used as the label. + /// In both cases, the description will be the opposite field of the label. + /// + public bool PreferDescriptionLabels { get; set; } = true; + /// /// Instantiates and caches a new builder for a specific . /// Check if a cached builder exists in before creating a new one! @@ -155,12 +169,13 @@ private IEnumerable> EnumerablePage(ModConfigurationKey public DataFeedValueField GenerateDataFeedField(ModConfigurationKey key) { AssertChildKey(key); AssertMatchingType(key); - string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; + string label = GetKeyLabel(key); + string description = GetKeyDescription(key); if (typeof(T).IsAssignableFrom(typeof(float)) && KeyFields.TryGetValue(key, out FieldInfo field) && TryGetRangeAttribute(field, out RangeAttribute range) && range.Min is T min && range.Max is T max) - return FeedBuilder.Slider(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key), min, max, range.TextFormat); + return FeedBuilder.Slider(key.Name, label, description, (field) => field.SyncWithModConfiguration(Config, key), min, max, range.TextFormat); // If range attribute wasn't limited to floats, we could also make ClampedValueField's else - return FeedBuilder.ValueField(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); + return FeedBuilder.ValueField(key.Name, label, description, (field) => field.SyncWithModConfiguration(Config, key)); } /// @@ -173,8 +188,9 @@ public DataFeedValueField GenerateDataFeedField(ModConfigurationKey key) { public DataFeedEnum GenerateDataFeedEnum(ModConfigurationKey key) where E : Enum { AssertChildKey(key); AssertMatchingType(key); - string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; - return FeedBuilder.Enum(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); + string label = GetKeyLabel(key); + string description = GetKeyDescription(key); + return FeedBuilder.Enum(key.Name, label, description, (field) => field.SyncWithModConfiguration(Config, key)); } /// @@ -184,14 +200,15 @@ public DataFeedEnum GenerateDataFeedEnum(ModConfigurationKey key) where E /// Automatically picks the best item type for the config key type. public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { AssertChildKey(key); - string label = (key.InternalAccessOnly ? "[INTERNAL] " : "") + key.Description ?? key.Name; + string label = GetKeyLabel(key); + string description = GetKeyDescription(key); Type valueType = key.ValueType(); if (valueType == typeof(dummy)) - return FeedBuilder.Label(key.Name, label); + return FeedBuilder.Label(key.Name, label, description); else if (valueType == typeof(bool)) - return FeedBuilder.Toggle(key.Name, label, (field) => field.SyncWithModConfiguration(Config, key)); + return FeedBuilder.Toggle(key.Name, label, description, (field) => field.SyncWithModConfiguration(Config, key)); else if (valueType != typeof(string) && valueType != typeof(Uri) && typeof(IEnumerable).IsAssignableFrom(valueType)) - return FeedBuilder.Category(key.Name, label); + return FeedBuilder.Category(key.Name, label, description); else if (valueType.InheritsFrom(typeof(Enum))) return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedEnum)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); else @@ -214,17 +231,51 @@ public DataFeedGrid GenerateSaveControlButtons() { [SyncMethod(typeof(Action), [])] private static void SaveConfig(string configName) { - + if (ModConfiguration.configNameMap.TryGetValue(configName, out var config)) { + config.SaveQueue(false, true); + NotificationMessage.SpawnTextMessage("Saved successfully", colorX.White); + } else + NotificationMessage.SpawnTextMessage("Failed to save!", colorX.Red); } [SyncMethod(typeof(Action), [])] private static void DiscardConfig(string configName) { - + Userspace.OpenContextMenu( + Userspace.UserspaceWorld.GetGloballyRegisteredComponent().Slot, + new ContextMenuOptions { disableFlick = true }, + async (menu) => { + menu.AddItem( + "Really discard changes", + OfficialAssets.Graphics.Icons.Inspector.DestroyPreservingAssets, + colorX.Red + ).Button.LocalPressed += (_, _) => { + NotificationMessage.SpawnTextMessage("Not implemented", colorX.Yellow); + menu.Close(); + }; + menu.AddItem("Cancel", (Uri)null!, colorX.White) + .Button.LocalPressed += (_, _) => menu.Close(); + } + ); } [SyncMethod(typeof(Action), [])] private static void ResetConfig(string configName) { - + Userspace.OpenContextMenu( + Userspace.UserspaceWorld.GetGloballyRegisteredComponent().Slot, + new ContextMenuOptions { disableFlick = true }, + async (menu) => { + menu.AddItem( + "Really reset configuration", + OfficialAssets.Graphics.Icons.Inspector.Destroy, + colorX.Red + ).Button.LocalPressed += (_, _) => { + NotificationMessage.SpawnTextMessage("Not implemented", colorX.Yellow); + menu.Close(); + }; + menu.AddItem("Cancel", (Uri)null!, colorX.White) + .Button.LocalPressed += (_, _) => menu.Close(); + } + ); } } From ee4b0ce0b9028b8bb2b8ac147818dcafb5c035b1 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 19 Nov 2024 18:22:16 -0600 Subject: [PATCH 20/20] I havent touched this repo in months but I left git dirty so here's what I forgot to commit --- ResoniteModLoader/Logger.cs | 4 ++-- ResoniteModLoader/ModConfigurationDataFeed.cs | 4 +++- ResoniteModLoader/ModConfigurationFeedBuilder.cs | 8 ++++++++ ResoniteModLoader/ResoniteModLoader.csproj | 4 ++++ .../Resources/ConfigurationItemMapper.brson | 0 doc/datafeed.md | 9 +++++++++ 6 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 ResoniteModLoader/Resources/ConfigurationItemMapper.brson create mode 100644 doc/datafeed.md diff --git a/ResoniteModLoader/Logger.cs b/ResoniteModLoader/Logger.cs index d79ecd2..c3d5ab0 100644 --- a/ResoniteModLoader/Logger.cs +++ b/ResoniteModLoader/Logger.cs @@ -20,7 +20,7 @@ public enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR } /// /// Represents a single log entry. /// - public readonly struct MessageItem { + public class MessageItem { internal MessageItem(ResoniteModBase? mod, LogLevel level, string message, StackTrace? trace = null) { Time = DateTime.Now; Mod = mod; @@ -50,7 +50,7 @@ internal MessageItem(ResoniteModBase? mod, LogLevel level, string message, Stack /// /// Represents an exception that was caught or passed for logging. /// - public readonly struct ExceptionItem { + public class ExceptionItem { internal ExceptionItem(System.Exception exception) { Time = DateTime.Now; Exception = exception; diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs index 6ce0859..40da8c0 100644 --- a/ResoniteModLoader/ModConfigurationDataFeed.cs +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -223,18 +223,20 @@ internal static IEnumerable GenerateModConfigurationFeed(this Reso } List items = new(); + bool failed = false; if (!forceDefaultBuilder) { try { items = mod.BuildConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal).ToList(); } catch (Exception ex) { + failed = true; Logger.ProcessException(ex, mod.ModAssembly!.Assembly); Logger.ErrorInternal($"Exception was thrown while running {mod.Name}'s BuildConfigurationFeed method"); } } - if (!items.Any()) { + if (failed || !items.Any()) { ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); builder ??= new ModConfigurationFeedBuilder(config); items = builder.RootPage(searchPhrase, includeInternal).ToList(); diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs index a45b2a2..6044855 100644 --- a/ResoniteModLoader/ModConfigurationFeedBuilder.cs +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -109,6 +109,14 @@ public ModConfigurationFeedBuilder(ModConfiguration config) { } } + public IEnumerable Page(IReadOnlyList path, string searchPhrase = "", bool includeInternal = false) + { + if (path is null || !path.Any()) + foreach (DataFeedItem item in RootPage(searchPhrase, includeInternal)) + yield return item; + + } + /// /// Generates a root config page containing all defined config keys. /// diff --git a/ResoniteModLoader/ResoniteModLoader.csproj b/ResoniteModLoader/ResoniteModLoader.csproj index fcf97a8..eaf186a 100644 --- a/ResoniteModLoader/ResoniteModLoader.csproj +++ b/ResoniteModLoader/ResoniteModLoader.csproj @@ -65,6 +65,10 @@ + + + + diff --git a/ResoniteModLoader/Resources/ConfigurationItemMapper.brson b/ResoniteModLoader/Resources/ConfigurationItemMapper.brson new file mode 100644 index 0000000..e69de29 diff --git a/doc/datafeed.md b/doc/datafeed.md new file mode 100644 index 0000000..143f37d --- /dev/null +++ b/doc/datafeed.md @@ -0,0 +1,9 @@ +# Configuration feeds & you + +FeedBuilder +Logger +ModConfigurationDataFeed +ModConfigurationFeedBuilder +ModConfigurationValueSync +ResoniteMod.BuildConfigurationFeed +AutoRegisterConfigKeyAttribute.Group