Improve filter stuff, add start of management tab.

This commit is contained in:
Ottermandias 2026-01-22 13:21:00 +01:00
parent 3b4cab2a1a
commit 0bf7278bb8
30 changed files with 613 additions and 258 deletions

2
Luna

@ -1 +1 @@
Subproject commit 1153628fae18b5e720841b73c5bff9a56652ab7b Subproject commit 06094555dc93eb302d7e823a84edab5926450db9

@ -1 +1 @@
Subproject commit 52a3216a525592205198303df2844435e382cf87 Subproject commit 247b173d2fdee2d0c18666972114e61f77aef6b6

View file

@ -7,7 +7,7 @@ using Penumbra.Services;
namespace Penumbra.Api.Api; namespace Penumbra.Api.Api;
public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public sealed partial class ModsApi : IPenumbraApiMods, IApiService, IDisposable
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly ModManager _modManager; private readonly ModManager _modManager;
@ -15,10 +15,11 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModFileSystem _modFileSystem; private readonly ModFileSystem _modFileSystem;
private readonly MigrationManager _migrationManager; private readonly MigrationManager _migrationManager;
private readonly ModConfigUpdater _modConfigUpdater;
private readonly Logger _log; private readonly Logger _log;
public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem, 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; _modManager = modManager;
_modImportManager = modImportManager; _modImportManager = modImportManager;
@ -27,6 +28,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
_communicator = communicator; _communicator = communicator;
_migrationManager = migrationManager; _migrationManager = migrationManager;
_log = log; _log = log;
_modConfigUpdater = modConfigUpdater;
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods);
_communicator.PcpCreation.Subscribe(OnPcpCreation, PcpCreation.Priority.ApiMods); _communicator.PcpCreation.Subscribe(OnPcpCreation, PcpCreation.Priority.ApiMods);
_communicator.PcpParsing.Subscribe(OnPcpParsing, PcpParsing.Priority.ApiMods); _communicator.PcpParsing.Subscribe(OnPcpParsing, PcpParsing.Priority.ApiMods);
@ -120,6 +122,12 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
public event Action<JObject, ushort, string>? CreatingPcp; public event Action<JObject, ushort, string>? CreatingPcp;
public event Action<JObject, string, Guid>? ParsingPcp; public event Action<JObject, string, Guid>? ParsingPcp;
public event Action<string, string, Dictionary<Assembly, (bool MarkUsed, string Note)>>? ModUsageQueried
{
add => _modConfigUpdater.ModUsageQueried += value;
remove => _modConfigUpdater.ModUsageQueried -= value;
}
public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName) public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
{ {
if (!_modManager.TryGetMod(modDirectory, modName, out var mod) || mod.Node is not { } node) if (!_modManager.TryGetMod(modDirectory, modName, out var mod) || mod.Node is not { } node)

View file

@ -56,6 +56,7 @@ public sealed class IpcProviders : IDisposable, IApiService, IRequiredService
IpcSubscribers.ModMoved.Provider(pi, api.Mods), IpcSubscribers.ModMoved.Provider(pi, api.Mods),
IpcSubscribers.CreatingPcp.Provider(pi, api.Mods), IpcSubscribers.CreatingPcp.Provider(pi, api.Mods),
IpcSubscribers.ParsingPcp.Provider(pi, api.Mods), IpcSubscribers.ParsingPcp.Provider(pi, api.Mods),
IpcSubscribers.ModUsageQueried.Provider(pi, api.Mods),
IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.GetModPath.Provider(pi, api.Mods),
IpcSubscribers.SetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods),
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),

View file

@ -1,6 +1,7 @@
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Luna; using Luna;
using Luna.Generators;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
@ -8,13 +9,12 @@ using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab;
using Penumbra.UI.ModsTab.Selector; using Penumbra.UI.ModsTab.Selector;
using Penumbra.UI.ResourceWatcher;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra; namespace Penumbra;
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration, ISavable, IService public partial class Configuration : IPluginConfiguration, ISavable, IService
{ {
[JsonIgnore] [JsonIgnore]
private readonly SaveService _saveService; private readonly SaveService _saveService;
@ -56,24 +56,45 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool AutoSelectCollection { get; set; } = false; public bool AutoSelectCollection { get; set; } = false;
public bool ShowModsInLobby { get; set; } = true; public bool ShowModsInLobby { get; set; } = true;
public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true;
public bool UseCharacterCollectionsInCards { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true;
public bool UseCharacterCollectionInInspect { get; set; } = true; public bool UseCharacterCollectionInInspect { get; set; } = true;
public bool UseCharacterCollectionInTryOn { get; set; } = true; public bool UseCharacterCollectionInTryOn { get; set; } = true;
public bool UseOwnerNameForCharacterCollection { get; set; } = true; public bool UseOwnerNameForCharacterCollection { get; set; } = true;
public bool UseNoModsInInspect { get; set; } = false; public bool UseNoModsInInspect { get; set; } = false;
public bool HideChangedItemFilters { get; set; } = false; public bool HideChangedItemFilters { get; set; } = false;
public bool ReplaceNonAsciiOnImport { get; set; } = false; public bool ReplaceNonAsciiOnImport { get; set; } = false;
public bool HidePrioritiesInSelector { get; set; } = false; public bool HidePrioritiesInSelector { get; set; } = false;
public bool HideRedrawBar { get; set; } = false; public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public bool DefaultTemporaryMode { get; set; } = false; public bool DefaultTemporaryMode { get; set; } = false;
public bool EnableDirectoryWatch { get; set; } = false; public bool EnableDirectoryWatch { get; set; } = false;
public bool EnableAutomaticModImport { get; set; } = false; public bool EnableAutomaticModImport { get; set; } = false;
public bool EnableCustomShapes { get; set; } = true; public bool EnableCustomShapes { get; set; } = true;
public PcpSettings PcpSettings = new(); public PcpSettings PcpSettings = new();
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
[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 ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
public int OptionGroupCollapsibleMin { get; set; } = 5; public int OptionGroupCollapsibleMin { get; set; } = 5;
@ -84,7 +105,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
#else #else
public bool DebugMode { get; set; } = false; public bool DebugMode { get; set; } = false;
#endif #endif
public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries;
[JsonConverter(typeof(SortModeConverter))] [JsonConverter(typeof(SortModeConverter))]
[JsonProperty(Order = int.MaxValue)] [JsonProperty(Order = int.MaxValue)]

View file

@ -1,39 +1,39 @@
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Luna; using Luna;
using Luna.Generators;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Enums;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.Classes; using Penumbra.UI.ManagementTab;
using Penumbra.UI.ResourceWatcher; using Penumbra.UI.ModsTab;
using Penumbra.UI.Tabs; using Penumbra.UI.Tabs;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
using TabType = Penumbra.Api.Enums.TabType; using TabType = Penumbra.Api.Enums.TabType;
namespace Penumbra; namespace Penumbra;
public class EphemeralConfig : ISavable, IService public sealed partial class EphemeralConfig : ISavable, IService
{ {
[JsonIgnore] [JsonIgnore]
private readonly SaveService _saveService; private readonly SaveService _saveService;
public int Version { get; set; } = Configuration.Constants.CurrentVersion; public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false; public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0; public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false; public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment;
public string ResourceLoggingFilter { get; set; } = string.Empty; public TabType SelectedTab { get; set; } = TabType.Settings;
public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true; [ConfigProperty]
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; private ManagementTabType _selectedManagementTab = ManagementTabType.UnusedMods;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; [ConfigProperty]
public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment; private ModPanelTab _selectedModPanelTab = ModPanelTab.Settings;
public TabType SelectedTab { get; set; } = TabType.Settings;
public bool FixMainWindow { get; set; } = false; public bool FixMainWindow { get; set; } = false;
public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = []; public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = [];
public bool ForceRedrawOnFileChange { get; set; } = false; public bool ForceRedrawOnFileChange { get; set; } = false;
public bool IncognitoMode { get; set; } = false; public bool IncognitoMode { get; set; } = false;
/// <summary> /// <summary>
/// Load the current configuration. /// Load the current configuration.
@ -41,7 +41,7 @@ public class EphemeralConfig : ISavable, IService
/// </summary> /// </summary>
public EphemeralConfig(SaveService saveService) public EphemeralConfig(SaveService saveService)
{ {
_saveService = saveService; _saveService = saveService;
Load(); Load();
} }

View file

@ -240,6 +240,13 @@ public sealed partial class FilterConfig : ConfigurationFile
[ConfigProperty] [ConfigProperty]
private ChangedItemIconFlag _onScreenTypeFilter = ChangedItemFlagExtensions.DefaultFlags; private ChangedItemIconFlag _onScreenTypeFilter = ChangedItemFlagExtensions.DefaultFlags;
public void ClearOnScreenFilters()
{
_onScreenCharacterFilter = string.Empty;
_onScreenItemFilter = string.Empty;
_onScreenTypeFilter = ChangedItemFlagExtensions.DefaultFlags;
}
private void WriteOnScreenTab(JsonTextWriter j) private void WriteOnScreenTab(JsonTextWriter j)
{ {
if (OnScreenCharacterFilter.Length is 0 if (OnScreenCharacterFilter.Length is 0

View file

@ -169,6 +169,8 @@ public unsafe class ResourceLoader : IDisposable, Luna.IService
return; return;
CompareHash(ComputeHash(path.Path, parameters), hash, path); 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. // If no replacements are being made, we still want to be able to trigger the event.
var resolvedData = _resolvedData.Value; var resolvedData = _resolvedData.Value;

View file

@ -1,3 +1,4 @@
using System.Collections.Frozen;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
@ -21,6 +22,19 @@ public class PathResolver : IDisposable, Luna.IService
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly GamePathPreProcessService _preprocessor; private readonly GamePathPreProcessService _preprocessor;
public static FrozenSet<uint> 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, public PathResolver(Configuration config, CollectionManager collectionManager, ResourceLoader loader,
SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState, SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState,
GamePathPreProcessService preprocessor) GamePathPreProcessService preprocessor)

View file

@ -23,9 +23,12 @@ public class ModConfigUpdater : IDisposable, IRequiredService
_communicator.ModSettingChanged.Subscribe(OnModSettingChanged, ModSettingChanged.Priority.ModConfigUpdater); _communicator.ModSettingChanged.Subscribe(OnModSettingChanged, ModSettingChanged.Priority.ModConfigUpdater);
} }
public IEnumerable<Mod> ListUnusedMods(TimeSpan age) public event Action<string, string, Dictionary<Assembly, (bool MarkUsed, string Note)>>? 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<Assembly, (bool InUse, string Notes)>();
foreach (var mod in _mods) foreach (var mod in _mods)
{ {
// Skip actively ignored 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)) if (_collections.Any(c => c.GetOwnSettings(mod.Index)?.Enabled is true || c.GetTempSettings(mod.Index) is not null))
continue; 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);
} }
} }

View file

@ -60,7 +60,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable
var json = JObject.Parse(text); var json = JObject.Parse(text);
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate; importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
lastConfigEdit = json[nameof(Mod.LastConfigEdit)]?.Value<long>() ?? lastConfigEdit; lastConfigEdit = json[nameof(Mod.LastConfigEdit)]?.Value<long>() ?? now;
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite; favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note; note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags; localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags;
@ -81,6 +81,9 @@ public readonly struct ModLocalData(Mod mod) : ISavable
if (importDate == 0) if (importDate == 0)
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (lastConfigEdit == now)
save = true;
ModDataChangeType changes = 0; ModDataChangeType changes = 0;
if (mod.ImportDate != importDate) if (mod.ImportDate != importDate)
{ {

View file

@ -252,7 +252,7 @@ public class Penumbra : IDalamudPlugin
sb.Append( sb.Append(
$"> **`Synchronous Load (Dalamud): `** {(_services.GetService<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); $"> **`Synchronous Load (Dalamud): `** {(_services.GetService<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n");
sb.Append( 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"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n");
GatherRelevantPlugins(sb); GatherRelevantPlugins(sb);
sb.AppendLine("**Mods**"); sb.AppendLine("**Mods**");

View file

@ -13,7 +13,7 @@
<PropertyGroup> <PropertyGroup>
<DefineConstants>PROFILING;</DefineConstants> <DefineConstants>PROFILING;</DefineConstants>
<Use_DalamudPackager>false</Use_DalamudPackager> <Use_DalamudPackager>false</Use_DalamudPackager>
<Use_Dalamud_ImGui >false</Use_Dalamud_ImGui> <Use_Dalamud_ImGui>false</Use_Dalamud_ImGui>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -109,20 +109,20 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu
_config.Version = 8; _config.Version = 8;
_config.Ephemeral.Version = 8; _config.Ephemeral.Version = 8;
_config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject<int>() ?? _config.Ephemeral.LastSeenVersion; _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject<int>() ?? _config.Ephemeral.LastSeenVersion;
_config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject<bool>() ?? _config.Ephemeral.DebugSeparateWindow; _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject<bool>() ?? _config.Ephemeral.DebugSeparateWindow;
_config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject<int>() ?? _config.Ephemeral.TutorialStep; _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject<int>() ?? _config.Ephemeral.TutorialStep;
_config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject<bool>() ?? _config.Ephemeral.EnableResourceLogging; _config.Filters.ResourceLoggerWriteToLog = _data["EnableResourceLogging"]?.ToObject<bool>() ?? _config.Filters.ResourceLoggerWriteToLog;
_config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject<string>() ?? _config.Ephemeral.ResourceLoggingFilter; _config.Filters.ResourceLoggerLogFilter = _data["ResourceLoggingFilter"]?.ToObject<string>() ?? _config.Filters.ResourceLoggerLogFilter;
_config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject<bool>() ?? _config.Ephemeral.EnableResourceWatcher; _config.Filters.ResourceLoggerEnabled = _data["EnableResourceWatcher"]?.ToObject<bool>() ?? _config.Filters.ResourceLoggerEnabled;
_config.Ephemeral.OnlyAddMatchingResources = _config.Filters.ResourceLoggerStoreOnlyMatching =
_data["OnlyAddMatchingResources"]?.ToObject<bool>() ?? _config.Ephemeral.OnlyAddMatchingResources; _data["OnlyAddMatchingResources"]?.ToObject<bool>() ?? _config.Filters.ResourceLoggerStoreOnlyMatching;
_config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject<ResourceTypeFlag>() _config.Filters.ResourceLoggerTypeFilter = _data["ResourceWatcherResourceTypes"]?.ToObject<ResourceTypeFlag>()
?? _config.Ephemeral.ResourceWatcherResourceTypes; ?? _config.Filters.ResourceLoggerTypeFilter;
_config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject<ResourceCategoryFlag>() _config.Filters.ResourceLoggerCategoryFilter = _data["ResourceWatcherResourceCategories"]?.ToObject<ResourceCategoryFlag>()
?? _config.Ephemeral.ResourceWatcherResourceCategories; ?? _config.Filters.ResourceLoggerCategoryFilter;
_config.Ephemeral.ResourceWatcherRecordTypes = _config.Filters.ResourceLoggerRecordFilter =
_data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes; _data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Filters.ResourceLoggerRecordFilter;
_config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionPanelMode>() ?? _config.Ephemeral.CollectionPanel; _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionPanelMode>() ?? _config.Ephemeral.CollectionPanel;
_config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab; _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab;
_config.Filters.ChangedItemTypeFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>() _config.Filters.ChangedItemTypeFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>()

View file

@ -203,22 +203,25 @@ public class ResourceTreeViewer(
} }
} }
var fieldWidth = (Im.ContentRegion.Available.X - checkSpacing * 2.0f - Im.Style.FrameHeightWithSpacing) / 2.0f; using (ImStyleSingle.FrameRounding.Push(0))
Im.Item.SetNextWidth(fieldWidth); {
var filter = config.Filters.OnScreenCharacterFilter; var fieldWidth = (Im.ContentRegion.Available.X - checkSpacing * 2.0f - Im.Style.FrameHeightWithSpacing) / 2.0f;
if (Im.Input.Text("##TreeNameFilter"u8, ref filter, "Filter by Character/Entity Name..."u8)) Im.Item.SetNextWidth(fieldWidth);
{ var filter = config.Filters.OnScreenCharacterFilter;
filterChanged = true; if (Im.Input.Text("##TreeNameFilter"u8, ref filter, "Filter by Character/Entity Name..."u8))
config.Filters.OnScreenCharacterFilter = filter; {
} filterChanged = true;
config.Filters.OnScreenCharacterFilter = filter;
Im.Line.Same(0, checkSpacing); }
Im.Item.SetNextWidth(fieldWidth);
filter = config.Filters.OnScreenItemFilter; Im.Line.Same(0, checkSpacing);
if (Im.Input.Text("##NodeFilter"u8, ref filter, "Filter by Item/Part Name or Path..."u8)) Im.Item.SetNextWidth(fieldWidth);
{ filter = config.Filters.OnScreenItemFilter;
filterChanged = true; if (Im.Input.Text("##NodeFilter"u8, ref filter, "Filter by Item/Part Name or Path..."u8))
config.Filters.OnScreenItemFilter = filter; {
filterChanged = true;
config.Filters.OnScreenItemFilter = filter;
}
} }
Im.Line.Same(0, checkSpacing); Im.Line.Same(0, checkSpacing);
@ -412,7 +415,7 @@ public class ResourceTreeViewer(
if (node.Internal && !debugMode) if (node.Internal && !debugMode)
return NodeVisibility.Hidden; 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)) if (MatchesFilter(node, filterIcon))
return NodeVisibility.Visible; return NodeVisibility.Visible;
@ -430,7 +433,7 @@ public class ResourceTreeViewer(
if (!config.Filters.OnScreenTypeFilter.HasFlag(filterIcon)) if (!config.Filters.OnScreenTypeFilter.HasFlag(filterIcon))
return false; return false;
if (config.Filters.OnScreenItemFilter.Length == 0) if (config.Filters.OnScreenItemFilter.Length is 0)
return true; return true;
return node.Name != null && node.Name.Contains(config.Filters.OnScreenItemFilter, StringComparison.OrdinalIgnoreCase) return node.Name != null && node.Name.Contains(config.Filters.OnScreenItemFilter, StringComparison.OrdinalIgnoreCase)

View file

@ -5,10 +5,11 @@ namespace Penumbra.UI.CollectionTab;
public sealed class CollectionFilter : TextFilterBase<CollectionSelector.Entry>, IUiService public sealed class CollectionFilter : TextFilterBase<CollectionSelector.Entry>, IUiService
{ {
public CollectionFilter(FilterConfig filterConfig) public CollectionFilter(Configuration config)
{ {
Set(filterConfig.CollectionFilter); if (config.RememberCollectionFilters)
FilterChanged += () => filterConfig.CollectionFilter = Text; Set(config.Filters.CollectionFilter);
FilterChanged += () => config.Filters.CollectionFilter = Text;
} }
public override bool WouldBeVisible(in CollectionSelector.Entry item, int globalIndex) public override bool WouldBeVisible(in CollectionSelector.Entry item, int globalIndex)

View file

@ -69,7 +69,7 @@ public sealed class CollectionPanel(
DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth); DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth);
DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, 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) .Then("Assignments take precedence before anything else and only apply to one specific character or monster."u8)
.End(); .End();
Im.Dummy(1); Im.Dummy(1);

View file

@ -16,7 +16,7 @@ public sealed class ChangedItemsTab(
CollectionSelectHeader collectionHeader, CollectionSelectHeader collectionHeader,
ChangedItemDrawer drawer, ChangedItemDrawer drawer,
CommunicatorService communicator, CommunicatorService communicator,
FilterConfig filterConfig) Configuration config)
: ITab<TabType> : ITab<TabType>
{ {
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
@ -27,34 +27,46 @@ public sealed class ChangedItemsTab(
private Vector2 _buttonSize; 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<Item> private sealed class ChangedItemFilter : IFilter<Item>
{ {
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) public bool WouldBeVisible(in Item item, int globalIndex)
=> drawer.FilterChangedItemGlobal(item.Name, item.Data, filterConfig.ChangedItemItemFilter) => _drawer.FilterChangedItemGlobal(item.Name, item.Data, _filterConfig.ChangedItemItemFilter)
&& (filterConfig.ChangedItemModFilter.Length is 0 && (_filterConfig.ChangedItemModFilter.Length is 0
|| item.Mods.Any(m => m.Name.Contains(filterConfig.ChangedItemModFilter, StringComparison.OrdinalIgnoreCase))); || item.Mods.Any(m => m.Name.Contains(_filterConfig.ChangedItemModFilter, StringComparison.OrdinalIgnoreCase)));
public event Action? FilterChanged; public event Action? FilterChanged;
public bool DrawFilter(ReadOnlySpan<byte> label, Vector2 availableRegion) public bool DrawFilter(ReadOnlySpan<byte> label, Vector2 availableRegion)
{ {
using var style = ImStyleSingle.FrameRounding.Push(0);
var varWidth = Im.ContentRegion.Available.X var varWidth = Im.ContentRegion.Available.X
- 450 * Im.Style.GlobalScale - 450 * Im.Style.GlobalScale
- Im.Style.ItemSpacing.X; - Im.Style.ItemSpacing.X;
Im.Item.SetNextWidth(450 * Im.Style.GlobalScale); 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); var ret = Im.Input.Text("##changedItemsFilter"u8, ref filter, "Filter Item..."u8);
if (ret) if (ret)
filterConfig.ChangedItemItemFilter = filter; _filterConfig.ChangedItemItemFilter = filter;
Im.Line.Same(); Im.Line.Same();
Im.Item.SetNextWidth(varWidth); Im.Item.SetNextWidth(varWidth);
filter = filterConfig.ChangedItemModFilter; filter = _filterConfig.ChangedItemModFilter;
if (Im.Input.Text("##changedItemsModFilter"u8, ref filter, "Filter Mods..."u8)) if (Im.Input.Text("##changedItemsModFilter"u8, ref filter, "Filter Mods..."u8))
{ {
ret = true; ret = true;
filterConfig.ChangedItemModFilter = filter; _filterConfig.ChangedItemModFilter = filter;
} }
if (ret) if (ret)
@ -64,16 +76,16 @@ public sealed class ChangedItemsTab(
public void Clear() public void Clear()
{ {
filterConfig.ChangedItemModFilter = string.Empty; _filterConfig.ChangedItemModFilter = string.Empty;
filterConfig.ChangedItemItemFilter = string.Empty; _filterConfig.ChangedItemItemFilter = string.Empty;
filterConfig.ChangedItemTypeFilter = ChangedItemFlagExtensions.DefaultFlags; _filterConfig.ChangedItemTypeFilter = ChangedItemFlagExtensions.DefaultFlags;
FilterChanged?.Invoke(); FilterChanged?.Invoke();
} }
public bool IsEmpty public bool IsEmpty
=> filterConfig.ChangedItemModFilter.Length is 0 => _filterConfig.ChangedItemModFilter.Length is 0
&& filterConfig.ChangedItemItemFilter.Length is 0 && _filterConfig.ChangedItemItemFilter.Length is 0
&& filterConfig.ChangedItemTypeFilter is ChangedItemFlagExtensions.DefaultFlags; && _filterConfig.ChangedItemTypeFilter is ChangedItemFlagExtensions.DefaultFlags;
} }
private readonly record struct Item(string Label, IIdentifiedObjectData Data, SingleArray<IMod> Mods) private readonly record struct Item(string Label, IIdentifiedObjectData Data, SingleArray<IMod> Mods)

View file

@ -16,7 +16,7 @@ public sealed class EffectiveTab(
CollectionManager collectionManager, CollectionManager collectionManager,
CollectionSelectHeader collectionHeader, CollectionSelectHeader collectionHeader,
CommunicatorService communicatorService, CommunicatorService communicatorService,
FilterConfig filterConfig) Configuration config)
: ITab<TabType> : ITab<TabType>
{ {
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
@ -32,7 +32,7 @@ public sealed class EffectiveTab(
public TabType Identifier public TabType Identifier
=> TabType.EffectiveChanges; => TabType.EffectiveChanges;
private readonly PairFilter<Item> _filter = new(new GamePathFilter(filterConfig), new FullPathFilter(filterConfig)); private readonly PairFilter<Item> _filter = new(new GamePathFilter(config), new FullPathFilter(config));
private sealed class Cache : BasicFilterCache<Item>, IPanel private sealed class Cache : BasicFilterCache<Item>, IPanel
{ {
@ -143,10 +143,11 @@ public sealed class EffectiveTab(
private sealed class GamePathFilter : RegexFilterBase<Item> private sealed class GamePathFilter : RegexFilterBase<Item>
{ {
public GamePathFilter(FilterConfig config) public GamePathFilter(Configuration config)
{ {
Set(config.EffectiveChangesGamePathFilter); if (config.RememberEffectiveChangesFilters)
FilterChanged += () => config.EffectiveChangesGamePathFilter = Text; Set(config.Filters.EffectiveChangesGamePathFilter);
FilterChanged += () => config.Filters.EffectiveChangesGamePathFilter = Text;
} }
protected override string ToFilterString(in Item item, int globalIndex) protected override string ToFilterString(in Item item, int globalIndex)
@ -155,10 +156,11 @@ public sealed class EffectiveTab(
private sealed class FullPathFilter : RegexFilterBase<Item> private sealed class FullPathFilter : RegexFilterBase<Item>
{ {
public FullPathFilter(FilterConfig config) public FullPathFilter(Configuration config)
{ {
Set(config.EffectiveChangesFilePathFilter); if (config.RememberEffectiveChangesFilters)
FilterChanged += () => config.EffectiveChangesFilePathFilter = Text; Set(config.Filters.EffectiveChangesFilePathFilter);
FilterChanged += () => config.Filters.EffectiveChangesFilePathFilter = Text;
} }
protected override string ToFilterString(in Item item, int globalIndex) protected override string ToFilterString(in Item item, int globalIndex)

View file

@ -26,9 +26,13 @@ public sealed class MainTabBar : TabBar<TabType>, IDisposable
ResourceTab resources, ResourceTab resources,
Watcher watcher, Watcher watcher,
OnScreenTab onScreen, 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, : base(nameof(MainTabBar), log, settings, collections, mods, changedItems, effectiveChanges, onScreen,
resources, watcher, debug, messages) resources, watcher, debug, messages, management)
{ {
_config = config; _config = config;
_modFileSystem = modFileSystem; _modFileSystem = modFileSystem;

View file

@ -4,9 +4,18 @@ using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.MainWindow; namespace Penumbra.UI.MainWindow;
public sealed class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab<TabType> public sealed class OnScreenTab : ITab<TabType>
{ {
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<byte> Label public ReadOnlySpan<byte> Label
=> "On-Screen"u8; => "On-Screen"u8;

View file

@ -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<ManagementTabType>
{
public ReadOnlySpan<byte> 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<CacheItem> 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<ManagementTabType>
{
public ReadOnlySpan<byte> 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<CacheItem> 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<ManagementTabType>
{
public ReadOnlySpan<byte> Label
=> "General Cleanup"u8;
public ManagementTabType Identifier
=> ManagementTabType.Cleanup;
public void DrawContent()
{ }
}
public sealed class ManagementTab : TabBar<ManagementTabType>, ITab<TabType>
{
public new ReadOnlySpan<byte> 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();
}

View file

@ -29,7 +29,7 @@ public class ModPanelTabBar : TabBar<ModPanelTab>
public ModPanelTabBar(ModEditWindowFactory modEditWindowFactory, ModPanelSettingsTab settings, ModPanelDescriptionTab description, public ModPanelTabBar(ModEditWindowFactory modEditWindowFactory, ModPanelSettingsTab settings, ModPanelDescriptionTab description,
ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, 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) : base(nameof(ModPanelTabBar), log, settings, description, conflicts, changedItems, collections, edit)
{ {
Flags = TabBarFlags.NoTooltip | TabBarFlags.FittingPolicyScroll; Flags = TabBarFlags.NoTooltip | TabBarFlags.FittingPolicyScroll;
@ -37,7 +37,9 @@ public class ModPanelTabBar : TabBar<ModPanelTab>
Edit = edit; Edit = edit;
_modManager = modManager; _modManager = modManager;
_tutorial = tutorial; _tutorial = tutorial;
NextTab = config.SelectedModPanelTab;
Buttons.AddButton(new AdvancedEditingButton(this, modEditWindowFactory), 0); Buttons.AddButton(new AdvancedEditingButton(this, modEditWindowFactory), 0);
TabSelected.Subscribe((in v) => config.SelectedModPanelTab = v, 0);
} }
private sealed class AdvancedEditingButton(ModPanelTabBar parent, ModEditWindowFactory editFactory) : BaseButton private sealed class AdvancedEditingButton(ModPanelTabBar parent, ModEditWindowFactory editFactory) : BaseButton

View file

@ -0,0 +1,34 @@
using ImSharp;
using Luna;
namespace Penumbra.UI.ModsTab.Selector;
public sealed class MoveModInput(ModFileSystemDrawer fileSystem) : BaseButton<IFileSystemData>
{
/// <inheritdoc/>
public override ReadOnlySpan<byte> Label(in IFileSystemData _)
=> "##Move"u8;
/// <summary> Replaces the normal menu item handling for a text input, so the other fields are not used. </summary>
/// <inheritdoc/>
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;
}
}

View file

@ -0,0 +1,34 @@
using ImSharp;
using Luna;
using Penumbra.Mods;
namespace Penumbra.UI.ModsTab.Selector;
public sealed class RenameModInput(ModFileSystemDrawer fileSystem) : BaseButton<IFileSystemData>
{
/// <inheritdoc/>
public override ReadOnlySpan<byte> Label(in IFileSystemData _)
=> "##Rename"u8;
/// <summary> Replaces the normal menu item handling for a text input, so the other fields are not used. </summary>
/// <inheritdoc/>
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;
}
}

View file

@ -16,16 +16,20 @@ public sealed class ModFilter : TokenizedFilter<ModFilterTokenType, ModFileSyste
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly ActiveCollections _collections; private readonly ActiveCollections _collections;
public ModFilter(ModManager modManager, ActiveCollections collections, FilterConfig filterConfig) public ModFilter(ModManager modManager, ActiveCollections collections, Configuration config)
{ {
_modManager = modManager; _modManager = modManager;
_collections = collections; _collections = collections;
_stateFilter = filterConfig.ModTypeFilter; if (config.RememberModFilters)
Set(filterConfig.ModFilter); {
_stateFilter = config.Filters.ModTypeFilter;
Set(config.Filters.ModFilter);
}
FilterChanged += () => FilterChanged += () =>
{ {
filterConfig.ModFilter = Text; config.Filters.ModFilter = Text;
filterConfig.ModTypeFilter = StateFilter; config.Filters.ModTypeFilter = StateFilter;
}; };
} }

View file

@ -1,4 +1,3 @@
using ImSharp;
using Luna; using Luna;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Mods; using Penumbra.Mods;
@ -8,7 +7,7 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.ModsTab.Selector; namespace Penumbra.UI.ModsTab.Selector;
public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.ModData> public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.ModData>, IDisposable
{ {
public readonly ModManager ModManager; public readonly ModManager ModManager;
public readonly CollectionManager CollectionManager; public readonly CollectionManager CollectionManager;
@ -20,7 +19,7 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.Mo
public ModFileSystemDrawer(ModFileSystem fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config, public ModFileSystemDrawer(ModFileSystem fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config,
ModImportManager modImport, FileDialogService fileService, TutorialService tutorial, CommunicatorService communicator) ModImportManager modImport, FileDialogService fileService, TutorialService tutorial, CommunicatorService communicator)
: base(fileSystem, new ModFilter(modManager, collectionManager.Active, config.Filters)) : base(fileSystem, new ModFilter(modManager, collectionManager.Active, config))
{ {
ModManager = modManager; ModManager = modManager;
CollectionManager = collectionManager; CollectionManager = collectionManager;
@ -31,6 +30,8 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.Mo
Communicator = communicator; Communicator = communicator;
SortMode = Config.SortMode; SortMode = Config.SortMode;
Config.ShowRenameChanged += SetRenameFields;
MainContext.AddButton(new ClearTemporarySettingsButton(this), 105); MainContext.AddButton(new ClearTemporarySettingsButton(this), 105);
MainContext.AddButton(new ClearDefaultImportFolderButton(this), -10); MainContext.AddButton(new ClearDefaultImportFolderButton(this), -10);
MainContext.AddButton(new ClearQuickMoveFoldersButtons(this), -20); MainContext.AddButton(new ClearQuickMoveFoldersButtons(this), -20);
@ -45,6 +46,7 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.Mo
DataContext.AddButton(new ToggleFavoriteButton(this), 10); DataContext.AddButton(new ToggleFavoriteButton(this), 10);
DataContext.AddButton(new TemporaryButtons(this), 20); DataContext.AddButton(new TemporaryButtons(this), 20);
DataContext.AddButton(new MoveToQuickMoveFoldersButtons(this), -100); DataContext.AddButton(new MoveToQuickMoveFoldersButtons(this), -100);
SetRenameFields(Config.ShowRename, default);
Footer.Buttons.AddButton(new AddNewModButton(this), 1000); Footer.Buttons.AddButton(new AddNewModButton(this), 1000);
Footer.Buttons.AddButton(new ImportModButton(this), 900); Footer.Buttons.AddButton(new ImportModButton(this), 900);
@ -72,4 +74,26 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.Mo
else else
CollectionManager.Editor.SetMultipleModStates(CollectionManager.Active.Current, mods, enabled); CollectionManager.Editor.SetMultipleModStates(CollectionManager.Active.Current, mods, enabled);
} }
public void Dispose()
=> Config.ShowRenameChanged -= SetRenameFields;
private void SetRenameFields(RenameField newField, RenameField _)
{
DataContext.RemoveButtons<MoveModInput>();
DataContext.RemoveButtons<RenameModInput>();
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;
}
}
} }

View file

@ -12,17 +12,14 @@ using Penumbra.Interop.Hooks.Resources;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ResourceWatcher; namespace Penumbra.UI.ResourceWatcher;
public sealed class ResourceWatcher : IDisposable, ITab<TabType> public sealed class ResourceWatcher : IDisposable, ITab<TabType>
{ {
public const int DefaultMaxEntries = 1024; public const int DefaultMaxEntries = 500;
public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction;
private readonly Configuration _config; private readonly FilterConfig _config;
private readonly EphemeralConfig _ephemeral;
private readonly ResourceService _resources; private readonly ResourceService _resources;
private readonly ResourceLoader _loader; private readonly ResourceLoader _loader;
private readonly ResourceHandleDestructor _destructor; private readonly ResourceHandleDestructor _destructor;
@ -30,50 +27,54 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
private readonly ObservableList<Record> _records = []; private readonly ObservableList<Record> _records = [];
private readonly ConcurrentQueue<Record> _newRecords = []; private readonly ConcurrentQueue<Record> _newRecords = [];
private readonly ResourceWatcherTable _table; private readonly ResourceWatcherTable _table;
private string _logFilter = string.Empty; private readonly RegexFilter _filter = new();
private Regex? _logRegex;
private int _newMaxEntries;
public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader, public unsafe ResourceWatcher(ActorManager actors, FilterConfig config, ResourceService resources, ResourceLoader loader,
ResourceHandleDestructor destructor) ResourceHandleDestructor destructor)
{ {
_actors = actors;
_config = config; _config = config;
_ephemeral = config.Ephemeral; _actors = actors;
_resources = resources; _resources = resources;
_destructor = destructor; _destructor = destructor;
_loader = loader; _loader = loader;
_table = new ResourceWatcherTable(config.Filters, _records); _table = new ResourceWatcherTable(_config, _records);
_resources.ResourceRequested += OnResourceRequested; _resources.ResourceRequested += OnResourceRequested;
_destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher);
_loader.ResourceLoaded += OnResourceLoaded; _loader.ResourceLoaded += OnResourceLoaded;
_loader.ResourceComplete += OnResourceComplete; _loader.ResourceComplete += OnResourceComplete;
_loader.FileLoaded += OnFileLoaded; _loader.FileLoaded += OnFileLoaded;
_loader.PapRequested += OnPapRequested; _loader.PapRequested += OnPapRequested;
UpdateFilter(_ephemeral.ResourceLoggingFilter, false); _filter.Set(_config.ResourceLoggerLogFilter);
_newMaxEntries = _config.MaxResourceWatcherRecords; _filter.FilterChanged += () => _config.ResourceLoggerLogFilter = _filter.Text;
} }
private void OnPapRequested(Utf8GamePath original, FullPath? _1, ResolveData _2) 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) if (_1.HasValue)
Penumbra.Log.Information( 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; return;
var record = _1.HasValue var record = _1.HasValue
? Record.CreateRequest(original.Path, false, _1.Value, _2) ? Record.CreateRequest(original.Path, false, _1.Value, _2)
: Record.CreateRequest(original.Path, false); : Record.CreateRequest(original.Path, false);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record))
Enqueue(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() public unsafe void Dispose()
{ {
Clear(); Clear();
@ -103,12 +104,8 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
UpdateRecords(); UpdateRecords();
Im.Cursor.Y += Im.Style.TextHeightWithSpacing / 2; Im.Cursor.Y += Im.Style.TextHeightWithSpacing / 2;
var isEnabled = _ephemeral.EnableResourceWatcher; if (Im.Checkbox("Enable"u8, _config.ResourceLoggerEnabled))
if (Im.Checkbox("Enable"u8, ref isEnabled)) _config.ResourceLoggerEnabled ^= true;
{
_ephemeral.EnableResourceWatcher = isEnabled;
_ephemeral.Save();
}
Im.Line.Same(); Im.Line.Same();
DrawMaxEntries(); DrawMaxEntries();
@ -117,20 +114,12 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
Clear(); Clear();
Im.Line.Same(); Im.Line.Same();
var onlyMatching = _ephemeral.OnlyAddMatchingResources; if (Im.Checkbox("Store Only Matching"u8, _config.ResourceLoggerStoreOnlyMatching))
if (Im.Checkbox("Store Only Matching"u8, ref onlyMatching)) _config.ResourceLoggerStoreOnlyMatching ^= true;
{
_ephemeral.OnlyAddMatchingResources = onlyMatching;
_ephemeral.Save();
}
Im.Line.Same(); Im.Line.Same();
var writeToLog = _ephemeral.EnableResourceLogging; if (Im.Checkbox("Write to Log"u8, _config.ResourceLoggerWriteToLog))
if (Im.Checkbox("Write to Log"u8, ref writeToLog)) _config.ResourceLoggerWriteToLog ^= true;
{
_ephemeral.EnableResourceLogging = writeToLog;
_ephemeral.Save();
}
Im.Line.Same(); Im.Line.Same();
DrawFilterInput(); DrawFilterInput();
@ -141,70 +130,22 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
} }
private void DrawFilterInput() private void DrawFilterInput()
{ => _filter.DrawFilter("If path matches this Regex..."u8, Im.ContentRegion.Available);
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);
}
private void DrawMaxEntries() private void DrawMaxEntries()
{ {
Im.Item.SetNextWidthScaled(80); Im.Item.SetNextWidthScaled(80);
Im.Input.Scalar("Max. Entries"u8, ref _newMaxEntries); if (ImEx.InputOnDeactivation.Scalar("Max. Entries"u8, _config.ResourceLoggerMaxEntries, out var newValue))
var change = Im.Item.DeactivatedAfterEdit; _config.ResourceLoggerMaxEntries = Math.Max(16, newValue);
if (Im.Item.RightClicked() && Im.Io.KeyControl) if (Im.Item.RightClicked() && Im.Io.KeyControl)
{ _config.ResourceLoggerMaxEntries = DefaultMaxEntries;
change = true;
_newMaxEntries = DefaultMaxEntries;
}
var maxEntries = _config.MaxResourceWatcherRecords; if (_config.ResourceLoggerMaxEntries is not DefaultMaxEntries && Im.Item.Hovered())
if (maxEntries != DefaultMaxEntries && Im.Item.Hovered()) Im.Tooltip.Set("Control + Right-Click to reset to default 500."u8);
Im.Tooltip.Set($"CTRL + Right-Click to reset to default {DefaultMaxEntries}.");
if (!change) if (_records.Count > _config.ResourceLoggerMaxEntries)
return; _records.RemoveRange(0, _records.Count - _config.ResourceLoggerMaxEntries);
_newMaxEntries = Math.Max(16, _newMaxEntries);
if (_newMaxEntries == maxEntries)
return;
_config.MaxResourceWatcherRecords = _newMaxEntries;
_config.Save();
if (_newMaxEntries > _records.Count)
_records.RemoveRange(0, _records.Count - _newMaxEntries);
} }
private void UpdateRecords() private void UpdateRecords()
@ -216,49 +157,49 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
while (_newRecords.TryDequeue(out var rec) && count-- > 0) while (_newRecords.TryDequeue(out var rec) && count-- > 0)
_records.Add(rec); _records.Add(rec);
if (_records.Count > _config.MaxResourceWatcherRecords) if (_records.Count > _config.ResourceLoggerMaxEntries)
_records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); _records.RemoveRange(0, _records.Count - _config.ResourceLoggerMaxEntries);
} }
private unsafe void OnResourceRequested(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, 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) 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.")}"); Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}");
if (!_ephemeral.EnableResourceWatcher) if (!_config.ResourceLoggerEnabled)
return; return;
var record = Record.CreateRequest(original.Path, sync); var record = Record.CreateRequest(original.Path, sync);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record))
Enqueue(record); Enqueue(record);
} }
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) 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; var name2 = string.Empty;
if (manipulatedPath != null) if (manipulatedPath is not null)
log |= FilterMatch(manipulatedPath.Value.InternalName, out name2); log |= Filter(manipulatedPath.Value.InternalName, out name2);
if (log) 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( 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}) "); $"[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; return;
var record = manipulatedPath == null var record = manipulatedPath == null
? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data))
: Record.CreateLoad(manipulatedPath.Value, 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); Enqueue(record);
} }
@ -268,43 +209,43 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
if (!isAsync) if (!isAsync)
return; return;
if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) if (_config.ResourceLoggerWriteToLog && Filter(path, out var match))
Penumbra.Log.Information( Penumbra.Log.Information(
$"[ResourceLoader] [DONE] [{resource->FileType}] Finished loading {match} into 0x{(ulong)resource:X}, state {resource->LoadState}."); $"[ResourceLoader] [DONE] [{resource->FileType}] Finished loading {match} into 0x{(ulong)resource:X}, state {resource->LoadState}.");
if (!_ephemeral.EnableResourceWatcher) if (!_config.ResourceLoggerEnabled)
return; return;
var record = Record.CreateResourceComplete(path, resource, original, additionalData); var record = Record.CreateResourceComplete(path, resource, original, additionalData);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record))
Enqueue(record); Enqueue(record);
} }
private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan<byte> _) private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan<byte> _)
{ {
if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) if (_config.ResourceLoggerWriteToLog && Filter(path, out var match))
Penumbra.Log.Information( Penumbra.Log.Information(
$"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}."); $"[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; return;
var record = Record.CreateFileLoad(path, resource, success, custom); var record = Record.CreateFileLoad(path, resource, success, custom);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record))
Enqueue(record); Enqueue(record);
} }
private unsafe void OnResourceDestroyed(in ResourceHandleDestructor.Arguments arguments) 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( Penumbra.Log.Information(
$"[ResourceLoader] [DEST] [{arguments.ResourceHandle->FileType}] Destroyed {match} at 0x{(ulong)arguments.ResourceHandle:X}."); $"[ResourceLoader] [DEST] [{arguments.ResourceHandle->FileType}] Destroyed {match} at 0x{(ulong)arguments.ResourceHandle:X}.");
if (!_ephemeral.EnableResourceWatcher) if (!_config.ResourceLoggerEnabled)
return; return;
var record = Record.CreateDestruction(arguments.ResourceHandle); var record = Record.CreateDestruction(arguments.ResourceHandle);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_config.ResourceLoggerStoreOnlyMatching || _table.WouldBeVisible(record))
Enqueue(record); Enqueue(record);
} }
@ -337,7 +278,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
private void Enqueue(Record record) private void Enqueue(Record record)
{ {
// Discard entries that exceed the number of records. // Discard entries that exceed the number of records.
while (_newRecords.Count >= _config.MaxResourceWatcherRecords) while (_newRecords.Count >= _config.ResourceLoggerMaxEntries)
_newRecords.TryDequeue(out _); _newRecords.TryDequeue(out _);
_newRecords.Enqueue(record); _newRecords.Enqueue(record);
} }

View file

@ -20,14 +20,15 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res
public bool IsVisible public bool IsVisible
=> config.DebugMode; => config.DebugMode;
public readonly ResourceFilter Filter = new(config.Filters); public readonly ResourceFilter Filter = new(config);
public sealed class ResourceFilter : Utf8FilterBase<ResourceHandle> public sealed class ResourceFilter : Utf8FilterBase<ResourceHandle>
{ {
public ResourceFilter(FilterConfig filterConfig) public ResourceFilter(Configuration config)
{ {
Set(new StringU8(filterConfig.ResourceManagerFilter)); if (config.RememberResourceManagerFilters)
FilterChanged += () => filterConfig.ResourceManagerFilter = Text.ToString(); Set(new StringU8(config.Filters.ResourceManagerFilter));
FilterChanged += () => config.Filters.ResourceManagerFilter = Text.ToString();
} }
protected override ReadOnlySpan<byte> ToFilterString(in ResourceHandle item, int globalIndex) protected override ReadOnlySpan<byte> ToFilterString(in ResourceHandle item, int globalIndex)
@ -58,9 +59,9 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res
Penumbra.Dynamis.DrawPointer(resourceManager.ResourceManager); Penumbra.Dynamis.DrawPointer(resourceManager.ResourceManager);
} }
private float _hashColumnWidth; private float _hashColumnWidth;
private float _pathColumnWidth; private float _pathColumnWidth;
private float _refsColumnWidth; private float _refsColumnWidth;
/// <summary> Draw a single resource map. </summary> /// <summary> Draw a single resource map. </summary>
private unsafe void DrawResourceMap(ResourceCategory category, uint ext, private unsafe void DrawResourceMap(ResourceCategory category, uint ext,
@ -91,19 +92,23 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res
return; return;
Im.Table.DrawColumn($"0x{hash:X8}"); Im.Table.DrawColumn($"0x{hash:X8}");
if (Im.Item.Clicked())
Im.Clipboard.Set($"0x{hash:X8}");
Im.Table.NextColumn(); Im.Table.NextColumn();
Penumbra.Dynamis.DrawPointer(r); Penumbra.Dynamis.DrawPointer(r);
var resource = (Interop.Structs.ResourceHandle*)r; var resource = (Interop.Structs.ResourceHandle*)r;
Im.Table.DrawColumn(resource->FileName().Span); Im.Table.DrawColumn(resource->FileName().Span);
if (Im.Item.Clicked()) if (Im.Item.Clicked())
{
Im.Clipboard.Set(resource->FileName().Span); Im.Clipboard.Set(resource->FileName().Span);
}
else if (Im.Item.RightClicked()) else if (Im.Item.RightClicked())
{ {
var data = resource->CsHandle.GetData(); var data = resource->CsHandle.GetData();
if (data != null) if (data != null)
{ {
var length = (int)resource->CsHandle.GetLength(); var length = (int)resource->CsHandle.GetLength();
Im.Clipboard.Set(StringU8.Join((byte) ' ', Im.Clipboard.Set(StringU8.Join((byte)' ',
new ReadOnlySpan<byte>(data, length).ToArray().Select(b => b.ToString("X2")))); new ReadOnlySpan<byte>(data, length).ToArray().Select(b => b.ToString("X2"))));
} }
} }

View file

@ -413,6 +413,27 @@ public sealed class SettingsTab : ITab<TabType>
_config.HideUiInGPose = v; _config.HideUiInGPose = v;
_pluginInterface.UiBuilder.DisableGposeUiHide = !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);
} }
/// <summary> Draw all settings that do not fit into other categories. </summary> /// <summary> Draw all settings that do not fit into other categories. </summary>
@ -433,7 +454,7 @@ public sealed class SettingsTab : ITab<TabType>
if (v) if (v)
{ {
_config.Filters.ModChangedItemTypeFilter = ChangedItemFlagExtensions.AllFlags; _config.Filters.ModChangedItemTypeFilter = ChangedItemFlagExtensions.AllFlags;
_config.Filters.ChangedItemTypeFilter = ChangedItemFlagExtensions.AllFlags; _config.Filters.ChangedItemTypeFilter = ChangedItemFlagExtensions.AllFlags;
_config.Ephemeral.Save(); _config.Ephemeral.Save();
} }
}); });
@ -520,15 +541,10 @@ public sealed class SettingsTab : ITab<TabType>
using (var combo = Im.Combo.Begin("##renameSettings"u8, _config.ShowRename.ToNameU8())) using (var combo = Im.Combo.Begin("##renameSettings"u8, _config.ShowRename.ToNameU8()))
{ {
if (combo) if (combo)
foreach (var value in Enum.GetValues<RenameField>()) foreach (var value in RenameField.Values)
{ {
if (Im.Selectable(value.ToNameU8(), _config.ShowRename == value)) if (Im.Selectable(value.ToNameU8(), _config.ShowRename == value))
{
_config.ShowRename = value; _config.ShowRename = value;
// TODO
// _selector.SetRenameSearchPath(value);
_config.Save();
}
Im.Tooltip.OnHover(value.Tooltip()); Im.Tooltip.OnHover(value.Tooltip());
} }