diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 3064d326..09625a73 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -88,10 +88,10 @@ public class TempModManager : IDisposable _communicator.TemporaryGlobalModChange.Invoke(mod, created, removed); } } - - /// - /// Apply a mod change to a set of collections. - /// + + /// + /// Apply a mod change to a set of collections. + /// public static void OnGlobalModChange(IEnumerable collections, Mod.TemporaryMod mod, bool created, bool removed) { if (removed) diff --git a/Penumbra/Interop/Loader/CreateFileWHook.cs b/Penumbra/Interop/Loader/CreateFileWHook.cs index 7ba4ba22..5d927b95 100644 --- a/Penumbra/Interop/Loader/CreateFileWHook.cs +++ b/Penumbra/Interop/Loader/CreateFileWHook.cs @@ -67,7 +67,7 @@ public unsafe class CreateFileWHook : IDisposable { // Use static storage. var ptr = WriteFileName( name ); - Penumbra.Log.Verbose( $"Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." ); + Penumbra.Log.Verbose( $"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe( name, false )}." ); return _createFileWHook.OriginalDisposeSafe( ptr, access, shareMode, security, creation, flags, template ); } diff --git a/Penumbra/Interop/Loader/FileReadHooks.cs b/Penumbra/Interop/Loader/FileReadHooks.cs new file mode 100644 index 00000000..03bc7d24 --- /dev/null +++ b/Penumbra/Interop/Loader/FileReadHooks.cs @@ -0,0 +1,50 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Loader; + +public unsafe class FileReadHooks : IDisposable +{ + private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); + + [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] + private readonly Hook _readSqPackHook = null!; + + public FileReadHooks() + { + SignatureHelper.Initialise(this); + _readSqPackHook.Enable(); + } + + /// Invoked when a file is supposed to be read from SqPack. + /// The file descriptor containing what file to read. + /// The games priority. Should not generally be changed. + /// Whether the file needs to be loaded synchronously. Should not generally be changed. + /// Whether to call the original function after the event is finished. + public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ReadSqPackDelegate? ReadSqPack; + + private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) + { + var callOriginal = true; + ReadSqPack?.Invoke(ref *fileDescriptor, ref priority, ref isSync, ref callOriginal); + return callOriginal + ? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync) + : (byte)1; + } + + public void Dispose() + { + _readSqPackHook.Disable(); + _readSqPackHook.Dispose(); + } +} diff --git a/Penumbra/Interop/Loader/ResourceHook.cs b/Penumbra/Interop/Loader/ResourceHook.cs new file mode 100644 index 00000000..475ba3ed --- /dev/null +++ b/Penumbra/Interop/Loader/ResourceHook.cs @@ -0,0 +1,182 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.String; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; + +namespace Penumbra.Interop.Loader; + +public unsafe class ResourceHook : IDisposable +{ + public ResourceHook() + { + SignatureHelper.Initialise(this); + _getResourceSyncHook.Enable(); + _getResourceAsyncHook.Enable(); + _resourceHandleDestructorHook.Enable(); + } + + public void Dispose() + { + _getResourceSyncHook.Dispose(); + _getResourceAsyncHook.Dispose(); + } + + #region GetResource + + /// Called before a resource is requested. + /// The resource category. Should not generally be changed. + /// The resource type. Should not generally be changed. + /// The resource hash. Should generally fit to the path. + /// The path of the requested resource. + /// Mainly used for SCD streaming. + /// Whether to request the resource synchronously or asynchronously. + public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref ByteString path, + ref GetResourceParameters parameters, ref bool sync); + + /// + /// Subscribers should be exception-safe. + public event GetResourcePreDelegate? GetResourcePre; + + /// + /// The returned resource handle obtained from a resource request. Contains all the other information from the request. + /// + public delegate void GetResourcePostDelegate(ref ResourceHandle handle); + + /// + /// Subscribers should be exception-safe. + public event GetResourcePostDelegate? GetResourcePost; + + + private delegate ResourceHandle* GetResourceSyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams); + + private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, bool isUnknown); + + [Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))] + private readonly Hook _getResourceSyncHook = null!; + + [Signature(Sigs.GetResourceAsync, DetourName = nameof(GetResourceAsyncDetour))] + private readonly Hook _getResourceAsyncHook = null!; + + private ResourceHandle* GetResourceSyncDetour(ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, + int* resourceHash, byte* path, GetResourceParameters* pGetResParams) + => GetResourceHandler(true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, false); + + private ResourceHandle* GetResourceAsyncDetour(ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, + int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) + => GetResourceHandler(false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + + /// + /// 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. + /// + private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) + { + var byteString = new ByteString(path); + GetResourcePre?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref byteString, ref *pGetResParams, ref isSync); + var ret = isSync + ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, byteString.Path, pGetResParams) + : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, byteString.Path, pGetResParams, isUnk); + GetResourcePost?.Invoke(ref *ret); + return ret; + } + + #endregion + + private delegate IntPtr ResourceHandlePrototype(ResourceHandle* handle); + + #region IncRef + + /// Invoked before a resource handle reference count is incremented. + /// The resource handle. + /// Whether to call original after the event has run. + /// The return value to use if not calling original. + public delegate void ResourceHandleIncRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref nint returnValue); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceHandleIncRefDelegate? ResourceHandleIncRef; + + public nint IncRef(ref ResourceHandle handle) + { + fixed (ResourceHandle* ptr = &handle) + { + return _incRefHook.Original(ptr); + } + } + + private readonly Hook _incRefHook; + private nint ResourceHandleIncRefDetour(ResourceHandle* handle) + { + var callOriginal = true; + var ret = IntPtr.Zero; + ResourceHandleIncRef?.Invoke(ref *handle, ref callOriginal, ref ret); + return callOriginal ? _incRefHook.Original(handle) : ret; + } + + #endregion + + #region DecRef + + /// Invoked before a resource handle reference count is decremented. + /// The resource handle. + /// Whether to call original after the event has run. + /// The return value to use if not calling original. + public delegate void ResourceHandleDecRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref byte returnValue); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceHandleDecRefDelegate? ResourceHandleDecRef; + + public byte DecRef(ref ResourceHandle handle) + { + fixed (ResourceHandle* ptr = &handle) + { + return _incRefHook.Original(ptr); + } + } + + private delegate byte ResourceHandleDecRefPrototype(ResourceHandle* handle); + private readonly Hook _decRefHook; + private byte ResourceHandleDecRefDetour(ResourceHandle* handle) + { + var callOriginal = true; + var ret = byte.MinValue; + ResourceHandleDecRef?.Invoke(ref *handle, ref callOriginal, ref ret); + return callOriginal ? _decRefHook!.Original(handle) : ret; + } + + #endregion + + /// Invoked before a resource handle is destructed. + /// The resource handle. + public delegate void ResourceHandleDtorDelegate(ref ResourceHandle handle); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceHandleDtorDelegate? ResourceHandleDestructor; + + [Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))] + private readonly Hook _resourceHandleDestructorHook = null!; + + private nint ResourceHandleDestructorDetour(ResourceHandle* handle) + { + ResourceHandleDestructor?.Invoke(ref *handle); + return _resourceHandleDestructorHook!.Original(handle); + } + + #endregion +} diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index 27dac9b3..ca1612f1 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -11,157 +11,138 @@ using Penumbra.Util; using System; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; +using static Penumbra.Interop.Loader.ResourceLoader; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.Loader; +public unsafe class FileReadHooks : IDisposable +{ + private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); + + [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] + private readonly Hook _readSqPackHook = null!; + + public FileReadHooks() + { + SignatureHelper.Initialise(this); + _readSqPackHook.Enable(); + } + + public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal); + + public event ReadSqPackDelegate? ReadSqPack; + + private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) + { + var callOriginal = true; + ReadSqPack?.Invoke(ref *fileDescriptor, ref priority, ref isSync, ref callOriginal); + return callOriginal + ? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync) + : (byte)1; + } + + public void Dispose() + { + _readSqPackHook.Disable(); + _readSqPackHook.Dispose(); + } +} + public unsafe partial class ResourceLoader { - private readonly CreateFileWHook _createFileWHook = new(); - // 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. - - [StructLayout( LayoutKind.Explicit )] - public struct GetResourceParameters + [Conditional("DEBUG")] + private static void CompareHash(int local, int game, Utf8GamePath path) { - [FieldOffset( 16 )] - public uint SegmentOffset; - - [FieldOffset( 20 )] - public uint SegmentLength; - - public bool IsPartialRead - => SegmentLength != 0; + if (local != game) + Penumbra.Log.Warning($"Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}."); } - public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams ); + private event Action? PathResolved; - [Signature( Sigs.GetResourceSync, DetourName = nameof( GetResourceSyncDetour ) )] - public readonly Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!; - - public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, bool isUnknown ); - - [Signature( Sigs.GetResourceAsync, DetourName = nameof( GetResourceAsyncDetour ) )] - public readonly Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!; - - private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, GetResourceParameters* pGetResParams ) - => GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, false ); - - private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) - => GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - - private ResourceHandle* CallOriginalHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) - => isSync - ? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams ) - : GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - - - [Conditional( "DEBUG" )] - private static void CompareHash( int local, int game, Utf8GamePath path ) - { - if( local != game ) - { - Penumbra.Log.Warning( $"Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}." ); - } - } - - private event Action< Utf8GamePath, ResourceType, FullPath?, object? >? PathResolved; - - public ResourceHandle* ResolvePathSync( ResourceCategory category, ResourceType type, ByteString path ) + public ResourceHandle* ResolvePathSync(ResourceCategory category, ResourceType type, ByteString path) { var hash = path.Crc32; - return GetResourceHandler( true, *ResourceManager, &category, &type, &hash, path.Path, null, false ); + return GetResourceHandler(true, *ResourceManager, &category, &type, &hash, path.Path, null, false); } - private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) + private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) { - using var performance = Penumbra.Performance.Measure( PerformanceType.GetResourceHandler ); + using var performance = Penumbra.Performance.Measure(PerformanceType.GetResourceHandler); ResourceHandle* ret; - if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) + if (!Utf8GamePath.FromPointer(path, out var gamePath)) { - Penumbra.Log.Error( "Could not create GamePath from resource path." ); - return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); + Penumbra.Log.Error("Could not create GamePath from resource path."); + return CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); } - CompareHash( ComputeHash( gamePath.Path, pGetResParams ), *resourceHash, gamePath ); + CompareHash(ComputeHash(gamePath.Path, pGetResParams), *resourceHash, gamePath); - ResourceRequested?.Invoke( gamePath, isSync ); + ResourceRequested?.Invoke(gamePath, isSync); // If no replacements are being made, we still want to be able to trigger the event. - var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); - PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); - if( resolvedPath == null ) + var (resolvedPath, data) = ResolvePath(gamePath, *categoryId, *resourceType, *resourceHash); + PathResolved?.Invoke(gamePath, *resourceType, resolvedPath ?? (gamePath.IsRooted() ? new FullPath(gamePath) : null), data); + if (resolvedPath == null) { - ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, null, data ); + ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, null, data); return ret; } // Replace the hash and path with the correct one for the replacement. - *resourceHash = ComputeHash( resolvedPath.Value.InternalName, pGetResParams ); + *resourceHash = ComputeHash(resolvedPath.Value.InternalName, pGetResParams); path = resolvedPath.Value.InternalName.Path; - ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); - ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, resolvedPath.Value, data ); + ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, resolvedPath.Value, data); return ret; } // Use the default method of path replacement. - public static (FullPath?, ResolveData) DefaultResolver( Utf8GamePath path ) + public static (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) { - var resolved = Penumbra.CollectionManager.Default.ResolvePath( path ); - return ( resolved, Penumbra.CollectionManager.Default.ToResolveData() ); + var resolved = Penumbra.CollectionManager.Default.ResolvePath(path); + return (resolved, Penumbra.CollectionManager.Default.ToResolveData()); } // Try all resolve path subscribers or use the default replacer. - private (FullPath?, ResolveData) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) + private (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash) { - if( !DoReplacements || _incMode.Value ) - { - return ( null, ResolveData.Invalid ); - } + if (!DoReplacements || _incMode.Value) + return (null, ResolveData.Invalid); path = path.ToLower(); - switch( category ) + switch (category) { // Only Interface collection. case ResourceCategory.Ui: { - var resolved = Penumbra.CollectionManager.Interface.ResolvePath( path ); - return ( resolved, Penumbra.CollectionManager.Interface.ToResolveData() ); + var resolved = Penumbra.CollectionManager.Interface.ResolvePath(path); + return (resolved, Penumbra.CollectionManager.Interface.ToResolveData()); } // Never allow changing scripts. case ResourceCategory.UiScript: case ResourceCategory.GameScript: - return ( null, ResolveData.Invalid ); + return (null, ResolveData.Invalid); // Use actual resolving. case ResourceCategory.Chara: case ResourceCategory.Shader: case ResourceCategory.Vfx: case ResourceCategory.Sound: - if( ResolvePathCustomization != null ) - { - foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) + if (ResolvePathCustomization != null) + foreach (var resolver in ResolvePathCustomization.GetInvocationList()) { - if( ( ( ResolvePathDelegate )resolver ).Invoke( path, category, resourceType, resourceHash, out var ret ) ) - { + if (((ResolvePathDelegate)resolver).Invoke(path, category, resourceType, resourceHash, out var ret)) return ret; - } } - } break; // None of these files are ever associated with specific characters, @@ -176,65 +157,57 @@ public unsafe partial class ResourceLoader break; } - return DefaultResolver( path ); + return DefaultResolver(path); } // 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 ); + public delegate byte ReadFileDelegate(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + bool isSync); - [Signature( Sigs.ReadFile )] + [Signature(Sigs.ReadFile)] public readonly ReadFileDelegate ReadFile = null!; // We hook ReadSqPack to redirect rooted files to ReadFile. - public delegate byte ReadSqPackPrototype( ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync ); + public delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); - [Signature( Sigs.ReadSqPack, DetourName = nameof( ReadSqPackDetour ) )] - public readonly Hook< ReadSqPackPrototype > ReadSqPackHook = null!; + [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))] + public readonly Hook ReadSqPackHook = null!; - private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + private byte ReadSqPackDetour(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) { - using var performance = Penumbra.Performance.Measure( PerformanceType.ReadSqPack ); + using var performance = Penumbra.Performance.Measure(PerformanceType.ReadSqPack); - if( !DoReplacements ) + if (!DoReplacements) + return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); + + if (fileDescriptor == null || fileDescriptor->ResourceHandle == null) { - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + Penumbra.Log.Error("Failure to load file from SqPack: invalid File Descriptor."); + return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); } - if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) - { - Penumbra.Log.Error( "Failure to load file from SqPack: invalid File Descriptor." ); - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - } - - if( !fileDescriptor->ResourceHandle->GamePath( out var gamePath ) || gamePath.Length == 0 ) - { - return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - } + if (!fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0) + return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); // Paths starting with a '|' are handled separately to allow for special treatment. // They are expected to also have a closing '|'. - if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) - { - return DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync ); - } + if (ResourceLoadCustomization == null || gamePath.Path[0] != (byte)'|') + return DefaultLoadResource(gamePath.Path, resourceManager, fileDescriptor, priority, isSync); // Split the path into the special-treatment part (between the first and second '|') // and the actual path. byte ret = 0; - var split = gamePath.Path.Split( ( byte )'|', 3, false ); - fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; + var split = gamePath.Path.Split((byte)'|', 3, false); + fileDescriptor->ResourceHandle->FileNameData = split[2].Path; + fileDescriptor->ResourceHandle->FileNameLength = split[2].Length; var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui && ResourceLoadCustomization.GetInvocationList() - .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) - .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); + .Any(f => ((ResourceLoadCustomizationDelegate)f) + .Invoke(split[1], split[2], resourceManager, fileDescriptor, priority, isSync, out ret)); - if( !funcFound ) - { - ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync ); - } + if (!funcFound) + ret = DefaultLoadResource(split[2], resourceManager, fileDescriptor, priority, isSync); // Return original resource handle path so that they can be loaded separately. fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; @@ -244,18 +217,18 @@ public unsafe partial class ResourceLoader } // Load the resource from an SqPack and trigger the FileLoaded event. - private byte DefaultResourceLoad( ByteString path, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + private byte DefaultResourceLoad(ByteString path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync) { - var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( fileDescriptor->ResourceHandle, path, ret != 0, false ); + var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); + FileLoaded?.Invoke(fileDescriptor->ResourceHandle, path, ret != 0, false); return ret; } /// Load the resource from a path on the users hard drives. /// - private byte DefaultRootedResourceLoad( ByteString gamePath, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + private byte DefaultRootedResourceLoad(ByteString gamePath, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync) { // 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. @@ -263,22 +236,22 @@ public unsafe partial class ResourceLoader // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. var fd = stackalloc char[0x11 + 0x0B + 14]; - fileDescriptor->FileDescriptor = (byte*) fd + 1; - CreateFileWHook.WritePtr( fd + 0x11, gamePath.Path, gamePath.Length ); - CreateFileWHook.WritePtr( &fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length ); + fileDescriptor->FileDescriptor = (byte*)fd + 1; + CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); + CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length); // Use the SE ReadFile function. - var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( fileDescriptor->ResourceHandle, gamePath, ret != 0, true ); + var ret = ReadFile(resourceManager, fileDescriptor, priority, isSync); + FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, true); return ret; } // Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. - internal byte DefaultLoadResource( ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, - bool isSync ) - => Utf8GamePath.IsRooted( gamePath ) - ? DefaultRootedResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ) - : DefaultResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ); + internal byte DefaultLoadResource(ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + bool isSync) + => Utf8GamePath.IsRooted(gamePath) + ? DefaultRootedResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync) + : DefaultResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync); private void DisposeHooks() { @@ -291,21 +264,19 @@ public unsafe partial class ResourceLoader _incRefHook.Dispose(); } - private static int ComputeHash( ByteString path, GetResourceParameters* pGetResParams ) + private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams) { - if( pGetResParams == null || !pGetResParams->IsPartialRead ) - { + if (pGetResParams == null || !pGetResParams->IsPartialRead) return path.Crc32; - } // When the game requests file only partially, crc32 includes that information, in format of: // path/to/file.ext.hex_offset.hex_size // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 return ByteString.Join( - ( byte )'.', + (byte)'.', path, - ByteString.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), - ByteString.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) + ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true), + ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true) ).Crc32; } @@ -314,19 +285,17 @@ public unsafe partial class ResourceLoader // This means, that if the path determined from that is different than the resources path, // a different resource gets loaded or incremented, while the IncRef'd resource stays at 0. // This causes some problems and is hopefully prevented with this. - private readonly ThreadLocal< bool > _incMode = new(); - private readonly Hook< ResourceHandleDestructor > _incRefHook; + private readonly ThreadLocal _incMode = new(); + private readonly Hook _incRefHook; - private IntPtr ResourceHandleIncRefDetour( ResourceHandle* handle ) + private IntPtr ResourceHandleIncRefDetour(ResourceHandle* handle) { - if( handle->RefCount > 0 ) - { - return _incRefHook.Original( handle ); - } + if (handle->RefCount > 0) + return _incRefHook.Original(handle); _incMode.Value = true; - var ret = _incRefHook.Original( handle ); + var ret = _incRefHook.Original(handle); _incMode.Value = false; return ret; } -} \ No newline at end of file +} diff --git a/Penumbra/Interop/Structs/GetResourceParameters.cs b/Penumbra/Interop/Structs/GetResourceParameters.cs new file mode 100644 index 00000000..eb413ead --- /dev/null +++ b/Penumbra/Interop/Structs/GetResourceParameters.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout(LayoutKind.Explicit)] +public struct GetResourceParameters +{ + [FieldOffset(16)] + public uint SegmentOffset; + + [FieldOffset(20)] + public uint SegmentLength; + + public bool IsPartialRead + => SegmentLength != 0; +}