mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-21 23:37:47 +01:00
Working on PathResolver
This commit is contained in:
parent
6f527a1dbc
commit
e7282384f5
29 changed files with 1170 additions and 527 deletions
205
Penumbra/Interop/Loader/ResourceLoader.Debug.cs
Normal file
205
Penumbra/Interop/Loader/ResourceLoader.Debug.cs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
using FFXIVClientStructs.STD;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Interop.Loader;
|
||||
|
||||
public unsafe partial class ResourceLoader
|
||||
{
|
||||
// A static pointer to the SE Resource Manager
|
||||
[Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0", ScanType = ScanType.StaticAddress, UseFlags = SignatureUseFlags.Pointer )]
|
||||
public static ResourceManager** ResourceManager;
|
||||
|
||||
// Gather some debugging data about penumbra-loaded objects.
|
||||
public struct DebugData
|
||||
{
|
||||
public ResourceHandle* OriginalResource;
|
||||
public ResourceHandle* ManipulatedResource;
|
||||
public Utf8GamePath OriginalPath;
|
||||
public FullPath ManipulatedPath;
|
||||
public ResourceCategory Category;
|
||||
public object? ResolverInfo;
|
||||
public uint Extension;
|
||||
}
|
||||
|
||||
private readonly SortedDictionary< FullPath, DebugData > _debugList = new();
|
||||
private readonly List< (FullPath, DebugData?) > _deleteList = new();
|
||||
|
||||
public IReadOnlyDictionary< FullPath, DebugData > DebugList
|
||||
=> _debugList;
|
||||
|
||||
public void EnableDebug()
|
||||
{
|
||||
ResourceLoaded += AddModifiedDebugInfo;
|
||||
}
|
||||
|
||||
public void DisableDebug()
|
||||
{
|
||||
ResourceLoaded -= AddModifiedDebugInfo;
|
||||
}
|
||||
|
||||
private void AddModifiedDebugInfo( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolverInfo )
|
||||
{
|
||||
if( manipulatedPath == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var crc = ( uint )originalPath.Path.Crc32;
|
||||
var originalResource = ( *ResourceManager )->FindResourceHandle( &handle->Category, &handle->FileType, &crc );
|
||||
_debugList[ manipulatedPath.Value ] = new DebugData()
|
||||
{
|
||||
OriginalResource = originalResource,
|
||||
ManipulatedResource = handle,
|
||||
Category = handle->Category,
|
||||
Extension = handle->FileType,
|
||||
OriginalPath = originalPath.Clone(),
|
||||
ManipulatedPath = manipulatedPath.Value,
|
||||
ResolverInfo = resolverInfo,
|
||||
};
|
||||
}
|
||||
|
||||
// Find a key in a StdMap.
|
||||
private static TValue* FindInMap< TKey, TValue >( StdMap< TKey, TValue >* map, in TKey key )
|
||||
where TKey : unmanaged, IComparable< TKey >
|
||||
where TValue : unmanaged
|
||||
{
|
||||
if( map == null || map->Count == 0 )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var node = map->Head->Parent;
|
||||
while( !node->IsNil )
|
||||
{
|
||||
switch( key.CompareTo( node->KeyValuePair.Item1 ) )
|
||||
{
|
||||
case 0: return &node->KeyValuePair.Item2;
|
||||
case < 0:
|
||||
node = node->Left;
|
||||
break;
|
||||
default:
|
||||
node = node->Right;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Iterate in tree-order through a map, applying action to each KeyValuePair.
|
||||
private static void IterateMap< TKey, TValue >( StdMap< TKey, TValue >* map, Action< TKey, TValue > action )
|
||||
where TKey : unmanaged
|
||||
where TValue : unmanaged
|
||||
{
|
||||
if( map == null || map->Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for( var node = map->SmallestValue; !node->IsNil; node = node->Next() )
|
||||
{
|
||||
action( node->KeyValuePair.Item1, node->KeyValuePair.Item2 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Find a resource in the resource manager by its category, extension and crc-hash
|
||||
public static ResourceHandle* FindResource( ResourceCategory cat, uint ext, uint crc32 )
|
||||
{
|
||||
var manager = *ResourceManager;
|
||||
var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat;
|
||||
var extMap = FindInMap( category->MainMap, ext );
|
||||
if( extMap == null )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ret = FindInMap( extMap->Value, crc32 );
|
||||
return ret == null ? null : ret->Value;
|
||||
}
|
||||
|
||||
public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph );
|
||||
public delegate void ResourceMapAction( uint ext, StdMap< uint, Pointer< ResourceHandle > >* graph );
|
||||
public delegate void ResourceAction( uint crc32, ResourceHandle* graph );
|
||||
|
||||
// Iteration functions through the resource manager.
|
||||
public static void IterateGraphs( ExtMapAction action )
|
||||
{
|
||||
var manager = *ResourceManager;
|
||||
foreach( var resourceType in Enum.GetValues< ResourceCategory >().SkipLast( 1 ) )
|
||||
{
|
||||
var graph = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )resourceType;
|
||||
action( resourceType, graph->MainMap );
|
||||
}
|
||||
}
|
||||
|
||||
public static void IterateExtMap( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map, ResourceMapAction action )
|
||||
=> IterateMap( map, ( ext, m ) => action( ext, m.Value ) );
|
||||
|
||||
public static void IterateResourceMap( StdMap< uint, Pointer< ResourceHandle > >* map, ResourceAction action )
|
||||
=> IterateMap( map, ( crc, r ) => action( crc, r.Value ) );
|
||||
|
||||
public static void IterateResources( ResourceAction action )
|
||||
{
|
||||
IterateGraphs( ( _, extMap )
|
||||
=> IterateExtMap( extMap, ( _, resourceMap )
|
||||
=> IterateResourceMap( resourceMap, action ) ) );
|
||||
}
|
||||
|
||||
public void UpdateDebugInfo()
|
||||
{
|
||||
var manager = *ResourceManager;
|
||||
_deleteList.Clear();
|
||||
foreach( var data in _debugList.Values )
|
||||
{
|
||||
var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 );
|
||||
var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 );
|
||||
if( modifiedResource == null )
|
||||
{
|
||||
_deleteList.Add( ( data.ManipulatedPath, null ) );
|
||||
}
|
||||
else if( regularResource != data.OriginalResource || modifiedResource != data.ManipulatedResource )
|
||||
{
|
||||
_deleteList.Add( ( data.ManipulatedPath, data with
|
||||
{
|
||||
OriginalResource = regularResource,
|
||||
ManipulatedResource = modifiedResource,
|
||||
} ) );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var (path, data) in _deleteList )
|
||||
{
|
||||
if( data == null )
|
||||
{
|
||||
_debugList.Remove( path );
|
||||
}
|
||||
else
|
||||
{
|
||||
_debugList[ path ] = data.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logging functions for EnableFullLogging.
|
||||
private static void LogPath( Utf8GamePath path, bool synchronous )
|
||||
=> PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" );
|
||||
|
||||
private static void LogResource( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ )
|
||||
{
|
||||
var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString();
|
||||
PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" );
|
||||
}
|
||||
|
||||
private static void LogLoadedFile( Utf8String path, bool success, bool custom )
|
||||
=> PluginLog.Information( success
|
||||
? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}"
|
||||
: $"[ResourceLoader] Failed to load {path} from {( custom ? "local files" : "SqPack" )}." );
|
||||
}
|
||||
178
Penumbra/Interop/Loader/ResourceLoader.Replacement.cs
Normal file
178
Penumbra/Interop/Loader/ResourceLoader.Replacement.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Interop.Structs;
|
||||
using FileMode = Penumbra.Interop.Structs.FileMode;
|
||||
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
||||
|
||||
namespace Penumbra.Interop.Loader;
|
||||
|
||||
public unsafe partial class ResourceLoader
|
||||
{
|
||||
// Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases.
|
||||
// Both work basically the same, so we can reduce the main work to one function used by both hooks.
|
||||
public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId,
|
||||
uint* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown );
|
||||
|
||||
[Signature( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00", DetourName = "GetResourceSyncDetour" )]
|
||||
public Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!;
|
||||
|
||||
public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId,
|
||||
uint* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown, bool isUnknown );
|
||||
|
||||
[Signature( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00", DetourName = "GetResourceAsyncDetour" )]
|
||||
public Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!;
|
||||
|
||||
private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType,
|
||||
int* resourceHash, byte* path, void* unk )
|
||||
=> GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, unk, false );
|
||||
|
||||
private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType,
|
||||
int* resourceHash, byte* path, void* unk, bool isUnk )
|
||||
=> GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
||||
|
||||
private ResourceHandle* CallOriginalHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId,
|
||||
uint* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk )
|
||||
=> isSync
|
||||
? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk )
|
||||
: GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
||||
|
||||
|
||||
[Conditional( "DEBUG" )]
|
||||
private static void CompareHash( int local, int game, Utf8GamePath path )
|
||||
{
|
||||
if( local != game )
|
||||
{
|
||||
PluginLog.Warning( "Hash function appears to have changed. {Hash1:X8} vs {Hash2:X8} for {Path}.", game, local, path );
|
||||
}
|
||||
}
|
||||
|
||||
private event Action< Utf8GamePath, FullPath?, object? >? PathResolved;
|
||||
|
||||
private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType,
|
||||
int* resourceHash, byte* path, void* unk, bool isUnk )
|
||||
{
|
||||
if( !Utf8GamePath.FromPointer( path, out var gamePath ) )
|
||||
{
|
||||
PluginLog.Error( "Could not create GamePath from resource path." );
|
||||
return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
||||
}
|
||||
|
||||
CompareHash( gamePath.Path.Crc32, *resourceHash, gamePath );
|
||||
|
||||
ResourceRequested?.Invoke( gamePath, isSync );
|
||||
|
||||
// If no replacements are being made, we still want to be able to trigger the event.
|
||||
var (resolvedPath, data) = DoReplacements ? ResolvePath( gamePath.ToLower() ) : ( null, null );
|
||||
PathResolved?.Invoke( gamePath, resolvedPath, data );
|
||||
if( resolvedPath == null )
|
||||
{
|
||||
var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
||||
ResourceLoaded?.Invoke( retUnmodified, gamePath, null, data );
|
||||
return retUnmodified;
|
||||
}
|
||||
|
||||
// Replace the hash and path with the correct one for the replacement.
|
||||
*resourceHash = resolvedPath.Value.InternalName.Crc32;
|
||||
path = resolvedPath.Value.InternalName.Path;
|
||||
var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
||||
ResourceLoaded?.Invoke( retModified, gamePath, resolvedPath.Value, data );
|
||||
return retModified;
|
||||
}
|
||||
|
||||
|
||||
// We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks.
|
||||
public delegate byte ReadFileDelegate( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority,
|
||||
bool isSync );
|
||||
|
||||
[Signature( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" )]
|
||||
public ReadFileDelegate ReadFile = null!;
|
||||
|
||||
// We hook ReadSqPack to redirect rooted files to ReadFile.
|
||||
public delegate byte ReadSqPackPrototype( ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync );
|
||||
|
||||
[Signature( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3", DetourName = "ReadSqPackDetour" )]
|
||||
public Hook< ReadSqPackPrototype > ReadSqPackHook = null!;
|
||||
|
||||
private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync )
|
||||
{
|
||||
if( !DoReplacements )
|
||||
{
|
||||
return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
||||
}
|
||||
|
||||
if( fileDescriptor == null || fileDescriptor->ResourceHandle == null )
|
||||
{
|
||||
PluginLog.Error( "Failure to load file from SqPack: invalid File Descriptor." );
|
||||
return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
||||
}
|
||||
|
||||
var valid = Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false );
|
||||
byte ret;
|
||||
// The internal buffer size does not allow for more than 260 characters.
|
||||
// We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack.
|
||||
if( !valid || !gamePath.IsRooted() )
|
||||
{
|
||||
if( valid && ResourceLoadCustomization != null && gamePath.Path[ 0 ] == ( byte )'|' )
|
||||
{
|
||||
ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync );
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
||||
FileLoaded?.Invoke( gamePath.Path, ret != 0, false );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Specify that we are loading unpacked files from the drive.
|
||||
// We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations,
|
||||
// but since we only allow ASCII in the game paths, this is just a matter of upcasting.
|
||||
fileDescriptor->FileMode = FileMode.LoadUnpackedResource;
|
||||
|
||||
var fd = stackalloc byte[0x20 + 2 * gamePath.Length + 0x16];
|
||||
fileDescriptor->FileDescriptor = fd;
|
||||
var fdPtr = ( char* )( fd + 0x21 );
|
||||
for( var i = 0; i < gamePath.Length; ++i )
|
||||
{
|
||||
( &fileDescriptor->Utf16FileName )[ i ] = ( char )gamePath.Path[ i ];
|
||||
fdPtr[ i ] = ( char )gamePath.Path[ i ];
|
||||
}
|
||||
|
||||
( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0';
|
||||
fdPtr[ gamePath.Length ] = '\0';
|
||||
|
||||
// Use the SE ReadFile function.
|
||||
ret = ReadFile( resourceManager, fileDescriptor, priority, isSync );
|
||||
FileLoaded?.Invoke( gamePath.Path, ret != 0, true );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Customize file loading for any GamePaths that start with "|".
|
||||
public delegate byte ResourceLoadCustomizationDelegate( Utf8GamePath gamePath, ResourceManager* resourceManager,
|
||||
SeFileDescriptor* fileDescriptor, int priority, bool isSync );
|
||||
|
||||
public ResourceLoadCustomizationDelegate? ResourceLoadCustomization;
|
||||
|
||||
|
||||
// Use the default method of path replacement.
|
||||
public static (FullPath?, object?) DefaultReplacer( Utf8GamePath path )
|
||||
{
|
||||
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path );
|
||||
return ( resolved, null );
|
||||
}
|
||||
|
||||
private void DisposeHooks()
|
||||
{
|
||||
DisableHooks();
|
||||
ReadSqPackHook.Dispose();
|
||||
GetResourceSyncHook.Dispose();
|
||||
GetResourceAsyncHook.Dispose();
|
||||
}
|
||||
}
|
||||
102
Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs
Normal file
102
Penumbra/Interop/Loader/ResourceLoader.TexMdl.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Interop.Loader;
|
||||
|
||||
// Since 6.0, Mdl and Tex Files require special treatment, probably due to datamining protection.
|
||||
public unsafe partial class ResourceLoader
|
||||
{
|
||||
// Custom ulong flag to signal our files as opposed to SE files.
|
||||
public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF);
|
||||
|
||||
// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files,
|
||||
// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes.
|
||||
private readonly HashSet< ulong > _customFileCrc = new();
|
||||
|
||||
public IReadOnlySet< ulong > CustomFileCrc
|
||||
=> _customFileCrc;
|
||||
|
||||
|
||||
// The function that checks a files CRC64 to determine whether it is 'protected'.
|
||||
// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag.
|
||||
public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 );
|
||||
|
||||
[Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = "CheckFileStateDetour" )]
|
||||
public Hook< CheckFileStatePrototype > CheckFileStateHook = null!;
|
||||
|
||||
private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 )
|
||||
=> _customFileCrc.Contains( crc64 ) ? CustomFileFlag : CheckFileStateHook.Original( ptr, crc64 );
|
||||
|
||||
|
||||
// We use the local functions for our own files in the extern hook.
|
||||
public delegate byte LoadTexFileLocalDelegate( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3 );
|
||||
|
||||
[Signature( "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20" )]
|
||||
public LoadTexFileLocalDelegate LoadTexFileLocal = null!;
|
||||
|
||||
public delegate byte LoadMdlFileLocalPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2 );
|
||||
|
||||
[Signature( "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00" )]
|
||||
public LoadMdlFileLocalPrototype LoadMdlFileLocal = null!;
|
||||
|
||||
|
||||
// We hook the extern functions to just return the local one if given the custom flag as last argument.
|
||||
public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 );
|
||||
|
||||
[Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = "LoadTexFileExternDetour" )]
|
||||
public Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!;
|
||||
|
||||
private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr )
|
||||
=> ptr.Equals( CustomFileFlag )
|
||||
? LoadTexFileLocal.Invoke( resourceHandle, unk1, unk2, unk3 )
|
||||
: LoadTexFileExternHook.Original( resourceHandle, unk1, unk2, unk3, ptr );
|
||||
|
||||
public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 );
|
||||
|
||||
|
||||
[Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = "LoadMdlFileExternDetour" )]
|
||||
public Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!;
|
||||
|
||||
private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr )
|
||||
=> ptr.Equals( CustomFileFlag )
|
||||
? LoadMdlFileLocal.Invoke( resourceHandle, unk1, unk2 )
|
||||
: LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr );
|
||||
|
||||
|
||||
private void AddCrc( Utf8GamePath _, FullPath? path, object? _2 )
|
||||
{
|
||||
if( path is { Extension: ".mdl" or ".tex" } p )
|
||||
{
|
||||
_customFileCrc.Add( p.Crc64 );
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableTexMdlTreatment()
|
||||
{
|
||||
PathResolved += AddCrc;
|
||||
CheckFileStateHook.Enable();
|
||||
LoadTexFileExternHook.Enable();
|
||||
LoadMdlFileExternHook.Enable();
|
||||
}
|
||||
|
||||
private void DisableTexMdlTreatment()
|
||||
{
|
||||
PathResolved -= AddCrc;
|
||||
_customFileCrc.Clear();
|
||||
_customFileCrc.TrimExcess();
|
||||
CheckFileStateHook.Disable();
|
||||
LoadTexFileExternHook.Disable();
|
||||
LoadMdlFileExternHook.Disable();
|
||||
}
|
||||
|
||||
private void DisposeTexMdlTreatment()
|
||||
{
|
||||
CheckFileStateHook.Dispose();
|
||||
LoadTexFileExternHook.Dispose();
|
||||
LoadMdlFileExternHook.Dispose();
|
||||
}
|
||||
}
|
||||
129
Penumbra/Interop/Loader/ResourceLoader.cs
Normal file
129
Penumbra/Interop/Loader/ResourceLoader.cs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
using System;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
||||
|
||||
namespace Penumbra.Interop.Loader;
|
||||
|
||||
public unsafe partial class ResourceLoader : IDisposable
|
||||
{
|
||||
// Toggle whether replacing paths is active, independently of hook and event state.
|
||||
public bool DoReplacements { get; private set; }
|
||||
|
||||
// Hooks are required for everything, even events firing.
|
||||
public bool HooksEnabled { get; private set; }
|
||||
|
||||
// This Logging just logs all file requests, returns and loads to the Dalamud log.
|
||||
// Events can be used to make smarter logging.
|
||||
public bool IsLoggingEnabled { get; private set; }
|
||||
|
||||
public void EnableFullLogging()
|
||||
{
|
||||
if( IsLoggingEnabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoggingEnabled = true;
|
||||
ResourceRequested += LogPath;
|
||||
ResourceLoaded += LogResource;
|
||||
FileLoaded += LogLoadedFile;
|
||||
EnableHooks();
|
||||
}
|
||||
|
||||
public void DisableFullLogging()
|
||||
{
|
||||
if( !IsLoggingEnabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoggingEnabled = false;
|
||||
ResourceRequested -= LogPath;
|
||||
ResourceLoaded -= LogResource;
|
||||
FileLoaded -= LogLoadedFile;
|
||||
}
|
||||
|
||||
public void EnableReplacements()
|
||||
{
|
||||
if( DoReplacements )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DoReplacements = true;
|
||||
EnableTexMdlTreatment();
|
||||
EnableHooks();
|
||||
}
|
||||
|
||||
public void DisableReplacements()
|
||||
{
|
||||
if( !DoReplacements )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DoReplacements = false;
|
||||
DisableTexMdlTreatment();
|
||||
}
|
||||
|
||||
public void EnableHooks()
|
||||
{
|
||||
if( HooksEnabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HooksEnabled = true;
|
||||
ReadSqPackHook.Enable();
|
||||
GetResourceSyncHook.Enable();
|
||||
GetResourceAsyncHook.Enable();
|
||||
}
|
||||
|
||||
public void DisableHooks()
|
||||
{
|
||||
if( !HooksEnabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HooksEnabled = false;
|
||||
ReadSqPackHook.Disable();
|
||||
GetResourceSyncHook.Disable();
|
||||
GetResourceAsyncHook.Disable();
|
||||
}
|
||||
|
||||
public ResourceLoader( Penumbra _ )
|
||||
{
|
||||
SignatureHelper.Initialise( this );
|
||||
}
|
||||
|
||||
// Event fired whenever a resource is requested.
|
||||
public delegate void ResourceRequestedDelegate( Utf8GamePath path, bool synchronous );
|
||||
public event ResourceRequestedDelegate? ResourceRequested;
|
||||
|
||||
// Event fired whenever a resource is returned.
|
||||
// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource.
|
||||
// resolveData is additional data returned by the current ResolvePath function and is user-defined.
|
||||
public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
|
||||
object? resolveData );
|
||||
|
||||
public event ResourceLoadedDelegate? ResourceLoaded;
|
||||
|
||||
|
||||
// Event fired whenever a resource is newly loaded.
|
||||
// Success indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded)
|
||||
// custom is true if the file was loaded from local files instead of the default SqPacks.
|
||||
public delegate void FileLoadedDelegate( Utf8String path, bool success, bool custom );
|
||||
public event FileLoadedDelegate? FileLoaded;
|
||||
|
||||
// Customization point to control how path resolving is handled.
|
||||
public Func< Utf8GamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisableFullLogging();
|
||||
DisposeHooks();
|
||||
DisposeTexMdlTreatment();
|
||||
}
|
||||
}
|
||||
98
Penumbra/Interop/Loader/ResourceLogger.cs
Normal file
98
Penumbra/Interop/Loader/ResourceLogger.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Interop.Loader;
|
||||
|
||||
// A logger class that contains the relevant data to log requested files via regex.
|
||||
// Filters are case-insensitive.
|
||||
public class ResourceLogger : IDisposable
|
||||
{
|
||||
// Enable or disable the logging of resources subject to the current filter.
|
||||
public void SetState( bool value )
|
||||
{
|
||||
if( value == Penumbra.Config.EnableResourceLogging )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Penumbra.Config.EnableResourceLogging = value;
|
||||
Penumbra.Config.Save();
|
||||
if( value )
|
||||
{
|
||||
_resourceLoader.ResourceRequested += OnResourceRequested;
|
||||
}
|
||||
else
|
||||
{
|
||||
_resourceLoader.ResourceRequested -= OnResourceRequested;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the current filter to a new string, doing all other necessary work.
|
||||
public void SetFilter( string newFilter )
|
||||
{
|
||||
if( newFilter == Filter )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Penumbra.Config.ResourceLoggingFilter = newFilter;
|
||||
Penumbra.Config.Save();
|
||||
SetupRegex();
|
||||
}
|
||||
|
||||
// Returns whether the current filter is a valid regular expression.
|
||||
public bool ValidRegex
|
||||
=> _filterRegex != null;
|
||||
|
||||
private readonly ResourceLoader _resourceLoader;
|
||||
private Regex? _filterRegex;
|
||||
|
||||
private static string Filter
|
||||
=> Penumbra.Config.ResourceLoggingFilter;
|
||||
|
||||
private void SetupRegex()
|
||||
{
|
||||
try
|
||||
{
|
||||
_filterRegex = new Regex( Filter, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant );
|
||||
}
|
||||
catch
|
||||
{
|
||||
_filterRegex = null;
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceLogger( ResourceLoader loader )
|
||||
{
|
||||
_resourceLoader = loader;
|
||||
SetupRegex();
|
||||
if( Penumbra.Config.EnableResourceLogging )
|
||||
{
|
||||
_resourceLoader.ResourceRequested += OnResourceRequested;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResourceRequested( Utf8GamePath data, bool synchronous )
|
||||
{
|
||||
var path = Match( data.Path );
|
||||
if( path != null )
|
||||
{
|
||||
PluginLog.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" );
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the converted string if the filter matches, and null otherwise.
|
||||
// The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it.
|
||||
private string? Match( Utf8String data )
|
||||
{
|
||||
var s = data.ToString();
|
||||
return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.InvariantCultureIgnoreCase ) )
|
||||
? s
|
||||
: null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _resourceLoader.ResourceRequested -= OnResourceRequested;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue