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

@ -88,10 +88,10 @@ public class TempModManager : IDisposable
_communicator.TemporaryGlobalModChange.Invoke(mod, created, removed); _communicator.TemporaryGlobalModChange.Invoke(mod, created, removed);
} }
} }
/// <summary> /// <summary>
/// Apply a mod change to a set of collections. /// Apply a mod change to a set of collections.
/// </summary> /// </summary>
public static void OnGlobalModChange(IEnumerable<ModCollection> collections, Mod.TemporaryMod mod, bool created, bool removed) public static void OnGlobalModChange(IEnumerable<ModCollection> collections, Mod.TemporaryMod mod, bool created, bool removed)
{ {
if (removed) if (removed)

View file

@ -67,7 +67,7 @@ public unsafe class CreateFileWHook : IDisposable
{ {
// Use static storage. // Use static storage.
var ptr = WriteFileName( name ); 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 ); 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,157 +11,138 @@ using Penumbra.Util;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using static Penumbra.Interop.Loader.ResourceLoader;
using FileMode = Penumbra.Interop.Structs.FileMode; using FileMode = Penumbra.Interop.Structs.FileMode;
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
namespace Penumbra.Interop.Loader; 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();
}
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 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. [Conditional("DEBUG")]
// Both work basically the same, so we can reduce the main work to one function used by both hooks. private static void CompareHash(int local, int game, Utf8GamePath path)
[StructLayout( LayoutKind.Explicit )]
public struct GetResourceParameters
{ {
[FieldOffset( 16 )] if (local != game)
public uint SegmentOffset; Penumbra.Log.Warning($"Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}.");
[FieldOffset( 20 )]
public uint SegmentLength;
public bool IsPartialRead
=> SegmentLength != 0;
} }
public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, private event Action<Utf8GamePath, ResourceType, FullPath?, object?>? PathResolved;
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams );
[Signature( Sigs.GetResourceSync, DetourName = nameof( GetResourceSyncDetour ) )] public ResourceHandle* ResolvePathSync(ResourceCategory category, ResourceType type, ByteString path)
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 )
{ {
var hash = path.Crc32; 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, private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId,
ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk ) 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; 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." ); Penumbra.Log.Error("Could not create GamePath from resource path.");
return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); 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. // If no replacements are being made, we still want to be able to trigger the event.
var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); var (resolvedPath, data) = ResolvePath(gamePath, *categoryId, *resourceType, *resourceHash);
PathResolved?.Invoke( gamePath, *resourceType, resolvedPath ?? ( gamePath.IsRooted() ? new FullPath( gamePath ) : null ), data ); PathResolved?.Invoke(gamePath, *resourceType, resolvedPath ?? (gamePath.IsRooted() ? new FullPath(gamePath) : null), data);
if( resolvedPath == null ) if (resolvedPath == null)
{ {
ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk);
ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, null, data ); ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, null, data);
return ret; return ret;
} }
// Replace the hash and path with the correct one for the replacement. // 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; path = resolvedPath.Value.InternalName.Path;
ret = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk ); ret = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk);
ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )ret, gamePath, resolvedPath.Value, data ); ResourceLoaded?.Invoke((Structs.ResourceHandle*)ret, gamePath, resolvedPath.Value, data);
return ret; return ret;
} }
// Use the default method of path replacement. // 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 ); var resolved = Penumbra.CollectionManager.Default.ResolvePath(path);
return ( resolved, Penumbra.CollectionManager.Default.ToResolveData() ); return (resolved, Penumbra.CollectionManager.Default.ToResolveData());
} }
// Try all resolve path subscribers or use the default replacer. // 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 ) if (!DoReplacements || _incMode.Value)
{ return (null, ResolveData.Invalid);
return ( null, ResolveData.Invalid );
}
path = path.ToLower(); path = path.ToLower();
switch( category ) switch (category)
{ {
// Only Interface collection. // Only Interface collection.
case ResourceCategory.Ui: case ResourceCategory.Ui:
{ {
var resolved = Penumbra.CollectionManager.Interface.ResolvePath( path ); var resolved = Penumbra.CollectionManager.Interface.ResolvePath(path);
return ( resolved, Penumbra.CollectionManager.Interface.ToResolveData() ); return (resolved, Penumbra.CollectionManager.Interface.ToResolveData());
} }
// Never allow changing scripts. // Never allow changing scripts.
case ResourceCategory.UiScript: case ResourceCategory.UiScript:
case ResourceCategory.GameScript: case ResourceCategory.GameScript:
return ( null, ResolveData.Invalid ); return (null, ResolveData.Invalid);
// Use actual resolving. // Use actual resolving.
case ResourceCategory.Chara: case ResourceCategory.Chara:
case ResourceCategory.Shader: case ResourceCategory.Shader:
case ResourceCategory.Vfx: case ResourceCategory.Vfx:
case ResourceCategory.Sound: case ResourceCategory.Sound:
if( ResolvePathCustomization != null ) if (ResolvePathCustomization != null)
{ foreach (var resolver in ResolvePathCustomization.GetInvocationList())
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; return ret;
}
} }
}
break; break;
// None of these files are ever associated with specific characters, // None of these files are ever associated with specific characters,
@ -176,65 +157,57 @@ public unsafe partial class ResourceLoader
break; 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. // 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, public delegate byte ReadFileDelegate(ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority,
bool isSync ); bool isSync);
[Signature( Sigs.ReadFile )] [Signature(Sigs.ReadFile)]
public readonly ReadFileDelegate ReadFile = null!; public readonly ReadFileDelegate ReadFile = null!;
// We hook ReadSqPack to redirect rooted files to ReadFile. // 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 ) )] [Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))]
public readonly Hook< ReadSqPackPrototype > ReadSqPackHook = null!; public readonly Hook<ReadSqPackPrototype> 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 ) if (!fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0)
{ 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->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. // Paths starting with a '|' are handled separately to allow for special treatment.
// They are expected to also have a closing '|'. // They are expected to also have a closing '|'.
if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) if (ResourceLoadCustomization == null || gamePath.Path[0] != (byte)'|')
{ return DefaultLoadResource(gamePath.Path, resourceManager, fileDescriptor, priority, isSync);
return DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync );
}
// Split the path into the special-treatment part (between the first and second '|') // Split the path into the special-treatment part (between the first and second '|')
// and the actual path. // and the actual path.
byte ret = 0; byte ret = 0;
var split = gamePath.Path.Split( ( byte )'|', 3, false ); var split = gamePath.Path.Split((byte)'|', 3, false);
fileDescriptor->ResourceHandle->FileNameData = split[ 2 ].Path; fileDescriptor->ResourceHandle->FileNameData = split[2].Path;
fileDescriptor->ResourceHandle->FileNameLength = split[ 2 ].Length; fileDescriptor->ResourceHandle->FileNameLength = split[2].Length;
var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui var funcFound = fileDescriptor->ResourceHandle->Category != ResourceCategory.Ui
&& ResourceLoadCustomization.GetInvocationList() && ResourceLoadCustomization.GetInvocationList()
.Any( f => ( ( ResourceLoadCustomizationDelegate )f ) .Any(f => ((ResourceLoadCustomizationDelegate)f)
.Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); .Invoke(split[1], split[2], resourceManager, fileDescriptor, priority, isSync, out ret));
if( !funcFound ) if (!funcFound)
{ ret = DefaultLoadResource(split[2], resourceManager, fileDescriptor, priority, isSync);
ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync );
}
// Return original resource handle path so that they can be loaded separately. // Return original resource handle path so that they can be loaded separately.
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; 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. // Load the resource from an SqPack and trigger the FileLoaded event.
private byte DefaultResourceLoad( ByteString path, ResourceManager* resourceManager, private byte DefaultResourceLoad(ByteString path, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync ) SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{ {
var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync);
FileLoaded?.Invoke( fileDescriptor->ResourceHandle, path, ret != 0, false ); FileLoaded?.Invoke(fileDescriptor->ResourceHandle, path, ret != 0, false);
return ret; return ret;
} }
/// <summary> Load the resource from a path on the users hard drives. </summary> /// <summary> Load the resource from a path on the users hard drives. </summary>
/// <remarks> <see cref="CreateFileWHook" /> </remarks> /// <remarks> <see cref="CreateFileWHook" /> </remarks>
private byte DefaultRootedResourceLoad( ByteString gamePath, ResourceManager* resourceManager, private byte DefaultRootedResourceLoad(ByteString gamePath, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync ) SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{ {
// Specify that we are loading unpacked files from the drive. // 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. // 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. // 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]; var fd = stackalloc char[0x11 + 0x0B + 14];
fileDescriptor->FileDescriptor = (byte*) fd + 1; fileDescriptor->FileDescriptor = (byte*)fd + 1;
CreateFileWHook.WritePtr( fd + 0x11, gamePath.Path, gamePath.Length ); CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length);
CreateFileWHook.WritePtr( &fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length ); CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length);
// Use the SE ReadFile function. // Use the SE ReadFile function.
var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); var ret = ReadFile(resourceManager, fileDescriptor, priority, isSync);
FileLoaded?.Invoke( fileDescriptor->ResourceHandle, gamePath, ret != 0, true ); FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, true);
return ret; return ret;
} }
// Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. // 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, internal byte DefaultLoadResource(ByteString gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority,
bool isSync ) bool isSync)
=> Utf8GamePath.IsRooted( gamePath ) => Utf8GamePath.IsRooted(gamePath)
? DefaultRootedResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ) ? DefaultRootedResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync)
: DefaultResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ); : DefaultResourceLoad(gamePath, resourceManager, fileDescriptor, priority, isSync);
private void DisposeHooks() private void DisposeHooks()
{ {
@ -291,21 +264,19 @@ public unsafe partial class ResourceLoader
_incRefHook.Dispose(); _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; return path.Crc32;
}
// When the game requests file only partially, crc32 includes that information, in format of: // When the game requests file only partially, crc32 includes that information, in format of:
// path/to/file.ext.hex_offset.hex_size // path/to/file.ext.hex_offset.hex_size
// ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000
return ByteString.Join( return ByteString.Join(
( byte )'.', (byte)'.',
path, path,
ByteString.FromStringUnsafe( pGetResParams->SegmentOffset.ToString( "x" ), true ), ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true),
ByteString.FromStringUnsafe( pGetResParams->SegmentLength.ToString( "x" ), true ) ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true)
).Crc32; ).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, // 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. // 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. // This causes some problems and is hopefully prevented with this.
private readonly ThreadLocal< bool > _incMode = new(); private readonly ThreadLocal<bool> _incMode = new();
private readonly Hook< ResourceHandleDestructor > _incRefHook; private readonly Hook<ResourceHandleDestructor> _incRefHook;
private IntPtr ResourceHandleIncRefDetour( ResourceHandle* handle ) private IntPtr ResourceHandleIncRefDetour(ResourceHandle* handle)
{ {
if( handle->RefCount > 0 ) if (handle->RefCount > 0)
{ return _incRefHook.Original(handle);
return _incRefHook.Original( handle );
}
_incMode.Value = true; _incMode.Value = true;
var ret = _incRefHook.Original( handle ); var ret = _incRefHook.Original(handle);
_incMode.Value = false; _incMode.Value = false;
return ret; return ret;
} }
} }

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