diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index f816069e..efabaaf2 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -36,30 +37,6 @@ public readonly struct ResolveData public override string ToString() => ModCollection.Name; - - public unsafe string AssociatedName() - { - if (AssociatedGameObject == IntPtr.Zero) - return "no associated object."; - - try - { - var id = Penumbra.Actors.FromObject((GameObject*)AssociatedGameObject, out _, false, true, true); - if (id.IsValid) - { - var name = id.ToString(); - var parts = name.Split(' ', 3); - return string.Join(" ", - parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); - } - } - catch - { - // ignored - } - - return $"0x{AssociatedGameObject:X}"; - } } public static class ResolveDataExtensions diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ff2a5e64..efe21ffe 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -62,9 +62,9 @@ public class Configuration : IPluginConfiguration, ISavable public bool OnlyAddMatchingResources { get; set; } = true; public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public ResourceWatcher.RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; [JsonConverter(typeof(SortModeConverter))] diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 3ec38c87..5d7ba16a 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -66,7 +66,7 @@ public unsafe class ResourceLoader : IDisposable _fileReadService.ReadSqPack -= ReadSqPackDetour; } - private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { if (returnValue != null) @@ -86,9 +86,10 @@ public unsafe class ResourceLoader : IDisposable _texMdlService.AddCrc(type, resolvedPath); // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); + var oldPath = path; path = p; returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); - ResourceLoaded?.Invoke(returnValue, p, resolvedPath.Value, data); + ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue) diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 1027ed5f..bced539c 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -60,7 +60,7 @@ public unsafe class ResourceService : IDisposable /// Mainly used for SCD streaming, can be null. /// Whether to request the resource synchronously or asynchronously. /// The returned resource handle. If this is not null, calling original will be skipped. - public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, + public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue); /// @@ -104,7 +104,7 @@ public unsafe class ResourceService : IDisposable } ResourceHandle* returnValue = null; - ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, pGetResParams, ref isSync, + ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, gamePath, pGetResParams, ref isSync, ref returnValue); if (returnValue != null) return returnValue; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs index 3cc82173..9b54cb85 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Record.cs @@ -7,112 +7,123 @@ using Penumbra.String; namespace Penumbra.UI; -public partial class ResourceWatcher +[Flags] +public enum RecordType : byte { - private unsafe struct Record + Request = 0x01, + ResourceLoad = 0x02, + FileLoad = 0x04, + Destruction = 0x08, +} + +internal unsafe struct Record +{ + public DateTime Time; + public ByteString Path; + public ByteString OriginalPath; + public string AssociatedGameObject; + public ModCollection? Collection; + public ResourceHandle* Handle; + public ResourceTypeFlag ResourceType; + public ResourceCategoryFlag Category; + public uint RefCount; + public RecordType RecordType; + public OptionalBool Synchronously; + public OptionalBool ReturnValue; + public OptionalBool CustomLoad; + + public static Record CreateRequest(ByteString path, bool sync) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = ByteString.Empty, + Collection = null, + Handle = null, + ResourceType = ResourceExtensions.Type(path).ToFlag(), + Category = ResourceExtensions.Category(path).ToFlag(), + RefCount = 0, + RecordType = RecordType.Request, + Synchronously = sync, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + AssociatedGameObject = string.Empty, + }; + + public static Record CreateDefaultLoad(ByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) { - public DateTime Time; - public ByteString Path; - public ByteString OriginalPath; - public ModCollection? Collection; - public ResourceHandle* Handle; - public ResourceTypeFlag ResourceType; - public ResourceCategoryFlag Category; - public uint RefCount; - public RecordType RecordType; - public OptionalBool Synchronously; - public OptionalBool ReturnValue; - public OptionalBool CustomLoad; - - public static Record CreateRequest( ByteString path, bool sync ) - => new() - { - Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, - Collection = null, - Handle = null, - ResourceType = ResourceExtensions.Type( path ).ToFlag(), - Category = ResourceExtensions.Category( path ).ToFlag(), - RefCount = 0, - RecordType = RecordType.Request, - Synchronously = sync, - ReturnValue = OptionalBool.Null, - CustomLoad = OptionalBool.Null, - }; - - public static Record CreateDefaultLoad( ByteString path, ResourceHandle* handle, ModCollection collection ) + path = path.IsOwned ? path : path.Clone(); + return new Record { - path = path.IsOwned ? path : path.Clone(); - return new Record - { - Time = DateTime.UtcNow, - Path = path, - OriginalPath = path, - Collection = collection, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.ResourceLoad, - Synchronously = OptionalBool.Null, - ReturnValue = OptionalBool.Null, - CustomLoad = false, - }; - } - - public static Record CreateLoad( ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection ) - => new() - { - Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), - Collection = collection, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.ResourceLoad, - Synchronously = OptionalBool.Null, - ReturnValue = OptionalBool.Null, - CustomLoad = true, - }; - - public static Record CreateDestruction( ResourceHandle* handle ) - { - var path = handle->FileName().Clone(); - return new Record - { - Time = DateTime.UtcNow, - Path = path, - OriginalPath = ByteString.Empty, - Collection = null, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.Destruction, - Synchronously = OptionalBool.Null, - ReturnValue = OptionalBool.Null, - CustomLoad = OptionalBool.Null, - }; - } - - public static Record CreateFileLoad( ByteString path, ResourceHandle* handle, bool ret, bool custom ) - => new() - { - Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, - Collection = null, - Handle = handle, - ResourceType = handle->FileType.ToFlag(), - Category = handle->Category.ToFlag(), - RefCount = handle->RefCount, - RecordType = RecordType.FileLoad, - Synchronously = OptionalBool.Null, - ReturnValue = ret, - CustomLoad = custom, - }; + Time = DateTime.UtcNow, + Path = path, + OriginalPath = path, + Collection = collection, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceLoad, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = false, + AssociatedGameObject = associatedGameObject, + }; } -} \ No newline at end of file + + public static Record CreateLoad(ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection, + string associatedGameObject) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), + Collection = collection, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceLoad, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = true, + AssociatedGameObject = associatedGameObject, + }; + + public static Record CreateDestruction(ResourceHandle* handle) + { + var path = handle->FileName().Clone(); + return new Record + { + Time = DateTime.UtcNow, + Path = path, + OriginalPath = ByteString.Empty, + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.Destruction, + Synchronously = OptionalBool.Null, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + }; + } + + public static Record CreateFileLoad(ByteString path, ResourceHandle* handle, bool ret, bool custom) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = ByteString.Empty, + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.FileLoad, + Synchronously = OptionalBool.Null, + ReturnValue = ret, + CustomLoad = custom, + }; +} diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs deleted file mode 100644 index 3b3fed73..00000000 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.RecordType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Penumbra.UI; - -public partial class ResourceWatcher -{ - [Flags] - public enum RecordType : byte - { - Request = 0x01, - ResourceLoad = 0x02, - FileLoad = 0x04, - Destruction = 0x08, - } - - public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; -} \ No newline at end of file diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs index 1cb787ef..d5ff7d7f 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs @@ -13,340 +13,328 @@ using Penumbra.String; namespace Penumbra.UI; -public partial class ResourceWatcher +internal sealed class ResourceWatcherTable : Table { - private sealed class Table : Table< Record > + public ResourceWatcherTable(Configuration config, ICollection records) + : base("##records", + records, + new PathColumn { Label = "Path" }, + new RecordTypeColumn(config) { Label = "Record" }, + new CollectionColumn { Label = "Collection" }, + new ObjectColumn { Label = "Game Object" }, + new CustomLoadColumn { Label = "Custom" }, + new SynchronousLoadColumn { Label = "Sync" }, + new OriginalPathColumn { Label = "Original Path" }, + new ResourceCategoryColumn { Label = "Category" }, + new ResourceTypeColumn { Label = "Type" }, + new HandleColumn { Label = "Resource" }, + new RefCountColumn { Label = "#Ref" }, + new DateColumn { Label = "Time" } + ) + { } + + public void Reset() + => FilterDirty = true; + + private sealed class PathColumn : ColumnString { - private static readonly PathColumn Path = new() { Label = "Path" }; - private static readonly RecordTypeColumn RecordType = new() { Label = "Record" }; - private static readonly DateColumn Date = new() { Label = "Time" }; + public override float Width + => 300 * UiHelpers.Scale; - private static readonly CollectionColumn Coll = new() { Label = "Collection" }; - private static readonly CustomLoadColumn Custom = new() { Label = "Custom" }; - private static readonly SynchronousLoadColumn Sync = new() { Label = "Sync" }; + public override string ToName(Record item) + => item.Path.ToString(); - private static readonly OriginalPathColumn Orig = new() { Label = "Original Path" }; - private static readonly ResourceCategoryColumn Cat = new() { Label = "Category" }; - private static readonly ResourceTypeColumn Type = new() { Label = "Type" }; + public override int Compare(Record lhs, Record rhs) + => lhs.Path.CompareTo(rhs.Path); - private static readonly HandleColumn Handle = new() { Label = "Resource" }; - private static readonly RefCountColumn Ref = new() { Label = "#Ref" }; + public override void DrawColumn(Record item, int _) + => DrawByteString(item.Path, 280 * UiHelpers.Scale); + } - public Table( ICollection< Record > records ) - : base( "##records", records, Path, RecordType, Coll, Custom, Sync, Orig, Cat, Type, Handle, Ref, Date ) - { } - - public void Reset() - => FilterDirty = true; - - private sealed class PathColumn : ColumnString< Record > + private static unsafe void DrawByteString(ByteString path, float length) + { + Vector2 vec; + ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0); + if (vec.X <= length) { - public override float Width - => 300 * UiHelpers.Scale; - - public override string ToName( Record item ) - => item.Path.ToString(); - - public override int Compare( Record lhs, Record rhs ) - => lhs.Path.CompareTo( rhs.Path ); - - public override void DrawColumn( Record item, int _ ) - => DrawByteString( item.Path, 290 * UiHelpers.Scale ); + ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); } - - private static unsafe void DrawByteString( ByteString path, float length ) + else { - Vector2 vec; - ImGuiNative.igCalcTextSize( &vec, path.Path, path.Path + path.Length, 0, 0 ); - if( vec.X <= length ) + var fileName = path.LastIndexOf((byte)'/'); + ByteString shortPath; + if (fileName != -1) { - ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale)); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.EllipsisH.ToIconString()); + ImGui.SameLine(); + shortPath = path.Substring(fileName, path.Length - fileName); } else { - var fileName = path.LastIndexOf( ( byte )'/' ); - ByteString shortPath; - if( fileName != -1 ) - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 2 * UiHelpers.Scale ) ); - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - ImGui.TextUnformatted( FontAwesomeIcon.EllipsisH.ToIconString() ); - ImGui.SameLine(); - shortPath = path.Substring( fileName, path.Length - fileName ); - } - else - { - shortPath = path; - } - - ImGuiNative.igTextUnformatted( shortPath.Path, shortPath.Path + shortPath.Length ); - if( ImGui.IsItemClicked() ) - { - ImGuiNative.igSetClipboardText( path.Path ); - } - - if( ImGui.IsItemHovered() ) - { - ImGuiNative.igSetTooltip( path.Path ); - } - } - } - - private sealed class RecordTypeColumn : ColumnFlags< RecordType, Record > - { - public RecordTypeColumn() - => AllFlags = AllRecords; - - public override float Width - => 80 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterValue.HasFlag( item.RecordType ); - - public override RecordType FilterValue - => Penumbra.Config.ResourceWatcherRecordTypes; - - protected override void SetValue( RecordType value, bool enable ) - { - if( enable ) - { - Penumbra.Config.ResourceWatcherRecordTypes |= value; - } - else - { - Penumbra.Config.ResourceWatcherRecordTypes &= ~value; - } - - Penumbra.Config.Save(); + shortPath = path; } - public override void DrawColumn( Record item, int idx ) - { - ImGui.TextUnformatted( item.RecordType switch - { - ResourceWatcher.RecordType.Request => "REQ", - ResourceWatcher.RecordType.ResourceLoad => "LOAD", - ResourceWatcher.RecordType.FileLoad => "FILE", - ResourceWatcher.RecordType.Destruction => "DEST", - _ => string.Empty, - } ); - } - } + ImGuiNative.igTextUnformatted(shortPath.Path, shortPath.Path + shortPath.Length); + if (ImGui.IsItemClicked()) + ImGuiNative.igSetClipboardText(path.Path); - private sealed class DateColumn : Column< Record > - { - public override float Width - => 80 * UiHelpers.Scale; - - public override int Compare( Record lhs, Record rhs ) - => lhs.Time.CompareTo( rhs.Time ); - - public override void DrawColumn( Record item, int _ ) - => ImGui.TextUnformatted( $"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}" ); - } - - - private sealed class CollectionColumn : ColumnString< Record > - { - public override float Width - => 80 * UiHelpers.Scale; - - public override string ToName( Record item ) - => item.Collection?.Name ?? string.Empty; - } - - private sealed class OriginalPathColumn : ColumnString< Record > - { - public override float Width - => 200 * UiHelpers.Scale; - - public override string ToName( Record item ) - => item.OriginalPath.ToString(); - - public override int Compare( Record lhs, Record rhs ) - => lhs.OriginalPath.CompareTo( rhs.OriginalPath ); - - public override void DrawColumn( Record item, int _ ) - => DrawByteString( item.OriginalPath, 190 * UiHelpers.Scale ); - } - - private sealed class ResourceCategoryColumn : ColumnFlags< ResourceCategoryFlag, Record > - { - public ResourceCategoryColumn() - => AllFlags = ResourceExtensions.AllResourceCategories; - - public override float Width - => 80 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterValue.HasFlag( item.Category ); - - public override ResourceCategoryFlag FilterValue - => Penumbra.Config.ResourceWatcherResourceCategories; - - protected override void SetValue( ResourceCategoryFlag value, bool enable ) - { - if( enable ) - { - Penumbra.Config.ResourceWatcherResourceCategories |= value; - } - else - { - Penumbra.Config.ResourceWatcherResourceCategories &= ~value; - } - - Penumbra.Config.Save(); - } - - public override void DrawColumn( Record item, int idx ) - { - ImGui.TextUnformatted( item.Category.ToString() ); - } - } - - private sealed class ResourceTypeColumn : ColumnFlags< ResourceTypeFlag, Record > - { - public ResourceTypeColumn() - { - AllFlags = Enum.GetValues< ResourceTypeFlag >().Aggregate( ( v, f ) => v | f ); - for( var i = 0; i < Names.Length; ++i ) - { - Names[ i ] = Names[ i ].ToLowerInvariant(); - } - } - - public override float Width - => 50 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterValue.HasFlag( item.ResourceType ); - - public override ResourceTypeFlag FilterValue - => Penumbra.Config.ResourceWatcherResourceTypes; - - protected override void SetValue( ResourceTypeFlag value, bool enable ) - { - if( enable ) - { - Penumbra.Config.ResourceWatcherResourceTypes |= value; - } - else - { - Penumbra.Config.ResourceWatcherResourceTypes &= ~value; - } - - Penumbra.Config.Save(); - } - - public override void DrawColumn( Record item, int idx ) - { - ImGui.TextUnformatted( item.ResourceType.ToString().ToLowerInvariant() ); - } - } - - private sealed class HandleColumn : ColumnString< Record > - { - public override float Width - => 120 * UiHelpers.Scale; - - public override unsafe string ToName( Record item ) - => item.Handle == null ? string.Empty : $"0x{( ulong )item.Handle:X}"; - - public override unsafe void DrawColumn( Record item, int _ ) - { - using var font = ImRaii.PushFont( UiBuilder.MonoFont, item.Handle != null ); - ImGuiUtil.RightAlign( ToName( item ) ); - } - } - - [Flags] - private enum BoolEnum : byte - { - True = 0x01, - False = 0x02, - Unknown = 0x04, - } - - private class OptBoolColumn : ColumnFlags< BoolEnum, Record > - { - private BoolEnum _filter; - - public OptBoolColumn() - { - AllFlags = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown; - _filter = AllFlags; - Flags &= ~ImGuiTableColumnFlags.NoSort; - } - - protected bool FilterFunc( OptionalBool b ) - => b.Value switch - { - null => _filter.HasFlag( BoolEnum.Unknown ), - true => _filter.HasFlag( BoolEnum.True ), - false => _filter.HasFlag( BoolEnum.False ), - }; - - public override BoolEnum FilterValue - => _filter; - - protected override void SetValue( BoolEnum value, bool enable ) - { - if( enable ) - { - _filter |= value; - } - else - { - _filter &= ~value; - } - } - - protected static void DrawColumn( OptionalBool b ) - { - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - ImGui.TextUnformatted( b.Value switch - { - null => string.Empty, - true => FontAwesomeIcon.Check.ToIconString(), - false => FontAwesomeIcon.Times.ToIconString(), - } ); - } - } - - private sealed class CustomLoadColumn : OptBoolColumn - { - public override float Width - => 60 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterFunc( item.CustomLoad ); - - public override void DrawColumn( Record item, int idx ) - => DrawColumn( item.CustomLoad ); - } - - private sealed class SynchronousLoadColumn : OptBoolColumn - { - public override float Width - => 45 * UiHelpers.Scale; - - public override bool FilterFunc( Record item ) - => FilterFunc( item.Synchronously ); - - public override void DrawColumn( Record item, int idx ) - => DrawColumn( item.Synchronously ); - } - - private sealed class RefCountColumn : Column< Record > - { - public override float Width - => 30 * UiHelpers.Scale; - - public override void DrawColumn( Record item, int _ ) - => ImGuiUtil.RightAlign( item.RefCount.ToString() ); - - public override int Compare( Record lhs, Record rhs ) - => lhs.RefCount.CompareTo( rhs.RefCount ); + if (ImGui.IsItemHovered()) + ImGuiNative.igSetTooltip(path.Path); } } -} \ No newline at end of file + + private sealed class RecordTypeColumn : ColumnFlags + { + private readonly Configuration _config; + + public RecordTypeColumn(Configuration config) + { + AllFlags = ResourceWatcher.AllRecords; + _config = config; + } + + public override float Width + => 80 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterValue.HasFlag(item.RecordType); + + public override RecordType FilterValue + => _config.ResourceWatcherRecordTypes; + + protected override void SetValue(RecordType value, bool enable) + { + if (enable) + _config.ResourceWatcherRecordTypes |= value; + else + _config.ResourceWatcherRecordTypes &= ~value; + + Penumbra.Config.Save(); + } + + public override void DrawColumn(Record item, int idx) + { + ImGui.TextUnformatted(item.RecordType switch + { + RecordType.Request => "REQ", + RecordType.ResourceLoad => "LOAD", + RecordType.FileLoad => "FILE", + RecordType.Destruction => "DEST", + _ => string.Empty, + }); + } + } + + private sealed class DateColumn : Column + { + public override float Width + => 80 * UiHelpers.Scale; + + public override int Compare(Record lhs, Record rhs) + => lhs.Time.CompareTo(rhs.Time); + + public override void DrawColumn(Record item, int _) + => ImGui.TextUnformatted($"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}"); + } + + + private sealed class CollectionColumn : ColumnString + { + public override float Width + => 80 * UiHelpers.Scale; + + public override string ToName(Record item) + => item.Collection?.Name ?? string.Empty; + } + + private sealed class ObjectColumn : ColumnString + { + public override float Width + => 200 * UiHelpers.Scale; + + public override string ToName(Record item) + => item.AssociatedGameObject; + } + + private sealed class OriginalPathColumn : ColumnString + { + public override float Width + => 200 * UiHelpers.Scale; + + public override string ToName(Record item) + => item.OriginalPath.ToString(); + + public override int Compare(Record lhs, Record rhs) + => lhs.OriginalPath.CompareTo(rhs.OriginalPath); + + public override void DrawColumn(Record item, int _) + => DrawByteString(item.OriginalPath, 190 * UiHelpers.Scale); + } + + private sealed class ResourceCategoryColumn : ColumnFlags + { + public ResourceCategoryColumn() + => AllFlags = ResourceExtensions.AllResourceCategories; + + public override float Width + => 80 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterValue.HasFlag(item.Category); + + public override ResourceCategoryFlag FilterValue + => Penumbra.Config.ResourceWatcherResourceCategories; + + protected override void SetValue(ResourceCategoryFlag value, bool enable) + { + if (enable) + Penumbra.Config.ResourceWatcherResourceCategories |= value; + else + Penumbra.Config.ResourceWatcherResourceCategories &= ~value; + + Penumbra.Config.Save(); + } + + public override void DrawColumn(Record item, int idx) + { + ImGui.TextUnformatted(item.Category.ToString()); + } + } + + private sealed class ResourceTypeColumn : ColumnFlags + { + public ResourceTypeColumn() + { + AllFlags = Enum.GetValues().Aggregate((v, f) => v | f); + for (var i = 0; i < Names.Length; ++i) + Names[i] = Names[i].ToLowerInvariant(); + } + + public override float Width + => 50 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterValue.HasFlag(item.ResourceType); + + public override ResourceTypeFlag FilterValue + => Penumbra.Config.ResourceWatcherResourceTypes; + + protected override void SetValue(ResourceTypeFlag value, bool enable) + { + if (enable) + Penumbra.Config.ResourceWatcherResourceTypes |= value; + else + Penumbra.Config.ResourceWatcherResourceTypes &= ~value; + + Penumbra.Config.Save(); + } + + public override void DrawColumn(Record item, int idx) + { + ImGui.TextUnformatted(item.ResourceType.ToString().ToLowerInvariant()); + } + } + + private sealed class HandleColumn : ColumnString + { + public override float Width + => 120 * UiHelpers.Scale; + + public override unsafe string ToName(Record item) + => item.Handle == null ? string.Empty : $"0x{(ulong)item.Handle:X}"; + + public override unsafe void DrawColumn(Record item, int _) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, item.Handle != null); + ImGuiUtil.RightAlign(ToName(item)); + } + } + + [Flags] + private enum BoolEnum : byte + { + True = 0x01, + False = 0x02, + Unknown = 0x04, + } + + private class OptBoolColumn : ColumnFlags + { + private BoolEnum _filter; + + public OptBoolColumn() + { + AllFlags = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown; + _filter = AllFlags; + Flags &= ~ImGuiTableColumnFlags.NoSort; + } + + protected bool FilterFunc(OptionalBool b) + => b.Value switch + { + null => _filter.HasFlag(BoolEnum.Unknown), + true => _filter.HasFlag(BoolEnum.True), + false => _filter.HasFlag(BoolEnum.False), + }; + + public override BoolEnum FilterValue + => _filter; + + protected override void SetValue(BoolEnum value, bool enable) + { + if (enable) + _filter |= value; + else + _filter &= ~value; + } + + protected static void DrawColumn(OptionalBool b) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(b.Value switch + { + null => string.Empty, + true => FontAwesomeIcon.Check.ToIconString(), + false => FontAwesomeIcon.Times.ToIconString(), + }); + } + } + + private sealed class CustomLoadColumn : OptBoolColumn + { + public override float Width + => 60 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterFunc(item.CustomLoad); + + public override void DrawColumn(Record item, int idx) + => DrawColumn(item.CustomLoad); + } + + private sealed class SynchronousLoadColumn : OptBoolColumn + { + public override float Width + => 45 * UiHelpers.Scale; + + public override bool FilterFunc(Record item) + => FilterFunc(item.Synchronously); + + public override void DrawColumn(Record item, int idx) + => DrawColumn(item.Synchronously); + } + + private sealed class RefCountColumn : Column + { + public override float Width + => 30 * UiHelpers.Scale; + + public override void DrawColumn(Record item, int _) + => ImGuiUtil.RightAlign(item.RefCount.ToString()); + + public override int Compare(Record lhs, Record rhs) + => lhs.RefCount.CompareTo(rhs.RefCount); + } +} diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 66ece068..8b054033 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,18 +1,19 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; +using System.Linq; using System.Text.RegularExpressions; -using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -21,24 +22,27 @@ namespace Penumbra.UI; public partial class ResourceWatcher : IDisposable, ITab { - public const int DefaultMaxEntries = 1024; + public const int DefaultMaxEntries = 1024; + public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; private readonly Configuration _config; private readonly ResourceService _resources; private readonly ResourceLoader _loader; + private readonly ActorService _actors; private readonly List _records = new(); private readonly ConcurrentQueue _newRecords = new(); - private readonly Table _table; + private readonly ResourceWatcherTable _table; private string _logFilter = string.Empty; private Regex? _logRegex; private int _newMaxEntries; - public unsafe ResourceWatcher(Configuration config, ResourceService resources, ResourceLoader loader) + public unsafe ResourceWatcher(ActorService actors, Configuration config, ResourceService resources, ResourceLoader loader) { + _actors = actors; _config = config; _resources = resources; _loader = loader; - _table = new Table(_records); + _table = new ResourceWatcherTable(config, _records); _resources.ResourceRequested += OnResourceRequested; _resources.ResourceHandleDestructor += OnResourceDestroyed; _loader.ResourceLoaded += OnResourceLoaded; @@ -75,8 +79,8 @@ public partial class ResourceWatcher : IDisposable, ITab var isEnabled = _config.EnableResourceWatcher; if (ImGui.Checkbox("Enable", ref isEnabled)) { - Penumbra.Config.EnableResourceWatcher = isEnabled; - Penumbra.Config.Save(); + _config.EnableResourceWatcher = isEnabled; + _config.Save(); } ImGui.SameLine(); @@ -89,16 +93,16 @@ public partial class ResourceWatcher : IDisposable, ITab var onlyMatching = _config.OnlyAddMatchingResources; if (ImGui.Checkbox("Store Only Matching", ref onlyMatching)) { - Penumbra.Config.OnlyAddMatchingResources = onlyMatching; - Penumbra.Config.Save(); + _config.OnlyAddMatchingResources = onlyMatching; + _config.Save(); } ImGui.SameLine(); var writeToLog = _config.EnableResourceLogging; if (ImGui.Checkbox("Write to Log", ref writeToLog)) { - Penumbra.Config.EnableResourceLogging = writeToLog; - Penumbra.Config.Save(); + _config.EnableResourceLogging = writeToLog; + _config.Save(); } ImGui.SameLine(); @@ -137,8 +141,8 @@ public partial class ResourceWatcher : IDisposable, ITab if (config) { - Penumbra.Config.ResourceLoggingFilter = newString; - Penumbra.Config.Save(); + _config.ResourceLoggingFilter = newString; + _config.Save(); } } @@ -168,43 +172,44 @@ public partial class ResourceWatcher : IDisposable, ITab return; _newMaxEntries = Math.Max(16, _newMaxEntries); - if (_newMaxEntries != maxEntries) - { - _config.MaxResourceWatcherRecords = _newMaxEntries; - Penumbra.Config.Save(); - if (_newMaxEntries > _records.Count) - _records.RemoveRange(0, _records.Count - _newMaxEntries); - } + if (_newMaxEntries == maxEntries) + return; + + _config.MaxResourceWatcherRecords = _newMaxEntries; + _config.Save(); + if (_newMaxEntries > _records.Count) + _records.RemoveRange(0, _records.Count - _newMaxEntries); } private void UpdateRecords() { var count = _newRecords.Count; - if (count > 0) - { - while (_newRecords.TryDequeue(out var rec) && count-- > 0) - _records.Add(rec); + if (count <= 0) + return; - if (_records.Count > _config.MaxResourceWatcherRecords) - _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); + while (_newRecords.TryDequeue(out var rec) && count-- > 0) + _records.Add(rec); - _table.Reset(); - } + if (_records.Count > _config.MaxResourceWatcherRecords) + _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); + + _table.Reset(); } 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 (_config.EnableResourceLogging && FilterMatch(path.Path, out var match)) + if (_config.EnableResourceLogging && FilterMatch(original.Path, out var match)) Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}"); - if (_config.EnableResourceWatcher) - { - var record = Record.CreateRequest(path.Path, sync); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); - } + if (!_config.EnableResourceWatcher) + return; + + var record = Record.CreateRequest(original.Path, sync); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) @@ -220,19 +225,18 @@ public partial class ResourceWatcher : IDisposable, ITab { var pathString = manipulatedPath != 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.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) "); + $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.AnonymizedName} for {Name(data, "no associated object.")} (Refcount {handle->RefCount}) "); } } - if (_config.EnableResourceWatcher) - { - var record = manipulatedPath == null - ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection) - : Record.CreateLoad(path.Path, manipulatedPath.Value.InternalName, handle, - data.ModCollection); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); - } + if (!_config.EnableResourceWatcher) + return; + + var record = manipulatedPath == null + ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) + : Record.CreateLoad(manipulatedPath.Value.InternalName, path.Path, handle, data.ModCollection, Name(data)); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _) @@ -241,12 +245,12 @@ public partial class ResourceWatcher : IDisposable, ITab Penumbra.Log.Information( $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}."); - if (_config.EnableResourceWatcher) - { - var record = Record.CreateFileLoad(path, resource, success, custom); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); - } + if (!_config.EnableResourceWatcher) + return; + + var record = Record.CreateFileLoad(path, resource, success, custom); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); } private unsafe void OnResourceDestroyed(ResourceHandle* resource) @@ -255,11 +259,37 @@ public partial class ResourceWatcher : IDisposable, ITab Penumbra.Log.Information( $"[ResourceLoader] [DEST] [{resource->FileType}] Destroyed {match} at 0x{(ulong)resource:X}."); - if (_config.EnableResourceWatcher) + if (!_config.EnableResourceWatcher) + return; + + var record = Record.CreateDestruction(resource); + if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); + } + + public unsafe string Name(ResolveData resolve, string none = "") + { + if (resolve.AssociatedGameObject == IntPtr.Zero || !_actors.Valid) + return none; + + try { - var record = Record.CreateDestruction(resource); - if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); + var id = _actors.AwaitedService.FromObject((GameObject*)resolve.AssociatedGameObject, out _, false, true, true); + if (id.IsValid) + { + if (id.Type is not (IdentifierType.Player or IdentifierType.Owned)) + return id.ToString(); + + var parts = id.ToString().Split(' ', 3); + return string.Join(" ", + parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2])); + } } + catch + { + // ignored + } + + return $"0x{resolve.AssociatedGameObject:X}"; } }