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;
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<JObject, ushort, string>? CreatingPcp;
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)
{
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.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),

View file

@ -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)]

View file

@ -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<string> 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<string> AdvancedEditingOpenForModPaths { get; set; } = [];
public bool ForceRedrawOnFileChange { get; set; } = false;
public bool IncognitoMode { get; set; } = false;
/// <summary>
/// Load the current configuration.
@ -41,7 +41,7 @@ public class EphemeralConfig : ISavable, IService
/// </summary>
public EphemeralConfig(SaveService saveService)
{
_saveService = saveService;
_saveService = saveService;
Load();
}

View file

@ -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

View file

@ -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;

View file

@ -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<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,
SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState,
GamePathPreProcessService preprocessor)

View file

@ -23,9 +23,12 @@ public class ModConfigUpdater : IDisposable, IRequiredService
_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)
{
// 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);
}
}

View file

@ -60,7 +60,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable
var json = JObject.Parse(text);
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;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
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)
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (lastConfigEdit == now)
save = true;
ModDataChangeType changes = 0;
if (mod.ImportDate != importDate)
{

View file

@ -252,7 +252,7 @@ public class Penumbra : IDalamudPlugin
sb.Append(
$"> **`Synchronous Load (Dalamud): `** {(_services.GetService<DalamudConfigService>().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**");

View file

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

View file

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

View file

@ -5,10 +5,11 @@ namespace Penumbra.UI.CollectionTab;
public sealed class CollectionFilter : TextFilterBase<CollectionSelector.Entry>, 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)

View file

@ -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);

View file

@ -16,7 +16,7 @@ public sealed class ChangedItemsTab(
CollectionSelectHeader collectionHeader,
ChangedItemDrawer drawer,
CommunicatorService communicator,
FilterConfig filterConfig)
Configuration config)
: ITab<TabType>
{
public ReadOnlySpan<byte> 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<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)
=> 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<byte> 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<IMod> Mods)

View file

@ -16,7 +16,7 @@ public sealed class EffectiveTab(
CollectionManager collectionManager,
CollectionSelectHeader collectionHeader,
CommunicatorService communicatorService,
FilterConfig filterConfig)
Configuration config)
: ITab<TabType>
{
public ReadOnlySpan<byte> Label
@ -32,7 +32,7 @@ public sealed class EffectiveTab(
public TabType Identifier
=> 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
{
@ -143,10 +143,11 @@ public sealed class EffectiveTab(
private sealed class GamePathFilter : RegexFilterBase<Item>
{
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<Item>
{
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)

View file

@ -26,9 +26,13 @@ public sealed class MainTabBar : TabBar<TabType>, 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;

View file

@ -4,9 +4,18 @@ using Penumbra.UI.AdvancedWindow;
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
=> "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,
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<ModPanelTab>
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

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 ActiveCollections _collections;
public ModFilter(ModManager modManager, ActiveCollections collections, FilterConfig filterConfig)
public ModFilter(ModManager modManager, ActiveCollections collections, Configuration config)
{
_modManager = modManager;
_collections = collections;
_stateFilter = filterConfig.ModTypeFilter;
Set(filterConfig.ModFilter);
if (config.RememberModFilters)
{
_stateFilter = config.Filters.ModTypeFilter;
Set(config.Filters.ModFilter);
}
FilterChanged += () =>
{
filterConfig.ModFilter = Text;
filterConfig.ModTypeFilter = StateFilter;
config.Filters.ModFilter = Text;
config.Filters.ModTypeFilter = StateFilter;
};
}

View file

@ -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<ModFileSystemCache.ModData>
public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.ModData>, IDisposable
{
public readonly ModManager ModManager;
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,
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;
CollectionManager = collectionManager;
@ -31,6 +30,8 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.Mo
Communicator = communicator;
SortMode = Config.SortMode;
Config.ShowRenameChanged += SetRenameFields;
MainContext.AddButton(new ClearTemporarySettingsButton(this), 105);
MainContext.AddButton(new ClearDefaultImportFolderButton(this), -10);
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 TemporaryButtons(this), 20);
DataContext.AddButton(new MoveToQuickMoveFoldersButtons(this), -100);
SetRenameFields(Config.ShowRename, default);
Footer.Buttons.AddButton(new AddNewModButton(this), 1000);
Footer.Buttons.AddButton(new ImportModButton(this), 900);
@ -72,4 +74,26 @@ public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.Mo
else
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.String;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ResourceWatcher;
public sealed class ResourceWatcher : IDisposable, ITab<TabType>
{
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<TabType>
private readonly ObservableList<Record> _records = [];
private readonly ConcurrentQueue<Record> _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<TabType>
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<TabType>
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<TabType>
}
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<TabType>
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<TabType>
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<byte> _)
{
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<TabType>
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);
}

View file

@ -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<ResourceHandle>
{
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<byte> 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;
/// <summary> Draw a single resource map. </summary>
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<byte>(data, length).ToArray().Select(b => b.ToString("X2"))));
}
}

View file

@ -413,6 +413,27 @@ public sealed class SettingsTab : ITab<TabType>
_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);
}
/// <summary> Draw all settings that do not fit into other categories. </summary>
@ -433,7 +454,7 @@ public sealed class SettingsTab : ITab<TabType>
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<TabType>
using (var combo = Im.Combo.Begin("##renameSettings"u8, _config.ShowRename.ToNameU8()))
{
if (combo)
foreach (var value in Enum.GetValues<RenameField>())
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());
}