This commit is contained in:
Ottermandias 2023-03-11 21:12:01 +01:00
parent bdaff7b781
commit 99fd4b7806
6 changed files with 377 additions and 160 deletions

View file

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

View file

@ -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<ReadSqPackPrototype> _readSqPackHook = null!;
public FileReadHooks()
{
SignatureHelper.Initialise(this);
_readSqPackHook.Enable();
}
/// <summary> Invoked when a file is supposed to be read from SqPack. </summary>
/// <param name="fileDescriptor">The file descriptor containing what file to read.</param>
/// <param name="priority">The games priority. Should not generally be changed.</param>
/// <param name="isSync">Whether the file needs to be loaded synchronously. Should not generally be changed.</param>
/// <param name="callOriginal">Whether to call the original function after the event is finished.</param>
public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal);
/// <summary>
/// <inheritdoc cref="ReadSqPackDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
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();
}
}

View file

@ -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
/// <summary> Called before a resource is requested. </summary>
/// <param name="category">The resource category. Should not generally be changed.</param>
/// <param name="type">The resource type. Should not generally be changed.</param>
/// <param name="hash">The resource hash. Should generally fit to the path.</param>
/// <param name="path">The path of the requested resource.</param>
/// <param name="parameters">Mainly used for SCD streaming.</param>
/// <param name="sync">Whether to request the resource synchronously or asynchronously.</param>
public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref ByteString path,
ref GetResourceParameters parameters, ref bool sync);
/// <summary> <inheritdoc cref="GetResourcePreDelegate"/> <para/>
/// Subscribers should be exception-safe.</summary>
public event GetResourcePreDelegate? GetResourcePre;
/// <summary>
/// The returned resource handle obtained from a resource request. Contains all the other information from the request.
/// </summary>
public delegate void GetResourcePostDelegate(ref ResourceHandle handle);
/// <summary> <inheritdoc cref="GetResourcePostDelegate"/> <para/>
/// Subscribers should be exception-safe.</summary>
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<GetResourceSyncPrototype> _getResourceSyncHook = null!;
[Signature(Sigs.GetResourceAsync, DetourName = nameof(GetResourceAsyncDetour))]
private 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);
/// <summary>
/// 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.
/// </summary>
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
/// <summary> Invoked before a resource handle reference count is incremented. </summary>
/// <param name="handle">The resource handle.</param>
/// <param name="callOriginal">Whether to call original after the event has run.</param>
/// <param name="returnValue">The return value to use if not calling original.</param>
public delegate void ResourceHandleIncRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref nint returnValue);
/// <summary>
/// <inheritdoc cref="ResourceHandleIncRefDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ResourceHandleIncRefDelegate? ResourceHandleIncRef;
public nint IncRef(ref ResourceHandle handle)
{
fixed (ResourceHandle* ptr = &handle)
{
return _incRefHook.Original(ptr);
}
}
private readonly Hook<ResourceHandlePrototype> _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
/// <summary> Invoked before a resource handle reference count is decremented. </summary>
/// <param name="handle">The resource handle.</param>
/// <param name="callOriginal">Whether to call original after the event has run.</param>
/// <param name="returnValue">The return value to use if not calling original.</param>
public delegate void ResourceHandleDecRefDelegate(ref ResourceHandle handle, ref bool callOriginal, ref byte returnValue);
/// <summary>
/// <inheritdoc cref="ResourceHandleDecRefDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
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<ResourceHandleDecRefPrototype> _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
/// <summary> Invoked before a resource handle is destructed. </summary>
/// <param name="handle">The resource handle.</param>
public delegate void ResourceHandleDtorDelegate(ref ResourceHandle handle);
/// <summary>
/// <inheritdoc cref="ResourceHandleDtorDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ResourceHandleDtorDelegate? ResourceHandleDestructor;
[Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))]
private readonly Hook<ResourceHandlePrototype> _resourceHandleDestructorHook = null!;
private nint ResourceHandleDestructorDetour(ResourceHandle* handle)
{
ResourceHandleDestructor?.Invoke(ref *handle);
return _resourceHandleDestructorHook!.Original(handle);
}
#endregion
}

View file

@ -11,68 +11,55 @@ 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 partial class ResourceLoader
public unsafe class FileReadHooks : IDisposable
{
private readonly CreateFileWHook _createFileWHook = new();
private delegate byte ReadSqPackPrototype(ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync);
// 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.
[Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))]
private readonly Hook<ReadSqPackPrototype> _readSqPackHook = null!;
[StructLayout( LayoutKind.Explicit )]
public struct GetResourceParameters
public FileReadHooks()
{
[FieldOffset( 16 )]
public uint SegmentOffset;
[FieldOffset( 20 )]
public uint SegmentLength;
public bool IsPartialRead
=> SegmentLength != 0;
SignatureHelper.Initialise(this);
_readSqPackHook.Enable();
}
public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId,
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams );
public delegate void ReadSqPackDelegate(ref SeFileDescriptor fileDescriptor, ref int priority, ref bool isSync, ref bool callOriginal);
[Signature( Sigs.GetResourceSync, DetourName = nameof( GetResourceSyncDetour ) )]
public readonly Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!;
public event ReadSqPackDelegate? ReadSqPack;
public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId,
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, bool isUnknown );
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;
}
[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 );
public void Dispose()
{
_readSqPackHook.Disable();
_readSqPackHook.Dispose();
}
}
public unsafe partial class ResourceLoader
{
[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;
@ -130,9 +117,7 @@ public unsafe partial class ResourceLoader
private (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash)
{
if (!DoReplacements || _incMode.Value)
{
return (null, ResolveData.Invalid);
}
path = path.ToLower();
switch (category)
@ -153,15 +138,11 @@ public unsafe partial class ResourceLoader
case ResourceCategory.Vfx:
case ResourceCategory.Sound:
if (ResolvePathCustomization != null)
{
foreach (var resolver in ResolvePathCustomization.GetInvocationList())
{
if (((ResolvePathDelegate)resolver).Invoke(path, category, resourceType, resourceHash, out var ret))
{
return ret;
}
}
}
break;
// None of these files are ever associated with specific characters,
@ -198,9 +179,7 @@ public unsafe partial class ResourceLoader
using var performance = Penumbra.Performance.Measure(PerformanceType.ReadSqPack);
if (!DoReplacements)
{
return ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync);
}
if (fileDescriptor == null || fileDescriptor->ResourceHandle == null)
{
@ -209,16 +188,12 @@ public unsafe partial class ResourceLoader
}
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);
}
// Split the path into the special-treatment part (between the first and second '|')
// and the actual path.
@ -232,9 +207,7 @@ public unsafe partial class ResourceLoader
.Invoke(split[1], split[2], resourceManager, fileDescriptor, priority, isSync, out ret));
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;
@ -294,9 +267,7 @@ public unsafe partial class ResourceLoader
private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams)
{
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
@ -320,9 +291,7 @@ public unsafe partial class ResourceLoader
private IntPtr ResourceHandleIncRefDetour(ResourceHandle* handle)
{
if (handle->RefCount > 0)
{
return _incRefHook.Original(handle);
}
_incMode.Value = true;
var ret = _incRefHook.Original(handle);

View file

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