Fix some issues with ResourceWatcher.

This commit is contained in:
Ottermandias 2023-03-29 14:42:34 +02:00
parent a8000fbf14
commit 185be81e73
8 changed files with 519 additions and 529 deletions

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.GameData.Actors;
namespace Penumbra.Collections; namespace Penumbra.Collections;
@ -36,30 +37,6 @@ public readonly struct ResolveData
public override string ToString() public override string ToString()
=> ModCollection.Name; => 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 public static class ResolveDataExtensions

View file

@ -62,9 +62,9 @@ public class Configuration : IPluginConfiguration, ISavable
public bool OnlyAddMatchingResources { get; set; } = true; public bool OnlyAddMatchingResources { get; set; } = true;
public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public ResourceWatcher.RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
[JsonConverter(typeof(SortModeConverter))] [JsonConverter(typeof(SortModeConverter))]

View file

@ -66,7 +66,7 @@ public unsafe class ResourceLoader : IDisposable
_fileReadService.ReadSqPack -= ReadSqPackDetour; _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) GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue)
{ {
if (returnValue != null) if (returnValue != null)
@ -86,9 +86,10 @@ public unsafe class ResourceLoader : IDisposable
_texMdlService.AddCrc(type, resolvedPath); _texMdlService.AddCrc(type, resolvedPath);
// Replace the hash and path with the correct one for the replacement. // Replace the hash and path with the correct one for the replacement.
hash = ComputeHash(resolvedPath.Value.InternalName, parameters); hash = ComputeHash(resolvedPath.Value.InternalName, parameters);
var oldPath = path;
path = p; path = p;
returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); 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) private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue)

View file

@ -60,7 +60,7 @@ public unsafe class ResourceService : IDisposable
/// <param name="parameters">Mainly used for SCD streaming, can be null.</param> /// <param name="parameters">Mainly used for SCD streaming, can be null.</param>
/// <param name="sync">Whether to request the resource synchronously or asynchronously.</param> /// <param name="sync">Whether to request the resource synchronously or asynchronously.</param>
/// <param name="returnValue">The returned resource handle. If this is not null, calling original will be skipped. </param> /// <param name="returnValue">The returned resource handle. If this is not null, calling original will be skipped. </param>
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); GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue);
/// <summary> <inheritdoc cref="GetResourcePreDelegate"/> <para/> /// <summary> <inheritdoc cref="GetResourcePreDelegate"/> <para/>
@ -104,7 +104,7 @@ public unsafe class ResourceService : IDisposable
} }
ResourceHandle* returnValue = null; 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); ref returnValue);
if (returnValue != null) if (returnValue != null)
return returnValue; return returnValue;

View file

@ -7,112 +7,123 @@ using Penumbra.String;
namespace Penumbra.UI; namespace Penumbra.UI;
public partial class ResourceWatcher [Flags]
public enum RecordType : byte
{ {
private unsafe struct Record Request = 0x01,
{ ResourceLoad = 0x02,
public DateTime Time; FileLoad = 0x04,
public ByteString Path; Destruction = 0x08,
public ByteString OriginalPath; }
public ModCollection? Collection;
public ResourceHandle* Handle; internal unsafe struct Record
public ResourceTypeFlag ResourceType; {
public ResourceCategoryFlag Category; public DateTime Time;
public uint RefCount; public ByteString Path;
public RecordType RecordType; public ByteString OriginalPath;
public OptionalBool Synchronously; public string AssociatedGameObject;
public OptionalBool ReturnValue; public ModCollection? Collection;
public OptionalBool CustomLoad; public ResourceHandle* Handle;
public ResourceTypeFlag ResourceType;
public static Record CreateRequest( ByteString path, bool sync ) public ResourceCategoryFlag Category;
=> new() public uint RefCount;
{ public RecordType RecordType;
Time = DateTime.UtcNow, public OptionalBool Synchronously;
Path = path.IsOwned ? path : path.Clone(), public OptionalBool ReturnValue;
OriginalPath = ByteString.Empty, public OptionalBool CustomLoad;
Collection = null,
Handle = null, public static Record CreateRequest(ByteString path, bool sync)
ResourceType = ResourceExtensions.Type( path ).ToFlag(), => new()
Category = ResourceExtensions.Category( path ).ToFlag(), {
RefCount = 0, Time = DateTime.UtcNow,
RecordType = RecordType.Request, Path = path.IsOwned ? path : path.Clone(),
Synchronously = sync, OriginalPath = ByteString.Empty,
ReturnValue = OptionalBool.Null, Collection = null,
CustomLoad = OptionalBool.Null, Handle = null,
}; ResourceType = ResourceExtensions.Type(path).ToFlag(),
Category = ResourceExtensions.Category(path).ToFlag(),
public static Record CreateDefaultLoad( ByteString path, ResourceHandle* handle, ModCollection collection ) RefCount = 0,
{ RecordType = RecordType.Request,
path = path.IsOwned ? path : path.Clone(); Synchronously = sync,
return new Record ReturnValue = OptionalBool.Null,
{ CustomLoad = OptionalBool.Null,
Time = DateTime.UtcNow, AssociatedGameObject = string.Empty,
Path = path, };
OriginalPath = path,
Collection = collection, public static Record CreateDefaultLoad(ByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject)
Handle = handle, {
ResourceType = handle->FileType.ToFlag(), path = path.IsOwned ? path : path.Clone();
Category = handle->Category.ToFlag(), return new Record
RefCount = handle->RefCount, {
RecordType = RecordType.ResourceLoad, Time = DateTime.UtcNow,
Synchronously = OptionalBool.Null, Path = path,
ReturnValue = OptionalBool.Null, OriginalPath = path,
CustomLoad = false, Collection = collection,
}; Handle = handle,
} ResourceType = handle->FileType.ToFlag(),
Category = handle->Category.ToFlag(),
public static Record CreateLoad( ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection ) RefCount = handle->RefCount,
=> new() RecordType = RecordType.ResourceLoad,
{ Synchronously = OptionalBool.Null,
Time = DateTime.UtcNow, ReturnValue = OptionalBool.Null,
Path = path.IsOwned ? path : path.Clone(), CustomLoad = false,
OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), AssociatedGameObject = associatedGameObject,
Collection = collection, };
Handle = handle, }
ResourceType = handle->FileType.ToFlag(),
Category = handle->Category.ToFlag(), public static Record CreateLoad(ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection,
RefCount = handle->RefCount, string associatedGameObject)
RecordType = RecordType.ResourceLoad, => new()
Synchronously = OptionalBool.Null, {
ReturnValue = OptionalBool.Null, Time = DateTime.UtcNow,
CustomLoad = true, Path = path.IsOwned ? path : path.Clone(),
}; OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(),
Collection = collection,
public static Record CreateDestruction( ResourceHandle* handle ) Handle = handle,
{ ResourceType = handle->FileType.ToFlag(),
var path = handle->FileName().Clone(); Category = handle->Category.ToFlag(),
return new Record RefCount = handle->RefCount,
{ RecordType = RecordType.ResourceLoad,
Time = DateTime.UtcNow, Synchronously = OptionalBool.Null,
Path = path, ReturnValue = OptionalBool.Null,
OriginalPath = ByteString.Empty, CustomLoad = true,
Collection = null, AssociatedGameObject = associatedGameObject,
Handle = handle, };
ResourceType = handle->FileType.ToFlag(),
Category = handle->Category.ToFlag(), public static Record CreateDestruction(ResourceHandle* handle)
RefCount = handle->RefCount, {
RecordType = RecordType.Destruction, var path = handle->FileName().Clone();
Synchronously = OptionalBool.Null, return new Record
ReturnValue = OptionalBool.Null, {
CustomLoad = OptionalBool.Null, Time = DateTime.UtcNow,
}; Path = path,
} OriginalPath = ByteString.Empty,
Collection = null,
public static Record CreateFileLoad( ByteString path, ResourceHandle* handle, bool ret, bool custom ) Handle = handle,
=> new() ResourceType = handle->FileType.ToFlag(),
{ Category = handle->Category.ToFlag(),
Time = DateTime.UtcNow, RefCount = handle->RefCount,
Path = path.IsOwned ? path : path.Clone(), RecordType = RecordType.Destruction,
OriginalPath = ByteString.Empty, Synchronously = OptionalBool.Null,
Collection = null, ReturnValue = OptionalBool.Null,
Handle = handle, CustomLoad = OptionalBool.Null,
ResourceType = handle->FileType.ToFlag(), };
Category = handle->Category.ToFlag(), }
RefCount = handle->RefCount,
RecordType = RecordType.FileLoad, public static Record CreateFileLoad(ByteString path, ResourceHandle* handle, bool ret, bool custom)
Synchronously = OptionalBool.Null, => new()
ReturnValue = ret, {
CustomLoad = custom, 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,
};
} }

View file

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

View file

@ -13,340 +13,328 @@ using Penumbra.String;
namespace Penumbra.UI; namespace Penumbra.UI;
public partial class ResourceWatcher internal sealed class ResourceWatcherTable : Table<Record>
{ {
private sealed class Table : Table< Record > public ResourceWatcherTable(Configuration config, ICollection<Record> 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<Record>
{ {
private static readonly PathColumn Path = new() { Label = "Path" }; public override float Width
private static readonly RecordTypeColumn RecordType = new() { Label = "Record" }; => 300 * UiHelpers.Scale;
private static readonly DateColumn Date = new() { Label = "Time" };
private static readonly CollectionColumn Coll = new() { Label = "Collection" }; public override string ToName(Record item)
private static readonly CustomLoadColumn Custom = new() { Label = "Custom" }; => item.Path.ToString();
private static readonly SynchronousLoadColumn Sync = new() { Label = "Sync" };
private static readonly OriginalPathColumn Orig = new() { Label = "Original Path" }; public override int Compare(Record lhs, Record rhs)
private static readonly ResourceCategoryColumn Cat = new() { Label = "Category" }; => lhs.Path.CompareTo(rhs.Path);
private static readonly ResourceTypeColumn Type = new() { Label = "Type" };
private static readonly HandleColumn Handle = new() { Label = "Resource" }; public override void DrawColumn(Record item, int _)
private static readonly RefCountColumn Ref = new() { Label = "#Ref" }; => DrawByteString(item.Path, 280 * UiHelpers.Scale);
}
public Table( ICollection< Record > records ) private static unsafe void DrawByteString(ByteString path, float length)
: base( "##records", records, Path, RecordType, Coll, Custom, Sync, Orig, Cat, Type, Handle, Ref, Date ) {
{ } Vector2 vec;
ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0);
public void Reset() if (vec.X <= length)
=> FilterDirty = true;
private sealed class PathColumn : ColumnString< Record >
{ {
public override float Width ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length);
=> 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 );
} }
else
private static unsafe void DrawByteString( ByteString path, float length )
{ {
Vector2 vec; var fileName = path.LastIndexOf((byte)'/');
ImGuiNative.igCalcTextSize( &vec, path.Path, path.Path + path.Length, 0, 0 ); ByteString shortPath;
if( vec.X <= length ) 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 else
{ {
var fileName = path.LastIndexOf( ( byte )'/' ); shortPath = path;
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();
} }
public override void DrawColumn( Record item, int idx ) ImGuiNative.igTextUnformatted(shortPath.Path, shortPath.Path + shortPath.Length);
{ if (ImGui.IsItemClicked())
ImGui.TextUnformatted( item.RecordType switch ImGuiNative.igSetClipboardText(path.Path);
{
ResourceWatcher.RecordType.Request => "REQ",
ResourceWatcher.RecordType.ResourceLoad => "LOAD",
ResourceWatcher.RecordType.FileLoad => "FILE",
ResourceWatcher.RecordType.Destruction => "DEST",
_ => string.Empty,
} );
}
}
private sealed class DateColumn : Column< Record > if (ImGui.IsItemHovered())
{ ImGuiNative.igSetTooltip(path.Path);
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 );
} }
} }
private sealed class RecordTypeColumn : ColumnFlags<RecordType, Record>
{
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<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 ObjectColumn : ColumnString<Record>
{
public override float Width
=> 200 * UiHelpers.Scale;
public override string ToName(Record item)
=> item.AssociatedGameObject;
}
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);
}
} }

View file

@ -1,18 +1,19 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using ImGuiNET; using ImGuiNET;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Services;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
@ -21,24 +22,27 @@ namespace Penumbra.UI;
public partial class ResourceWatcher : IDisposable, ITab 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 Configuration _config;
private readonly ResourceService _resources; private readonly ResourceService _resources;
private readonly ResourceLoader _loader; private readonly ResourceLoader _loader;
private readonly ActorService _actors;
private readonly List<Record> _records = new(); private readonly List<Record> _records = new();
private readonly ConcurrentQueue<Record> _newRecords = new(); private readonly ConcurrentQueue<Record> _newRecords = new();
private readonly Table _table; private readonly ResourceWatcherTable _table;
private string _logFilter = string.Empty; private string _logFilter = string.Empty;
private Regex? _logRegex; private Regex? _logRegex;
private int _newMaxEntries; 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; _config = config;
_resources = resources; _resources = resources;
_loader = loader; _loader = loader;
_table = new Table(_records); _table = new ResourceWatcherTable(config, _records);
_resources.ResourceRequested += OnResourceRequested; _resources.ResourceRequested += OnResourceRequested;
_resources.ResourceHandleDestructor += OnResourceDestroyed; _resources.ResourceHandleDestructor += OnResourceDestroyed;
_loader.ResourceLoaded += OnResourceLoaded; _loader.ResourceLoaded += OnResourceLoaded;
@ -75,8 +79,8 @@ public partial class ResourceWatcher : IDisposable, ITab
var isEnabled = _config.EnableResourceWatcher; var isEnabled = _config.EnableResourceWatcher;
if (ImGui.Checkbox("Enable", ref isEnabled)) if (ImGui.Checkbox("Enable", ref isEnabled))
{ {
Penumbra.Config.EnableResourceWatcher = isEnabled; _config.EnableResourceWatcher = isEnabled;
Penumbra.Config.Save(); _config.Save();
} }
ImGui.SameLine(); ImGui.SameLine();
@ -89,16 +93,16 @@ public partial class ResourceWatcher : IDisposable, ITab
var onlyMatching = _config.OnlyAddMatchingResources; var onlyMatching = _config.OnlyAddMatchingResources;
if (ImGui.Checkbox("Store Only Matching", ref onlyMatching)) if (ImGui.Checkbox("Store Only Matching", ref onlyMatching))
{ {
Penumbra.Config.OnlyAddMatchingResources = onlyMatching; _config.OnlyAddMatchingResources = onlyMatching;
Penumbra.Config.Save(); _config.Save();
} }
ImGui.SameLine(); ImGui.SameLine();
var writeToLog = _config.EnableResourceLogging; var writeToLog = _config.EnableResourceLogging;
if (ImGui.Checkbox("Write to Log", ref writeToLog)) if (ImGui.Checkbox("Write to Log", ref writeToLog))
{ {
Penumbra.Config.EnableResourceLogging = writeToLog; _config.EnableResourceLogging = writeToLog;
Penumbra.Config.Save(); _config.Save();
} }
ImGui.SameLine(); ImGui.SameLine();
@ -137,8 +141,8 @@ public partial class ResourceWatcher : IDisposable, ITab
if (config) if (config)
{ {
Penumbra.Config.ResourceLoggingFilter = newString; _config.ResourceLoggingFilter = newString;
Penumbra.Config.Save(); _config.Save();
} }
} }
@ -168,43 +172,44 @@ public partial class ResourceWatcher : IDisposable, ITab
return; return;
_newMaxEntries = Math.Max(16, _newMaxEntries); _newMaxEntries = Math.Max(16, _newMaxEntries);
if (_newMaxEntries != maxEntries) if (_newMaxEntries == maxEntries)
{ return;
_config.MaxResourceWatcherRecords = _newMaxEntries;
Penumbra.Config.Save(); _config.MaxResourceWatcherRecords = _newMaxEntries;
if (_newMaxEntries > _records.Count) _config.Save();
_records.RemoveRange(0, _records.Count - _newMaxEntries); if (_newMaxEntries > _records.Count)
} _records.RemoveRange(0, _records.Count - _newMaxEntries);
} }
private void UpdateRecords() private void UpdateRecords()
{ {
var count = _newRecords.Count; var count = _newRecords.Count;
if (count > 0) if (count <= 0)
{ return;
while (_newRecords.TryDequeue(out var rec) && count-- > 0)
_records.Add(rec);
if (_records.Count > _config.MaxResourceWatcherRecords) while (_newRecords.TryDequeue(out var rec) && count-- > 0)
_records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); _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, 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) 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.")}"); Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}");
if (_config.EnableResourceWatcher) if (!_config.EnableResourceWatcher)
{ return;
var record = Record.CreateRequest(path.Path, sync);
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) var record = Record.CreateRequest(original.Path, sync);
_newRecords.Enqueue(record); if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
} _newRecords.Enqueue(record);
} }
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data)
@ -220,19 +225,18 @@ public partial class ResourceWatcher : IDisposable, ITab
{ {
var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name; var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name;
Penumbra.Log.Information( Penumbra.Log.Information(
$"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.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) if (!_config.EnableResourceWatcher)
{ return;
var record = manipulatedPath == null
? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection) var record = manipulatedPath == null
: Record.CreateLoad(path.Path, manipulatedPath.Value.InternalName, handle, ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data))
data.ModCollection); : Record.CreateLoad(manipulatedPath.Value.InternalName, path.Path, handle, data.ModCollection, Name(data));
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record); _newRecords.Enqueue(record);
}
} }
private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _) 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( Penumbra.Log.Information(
$"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}."); $"[ResourceLoader] [FILE] [{resource->FileType}] Loading {match} from {(custom ? "local files" : "SqPack")} into 0x{(ulong)resource:X} returned {success}.");
if (_config.EnableResourceWatcher) if (!_config.EnableResourceWatcher)
{ return;
var record = Record.CreateFileLoad(path, resource, success, custom);
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) var record = Record.CreateFileLoad(path, resource, success, custom);
_newRecords.Enqueue(record); if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record))
} _newRecords.Enqueue(record);
} }
private unsafe void OnResourceDestroyed(ResourceHandle* resource) private unsafe void OnResourceDestroyed(ResourceHandle* resource)
@ -255,11 +259,37 @@ public partial class ResourceWatcher : IDisposable, ITab
Penumbra.Log.Information( Penumbra.Log.Information(
$"[ResourceLoader] [DEST] [{resource->FileType}] Destroyed {match} at 0x{(ulong)resource:X}."); $"[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); var id = _actors.AwaitedService.FromObject((GameObject*)resolve.AssociatedGameObject, out _, false, true, true);
if (!_config.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (id.IsValid)
_newRecords.Enqueue(record); {
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}";
} }
} }