From 8fca5c3130caf58f1bc15b42714472c721556d70 Mon Sep 17 00:00:00 2001 From: NepuShiro Date: Mon, 22 Sep 2025 23:54:56 -0500 Subject: [PATCH 01/12] Implement Search and addon support --- BepisModSettings/BepisModSettings.csproj | 2 +- BepisModSettings/DataFeeds/BepisPluginPage.cs | 20 ++ .../DataFeeds/BepisSettingsPage.cs | 299 ++++++++++++++++-- BepisModSettings/Locale/en.json | 5 +- README.md | 2 +- thunderstore.toml | 2 +- 6 files changed, 308 insertions(+), 22 deletions(-) diff --git a/BepisModSettings/BepisModSettings.csproj b/BepisModSettings/BepisModSettings.csproj index d45ee03..b10dd63 100644 --- a/BepisModSettings/BepisModSettings.csproj +++ b/BepisModSettings/BepisModSettings.csproj @@ -1,7 +1,7 @@  - 1.1.1 + 1.2.0 ResoniteModding net9.0 Bepis Mod Settings diff --git a/BepisModSettings/DataFeeds/BepisPluginPage.cs b/BepisModSettings/DataFeeds/BepisPluginPage.cs index 1a38854..c08f0c2 100644 --- a/BepisModSettings/DataFeeds/BepisPluginPage.cs +++ b/BepisModSettings/DataFeeds/BepisPluginPage.cs @@ -20,6 +20,8 @@ namespace BepisModSettings.DataFeeds; public static class BepisPluginPage { internal static readonly Dictionary, IAsyncEnumerable>> CategoryHandlers = new Dictionary, IAsyncEnumerable>>(); + + public static event Func, IAsyncEnumerable> CustomPluginConfigsPages; internal static async IAsyncEnumerable Enumerate(IReadOnlyList path) { @@ -27,6 +29,24 @@ internal static async IAsyncEnumerable Enumerate(IReadOnlyList x.Metadata.GUID != pluginId) && pluginId != "BepInEx.Core.Config") + { + if (CustomPluginConfigsPages != null) + { + foreach (Delegate del in CustomPluginConfigsPages.GetInvocationList()) + { + if (del is not Func, IAsyncEnumerable> handler) continue; + + await foreach (DataFeedItem item in handler(path)) + { + yield return item; + } + } + } + + yield break; + } + ConfigFile configFile; ModMeta metadata; diff --git a/BepisModSettings/DataFeeds/BepisSettingsPage.cs b/BepisModSettings/DataFeeds/BepisSettingsPage.cs index f6e378f..3e7d467 100644 --- a/BepisModSettings/DataFeeds/BepisSettingsPage.cs +++ b/BepisModSettings/DataFeeds/BepisSettingsPage.cs @@ -1,20 +1,53 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using BepInEx; using BepInEx.NET.Common; +using BepInExResoniteShim; using BepisLocaleLoader; using Elements.Core; using FrooxEngine; +using FrooxEngine.UIX; namespace BepisModSettings.DataFeeds; public static class BepisSettingsPage { + public static event Func, IAsyncEnumerable> CustomPluginsPages; + + public static string SearchString { get; set; } = ""; + internal static async IAsyncEnumerable Enumerate(IReadOnlyList path) { await Task.CompletedTask; + DataFeedGroup searchGroup = new DataFeedGroup(); + searchGroup.InitBase("SearchGroup", path, null, "Settings.BepInEx.Search".AsLocaleKey()); + yield return searchGroup; + + string[] searchGroupParam = ["SearchGroup"]; + + DataFeedValueField searchField = new DataFeedValueField(); + searchField.InitBase("SearchField", path, searchGroupParam, "Settings.BepInEx.SearchField".AsLocaleKey()); + searchField.InitSetupValue(field => + { + field.Value = SearchString; + field.Changed += FieldChanged; + + Slot slot = field.FindNearestParent(); + if (slot == null) return; + + slot.GetComponentInParents().LocalEditingFinished += _ => RefreshSettingsScreen(); + return; + + void FieldChanged(IChangeable _) + { + SearchString = field.Value; + } + }); + yield return searchField; + DataFeedGroup plguinsGroup = new DataFeedGroup(); plguinsGroup.InitBase("BepInExPlugins", path, null, "Settings.BepInEx.Plugins".AsLocaleKey()); yield return plguinsGroup; @@ -29,29 +62,46 @@ internal static async IAsyncEnumerable Enumerate(IReadOnlyList sortedPlugins = new List(NetChainloader.Instance.Plugins.Values); sortedPlugins.Sort((a, b) => string.Compare(a.Metadata.Name, b.Metadata.Name, StringComparison.OrdinalIgnoreCase)); - foreach (PluginInfo plugin in sortedPlugins) + + List filteredPlugins = FilterPlugins(sortedPlugins, SearchString).ToList(); + if (filteredPlugins.Count > 0) { - BepInPlugin metaData = plugin.Metadata; + foreach (PluginInfo plugin in filteredPlugins) + { + BepInPlugin metaData = plugin.Metadata; - string pluginname = metaData.Name; - string pluginGuid = metaData.GUID; + string pluginname = metaData.Name; + string pluginGuid = metaData.GUID; - LocaleString nameKey = pluginname; - LocaleString description = $"{pluginname}\n{pluginGuid}\n({metaData.Version})"; // "Settings.BepInEx.Plugin.Description".AsLocaleKey(("name", pluginname), ("guid", metaData.GUID), ("version", metaData.Version)); + LocaleString nameKey = pluginname; + LocaleString description = $"{pluginname}\n{pluginGuid}\n({metaData.Version})"; - if (LocaleLoader.PluginsWithLocales.Contains(plugin)) - { - nameKey = $"Settings.{pluginGuid}".AsLocaleKey(); - description = $"Settings.{pluginGuid}.Description".AsLocaleKey(); - } - else - { - LocaleLoader.AddLocaleString($"Settings.{pluginGuid}.Breadcrumb", pluginname, authors: PluginMetadata.AUTHORS); - } + if (LocaleLoader.PluginsWithLocales.Contains(plugin)) + { + nameKey = $"Settings.{pluginGuid}".AsLocaleKey(); + description = $"Settings.{pluginGuid}.Description".AsLocaleKey(); + } + else + { + LocaleLoader.AddLocaleString($"Settings.{pluginGuid}.Breadcrumb", pluginname, authors: PluginMetadata.AUTHORS); + } - DataFeedCategory loadedPlugin = new DataFeedCategory(); - loadedPlugin.InitBase(pluginGuid, path, loadedPluginsGroup, nameKey, description); - yield return loadedPlugin; + DataFeedCategory loadedPlugin = new DataFeedCategory(); + loadedPlugin.InitBase(pluginGuid, path, loadedPluginsGroup, nameKey, description); + yield return loadedPlugin; + } + } + else if (!string.IsNullOrWhiteSpace(SearchString)) + { + DataFeedLabel noResults = new DataFeedLabel(); + noResults.InitBase("NoSearchResults", path, loadedPluginsGroup, "Settings.BepInEx.Plugins.NoSearchResults".AsLocaleKey()); + yield return noResults; + } + else + { + DataFeedLabel noPlugins = new DataFeedLabel(); + noPlugins.InitBase("NoPlugins", path, loadedPluginsGroup, "Settings.BepInEx.Plugins.NoPlugins".AsLocaleKey()); + yield return noPlugins; } } else @@ -61,6 +111,19 @@ internal static async IAsyncEnumerable Enumerate(IReadOnlyList, IAsyncEnumerable> handler) continue; + + await foreach (DataFeedItem item in handler(path)) + { + yield return item; + } + } + } + DataFeedGroup coreGroup = new DataFeedGroup(); coreGroup.InitBase("BepInExCore", path, null, "Settings.BepInEx.Core".AsLocaleKey()); yield return coreGroup; @@ -71,4 +134,204 @@ internal static async IAsyncEnumerable Enumerate(IReadOnlyList FilterPlugins(List plugins, string searchString) + { + if (string.IsNullOrWhiteSpace(searchString)) + { + return plugins; + } + + string searchLower = searchString.ToLowerInvariant(); + + return plugins.Where(plugin => + { + BepInPlugin pMetadata = MetadataHelper.GetMetadata(plugin) ?? plugin.Metadata; + ResonitePlugin resonitePlugin = pMetadata as ResonitePlugin; + + ModMeta metadata = new ModMeta(pMetadata.Name, pMetadata.Version.ToString(), pMetadata.GUID, resonitePlugin?.Author, resonitePlugin?.Link); + + if (metadata.Name.Contains(searchLower, StringComparison.InvariantCultureIgnoreCase)) + return true; + + if (metadata.ID.Contains(searchLower, StringComparison.InvariantCultureIgnoreCase)) + return true; + + if (metadata.Version.Contains(searchLower, StringComparison.InvariantCultureIgnoreCase)) + return true; + + if (!string.IsNullOrEmpty(metadata.Author) && metadata.Author.Contains(searchLower, StringComparison.InvariantCultureIgnoreCase)) + return true; + + return false; + }); + } + + private static bool _isUpdatingSettings; + + public static bool GoUpOneSetting() + { + try + { + RootCategoryView rcv = GetRootCategoryView(); + if (rcv == null) + { + Plugin.Log.LogWarning("Cannot navigate up: RootCategoryView not found or not in BepInEx settings"); + return false; + } + + if (rcv.Path.Count <= 1) + { + Plugin.Log.LogInfo("Already at the root category, cannot go up further"); + return false; + } + + string[] newPath = rcv.Path.Take(rcv.Path.Count - 1).ToArray(); + return SetCategoryPathSafe(rcv, newPath); + } + catch (Exception e) + { + Plugin.Log.LogError($"Error navigating up one setting: {e}"); + return false; + } + } + + public static bool GoToSettingPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + Plugin.Log.LogWarning("Cannot navigate to empty or null path"); + return false; + } + + return GoToSettingPath(new[] { path }); + } + + public static bool GoToSettingPath(string[] path) + { + if (path == null || path.Length == 0) + { + Plugin.Log.LogWarning("Cannot navigate to null or empty path array"); + return false; + } + + try + { + RootCategoryView rcv = GetRootCategoryView(); + if (rcv == null) + { + Plugin.Log.LogWarning($"Cannot navigate to path [{string.Join("/", path)}]: RootCategoryView not found or not in BepInEx settings"); + return false; + } + + return SetCategoryPathSafe(rcv, path); + } + catch (Exception e) + { + Plugin.Log.LogError($"Error navigating to path [{string.Join("/", path)}]: {e}"); + return false; + } + } + + public static bool RefreshSettingsScreen() + { + if (_isUpdatingSettings) + { + Plugin.Log.LogInfo("Settings refresh already in progress, skipping"); + return false; + } + + try + { + _isUpdatingSettings = true; + + RootCategoryView rcv = GetRootCategoryView(); + if (rcv == null) + { + Plugin.Log.LogWarning("Cannot refresh settings: RootCategoryView not found or not in BepInEx settings"); + return false; + } + + // Store the current path + string[] currentPath = rcv.Path.ToArray(); + + // Navigate to empty path to trigger refresh + if (!SetCategoryPathSafe(rcv, new[] { string.Empty })) + { + Plugin.Log.LogError("Failed to navigate to empty path during refresh"); + return false; + } + + rcv.RunInUpdates(3, () => + { + try + { + SetCategoryPathSafe(rcv, currentPath); + } + catch (Exception e) + { + Plugin.Log.LogError($"Error returning to original path after refresh: {e}"); + } + finally + { + _isUpdatingSettings = false; + } + }); + + return true; + } + catch (Exception e) + { + Plugin.Log.LogError($"Error refreshing settings screen: {e}"); + _isUpdatingSettings = false; + return false; + } + } + + private static bool SetCategoryPathSafe(RootCategoryView rcv, string[] path) + { + if (rcv == null || path == null) + { + Plugin.Log.LogWarning("Cannot set category path: rcv or path is null"); + return false; + } + + try + { + rcv.SetCategoryPath(path); + return true; + } + catch (Exception e) + { + Plugin.Log.LogError($"Failed to set category path [{string.Join("/", path)}]: {e}"); + return false; + } + } + + private static RootCategoryView GetRootCategoryView() + { + try + { + World userspaceWorld = Userspace.UserspaceWorld; + SettingsDataFeed settingsDataFeed = userspaceWorld?.RootSlot?.GetComponentInChildren(); + RootCategoryView rootCategoryView = settingsDataFeed?.Slot?.GetComponent(); + + if (rootCategoryView?.Path == null || rootCategoryView.Path.Count == 0) + { + return null; + } + + if (rootCategoryView.Path.All(p => p?.Contains("BepInEx", StringComparison.OrdinalIgnoreCase) != true)) + { + return null; + } + + return rootCategoryView; + } + catch (Exception e) + { + Plugin.Log.LogError($"Error getting RootCategoryView: {e}"); + return null; + } + } } \ No newline at end of file diff --git a/BepisModSettings/Locale/en.json b/BepisModSettings/Locale/en.json index cd44786..3fd3290 100644 --- a/BepisModSettings/Locale/en.json +++ b/BepisModSettings/Locale/en.json @@ -9,6 +9,9 @@ "Settings.BepInEx": "BepInEx", "Settings.BepInEx.Breadcrumb": "BepInEx", + "Settings.BepInEx.Search": "Search", + "Settings.BepInEx.SearchField": "Search Field", + "Settings.BepInEx.Warning": "Warning", "Settings.BepInEx.NotUserspace": "This menu is only available in userspace!", @@ -21,6 +24,7 @@ "Settings.BepInEx.Plugins.LoadedPlugins": "Loaded Plugins", "Settings.BepInEx.Plugins.NoConfigs": "This Plugin has No Configs", "Settings.BepInEx.Plugins.NoPlugins": "There is no Plugins Loaded", + "Settings.BepInEx.Plugins.NoSearchResults": "No Plugins found", "Settings.BepInEx.Plugins.Config.FlagsEnum": "PlaceHolder", "Settings.BepInEx.Plugins.Config.FlagsEnum.Description": "Toggles the '{name}' enum flag.", @@ -46,7 +50,6 @@ "Settings.ResoniteModding.BepisModSettings.Breadcrumb": "Bepis Mod Settings", "Settings.ResoniteModding.BepisModSettings.Description": "Lets you edit your mod settings in settings tab of the dashboard.", - // TODO: Add a way to read these "Settings.ResoniteModding.BepisModSettings.Configs.ShowHidden": "Show Hidden Configs", "Settings.ResoniteModding.BepisModSettings.Configs.ShowHidden.Description": "Whether to show hidden Configs" } diff --git a/README.md b/README.md index 6373b4a..0133ea8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Configuration page footer:image Date: Tue, 23 Sep 2025 12:50:32 -0500 Subject: [PATCH 02/12] Add .idea to gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7fb3dce..1f4101d 100644 --- a/.gitignore +++ b/.gitignore @@ -429,4 +429,6 @@ FodyWeavers.xsd # Thunderstore packaging dist/ -build/ \ No newline at end of file +build/ + +.idea/ \ No newline at end of file From f1cdacfce8f120b66f4c93563944302fd0e13850 Mon Sep 17 00:00:00 2001 From: NepuShiro Date: Tue, 23 Sep 2025 17:12:14 -0500 Subject: [PATCH 03/12] Refactor `DataFeedHelpers` to improve `SettingsDataFeed` integration and streamline settings navigation and refresh logic --- .../DataFeeds/BepisSettingsPage.cs | 170 +-------------- BepisModSettings/DataFeeds/DataFeedHelpers.cs | 195 ++++++++++++++++-- BepisModSettings/Plugin.cs | 2 + 3 files changed, 182 insertions(+), 185 deletions(-) diff --git a/BepisModSettings/DataFeeds/BepisSettingsPage.cs b/BepisModSettings/DataFeeds/BepisSettingsPage.cs index 3e7d467..69c7ce5 100644 --- a/BepisModSettings/DataFeeds/BepisSettingsPage.cs +++ b/BepisModSettings/DataFeeds/BepisSettingsPage.cs @@ -38,7 +38,7 @@ internal static async IAsyncEnumerable Enumerate(IReadOnlyList(); if (slot == null) return; - slot.GetComponentInParents().LocalEditingFinished += _ => RefreshSettingsScreen(); + slot.GetComponentInParents().LocalEditingFinished += _ => DataFeedHelpers.RefreshSettingsScreen(); return; void FieldChanged(IChangeable _) @@ -166,172 +166,4 @@ private static IEnumerable FilterPlugins(List plugins, s return false; }); } - - private static bool _isUpdatingSettings; - - public static bool GoUpOneSetting() - { - try - { - RootCategoryView rcv = GetRootCategoryView(); - if (rcv == null) - { - Plugin.Log.LogWarning("Cannot navigate up: RootCategoryView not found or not in BepInEx settings"); - return false; - } - - if (rcv.Path.Count <= 1) - { - Plugin.Log.LogInfo("Already at the root category, cannot go up further"); - return false; - } - - string[] newPath = rcv.Path.Take(rcv.Path.Count - 1).ToArray(); - return SetCategoryPathSafe(rcv, newPath); - } - catch (Exception e) - { - Plugin.Log.LogError($"Error navigating up one setting: {e}"); - return false; - } - } - - public static bool GoToSettingPath(string path) - { - if (string.IsNullOrEmpty(path)) - { - Plugin.Log.LogWarning("Cannot navigate to empty or null path"); - return false; - } - - return GoToSettingPath(new[] { path }); - } - - public static bool GoToSettingPath(string[] path) - { - if (path == null || path.Length == 0) - { - Plugin.Log.LogWarning("Cannot navigate to null or empty path array"); - return false; - } - - try - { - RootCategoryView rcv = GetRootCategoryView(); - if (rcv == null) - { - Plugin.Log.LogWarning($"Cannot navigate to path [{string.Join("/", path)}]: RootCategoryView not found or not in BepInEx settings"); - return false; - } - - return SetCategoryPathSafe(rcv, path); - } - catch (Exception e) - { - Plugin.Log.LogError($"Error navigating to path [{string.Join("/", path)}]: {e}"); - return false; - } - } - - public static bool RefreshSettingsScreen() - { - if (_isUpdatingSettings) - { - Plugin.Log.LogInfo("Settings refresh already in progress, skipping"); - return false; - } - - try - { - _isUpdatingSettings = true; - - RootCategoryView rcv = GetRootCategoryView(); - if (rcv == null) - { - Plugin.Log.LogWarning("Cannot refresh settings: RootCategoryView not found or not in BepInEx settings"); - return false; - } - - // Store the current path - string[] currentPath = rcv.Path.ToArray(); - - // Navigate to empty path to trigger refresh - if (!SetCategoryPathSafe(rcv, new[] { string.Empty })) - { - Plugin.Log.LogError("Failed to navigate to empty path during refresh"); - return false; - } - - rcv.RunInUpdates(3, () => - { - try - { - SetCategoryPathSafe(rcv, currentPath); - } - catch (Exception e) - { - Plugin.Log.LogError($"Error returning to original path after refresh: {e}"); - } - finally - { - _isUpdatingSettings = false; - } - }); - - return true; - } - catch (Exception e) - { - Plugin.Log.LogError($"Error refreshing settings screen: {e}"); - _isUpdatingSettings = false; - return false; - } - } - - private static bool SetCategoryPathSafe(RootCategoryView rcv, string[] path) - { - if (rcv == null || path == null) - { - Plugin.Log.LogWarning("Cannot set category path: rcv or path is null"); - return false; - } - - try - { - rcv.SetCategoryPath(path); - return true; - } - catch (Exception e) - { - Plugin.Log.LogError($"Failed to set category path [{string.Join("/", path)}]: {e}"); - return false; - } - } - - private static RootCategoryView GetRootCategoryView() - { - try - { - World userspaceWorld = Userspace.UserspaceWorld; - SettingsDataFeed settingsDataFeed = userspaceWorld?.RootSlot?.GetComponentInChildren(); - RootCategoryView rootCategoryView = settingsDataFeed?.Slot?.GetComponent(); - - if (rootCategoryView?.Path == null || rootCategoryView.Path.Count == 0) - { - return null; - } - - if (rootCategoryView.Path.All(p => p?.Contains("BepInEx", StringComparison.OrdinalIgnoreCase) != true)) - { - return null; - } - - return rootCategoryView; - } - catch (Exception e) - { - Plugin.Log.LogError($"Error getting RootCategoryView: {e}"); - return null; - } - } } \ No newline at end of file diff --git a/BepisModSettings/DataFeeds/DataFeedHelpers.cs b/BepisModSettings/DataFeeds/DataFeedHelpers.cs index 1140556..45ee020 100644 --- a/BepisModSettings/DataFeeds/DataFeedHelpers.cs +++ b/BepisModSettings/DataFeeds/DataFeedHelpers.cs @@ -26,24 +26,14 @@ namespace BepisModSettings.DataFeeds; public static class DataFeedHelpers { - private static GridContainerScreen _settingsScreen; - - private static GridContainerScreen SettingsScreen - { - get - { - _settingsScreen = _settingsScreen?.FilterWorldElement() ?? Userspace.UserspaceWorld.RootSlot.GetComponentInChildren(x => x.Label.Value == "Settings"); - return _settingsScreen; - } - } + public static SettingsDataFeed SettingsDataFeed { get; set; } private static RootCategoryView _rootCategoryView; - private static RootCategoryView RootCategoryView { get { - _rootCategoryView = _rootCategoryView?.FilterWorldElement() ?? SettingsScreen.Slot.GetComponentInChildren(); + _rootCategoryView = _rootCategoryView?.FilterWorldElement() ?? SettingsDataFeed.Slot.GetComponent(); return _rootCategoryView; } } @@ -83,9 +73,9 @@ public static DataFeedValueField GenerateValueFieldMethod(string key, IRea - if (configKey.SettingType.IsTypeInjectable() && SettingsScreen?.Slot != null) + if (configKey.SettingType.IsTypeInjectable() && SettingsDataFeed != null) { - SettingsScreen.Slot.RunSynchronously(() => InjectNewTemplateType(configKey.SettingType)); + SettingsDataFeed.Slot.RunSynchronously(() => InjectNewTemplateType(configKey.SettingType)); } return valueField; @@ -97,9 +87,9 @@ public static DataFeedValueField GenerateProxyField(string key, IReadOnl valueField.InitBase($"{key}.{configKey.SettingType}", path, groupKeys, internalLocale.Key, internalLocale.Description); valueField.InitSetupValue(field => field.SyncProxyWithConfigKey(configKey)); - if (TomlTypeConverter.CanConvert(configKey.SettingType) && SettingsScreen?.Slot != null) + if (TomlTypeConverter.CanConvert(configKey.SettingType) && SettingsDataFeed != null) { - SettingsScreen.Slot.RunSynchronously(() => InjectNewTemplateType(typeof(string))); + SettingsDataFeed.Slot.RunSynchronously(() => InjectNewTemplateType(typeof(string))); } return valueField; @@ -453,4 +443,177 @@ private static void InjectNewTemplateType(Type typeToInject) Plugin.Log.LogError("Could not find Templates slot in DoInject!"); } } + + private static bool _isUpdatingSettings; + + public static bool GoUpOneSetting() + { + try + { + RootCategoryView rcv = GetRootCategoryView(); + if (rcv == null) + { + Plugin.Log.LogWarning("Cannot navigate up: RootCategoryView not found or not in BepInEx settings"); + return false; + } + + if (rcv.Path.Count <= 1) + { + Plugin.Log.LogInfo("Already at the root category, cannot go up further"); + return false; + } + + string[] newPath = rcv.Path.Take(rcv.Path.Count - 1).ToArray(); + return SetCategoryPathSafe(rcv, newPath); + } + catch (Exception e) + { + Plugin.Log.LogError($"Error navigating up one setting: {e}"); + return false; + } + } + + public static bool GoToSettingPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + Plugin.Log.LogWarning("Cannot navigate to empty or null path"); + return false; + } + + return GoToSettingPath(new[] { path }); + } + + public static bool GoToSettingPath(string[] path) + { + if (path == null || path.Length == 0) + { + Plugin.Log.LogWarning("Cannot navigate to null or empty path array"); + return false; + } + + try + { + RootCategoryView rcv = GetRootCategoryView(); + if (rcv == null) + { + Plugin.Log.LogWarning($"Cannot navigate to path [{string.Join("/", path)}]: RootCategoryView not found or not in BepInEx settings"); + return false; + } + + return SetCategoryPathSafe(rcv, path); + } + catch (Exception e) + { + Plugin.Log.LogError($"Error navigating to path [{string.Join("/", path)}]: {e}"); + return false; + } + } + + public static bool RefreshSettingsScreen() + { + if (_isUpdatingSettings) + { + Plugin.Log.LogInfo("Settings refresh already in progress, skipping"); + return false; + } + + try + { + _isUpdatingSettings = true; + + RootCategoryView rcv = GetRootCategoryView(); + if (rcv == null) + { + Plugin.Log.LogWarning("Cannot refresh settings: RootCategoryView not found or not in BepInEx settings"); + _isUpdatingSettings = false; + return false; + } + + // Store the current path + string[] currentPath = rcv.Path.ToArray(); + + // Navigate to empty path to trigger refresh + if (!SetCategoryPathSafe(rcv, new[] { string.Empty })) + { + Plugin.Log.LogError("Failed to navigate to empty path during refresh"); + _isUpdatingSettings = false; + return false; + } + + rcv.RunInUpdates(3, () => + { + try + { + SetCategoryPathSafe(rcv, currentPath); + } + catch (Exception e) + { + Plugin.Log.LogError($"Error returning to original path after refresh: {e}"); + } + finally + { + _isUpdatingSettings = false; + } + }); + + return true; + } + catch (Exception e) + { + Plugin.Log.LogError($"Error refreshing settings screen: {e}"); + _isUpdatingSettings = false; + return false; + } + } + + private static bool SetCategoryPathSafe(RootCategoryView rcv, string[] path) + { + if (rcv == null || path == null) + { + Plugin.Log.LogWarning("Cannot set category path: rcv or path is null"); + return false; + } + + try + { + rcv.SetCategoryPath(path); + return true; + } + catch (Exception e) + { + Plugin.Log.LogError($"Failed to set category path [{string.Join("/", path)}]: {e}"); + return false; + } + } + + private static RootCategoryView GetRootCategoryView() + { + try + { + RootCategoryView rcv = RootCategoryView; + if (rcv == null) + { + SettingsDataFeed settingsDataFeed = Userspace.UserspaceWorld?.RootSlot?.GetComponentInChildren(); + rcv = settingsDataFeed?.Slot?.GetComponent(); + } + + if (rcv?.Path == null || rcv.Path.Count == 0) + { + return null; + } + + if (rcv.Path.All(p => p?.Contains("BepInEx", StringComparison.OrdinalIgnoreCase) != true)) + { + return null; + } + + return rcv; + } + catch (Exception e) + { + Plugin.Log.LogError($"Error getting RootCategoryView: {e}"); + return null; + } + } } \ No newline at end of file diff --git a/BepisModSettings/Plugin.cs b/BepisModSettings/Plugin.cs index 1dcbba9..53c48db 100644 --- a/BepisModSettings/Plugin.cs +++ b/BepisModSettings/Plugin.cs @@ -97,6 +97,8 @@ private static IAsyncEnumerable Postfix(IAsyncEnumerable Date: Tue, 23 Sep 2025 17:24:27 -0500 Subject: [PATCH 04/12] Change SettingsDataFeed to internal --- BepisModSettings/DataFeeds/DataFeedHelpers.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/BepisModSettings/DataFeeds/DataFeedHelpers.cs b/BepisModSettings/DataFeeds/DataFeedHelpers.cs index 45ee020..430bc25 100644 --- a/BepisModSettings/DataFeeds/DataFeedHelpers.cs +++ b/BepisModSettings/DataFeeds/DataFeedHelpers.cs @@ -26,7 +26,7 @@ namespace BepisModSettings.DataFeeds; public static class DataFeedHelpers { - public static SettingsDataFeed SettingsDataFeed { get; set; } + internal static SettingsDataFeed SettingsDataFeed { get; set; } private static RootCategoryView _rootCategoryView; private static RootCategoryView RootCategoryView @@ -512,11 +512,7 @@ public static bool GoToSettingPath(string[] path) public static bool RefreshSettingsScreen() { - if (_isUpdatingSettings) - { - Plugin.Log.LogInfo("Settings refresh already in progress, skipping"); - return false; - } + if (_isUpdatingSettings) return false; try { From 0276274570a4fc8be20a9a86452f15d2e92418a4 Mon Sep 17 00:00:00 2001 From: NepuShiro Date: Tue, 23 Sep 2025 17:57:50 -0500 Subject: [PATCH 05/12] bump BepisLocaleLoader dependency version, remove unused comments --- thunderstore.toml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/thunderstore.toml b/thunderstore.toml index 035352c..8eba6ab 100644 --- a/thunderstore.toml +++ b/thunderstore.toml @@ -13,7 +13,7 @@ containsNsfwContent = false [package.dependencies] ResoniteModding-BepisLoader = "1.4.1" -ResoniteModding-BepisLocaleLoader = "1.0.0" +ResoniteModding-BepisLocaleLoader = "1.1.0" ResoniteModding-BepisResoniteWrapper = "1.0.0" [build] @@ -33,11 +33,6 @@ target = "plugins/BepisModSettings/" source = "./BepisModSettings/Locale" target = "plugins/BepisModSettings/Locale" -# Uncomment the following lines to include CHANGELOG file in your mod package. https://wiki.thunderstore.io/mods/updating-a-package#changelog -# [[build.copy]] -# source = "./CHANGELOG.md" -# target = "/" - [[build.copy]] source = "./LICENSE" target = "/" @@ -50,4 +45,4 @@ communities = ["resonite"] resonite = [ "dashboard", "misc", -] # TODO: Change this to your mod's categories, see https://thunderstore.io/api/experimental/community/resonite/category/ for a list of available categories +] \ No newline at end of file From 1137c60faa854b695d52274a24c3c8e00259bfba Mon Sep 17 00:00:00 2001 From: NepuShiro Date: Wed, 24 Sep 2025 09:34:15 -0500 Subject: [PATCH 06/12] Reformat code, Make RootCategoryView methods less verbose, use rcv.MoveUpInCategory --- .../ConfigAttributes/CustomDataFeed.cs | 10 +- .../ConfigAttributes/HiddenConfig.cs | 2 +- .../ConfigAttributes/ProtectedConfig.cs | 8 +- BepisModSettings/DataFeeds/BepisPluginPage.cs | 17 ++-- BepisModSettings/DataFeeds/DataFeedHelpers.cs | 94 +++++-------------- BepisModSettings/Plugin.cs | 4 +- 6 files changed, 47 insertions(+), 88 deletions(-) diff --git a/BepisModSettings/ConfigAttributes/CustomDataFeed.cs b/BepisModSettings/ConfigAttributes/CustomDataFeed.cs index 978ddb1..4586130 100644 --- a/BepisModSettings/ConfigAttributes/CustomDataFeed.cs +++ b/BepisModSettings/ConfigAttributes/CustomDataFeed.cs @@ -14,17 +14,19 @@ public sealed class CustomDataFeed(DataFeedMethod action) public static DataFeedMethod GetCustomFeedMethod(ConfigEntryBase config) { if (config?.Description?.Tags == null) return null; - foreach (var tag in config.Description.Tags) + foreach (object tag in config.Description.Tags) { - if(tag is CustomDataFeed customDataFeed) + if (tag is CustomDataFeed customDataFeed) { return customDataFeed._action; } - if(tag is Func, IReadOnlyList, IAsyncEnumerable> func) + + if (tag is Func, IReadOnlyList, IAsyncEnumerable> func) { - return (DataFeedMethod) Delegate.CreateDelegate(typeof(DataFeedMethod), func.Target, func.Method); + return (DataFeedMethod)Delegate.CreateDelegate(typeof(DataFeedMethod), func.Target, func.Method); } } + return null; } } \ No newline at end of file diff --git a/BepisModSettings/ConfigAttributes/HiddenConfig.cs b/BepisModSettings/ConfigAttributes/HiddenConfig.cs index 2d71914..2bc424d 100644 --- a/BepisModSettings/ConfigAttributes/HiddenConfig.cs +++ b/BepisModSettings/ConfigAttributes/HiddenConfig.cs @@ -3,7 +3,7 @@ namespace BepisModSettings.ConfigAttributes; -public class HiddenConfig +public class HiddenConfig { public static bool IsHidden(ConfigEntryBase config) { diff --git a/BepisModSettings/ConfigAttributes/ProtectedConfig.cs b/BepisModSettings/ConfigAttributes/ProtectedConfig.cs index d039f90..4cddd17 100644 --- a/BepisModSettings/ConfigAttributes/ProtectedConfig.cs +++ b/BepisModSettings/ConfigAttributes/ProtectedConfig.cs @@ -12,17 +12,19 @@ public ProtectedConfig() : this("*") { } public static string GetMask(ConfigEntryBase config) { if (config?.Description?.Tags == null) return null; - foreach (var tag in config.Description.Tags) + foreach (object tag in config.Description.Tags) { - if(tag is ProtectedConfig protectedConfig) + if (tag is ProtectedConfig protectedConfig) { return protectedConfig.MaskString; } - else if(tag as string == "Protected") + + if (tag as string == "Protected") { return "*"; } } + return null; } } \ No newline at end of file diff --git a/BepisModSettings/DataFeeds/BepisPluginPage.cs b/BepisModSettings/DataFeeds/BepisPluginPage.cs index edbde1c..bba5641 100644 --- a/BepisModSettings/DataFeeds/BepisPluginPage.cs +++ b/BepisModSettings/DataFeeds/BepisPluginPage.cs @@ -20,7 +20,7 @@ namespace BepisModSettings.DataFeeds; public static class BepisPluginPage { internal static readonly Dictionary, IAsyncEnumerable>> CategoryHandlers = new Dictionary, IAsyncEnumerable>>(); - + public static event Func, IAsyncEnumerable> CustomPluginConfigsPages; internal static async IAsyncEnumerable Enumerate(IReadOnlyList path) @@ -43,7 +43,7 @@ internal static async IAsyncEnumerable Enumerate(IReadOnlyList Enumerate(IReadOnlyList()?.Company; - var assUrl = resonitePlugin?.Link ?? - plugin.GetType().Assembly.GetCustomAttributes()?.FirstOrDefault(a => a.Key.ToLower().Contains("url"))?.Value; + string assCo = resonitePlugin?.Author ?? plugin.GetType().Assembly.GetCustomAttribute()?.Company; + string assUrl = resonitePlugin?.Link ?? plugin.GetType().Assembly.GetCustomAttributes()?.FirstOrDefault(a => a.Key.ToLower().Contains("url"))?.Value; configFile = plugin.Config; metadata = new ModMeta(pMetadata.Name, pMetadata.Version.ToString(), pluginId, assCo, assUrl); @@ -202,7 +200,7 @@ private static async IAsyncEnumerable EnumerateConfigs(ConfigFile { DataFeedItem dummyField = null; - var firstAction = config.Description.Tags.FirstOrDefault(x => x is ActionConfig || x is Action); + object firstAction = config.Description.Tags.FirstOrDefault(x => x is ActionConfig or Action); if (firstAction != null) { DataFeedAction actionField = new DataFeedAction(); @@ -211,8 +209,9 @@ private static async IAsyncEnumerable EnumerateConfigs(ConfigFile { Button btn = syncDelegate.Slot.GetComponent