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

View file

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

View file

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

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="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>
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);
/// <summary> <inheritdoc cref="GetResourcePreDelegate"/> <para/>
@ -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;

View file

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

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;
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" };
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);
}
}
}
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.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<Record> _records = new();
private readonly ConcurrentQueue<Record> _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}";
}
}