diff --git a/Luna b/Luna index 1153628f..06094555 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit 1153628fae18b5e720841b73c5bff9a56652ab7b +Subproject commit 06094555dc93eb302d7e823a84edab5926450db9 diff --git a/Penumbra.Api b/Penumbra.Api index 52a3216a..247b173d 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 52a3216a525592205198303df2844435e382cf87 +Subproject commit 247b173d2fdee2d0c18666972114e61f77aef6b6 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 5d4e2392..a90d1124 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -7,7 +7,7 @@ using Penumbra.Services; namespace Penumbra.Api.Api; -public class ModsApi : IPenumbraApiMods, IApiService, IDisposable +public sealed partial class ModsApi : IPenumbraApiMods, IApiService, IDisposable { private readonly CommunicatorService _communicator; private readonly ModManager _modManager; @@ -15,10 +15,11 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable private readonly Configuration _config; private readonly ModFileSystem _modFileSystem; private readonly MigrationManager _migrationManager; + private readonly ModConfigUpdater _modConfigUpdater; private readonly Logger _log; public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem, - CommunicatorService communicator, MigrationManager migrationManager, Logger log) + CommunicatorService communicator, MigrationManager migrationManager, Logger log, ModConfigUpdater modConfigUpdater) { _modManager = modManager; _modImportManager = modImportManager; @@ -27,6 +28,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable _communicator = communicator; _migrationManager = migrationManager; _log = log; + _modConfigUpdater = modConfigUpdater; _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods); _communicator.PcpCreation.Subscribe(OnPcpCreation, PcpCreation.Priority.ApiMods); _communicator.PcpParsing.Subscribe(OnPcpParsing, PcpParsing.Priority.ApiMods); @@ -120,6 +122,12 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public event Action? CreatingPcp; public event Action? ParsingPcp; + public event Action>? ModUsageQueried + { + add => _modConfigUpdater.ModUsageQueried += value; + remove => _modConfigUpdater.ModUsageQueried -= value; + } + public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName) { if (!_modManager.TryGetMod(modDirectory, modName, out var mod) || mod.Node is not { } node) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 4e574b8d..e49d4348 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -56,6 +56,7 @@ public sealed class IpcProviders : IDisposable, IApiService, IRequiredService IpcSubscribers.ModMoved.Provider(pi, api.Mods), IpcSubscribers.CreatingPcp.Provider(pi, api.Mods), IpcSubscribers.ParsingPcp.Provider(pi, api.Mods), + IpcSubscribers.ModUsageQueried.Provider(pi, api.Mods), IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods), IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), diff --git a/Penumbra/Config/Configuration.cs b/Penumbra/Config/Configuration.cs index 1e75378b..94f291a3 100644 --- a/Penumbra/Config/Configuration.cs +++ b/Penumbra/Config/Configuration.cs @@ -1,6 +1,7 @@ using Dalamud.Configuration; using Dalamud.Interface.ImGuiNotification; using Luna; +using Luna.Generators; using Newtonsoft.Json; using Penumbra.Import.Structs; using Penumbra.Interop.Services; @@ -8,13 +9,12 @@ using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab.Selector; -using Penumbra.UI.ResourceWatcher; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; [Serializable] -public class Configuration : IPluginConfiguration, ISavable, IService +public partial class Configuration : IPluginConfiguration, ISavable, IService { [JsonIgnore] private readonly SaveService _saveService; @@ -56,24 +56,45 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool AutoSelectCollection { get; set; } = false; - public bool ShowModsInLobby { get; set; } = true; - public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; - public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; - public bool HideChangedItemFilters { get; set; } = false; - public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public bool HideMachinistOffhandFromChangedItems { get; set; } = true; - public bool DefaultTemporaryMode { get; set; } = false; - public bool EnableDirectoryWatch { get; set; } = false; - public bool EnableAutomaticModImport { get; set; } = false; - public bool EnableCustomShapes { get; set; } = true; - public PcpSettings PcpSettings = new(); - public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public bool ShowModsInLobby { get; set; } = true; + public bool UseCharacterCollectionInMainWindow { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool UseNoModsInInspect { get; set; } = false; + public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public bool HideMachinistOffhandFromChangedItems { get; set; } = true; + public bool DefaultTemporaryMode { get; set; } = false; + public bool EnableDirectoryWatch { get; set; } = false; + public bool EnableAutomaticModImport { get; set; } = false; + public bool EnableCustomShapes { get; set; } = true; + public PcpSettings PcpSettings = new(); + + [ConfigProperty] + private bool _rememberModFilters = true; + + [ConfigProperty] + private bool _rememberCollectionFilters = true; + + [ConfigProperty] + private bool _rememberOnScreenFilters = true; + + [ConfigProperty] + private bool _rememberChangedItemFilters = true; + + [ConfigProperty] + private bool _rememberEffectiveChangesFilters = true; + + [ConfigProperty] + private bool _rememberResourceManagerFilters = true; + + [ConfigProperty(EventName = "ShowRenameChanged")] + private RenameField _showRename = RenameField.BothDataPrio; + public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; @@ -84,7 +105,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService #else public bool DebugMode { get; set; } = false; #endif - public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; [JsonConverter(typeof(SortModeConverter))] [JsonProperty(Order = int.MaxValue)] diff --git a/Penumbra/Config/EphemeralConfig.cs b/Penumbra/Config/EphemeralConfig.cs index 509c32dd..1e5bb390 100644 --- a/Penumbra/Config/EphemeralConfig.cs +++ b/Penumbra/Config/EphemeralConfig.cs @@ -1,39 +1,39 @@ using Dalamud.Interface.ImGuiNotification; using Luna; +using Luna.Generators; using Newtonsoft.Json; -using Penumbra.Enums; using Penumbra.Services; using Penumbra.UI; -using Penumbra.UI.Classes; -using Penumbra.UI.ResourceWatcher; +using Penumbra.UI.ManagementTab; +using Penumbra.UI.ModsTab; using Penumbra.UI.Tabs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; using TabType = Penumbra.Api.Enums.TabType; namespace Penumbra; -public class EphemeralConfig : ISavable, IService +public sealed partial class EphemeralConfig : ISavable, IService { [JsonIgnore] private readonly SaveService _saveService; - public int Version { get; set; } = Configuration.Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; - public bool DebugSeparateWindow { get; set; } = false; - public int TutorialStep { get; set; } = 0; - public bool EnableResourceLogging { get; set; } = false; - public string ResourceLoggingFilter { get; set; } = string.Empty; - public bool EnableResourceWatcher { get; set; } = false; - public bool OnlyAddMatchingResources { get; set; } = true; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; - public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment; - public TabType SelectedTab { get; set; } = TabType.Settings; - public bool FixMainWindow { get; set; } = false; - public HashSet AdvancedEditingOpenForModPaths { get; set; } = []; - public bool ForceRedrawOnFileChange { get; set; } = false; - public bool IncognitoMode { get; set; } = false; + public int Version { get; set; } = Configuration.Constants.CurrentVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; + public bool DebugSeparateWindow { get; set; } = false; + public int TutorialStep { get; set; } = 0; + public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment; + public TabType SelectedTab { get; set; } = TabType.Settings; + + [ConfigProperty] + private ManagementTabType _selectedManagementTab = ManagementTabType.UnusedMods; + + [ConfigProperty] + private ModPanelTab _selectedModPanelTab = ModPanelTab.Settings; + + public bool FixMainWindow { get; set; } = false; + public HashSet AdvancedEditingOpenForModPaths { get; set; } = []; + public bool ForceRedrawOnFileChange { get; set; } = false; + public bool IncognitoMode { get; set; } = false; /// /// Load the current configuration. @@ -41,7 +41,7 @@ public class EphemeralConfig : ISavable, IService /// public EphemeralConfig(SaveService saveService) { - _saveService = saveService; + _saveService = saveService; Load(); } diff --git a/Penumbra/Config/FilterConfig.cs b/Penumbra/Config/FilterConfig.cs index 44325038..acbcd169 100644 --- a/Penumbra/Config/FilterConfig.cs +++ b/Penumbra/Config/FilterConfig.cs @@ -240,6 +240,13 @@ public sealed partial class FilterConfig : ConfigurationFile [ConfigProperty] private ChangedItemIconFlag _onScreenTypeFilter = ChangedItemFlagExtensions.DefaultFlags; + public void ClearOnScreenFilters() + { + _onScreenCharacterFilter = string.Empty; + _onScreenItemFilter = string.Empty; + _onScreenTypeFilter = ChangedItemFlagExtensions.DefaultFlags; + } + private void WriteOnScreenTab(JsonTextWriter j) { if (OnScreenCharacterFilter.Length is 0 diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 42c8fb2b..e2d458e6 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -169,6 +169,8 @@ public unsafe class ResourceLoader : IDisposable, Luna.IService return; CompareHash(ComputeHash(path.Path, parameters), hash, path); + if (PathResolver.ForbiddenFiles.Contains((uint)hash)) + return; // If no replacements are being made, we still want to be able to trigger the event. var resolvedData = _resolvedData.Value; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 5fcf56a4..5c89c53b 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -21,6 +22,19 @@ public class PathResolver : IDisposable, Luna.IService private readonly CollectionResolver _collectionResolver; private readonly GamePathPreProcessService _preprocessor; + public static FrozenSet ForbiddenFiles = ((uint[]) + [ + 0x90E4EE2F, // common/graphics/texture/dummy.tex + 0x84815A1A, // chara/common/texture/white.tex + 0x749091FB, // chara/common/texture/black.tex + 0x5CB9681A, // chara/common/texture/id_16.tex + 0x7E78D000, // chara/common/texture/red.tex + 0xBDC0BFD3, // chara/common/texture/green.tex + 0xC410E850, // chara/common/texture/blue.tex + 0xD5CFA221, // chara/common/texture/null_normal.tex + 0xBE48CA67, // chara/common/texture/skin_mask.tex + ]).ToFrozenSet(); + public PathResolver(Configuration config, CollectionManager collectionManager, ResourceLoader loader, SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState, GamePathPreProcessService preprocessor) diff --git a/Penumbra/Mods/Manager/ModConfigUpdater.cs b/Penumbra/Mods/Manager/ModConfigUpdater.cs index 37412b26..69aecc99 100644 --- a/Penumbra/Mods/Manager/ModConfigUpdater.cs +++ b/Penumbra/Mods/Manager/ModConfigUpdater.cs @@ -23,9 +23,12 @@ public class ModConfigUpdater : IDisposable, IRequiredService _communicator.ModSettingChanged.Subscribe(OnModSettingChanged, ModSettingChanged.Priority.ModConfigUpdater); } - public IEnumerable ListUnusedMods(TimeSpan age) + public event Action>? ModUsageQueried; + + public IEnumerable<(Mod, (string Plugin, string Notes)[])> ListUnusedMods(TimeSpan age) { - var cutoff = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - (int)age.TotalMilliseconds; + var cutoff = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - (int)age.TotalMilliseconds; + var noteDictionary = new Dictionary(); foreach (var mod in _mods) { // Skip actively ignored mods. @@ -40,7 +43,17 @@ public class ModConfigUpdater : IDisposable, IRequiredService if (_collections.Any(c => c.GetOwnSettings(mod.Index)?.Enabled is true || c.GetTempSettings(mod.Index) is not null)) continue; - yield return mod; + // Check whether other plugins mark this mod as in use. + noteDictionary.Clear(); + ModUsageQueried?.Invoke(mod.Name, mod.Identifier, noteDictionary); + if (noteDictionary.Values.Any(n => n.InUse)) + continue; + + // Collect other plugin's notes for this mod. + var notes = noteDictionary.Where(t => !string.IsNullOrWhiteSpace(t.Value.Notes)) + .Select(t => (t.Key.GetName().Name ?? "Unknown Plugin", t.Value.Notes)).ToArray(); + + yield return (mod, notes); } } diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index 479cba28..74ad837a 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -60,7 +60,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable var json = JObject.Parse(text); importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; - lastConfigEdit = json[nameof(Mod.LastConfigEdit)]?.Value() ?? lastConfigEdit; + lastConfigEdit = json[nameof(Mod.LastConfigEdit)]?.Value() ?? now; favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; note = json[nameof(Mod.Note)]?.Value() ?? note; localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; @@ -81,6 +81,9 @@ public readonly struct ModLocalData(Mod mod) : ISavable if (importDate == 0) importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (lastConfigEdit == now) + save = true; + ModDataChangeType changes = 0; if (mod.ImportDate != importDate) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a4937bcd..b1951edc 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -252,7 +252,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); sb.Append( - $"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); + $"> **`Logging: `** Log: {_config.Filters.ResourceLoggerWriteToLog}, Watcher: {_config.Filters.ResourceLoggerEnabled} ({_config.Filters.ResourceLoggerMaxEntries})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); GatherRelevantPlugins(sb); sb.AppendLine("**Mods**"); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 47fb58dc..f9e6a408 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -13,7 +13,7 @@ PROFILING; false - false + false diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index f5c35c14..86ce1bb9 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -109,20 +109,20 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu _config.Version = 8; _config.Ephemeral.Version = 8; - _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion; - _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject() ?? _config.Ephemeral.DebugSeparateWindow; - _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject() ?? _config.Ephemeral.TutorialStep; - _config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject() ?? _config.Ephemeral.EnableResourceLogging; - _config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject() ?? _config.Ephemeral.ResourceLoggingFilter; - _config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject() ?? _config.Ephemeral.EnableResourceWatcher; - _config.Ephemeral.OnlyAddMatchingResources = - _data["OnlyAddMatchingResources"]?.ToObject() ?? _config.Ephemeral.OnlyAddMatchingResources; - _config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject() - ?? _config.Ephemeral.ResourceWatcherResourceTypes; - _config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject() - ?? _config.Ephemeral.ResourceWatcherResourceCategories; - _config.Ephemeral.ResourceWatcherRecordTypes = - _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; + _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion; + _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject() ?? _config.Ephemeral.DebugSeparateWindow; + _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject() ?? _config.Ephemeral.TutorialStep; + _config.Filters.ResourceLoggerWriteToLog = _data["EnableResourceLogging"]?.ToObject() ?? _config.Filters.ResourceLoggerWriteToLog; + _config.Filters.ResourceLoggerLogFilter = _data["ResourceLoggingFilter"]?.ToObject() ?? _config.Filters.ResourceLoggerLogFilter; + _config.Filters.ResourceLoggerEnabled = _data["EnableResourceWatcher"]?.ToObject() ?? _config.Filters.ResourceLoggerEnabled; + _config.Filters.ResourceLoggerStoreOnlyMatching = + _data["OnlyAddMatchingResources"]?.ToObject() ?? _config.Filters.ResourceLoggerStoreOnlyMatching; + _config.Filters.ResourceLoggerTypeFilter = _data["ResourceWatcherResourceTypes"]?.ToObject() + ?? _config.Filters.ResourceLoggerTypeFilter; + _config.Filters.ResourceLoggerCategoryFilter = _data["ResourceWatcherResourceCategories"]?.ToObject() + ?? _config.Filters.ResourceLoggerCategoryFilter; + _config.Filters.ResourceLoggerRecordFilter = + _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Filters.ResourceLoggerRecordFilter; _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; _config.Filters.ChangedItemTypeFilter = _data["ChangedItemFilter"]?.ToObject() diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 0674e5c5..59b11eb3 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -203,22 +203,25 @@ public class ResourceTreeViewer( } } - var fieldWidth = (Im.ContentRegion.Available.X - checkSpacing * 2.0f - Im.Style.FrameHeightWithSpacing) / 2.0f; - Im.Item.SetNextWidth(fieldWidth); - var filter = config.Filters.OnScreenCharacterFilter; - if (Im.Input.Text("##TreeNameFilter"u8, ref filter, "Filter by Character/Entity Name..."u8)) - { - filterChanged = true; - config.Filters.OnScreenCharacterFilter = filter; - } - - Im.Line.Same(0, checkSpacing); - Im.Item.SetNextWidth(fieldWidth); - filter = config.Filters.OnScreenItemFilter; - if (Im.Input.Text("##NodeFilter"u8, ref filter, "Filter by Item/Part Name or Path..."u8)) - { - filterChanged = true; - config.Filters.OnScreenItemFilter = filter; + using (ImStyleSingle.FrameRounding.Push(0)) + { + var fieldWidth = (Im.ContentRegion.Available.X - checkSpacing * 2.0f - Im.Style.FrameHeightWithSpacing) / 2.0f; + Im.Item.SetNextWidth(fieldWidth); + var filter = config.Filters.OnScreenCharacterFilter; + if (Im.Input.Text("##TreeNameFilter"u8, ref filter, "Filter by Character/Entity Name..."u8)) + { + filterChanged = true; + config.Filters.OnScreenCharacterFilter = filter; + } + + Im.Line.Same(0, checkSpacing); + Im.Item.SetNextWidth(fieldWidth); + filter = config.Filters.OnScreenItemFilter; + if (Im.Input.Text("##NodeFilter"u8, ref filter, "Filter by Item/Part Name or Path..."u8)) + { + filterChanged = true; + config.Filters.OnScreenItemFilter = filter; + } } Im.Line.Same(0, checkSpacing); @@ -412,7 +415,7 @@ public class ResourceTreeViewer( if (node.Internal && !debugMode) return NodeVisibility.Hidden; - var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; + var filterIcon = node.IconFlag is not 0 ? node.IconFlag : parentFilterIcon; if (MatchesFilter(node, filterIcon)) return NodeVisibility.Visible; @@ -430,7 +433,7 @@ public class ResourceTreeViewer( if (!config.Filters.OnScreenTypeFilter.HasFlag(filterIcon)) return false; - if (config.Filters.OnScreenItemFilter.Length == 0) + if (config.Filters.OnScreenItemFilter.Length is 0) return true; return node.Name != null && node.Name.Contains(config.Filters.OnScreenItemFilter, StringComparison.OrdinalIgnoreCase) diff --git a/Penumbra/UI/CollectionTab/CollectionFilter.cs b/Penumbra/UI/CollectionTab/CollectionFilter.cs index c572efdf..2c285253 100644 --- a/Penumbra/UI/CollectionTab/CollectionFilter.cs +++ b/Penumbra/UI/CollectionTab/CollectionFilter.cs @@ -5,10 +5,11 @@ namespace Penumbra.UI.CollectionTab; public sealed class CollectionFilter : TextFilterBase, IUiService { - public CollectionFilter(FilterConfig filterConfig) + public CollectionFilter(Configuration config) { - Set(filterConfig.CollectionFilter); - FilterChanged += () => filterConfig.CollectionFilter = Text; + if (config.RememberCollectionFilters) + Set(config.Filters.CollectionFilter); + FilterChanged += () => config.Filters.CollectionFilter = Text; } public override bool WouldBeVisible(in CollectionSelector.Entry item, int globalIndex) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 9811ab09..c114042f 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -69,7 +69,7 @@ public sealed class CollectionPanel( DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth); DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth); - ImEx.TextMultiColored("Individual"u8, ColorId.NewMod.Value()) + ImEx.TextMultiColored("Individual "u8, ColorId.NewMod.Value()) .Then("Assignments take precedence before anything else and only apply to one specific character or monster."u8) .End(); Im.Dummy(1); diff --git a/Penumbra/UI/MainWindow/ChangedItemsTab.cs b/Penumbra/UI/MainWindow/ChangedItemsTab.cs index 845b97a0..dc2d997f 100644 --- a/Penumbra/UI/MainWindow/ChangedItemsTab.cs +++ b/Penumbra/UI/MainWindow/ChangedItemsTab.cs @@ -16,7 +16,7 @@ public sealed class ChangedItemsTab( CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer, CommunicatorService communicator, - FilterConfig filterConfig) + Configuration config) : ITab { public ReadOnlySpan Label @@ -27,34 +27,46 @@ public sealed class ChangedItemsTab( private Vector2 _buttonSize; - private readonly ChangedItemFilter _filter = new(drawer, filterConfig); + private readonly ChangedItemFilter _filter = new(drawer, config); - private sealed class ChangedItemFilter(ChangedItemDrawer drawer, FilterConfig filterConfig) : IFilter + private sealed class ChangedItemFilter : IFilter { + private readonly ChangedItemDrawer _drawer; + private readonly FilterConfig _filterConfig; + + public ChangedItemFilter(ChangedItemDrawer drawer, Configuration config) + { + _drawer = drawer; + _filterConfig = config.Filters; + if (!config.RememberChangedItemFilters) + Clear(); + } + public bool WouldBeVisible(in Item item, int globalIndex) - => drawer.FilterChangedItemGlobal(item.Name, item.Data, filterConfig.ChangedItemItemFilter) - && (filterConfig.ChangedItemModFilter.Length is 0 - || item.Mods.Any(m => m.Name.Contains(filterConfig.ChangedItemModFilter, StringComparison.OrdinalIgnoreCase))); + => _drawer.FilterChangedItemGlobal(item.Name, item.Data, _filterConfig.ChangedItemItemFilter) + && (_filterConfig.ChangedItemModFilter.Length is 0 + || item.Mods.Any(m => m.Name.Contains(_filterConfig.ChangedItemModFilter, StringComparison.OrdinalIgnoreCase))); public event Action? FilterChanged; public bool DrawFilter(ReadOnlySpan label, Vector2 availableRegion) { + using var style = ImStyleSingle.FrameRounding.Push(0); var varWidth = Im.ContentRegion.Available.X - 450 * Im.Style.GlobalScale - Im.Style.ItemSpacing.X; Im.Item.SetNextWidth(450 * Im.Style.GlobalScale); - var filter = filterConfig.ChangedItemItemFilter; + var filter = _filterConfig.ChangedItemItemFilter; var ret = Im.Input.Text("##changedItemsFilter"u8, ref filter, "Filter Item..."u8); if (ret) - filterConfig.ChangedItemItemFilter = filter; + _filterConfig.ChangedItemItemFilter = filter; Im.Line.Same(); Im.Item.SetNextWidth(varWidth); - filter = filterConfig.ChangedItemModFilter; + filter = _filterConfig.ChangedItemModFilter; if (Im.Input.Text("##changedItemsModFilter"u8, ref filter, "Filter Mods..."u8)) { - ret = true; - filterConfig.ChangedItemModFilter = filter; + ret = true; + _filterConfig.ChangedItemModFilter = filter; } if (ret) @@ -64,16 +76,16 @@ public sealed class ChangedItemsTab( public void Clear() { - filterConfig.ChangedItemModFilter = string.Empty; - filterConfig.ChangedItemItemFilter = string.Empty; - filterConfig.ChangedItemTypeFilter = ChangedItemFlagExtensions.DefaultFlags; + _filterConfig.ChangedItemModFilter = string.Empty; + _filterConfig.ChangedItemItemFilter = string.Empty; + _filterConfig.ChangedItemTypeFilter = ChangedItemFlagExtensions.DefaultFlags; FilterChanged?.Invoke(); } public bool IsEmpty - => filterConfig.ChangedItemModFilter.Length is 0 - && filterConfig.ChangedItemItemFilter.Length is 0 - && filterConfig.ChangedItemTypeFilter is ChangedItemFlagExtensions.DefaultFlags; + => _filterConfig.ChangedItemModFilter.Length is 0 + && _filterConfig.ChangedItemItemFilter.Length is 0 + && _filterConfig.ChangedItemTypeFilter is ChangedItemFlagExtensions.DefaultFlags; } private readonly record struct Item(string Label, IIdentifiedObjectData Data, SingleArray Mods) diff --git a/Penumbra/UI/MainWindow/EffectiveTab.cs b/Penumbra/UI/MainWindow/EffectiveTab.cs index 216aed00..dcab008c 100644 --- a/Penumbra/UI/MainWindow/EffectiveTab.cs +++ b/Penumbra/UI/MainWindow/EffectiveTab.cs @@ -16,7 +16,7 @@ public sealed class EffectiveTab( CollectionManager collectionManager, CollectionSelectHeader collectionHeader, CommunicatorService communicatorService, - FilterConfig filterConfig) + Configuration config) : ITab { public ReadOnlySpan Label @@ -32,7 +32,7 @@ public sealed class EffectiveTab( public TabType Identifier => TabType.EffectiveChanges; - private readonly PairFilter _filter = new(new GamePathFilter(filterConfig), new FullPathFilter(filterConfig)); + private readonly PairFilter _filter = new(new GamePathFilter(config), new FullPathFilter(config)); private sealed class Cache : BasicFilterCache, IPanel { @@ -143,10 +143,11 @@ public sealed class EffectiveTab( private sealed class GamePathFilter : RegexFilterBase { - public GamePathFilter(FilterConfig config) + public GamePathFilter(Configuration config) { - Set(config.EffectiveChangesGamePathFilter); - FilterChanged += () => config.EffectiveChangesGamePathFilter = Text; + if (config.RememberEffectiveChangesFilters) + Set(config.Filters.EffectiveChangesGamePathFilter); + FilterChanged += () => config.Filters.EffectiveChangesGamePathFilter = Text; } protected override string ToFilterString(in Item item, int globalIndex) @@ -155,10 +156,11 @@ public sealed class EffectiveTab( private sealed class FullPathFilter : RegexFilterBase { - public FullPathFilter(FilterConfig config) + public FullPathFilter(Configuration config) { - Set(config.EffectiveChangesFilePathFilter); - FilterChanged += () => config.EffectiveChangesFilePathFilter = Text; + if (config.RememberEffectiveChangesFilters) + Set(config.Filters.EffectiveChangesFilePathFilter); + FilterChanged += () => config.Filters.EffectiveChangesFilePathFilter = Text; } protected override string ToFilterString(in Item item, int globalIndex) diff --git a/Penumbra/UI/MainWindow/MainTabBar.cs b/Penumbra/UI/MainWindow/MainTabBar.cs index 139c8945..2dcf07aa 100644 --- a/Penumbra/UI/MainWindow/MainTabBar.cs +++ b/Penumbra/UI/MainWindow/MainTabBar.cs @@ -26,9 +26,13 @@ public sealed class MainTabBar : TabBar, IDisposable ResourceTab resources, Watcher watcher, OnScreenTab onScreen, - MessagesTab messages, EphemeralConfig config, CommunicatorService communicator, ModFileSystem modFileSystem) + MessagesTab messages, + ManagementTab.ManagementTab management, + EphemeralConfig config, + CommunicatorService communicator, + ModFileSystem modFileSystem) : base(nameof(MainTabBar), log, settings, collections, mods, changedItems, effectiveChanges, onScreen, - resources, watcher, debug, messages) + resources, watcher, debug, messages, management) { _config = config; _modFileSystem = modFileSystem; diff --git a/Penumbra/UI/MainWindow/OnScreenTab.cs b/Penumbra/UI/MainWindow/OnScreenTab.cs index 2d1f4c62..d13430a6 100644 --- a/Penumbra/UI/MainWindow/OnScreenTab.cs +++ b/Penumbra/UI/MainWindow/OnScreenTab.cs @@ -4,9 +4,18 @@ using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.MainWindow; -public sealed class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab +public sealed class OnScreenTab : ITab { - private readonly ResourceTreeViewer _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); + private readonly ResourceTreeViewer _viewer; + + public OnScreenTab(Configuration config, ResourceTreeViewerFactory resourceTreeViewerFactory) + { + // Hack to handle config settings because no specific filters have been made yet. + if (!config.RememberOnScreenFilters) + config.Filters.ClearOnScreenFilters(); + + _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); + } public ReadOnlySpan Label => "On-Screen"u8; diff --git a/Penumbra/UI/ManagementTab/ManagementTab.cs b/Penumbra/UI/ManagementTab/ManagementTab.cs new file mode 100644 index 00000000..aab5cfe8 --- /dev/null +++ b/Penumbra/UI/ManagementTab/ManagementTab.cs @@ -0,0 +1,196 @@ +using ImSharp; +using Luna; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Mods; +using Penumbra.Mods.Manager; + +namespace Penumbra.UI.ManagementTab; + +public enum ManagementTabType +{ + UnusedMods, + DuplicateMods, + Cleanup, +} + +public sealed class DuplicateModsTab(ModManager mods, CollectionStorage collections) : ITab +{ + public ReadOnlySpan Label + => "Duplicate Mods"u8; + + public ManagementTabType Identifier + => ManagementTabType.DuplicateMods; + + public void DrawContent() + { + using var table = Im.Table.Begin("duplicates"u8, 4, TableFlags.RowBackground); + if (!table) + return; + + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(mods, collections)); + foreach (var item in cache.Items) + { + table.DrawFrameColumn(item.Name); + foreach (var (mod, date, path, enabledCollections) in item.Data) + { + table.GoToColumn(0); + table.DrawFrameColumn(path); + table.DrawFrameColumn(date); + table.DrawFrameColumn($"{enabledCollections.Length}"); + if (Im.Item.Hovered()) + { + using var tt = Im.Tooltip.Begin(); + foreach (var collection in enabledCollections) + Im.Text(collection.Identity.Name); + } + + table.NextRow(); + } + } + } + + private sealed class Cache(ModManager mods, CollectionStorage collections) : BasicCache + { + public readonly record struct CacheItem( + StringU8 Name, + (Mod Mod, StringU8 CreationDate, StringU8 Path, ModCollection[] Collections)[] Data); + + public List Items = []; + + public override void Update() + { + if (!Dirty.HasFlag(IManagedCache.DirtyFlags.Custom)) + return; + + Items.Clear(); + Items.AddRange(mods.GroupBy(m => m.Name) + .Select(kvp => new CacheItem(new StringU8(kvp.Key), + kvp.Select(m => (m, new StringU8($"{DateTimeOffset.FromUnixTimeMilliseconds(m.ImportDate)}"), + new StringU8(m.Path.CurrentPath), + collections.Where(c => c.GetActualSettings(m.Index).Settings?.Enabled is true).ToArray())) + .OrderByDescending(t => t.Item4.Length).ThenBy(t => t.m.ImportDate).ToArray())).Where(p => p.Data.Length > 1)); + + Dirty = IManagedCache.DirtyFlags.Clean; + } + } +} + +public sealed class UnusedModsTab(ModConfigUpdater modConfigUpdater) : ITab +{ + public ReadOnlySpan Label + => "Unused Mods"u8; + + public ManagementTabType Identifier + => ManagementTabType.UnusedMods; + + public void DrawContent() + { + using var table = Im.Table.Begin("unused"u8, 5, TableFlags.RowBackground | TableFlags.SizingFixedFit); + if (!table) + return; + + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(modConfigUpdater)); + using var clipper = new Im.ListClipper(cache.Data.Count, Im.Style.FrameHeightWithSpacing); + foreach (var item in clipper.Iterate(cache.Data)) + { + table.DrawFrameColumn(item.ModName); + table.DrawFrameColumn(item.ModPath); + table.DrawFrameColumn(item.Duration.Utf8); + table.DrawFrameColumn(item.ModSizeString.Utf8); + table.NextColumn(); + if (item.Notes.Length > 0) + { + ImEx.Icon.DrawAligned(LunaStyle.InfoIcon, LunaStyle.FavoriteColor); + var hovered = Im.Item.Hovered(); + Im.Line.SameInner(); + Im.Text("Notes"u8); + if (hovered || Im.Item.Hovered()) + { + using var tt = Im.Tooltip.Begin(); + using (Im.Group()) + { + foreach (var (plugin, _) in item.Notes) + Im.Text(plugin.Utf8); + } + + using (Im.Group()) + { + foreach (var (_, node) in item.Notes) + Im.Text(node.Utf8); + } + } + } + } + } + + + private sealed class Cache(ModConfigUpdater modConfigUpdater) : BasicCache + { + public readonly record struct CacheItem( + Mod Mod, + StringU8 ModName, + StringU8 ModPath, + long ModSize, + StringPair ModSizeString, + StringPair Duration, + (StringPair, StringPair)[] Notes) + { + public CacheItem(Mod mod, (string, string)[] Notes, DateTime now) + : this(mod, new StringU8(mod.Name), new StringU8(mod.Path.CurrentPath), 0, StringPair.Empty, + new StringPair(FormattingFunctions.DurationString(mod.LastConfigEdit, now)), + Notes.Select(n => (new StringPair(n.Item1), new StringPair(n.Item2))).ToArray()) + { } + } + + public List Data = []; + + public override void Update() + { + if (!Dirty.HasFlag(IManagedCache.DirtyFlags.Custom)) + return; + + var now = DateTime.UtcNow; + Data.Clear(); + foreach (var (mod, notes) in modConfigUpdater.ListUnusedMods(TimeSpan.Zero).OrderBy(mod => mod.Item1.LastConfigEdit)) + Data.Add(new CacheItem(mod, notes, now)); + Dirty = IManagedCache.DirtyFlags.Clean; + } + } +} + +public sealed class CleanupTab : ITab +{ + public ReadOnlySpan Label + => "General Cleanup"u8; + + public ManagementTabType Identifier + => ManagementTabType.Cleanup; + + public void DrawContent() + { } +} + +public sealed class ManagementTab : TabBar, ITab +{ + public new ReadOnlySpan Label + => base.Label; + + public TabType Identifier + => TabType.Management; + + public ManagementTab(Logger log, + EphemeralConfig config, + UnusedModsTab unusedMods, + DuplicateModsTab duplicateMods, + CleanupTab cleanup) + : base("Management", log, unusedMods, duplicateMods, cleanup) + { + NextTab = config.SelectedManagementTab; + TabSelected.Subscribe((in tab) => config.SelectedManagementTab = tab, 0); + } + + public void DrawContent() + => Draw(); +} diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 9a7b6c19..5f32e09d 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -29,7 +29,7 @@ public class ModPanelTabBar : TabBar public ModPanelTabBar(ModEditWindowFactory modEditWindowFactory, ModPanelSettingsTab settings, ModPanelDescriptionTab description, ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, - TutorialService tutorial, ModPanelCollectionsTab collections, Logger log) + TutorialService tutorial, ModPanelCollectionsTab collections, Logger log, EphemeralConfig config) : base(nameof(ModPanelTabBar), log, settings, description, conflicts, changedItems, collections, edit) { Flags = TabBarFlags.NoTooltip | TabBarFlags.FittingPolicyScroll; @@ -37,7 +37,9 @@ public class ModPanelTabBar : TabBar Edit = edit; _modManager = modManager; _tutorial = tutorial; + NextTab = config.SelectedModPanelTab; Buttons.AddButton(new AdvancedEditingButton(this, modEditWindowFactory), 0); + TabSelected.Subscribe((in v) => config.SelectedModPanelTab = v, 0); } private sealed class AdvancedEditingButton(ModPanelTabBar parent, ModEditWindowFactory editFactory) : BaseButton diff --git a/Penumbra/UI/ModsTab/Selector/Buttons/MoveModInput.cs b/Penumbra/UI/ModsTab/Selector/Buttons/MoveModInput.cs new file mode 100644 index 00000000..e0cb0eff --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/Buttons/MoveModInput.cs @@ -0,0 +1,34 @@ +using ImSharp; +using Luna; + +namespace Penumbra.UI.ModsTab.Selector; + +public sealed class MoveModInput(ModFileSystemDrawer fileSystem) : BaseButton +{ + /// + public override ReadOnlySpan Label(in IFileSystemData _) + => "##Move"u8; + + /// Replaces the normal menu item handling for a text input, so the other fields are not used. + /// + public override bool DrawMenuItem(in IFileSystemData data) + { + var currentPath = data.FullPath; + using var style = Im.Style.PushDefault(ImStyleDouble.FramePadding); + MenuSeparator.DrawSeparator(); + Im.Text("Move Mod:"u8); + if (Im.Window.Appearing) + Im.Keyboard.SetFocusHere(); + var ret = Im.Input.Text(Label(data), ref currentPath, flags: InputTextFlags.EnterReturnsTrue); + Im.Tooltip.OnHover( + "Enter a full path here to move the mod or change its search path. Creates all required parent directories, if possible."u8); + if (!ret) + return false; + + fileSystem.FileSystem.RenameAndMove(data, currentPath); + fileSystem.FileSystem.ExpandAllAncestors(data); + Im.Popup.CloseCurrent(); + + return ret; + } +} diff --git a/Penumbra/UI/ModsTab/Selector/Buttons/RenameModInput.cs b/Penumbra/UI/ModsTab/Selector/Buttons/RenameModInput.cs new file mode 100644 index 00000000..39b67bd4 --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/Buttons/RenameModInput.cs @@ -0,0 +1,34 @@ +using ImSharp; +using Luna; +using Penumbra.Mods; + +namespace Penumbra.UI.ModsTab.Selector; + +public sealed class RenameModInput(ModFileSystemDrawer fileSystem) : BaseButton +{ + /// + public override ReadOnlySpan Label(in IFileSystemData _) + => "##Rename"u8; + + /// Replaces the normal menu item handling for a text input, so the other fields are not used. + /// + public override bool DrawMenuItem(in IFileSystemData data) + { + var mod = (Mod)data.Value; + var currentName = mod.Name; + using var style = Im.Style.PushDefault(ImStyleDouble.FramePadding); + MenuSeparator.DrawSeparator(); + Im.Text("Rename Mod:"u8); + if (Im.Window.Appearing) + Im.Keyboard.SetFocusHere(); + var ret = Im.Input.Text(Label(data), ref currentName, flags: InputTextFlags.EnterReturnsTrue); + Im.Tooltip.OnHover("Enter a new name here to rename the changed mod."u8); + if (!ret) + return false; + + fileSystem.ModManager.DataEditor.ChangeModName(mod, currentName); + Im.Popup.CloseCurrent(); + + return ret; + } +} diff --git a/Penumbra/UI/ModsTab/Selector/Filter/ModFilter.cs b/Penumbra/UI/ModsTab/Selector/Filter/ModFilter.cs index 05d74a88..0247e949 100644 --- a/Penumbra/UI/ModsTab/Selector/Filter/ModFilter.cs +++ b/Penumbra/UI/ModsTab/Selector/Filter/ModFilter.cs @@ -16,16 +16,20 @@ public sealed class ModFilter : TokenizedFilter { - filterConfig.ModFilter = Text; - filterConfig.ModTypeFilter = StateFilter; + config.Filters.ModFilter = Text; + config.Filters.ModTypeFilter = StateFilter; }; } diff --git a/Penumbra/UI/ModsTab/Selector/ModFileSystemDrawer.cs b/Penumbra/UI/ModsTab/Selector/ModFileSystemDrawer.cs index 7ad61722..9c8995ba 100644 --- a/Penumbra/UI/ModsTab/Selector/ModFileSystemDrawer.cs +++ b/Penumbra/UI/ModsTab/Selector/ModFileSystemDrawer.cs @@ -1,4 +1,3 @@ -using ImSharp; using Luna; using Penumbra.Collections.Manager; using Penumbra.Mods; @@ -8,7 +7,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab.Selector; -public sealed class ModFileSystemDrawer : FileSystemDrawer +public sealed class ModFileSystemDrawer : FileSystemDrawer, IDisposable { public readonly ModManager ModManager; public readonly CollectionManager CollectionManager; @@ -20,7 +19,7 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer Config.ShowRenameChanged -= SetRenameFields; + + private void SetRenameFields(RenameField newField, RenameField _) + { + DataContext.RemoveButtons(); + DataContext.RemoveButtons(); + switch (newField) + { + case RenameField.RenameSearchPath: DataContext.AddButton(new RenameModInput(this), -1000); break; + case RenameField.RenameData: DataContext.AddButton(new MoveModInput(this), -1000); break; + case RenameField.BothSearchPathPrio: + DataContext.AddButton(new RenameModInput(this), -1000); + DataContext.AddButton(new MoveModInput(this), -1001); + break; + case RenameField.BothDataPrio: + DataContext.AddButton(new RenameModInput(this), -1001); + DataContext.AddButton(new MoveModInput(this), -1000); + break; + } + } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 483dce81..aeec6970 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -12,17 +12,14 @@ using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.UI.Classes; namespace Penumbra.UI.ResourceWatcher; public sealed class ResourceWatcher : IDisposable, ITab { - public const int DefaultMaxEntries = 1024; - public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; + public const int DefaultMaxEntries = 500; - private readonly Configuration _config; - private readonly EphemeralConfig _ephemeral; + private readonly FilterConfig _config; private readonly ResourceService _resources; private readonly ResourceLoader _loader; private readonly ResourceHandleDestructor _destructor; @@ -30,50 +27,54 @@ public sealed class ResourceWatcher : IDisposable, ITab private readonly ObservableList _records = []; private readonly ConcurrentQueue _newRecords = []; private readonly ResourceWatcherTable _table; - private string _logFilter = string.Empty; - private Regex? _logRegex; - private int _newMaxEntries; + private readonly RegexFilter _filter = new(); - public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader, + public unsafe ResourceWatcher(ActorManager actors, FilterConfig config, ResourceService resources, ResourceLoader loader, ResourceHandleDestructor destructor) { - _actors = actors; _config = config; - _ephemeral = config.Ephemeral; + _actors = actors; _resources = resources; _destructor = destructor; _loader = loader; - _table = new ResourceWatcherTable(config.Filters, _records); + _table = new ResourceWatcherTable(_config, _records); _resources.ResourceRequested += OnResourceRequested; _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); _loader.ResourceLoaded += OnResourceLoaded; _loader.ResourceComplete += OnResourceComplete; _loader.FileLoaded += OnFileLoaded; _loader.PapRequested += OnPapRequested; - UpdateFilter(_ephemeral.ResourceLoggingFilter, false); - _newMaxEntries = _config.MaxResourceWatcherRecords; + _filter.Set(_config.ResourceLoggerLogFilter); + _filter.FilterChanged += () => _config.ResourceLoggerLogFilter = _filter.Text; } private void OnPapRequested(Utf8GamePath original, FullPath? _1, ResolveData _2) { - if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) + if (_config.ResourceLoggerWriteToLog && Filter(original.Path, out var path)) { - Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested asynchronously."); + Penumbra.Log.Information($"[ResourceLoader] [REQ] {path} was requested asynchronously."); if (_1.HasValue) Penumbra.Log.Information( - $"[ResourceLoader] [LOAD] Resolved {_1.Value.FullName} for {match} from collection {_2.ModCollection} for object 0x{_2.AssociatedGameObject:X}."); + $"[ResourceLoader] [LOAD] Resolved {_1.Value.FullName} for {path} from collection {_2.ModCollection} for object 0x{_2.AssociatedGameObject:X}."); } - if (!_ephemeral.EnableResourceWatcher) + if (!_config.ResourceLoggerEnabled) return; var record = _1.HasValue ? Record.CreateRequest(original.Path, false, _1.Value, _2) : Record.CreateRequest(original.Path, false); - if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record)) Enqueue(record); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool Filter(CiByteString path, out string ret) + { + ret = path.ToString(); + return _filter.WouldBeVisible(ret); + } + public unsafe void Dispose() { Clear(); @@ -103,12 +104,8 @@ public sealed class ResourceWatcher : IDisposable, ITab UpdateRecords(); Im.Cursor.Y += Im.Style.TextHeightWithSpacing / 2; - var isEnabled = _ephemeral.EnableResourceWatcher; - if (Im.Checkbox("Enable"u8, ref isEnabled)) - { - _ephemeral.EnableResourceWatcher = isEnabled; - _ephemeral.Save(); - } + if (Im.Checkbox("Enable"u8, _config.ResourceLoggerEnabled)) + _config.ResourceLoggerEnabled ^= true; Im.Line.Same(); DrawMaxEntries(); @@ -117,20 +114,12 @@ public sealed class ResourceWatcher : IDisposable, ITab Clear(); Im.Line.Same(); - var onlyMatching = _ephemeral.OnlyAddMatchingResources; - if (Im.Checkbox("Store Only Matching"u8, ref onlyMatching)) - { - _ephemeral.OnlyAddMatchingResources = onlyMatching; - _ephemeral.Save(); - } + if (Im.Checkbox("Store Only Matching"u8, _config.ResourceLoggerStoreOnlyMatching)) + _config.ResourceLoggerStoreOnlyMatching ^= true; Im.Line.Same(); - var writeToLog = _ephemeral.EnableResourceLogging; - if (Im.Checkbox("Write to Log"u8, ref writeToLog)) - { - _ephemeral.EnableResourceLogging = writeToLog; - _ephemeral.Save(); - } + if (Im.Checkbox("Write to Log"u8, _config.ResourceLoggerWriteToLog)) + _config.ResourceLoggerWriteToLog ^= true; Im.Line.Same(); DrawFilterInput(); @@ -141,70 +130,22 @@ public sealed class ResourceWatcher : IDisposable, ITab } private void DrawFilterInput() - { - Im.Item.SetNextWidth(Im.ContentRegion.Available.X); - var tmp = _logFilter; - var invalidRegex = _logRegex is null && _logFilter.Length > 0; - using var color = ImStyleBorder.Frame.Push(Colors.RegexWarningBorder, Im.Style.GlobalScale, invalidRegex); - if (Im.Input.Text("##logFilter"u8, ref tmp, "If path matches this Regex..."u8)) - UpdateFilter(tmp, true); - } - - private void UpdateFilter(string newString, bool config) - { - if (newString == _logFilter) - return; - - _logFilter = newString; - try - { - _logRegex = new Regex(_logFilter, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - } - catch - { - _logRegex = null; - } - - if (config) - { - _ephemeral.ResourceLoggingFilter = newString; - _ephemeral.Save(); - } - } - - private bool FilterMatch(CiByteString path, out string match) - { - match = path.ToString(); - return _logFilter.Length == 0 || (_logRegex?.IsMatch(match) ?? false) || match.Contains(_logFilter, StringComparison.OrdinalIgnoreCase); - } - + => _filter.DrawFilter("If path matches this Regex..."u8, Im.ContentRegion.Available); private void DrawMaxEntries() { Im.Item.SetNextWidthScaled(80); - Im.Input.Scalar("Max. Entries"u8, ref _newMaxEntries); - var change = Im.Item.DeactivatedAfterEdit; + if (ImEx.InputOnDeactivation.Scalar("Max. Entries"u8, _config.ResourceLoggerMaxEntries, out var newValue)) + _config.ResourceLoggerMaxEntries = Math.Max(16, newValue); + if (Im.Item.RightClicked() && Im.Io.KeyControl) - { - change = true; - _newMaxEntries = DefaultMaxEntries; - } + _config.ResourceLoggerMaxEntries = DefaultMaxEntries; - var maxEntries = _config.MaxResourceWatcherRecords; - if (maxEntries != DefaultMaxEntries && Im.Item.Hovered()) - Im.Tooltip.Set($"CTRL + Right-Click to reset to default {DefaultMaxEntries}."); + if (_config.ResourceLoggerMaxEntries is not DefaultMaxEntries && Im.Item.Hovered()) + Im.Tooltip.Set("Control + Right-Click to reset to default 500."u8); - if (!change) - return; - - _newMaxEntries = Math.Max(16, _newMaxEntries); - if (_newMaxEntries == maxEntries) - return; - - _config.MaxResourceWatcherRecords = _newMaxEntries; - _config.Save(); - if (_newMaxEntries > _records.Count) - _records.RemoveRange(0, _records.Count - _newMaxEntries); + if (_records.Count > _config.ResourceLoggerMaxEntries) + _records.RemoveRange(0, _records.Count - _config.ResourceLoggerMaxEntries); } private void UpdateRecords() @@ -216,49 +157,49 @@ public sealed class ResourceWatcher : IDisposable, ITab while (_newRecords.TryDequeue(out var rec) && count-- > 0) _records.Add(rec); - if (_records.Count > _config.MaxResourceWatcherRecords) - _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); + if (_records.Count > _config.ResourceLoggerMaxEntries) + _records.RemoveRange(0, _records.Count - _config.ResourceLoggerMaxEntries); } private unsafe void OnResourceRequested(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { - if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) + if (_config.ResourceLoggerWriteToLog && Filter(original.Path, out var match)) Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}"); - if (!_ephemeral.EnableResourceWatcher) + if (!_config.ResourceLoggerEnabled) return; var record = Record.CreateRequest(original.Path, sync); - if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record)) Enqueue(record); } private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) { - if (_ephemeral.EnableResourceLogging) + if (_config.ResourceLoggerWriteToLog) { - var log = FilterMatch(path.Path, out var name); + var log = Filter(path.Path, out var name); var name2 = string.Empty; - if (manipulatedPath != null) - log |= FilterMatch(manipulatedPath.Value.InternalName, out name2); + if (manipulatedPath is not null) + log |= Filter(manipulatedPath.Value.InternalName, out name2); if (log) { - var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name; + var pathString = manipulatedPath is not null ? $"custom file {name2} instead of {name}" : name; Penumbra.Log.Information( $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.Identity.AnonymizedName} for {Name(data, "no associated object.")} (Refcount {handle->RefCount}) "); } } - if (!_ephemeral.EnableResourceWatcher) + if (!_config.ResourceLoggerEnabled) return; var record = manipulatedPath == null ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) : Record.CreateLoad(manipulatedPath.Value, path.Path, handle, data.ModCollection, Name(data)); - if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record)) Enqueue(record); } @@ -268,43 +209,43 @@ public sealed class ResourceWatcher : IDisposable, ITab if (!isAsync) return; - if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) + if (_config.ResourceLoggerWriteToLog && Filter(path, out var match)) Penumbra.Log.Information( $"[ResourceLoader] [DONE] [{resource->FileType}] Finished loading {match} into 0x{(ulong)resource:X}, state {resource->LoadState}."); - if (!_ephemeral.EnableResourceWatcher) + if (!_config.ResourceLoggerEnabled) return; var record = Record.CreateResourceComplete(path, resource, original, additionalData); - if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record)) Enqueue(record); } private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan _) { - if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) + if (_config.ResourceLoggerWriteToLog && Filter(path, out var match)) Penumbra.Log.Information( $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}."); - if (!_ephemeral.EnableResourceWatcher) + if (!_config.ResourceLoggerEnabled) return; var record = Record.CreateFileLoad(path, resource, success, custom); - if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record)) Enqueue(record); } private unsafe void OnResourceDestroyed(in ResourceHandleDestructor.Arguments arguments) { - if (_ephemeral.EnableResourceLogging && FilterMatch(arguments.ResourceHandle->FileName(), out var match)) + if (_config.ResourceLoggerWriteToLog && Filter(arguments.ResourceHandle->FileName(), out var match)) Penumbra.Log.Information( $"[ResourceLoader] [DEST] [{arguments.ResourceHandle->FileType}] Destroyed {match} at 0x{(ulong)arguments.ResourceHandle:X}."); - if (!_ephemeral.EnableResourceWatcher) + if (!_config.ResourceLoggerEnabled) return; var record = Record.CreateDestruction(arguments.ResourceHandle); - if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record)) Enqueue(record); } @@ -337,7 +278,7 @@ public sealed class ResourceWatcher : IDisposable, ITab private void Enqueue(Record record) { // Discard entries that exceed the number of records. - while (_newRecords.Count >= _config.MaxResourceWatcherRecords) + while (_newRecords.Count >= _config.ResourceLoggerMaxEntries) _newRecords.TryDequeue(out _); _newRecords.Enqueue(record); } diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 91cbcb82..c9ff1e87 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -20,14 +20,15 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res public bool IsVisible => config.DebugMode; - public readonly ResourceFilter Filter = new(config.Filters); + public readonly ResourceFilter Filter = new(config); public sealed class ResourceFilter : Utf8FilterBase { - public ResourceFilter(FilterConfig filterConfig) + public ResourceFilter(Configuration config) { - Set(new StringU8(filterConfig.ResourceManagerFilter)); - FilterChanged += () => filterConfig.ResourceManagerFilter = Text.ToString(); + if (config.RememberResourceManagerFilters) + Set(new StringU8(config.Filters.ResourceManagerFilter)); + FilterChanged += () => config.Filters.ResourceManagerFilter = Text.ToString(); } protected override ReadOnlySpan ToFilterString(in ResourceHandle item, int globalIndex) @@ -58,9 +59,9 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res Penumbra.Dynamis.DrawPointer(resourceManager.ResourceManager); } - private float _hashColumnWidth; - private float _pathColumnWidth; - private float _refsColumnWidth; + private float _hashColumnWidth; + private float _pathColumnWidth; + private float _refsColumnWidth; /// Draw a single resource map. private unsafe void DrawResourceMap(ResourceCategory category, uint ext, @@ -91,19 +92,23 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res return; Im.Table.DrawColumn($"0x{hash:X8}"); + if (Im.Item.Clicked()) + Im.Clipboard.Set($"0x{hash:X8}"); Im.Table.NextColumn(); Penumbra.Dynamis.DrawPointer(r); var resource = (Interop.Structs.ResourceHandle*)r; Im.Table.DrawColumn(resource->FileName().Span); if (Im.Item.Clicked()) + { Im.Clipboard.Set(resource->FileName().Span); + } else if (Im.Item.RightClicked()) { var data = resource->CsHandle.GetData(); if (data != null) { var length = (int)resource->CsHandle.GetLength(); - Im.Clipboard.Set(StringU8.Join((byte) ' ', + Im.Clipboard.Set(StringU8.Join((byte)' ', new ReadOnlySpan(data, length).ToArray().Select(b => b.ToString("X2")))); } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 22d9d1b1..78afd0bc 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -413,6 +413,27 @@ public sealed class SettingsTab : ITab _config.HideUiInGPose = v; _pluginInterface.UiBuilder.DisableGposeUiHide = !v; }); + + Im.Separator(); + Checkbox("Remember Mod Filters Across Sessions"u8, + "Whether filters in the Mods tab should remember their input and start with their respective lists filtered identically to the last session."u8, + _config.RememberModFilters, v => _config.RememberModFilters = v); + Checkbox("Remember Collection Filters Across Sessions"u8, + "Whether filters in the Collections tab should remember their input and start with their respective lists filtered identically to the last session."u8, + _config.RememberCollectionFilters, v => _config.RememberCollectionFilters = v); + Checkbox("Remember Changed Items Filters Across Sessions"u8, + "Whether filters in the Changed Items tab should remember their input and start with their respective lists filtered identically to the last session."u8, + _config.RememberChangedItemFilters, v => _config.RememberChangedItemFilters= v); + Checkbox("Remember Effective Changes Filters Across Sessions"u8, + "Whether filters in the Effective Changes tab should remember their input and start with their respective lists filtered identically to the last session."u8, + _config.RememberEffectiveChangesFilters, v => _config.RememberEffectiveChangesFilters = v); + Checkbox("Remember On-Screen Filters Across Sessions"u8, + "Whether filters in the On-Screen tab should remember their input and start with their respective lists filtered identically to the last session."u8, + _config.RememberOnScreenFilters, v => _config.RememberOnScreenFilters = v); + Checkbox("Remember Resource Manager Filters Across Sessions"u8, + "Whether filters in the Resource Manager tab should remember their input and start with their respective lists filtered identically to the last session."u8, + _config.RememberResourceManagerFilters, v => _config.RememberResourceManagerFilters = v); + } /// Draw all settings that do not fit into other categories. @@ -433,7 +454,7 @@ public sealed class SettingsTab : ITab if (v) { _config.Filters.ModChangedItemTypeFilter = ChangedItemFlagExtensions.AllFlags; - _config.Filters.ChangedItemTypeFilter = ChangedItemFlagExtensions.AllFlags; + _config.Filters.ChangedItemTypeFilter = ChangedItemFlagExtensions.AllFlags; _config.Ephemeral.Save(); } }); @@ -520,15 +541,10 @@ public sealed class SettingsTab : ITab using (var combo = Im.Combo.Begin("##renameSettings"u8, _config.ShowRename.ToNameU8())) { if (combo) - foreach (var value in Enum.GetValues()) + foreach (var value in RenameField.Values) { if (Im.Selectable(value.ToNameU8(), _config.ShowRename == value)) - { _config.ShowRename = value; - // TODO - // _selector.SetRenameSearchPath(value); - _config.Save(); - } Im.Tooltip.OnHover(value.Tooltip()); }