mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
tmp
This commit is contained in:
parent
bdaff7b781
commit
99fd4b7806
6 changed files with 377 additions and 160 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
50
Penumbra/Interop/Loader/FileReadHooks.cs
Normal file
50
Penumbra/Interop/Loader/FileReadHooks.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
182
Penumbra/Interop/Loader/ResourceHook.cs
Normal file
182
Penumbra/Interop/Loader/ResourceHook.cs
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
Penumbra/Interop/Structs/GetResourceParameters.cs
Normal file
16
Penumbra/Interop/Structs/GetResourceParameters.cs
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue