Rename interop folders

This commit is contained in:
Ottermandias 2023-03-23 20:30:23 +01:00
parent 49f1e2dcde
commit 56286e0123
17 changed files with 1 additions and 1 deletions

View file

@ -0,0 +1,177 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Dalamud.Hooking;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.String.Functions;
namespace Penumbra.Interop.Loader;
/// <summary>
/// To allow XIV to load files of arbitrary path length,
/// we use the fixed size buffers of their formats to only store pointers to the actual path instead.
/// Then we translate the stored pointer to the path in CreateFileW, if the prefix matches.
/// </summary>
public unsafe class CreateFileWHook : IDisposable
{
public const int RequiredSize = 28;
public CreateFileWHook()
{
_createFileWHook = Hook<CreateFileWDelegate>.FromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour);
_createFileWHook.Enable();
}
/// <summary>
/// Write the data read specifically in the CreateFileW hook to a buffer array.
/// </summary>
/// <param name="buffer">The buffer the data is written to.</param>
/// <param name="address">The pointer to the UTF8 string containing the path.</param>
/// <param name="length">The length of the path in bytes.</param>
public static void WritePtr(char* buffer, byte* address, int length)
{
// Set the prefix, which is not valid for any actual path.
buffer[0] = Prefix;
var ptr = (byte*)buffer;
var v = (ulong)address;
var l = (uint)length;
// Since the game calls wstrcpy without a length, we need to ensure
// that there is no wchar_t (i.e. 2 bytes) of 0-values before the end.
// Fill everything with 0xFF and use every second byte.
MemoryUtility.MemSet(ptr + 2, 0xFF, 23);
// Write the byte pointer.
ptr[2] = (byte)(v >> 0);
ptr[4] = (byte)(v >> 8);
ptr[6] = (byte)(v >> 16);
ptr[8] = (byte)(v >> 24);
ptr[10] = (byte)(v >> 32);
ptr[12] = (byte)(v >> 40);
ptr[14] = (byte)(v >> 48);
ptr[16] = (byte)(v >> 56);
// Write the length.
ptr[18] = (byte)(l >> 0);
ptr[20] = (byte)(l >> 8);
ptr[22] = (byte)(l >> 16);
ptr[24] = (byte)(l >> 24);
ptr[RequiredSize - 2] = 0;
ptr[RequiredSize - 1] = 0;
}
public void Dispose()
{
_createFileWHook.Disable();
_createFileWHook.Dispose();
foreach (var ptr in _fileNameStorage.Values)
Marshal.FreeHGlobal(ptr);
}
/// <remarks> Long paths in windows need to start with "\\?\", so we keep this static in the pointers. </remarks>
private static nint SetupStorage()
{
var ptr = (char*)Marshal.AllocHGlobal(2 * BufferSize);
ptr[0] = '\\';
ptr[1] = '\\';
ptr[2] = '?';
ptr[3] = '\\';
ptr[4] = '\0';
return (nint)ptr;
}
// The prefix is not valid for any actual path, so should never run into false-positives.
private const char Prefix = (char)((byte)'P' | (('?' & 0x00FF) << 8));
private const int BufferSize = Utf8GamePath.MaxGamePathLength;
private delegate nint CreateFileWDelegate(char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags,
nint template);
private readonly Hook<CreateFileWDelegate> _createFileWHook;
/// <summary> Some storage to skip repeated allocations. </summary>
private readonly ThreadLocal<nint> _fileNameStorage = new(SetupStorage, true);
private nint CreateFileWDetour(char* fileName, uint access, uint shareMode, nint security, uint creation, uint flags, nint template)
{
// Translate data if prefix fits.
if (CheckPtr(fileName, out var name))
{
// Use static storage.
var ptr = WriteFileName(name);
Penumbra.Log.Verbose($"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe(name, false)}.");
return _createFileWHook.OriginalDisposeSafe(ptr, access, shareMode, security, creation, flags, template);
}
return _createFileWHook.OriginalDisposeSafe(fileName, access, shareMode, security, creation, flags, template);
}
/// <remarks>Write the UTF8-encoded byte string as UTF16 into the static buffers,
/// replacing any forward-slashes with back-slashes and adding a terminating null-wchar_t.</remarks>
private char* WriteFileName(ReadOnlySpan<byte> actualName)
{
var span = new Span<char>((char*)_fileNameStorage.Value + 4, BufferSize - 4);
var written = Encoding.UTF8.GetChars(actualName, span);
for (var i = 0; i < written; ++i)
{
if (span[i] == '/')
span[i] = '\\';
}
span[written] = '\0';
return (char*)_fileNameStorage.Value;
}
private static bool CheckPtr(char* buffer, out ReadOnlySpan<byte> fileName)
{
if (buffer[0] is not Prefix)
{
fileName = ReadOnlySpan<byte>.Empty;
return false;
}
var ptr = (byte*)buffer;
// Read the byte pointer.
var address = 0ul;
address |= (ulong)ptr[2] << 0;
address |= (ulong)ptr[4] << 8;
address |= (ulong)ptr[6] << 16;
address |= (ulong)ptr[8] << 24;
address |= (ulong)ptr[10] << 32;
address |= (ulong)ptr[12] << 40;
address |= (ulong)ptr[14] << 48;
address |= (ulong)ptr[16] << 56;
// Read the length.
var length = 0u;
length |= (uint)ptr[18] << 0;
length |= (uint)ptr[20] << 8;
length |= (uint)ptr[22] << 16;
length |= (uint)ptr[24] << 24;
fileName = new ReadOnlySpan<byte>((void*)address, (int)length);
return true;
}
// ***** Old method *****
//[DllImport( "kernel32.dll" )]
//private static extern nint LoadLibrary( string dllName );
//
//[DllImport( "kernel32.dll" )]
//private static extern nint GetProcAddress( nint hModule, string procName );
//
//public CreateFileWHookOld()
//{
// var userApi = LoadLibrary( "kernel32.dll" );
// var createFileAddress = GetProcAddress( userApi, "CreateFileW" );
// _createFileWHook = Hook<CreateFileWDelegate>.FromAddress( createFileAddress, CreateFileWDetour );
//}
}

View file

@ -0,0 +1,90 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.GameData;
using Penumbra.Interop.Structs;
using Penumbra.Util;
namespace Penumbra.Interop.Loader;
public unsafe class FileReadService : IDisposable
{
public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager)
{
_resourceManager = resourceManager;
_performance = performance;
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="returnValue">The return value. If this is set, original will not be called.</param>
public delegate void ReadSqPackDelegate(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue);
/// <summary>
/// <inheritdoc cref="ReadSqPackDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ReadSqPackDelegate? ReadSqPack;
/// <summary>
/// Use the games ReadFile function to read a file from the hard drive instead of an SqPack.
/// </summary>
/// <param name="fileDescriptor">The file to load.</param>
/// <param name="priority">The games priority.</param>
/// <param name="isSync">Whether the file needs to be loaded synchronously.</param>
/// <returns>Unknown, not directly success/failure.</returns>
public byte ReadFile(SeFileDescriptor* fileDescriptor, int priority, bool isSync)
=> _readFile.Invoke(GetResourceManager(), fileDescriptor, priority, isSync);
public byte ReadDefaultSqPack(SeFileDescriptor* fileDescriptor, int priority, bool isSync)
=> _readSqPackHook.Original(GetResourceManager(), fileDescriptor, priority, isSync);
public void Dispose()
{
_readSqPackHook.Dispose();
}
private readonly PerformanceTracker _performance;
private readonly ResourceManagerService _resourceManager;
private delegate byte ReadSqPackPrototype(nint resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync);
[Signature(Sigs.ReadSqPack, DetourName = nameof(ReadSqPackDetour))]
private readonly Hook<ReadSqPackPrototype> _readSqPackHook = null!;
private byte ReadSqPackDetour(nint resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync)
{
using var performance = _performance.Measure(PerformanceType.ReadSqPack);
byte? ret = null;
_lastFileThreadResourceManager.Value = resourceManager;
ReadSqPack?.Invoke(fileDescriptor, ref priority, ref isSync, ref ret);
_lastFileThreadResourceManager.Value = IntPtr.Zero;
return ret ?? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync);
}
private delegate byte ReadFileDelegate(nint resourceManager, SeFileDescriptor* fileDescriptor, int priority,
bool isSync);
/// We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks.
[Signature(Sigs.ReadFile)]
private readonly ReadFileDelegate _readFile = null!;
private readonly ThreadLocal<nint> _lastFileThreadResourceManager = new(true);
/// <summary>
/// Usually files are loaded using the resource manager as a first pointer, but it seems some rare cases are using something else.
/// So we keep track of them per thread and use them.
/// </summary>
private nint GetResourceManager()
=> !_lastFileThreadResourceManager.IsValueCreated || _lastFileThreadResourceManager.Value == IntPtr.Zero
? (nint) _resourceManager.ResourceManager
: _lastFileThreadResourceManager.Value;
}

View file

@ -0,0 +1,228 @@
using System;
using System.Diagnostics;
using System.Threading;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Collections;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Loader;
public unsafe class ResourceLoader : IDisposable
{
private readonly ResourceService _resources;
private readonly FileReadService _fileReadService;
private readonly TexMdlService _texMdlService;
public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService,
CreateFileWHook _)
{
_resources = resources;
_fileReadService = fileReadService;
_texMdlService = texMdlService;
ResetResolvePath();
_resources.ResourceRequested += ResourceHandler;
_resources.ResourceHandleIncRef += IncRefProtection;
_resources.ResourceHandleDecRef += DecRefProtection;
_fileReadService.ReadSqPack += ReadSqPackDetour;
}
/// <summary> The function to use to resolve a given path. </summary>
public Func<Utf8GamePath, ResourceCategory, ResourceType, (FullPath?, ResolveData)> ResolvePath = null!;
/// <summary> Reset the ResolvePath function to always return null. </summary>
public void ResetResolvePath()
=> ResolvePath = (_1, _2, _3) => (null, ResolveData.Invalid);
public delegate void ResourceLoadedDelegate(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
ResolveData resolveData);
/// <summary>
/// Event fired whenever a resource is returned.
/// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource.
/// resolveData is additional data returned by the current ResolvePath function which can contain the collection and associated game object.
/// </summary>
public event ResourceLoadedDelegate? ResourceLoaded;
public delegate void FileLoadedDelegate(ResourceHandle* resource, ByteString path, bool returnValue, bool custom,
ByteString additionalData);
/// <summary>
/// Event fired whenever a resource is newly loaded.
/// ReturnValue indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded)
/// custom is true if the file was loaded from local files instead of the default SqPacks.
/// AdditionalData is either empty or the part of the path inside the leading pipes.
/// </summary>
public event FileLoadedDelegate? FileLoaded;
public void Dispose()
{
_resources.ResourceRequested -= ResourceHandler;
_resources.ResourceHandleIncRef -= IncRefProtection;
_resources.ResourceHandleDecRef -= DecRefProtection;
_fileReadService.ReadSqPack -= ReadSqPackDetour;
}
private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path,
GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue)
{
if (returnValue != null)
return;
CompareHash(ComputeHash(path.Path, parameters), hash, path);
// If no replacements are being made, we still want to be able to trigger the event.
var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) : ResolvePath(path, category, type);
if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p))
{
returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters);
ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data);
return;
}
_texMdlService.AddCrc(type, resolvedPath);
// Replace the hash and path with the correct one for the replacement.
hash = ComputeHash(resolvedPath.Value.InternalName, parameters);
path = p;
returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters);
ResourceLoaded?.Invoke(returnValue, p, resolvedPath.Value, data);
}
private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue)
{
if (fileDescriptor->ResourceHandle == null)
{
Penumbra.Log.Error("[ResourceLoader] Failure to load file from SqPack: invalid File Descriptor.");
return;
}
if (!fileDescriptor->ResourceHandle->GamePath(out var gamePath) || gamePath.Length == 0)
{
Penumbra.Log.Error("[ResourceLoader] Failure to load file from SqPack: invalid path specified.");
return;
}
// Paths starting with a '|' are handled separately to allow for special treatment.
// They are expected to also have a closing '|'.
if (gamePath.Path[0] != (byte)'|')
{
returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, ByteString.Empty);
return;
}
// Split the path into the special-treatment part (between the first and second '|')
// and the actual path.
var split = gamePath.Path.Split((byte)'|', 3, false);
fileDescriptor->ResourceHandle->FileNameData = split[2].Path;
fileDescriptor->ResourceHandle->FileNameLength = split[2].Length;
MtrlForceSync(fileDescriptor, ref isSync);
returnValue = DefaultLoadResource(split[2], fileDescriptor, priority, isSync, split[1]);
// Return original resource handle path so that they can be loaded separately.
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path;
fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length;
}
/// <summary> Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. </summary>
private byte DefaultLoadResource(ByteString gamePath, SeFileDescriptor* fileDescriptor, int priority,
bool isSync, ByteString additionalData)
{
if (Utf8GamePath.IsRooted(gamePath))
{
// Specify that we are loading unpacked files from the drive.
// We need to obtain the actual file path in UTF16 (Windows-Unicode) on two locations,
// but we write a pointer to the given string instead and use the CreateFileW hook to handle it,
// because otherwise we are limited to 260 characters.
fileDescriptor->FileMode = FileMode.LoadUnpackedResource;
// Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd.
var fd = stackalloc char[0x11 + 0x0B + 14];
fileDescriptor->FileDescriptor = (byte*)fd + 1;
CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length);
CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length);
// Use the SE ReadFile function.
var ret = _fileReadService.ReadFile(fileDescriptor, priority, isSync);
FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, true, additionalData);
return ret;
}
else
{
var ret = _fileReadService.ReadDefaultSqPack(fileDescriptor, priority, isSync);
FileLoaded?.Invoke(fileDescriptor->ResourceHandle, gamePath, ret != 0, false, additionalData);
return ret;
}
}
/// <summary> Special handling for materials. </summary>
private static void MtrlForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync)
{
// Force isSync = true for Materials. I don't really understand why,
// or where the difference even comes from.
// Was called with True on my client and with false on other peoples clients,
// which caused problems.
isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl;
}
/// <summary>
/// A resource with ref count 0 that gets incremented goes through GetResourceAsync again.
/// This means, that if the path determined from that is different than the resources path,
/// a different resource gets loaded or incremented, while the IncRef'd resource stays at 0.
/// This causes some problems and is hopefully prevented with this.
/// </summary>
private readonly ThreadLocal<bool> _incMode = new(() => false, true);
/// <inheritdoc cref="_incMode"/>
private void IncRefProtection(ResourceHandle* handle, ref nint? returnValue)
{
if (handle->RefCount != 0)
return;
_incMode.Value = true;
returnValue = _resources.IncRef(handle);
_incMode.Value = false;
}
/// <summary>
/// Catch weird errors with invalid decrements of the reference count.
/// </summary>
private void DecRefProtection(ResourceHandle* handle, ref byte? returnValue)
{
if (handle->RefCount != 0)
return;
Penumbra.Log.Error(
$"[ResourceLoader] Caught decrease of Reference Counter for {handle->FileName()} at 0x{(ulong)handle} below 0.");
returnValue = 1;
}
/// <summary> Compute the CRC32 hash for a given path together with potential resource parameters. </summary>
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
// ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000
return ByteString.Join(
(byte)'.',
path,
ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true),
ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true)
).Crc32;
}
/// <summary>
/// In Debug build, compare the hashes the game computes with those Penumbra computes to notice potential changes in the CRC32 algorithm or resource parameters.
/// </summary>
[Conditional("DEBUG")]
private static void CompareHash(int local, int game, Utf8GamePath path)
{
if (local != game)
Penumbra.Log.Warning($"[ResourceLoader] Hash function appears to have changed. Computed {local:X8} vs Game {game:X8} for {path}.");
}
}

View file

@ -0,0 +1,114 @@
using System;
using System.Linq;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop;
using FFXIVClientStructs.STD;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
namespace Penumbra.Interop.Loader;
public unsafe class ResourceManagerService
{
public ResourceManagerService()
=> SignatureHelper.Initialise(this);
/// <summary> The SE Resource Manager as pointer. </summary>
public ResourceManager* ResourceManager
=> *ResourceManagerAddress;
/// <summary> Find a resource in the resource manager by its category, extension and crc-hash. </summary>
public ResourceHandle* FindResource(ResourceCategory cat, ResourceType ext, uint crc32)
{
ref var manager = ref *ResourceManager;
var catIdx = (uint)cat >> 0x18;
cat = (ResourceCategory)(ushort)cat;
ref var category = ref manager.ResourceGraph->ContainerArraySpan[(int)cat];
var extMap = FindInMap(category.CategoryMapsSpan[(int)catIdx].Value, (uint)ext);
if (extMap == null)
return null;
var ret = FindInMap(extMap->Value, crc32);
return ret == null ? null : ret->Value;
}
public delegate void ExtMapAction(ResourceCategory category, StdMap<uint, Pointer<StdMap<uint, Pointer<ResourceHandle>>>>* graph, int idx);
public delegate void ResourceMapAction(uint ext, StdMap<uint, Pointer<ResourceHandle>>* graph);
public delegate void ResourceAction(uint crc32, ResourceHandle* graph);
/// <summary> Iterate through the entire graph calling an action on every ExtMap. </summary>
public void IterateGraphs(ExtMapAction action)
{
ref var manager = ref *ResourceManager;
foreach (var resourceType in Enum.GetValues<ResourceCategory>().SkipLast(1))
{
ref var graph = ref manager.ResourceGraph->ContainerArraySpan[(int)resourceType];
for (var i = 0; i < 20; ++i)
{
var map = graph.CategoryMapsSpan[i];
if (map.Value != null)
action(resourceType, map, i);
}
}
}
/// <summary> Iterate through a specific ExtMap calling an action on every resource map. </summary>
public void IterateExtMap(StdMap<uint, Pointer<StdMap<uint, Pointer<ResourceHandle>>>>* map, ResourceMapAction action)
=> IterateMap(map, (ext, m) => action(ext, m.Value));
/// <summary> Iterate through a specific resource map calling an action on every resource. </summary>
public void IterateResourceMap(StdMap<uint, Pointer<ResourceHandle>>* map, ResourceAction action)
=> IterateMap(map, (crc, r) => action(crc, r.Value));
/// <summary> Iterate through the entire graph calling an action on every resource. </summary>
public void IterateResources(ResourceAction action)
{
IterateGraphs((_, extMap, _)
=> IterateExtMap(extMap, (_, resourceMap)
=> IterateResourceMap(resourceMap, action)));
}
/// <summary> A static pointer to the SE Resource Manager. </summary>
[Signature(Sigs.ResourceManager, ScanType = ScanType.StaticAddress)]
internal readonly ResourceManager** ResourceManagerAddress = null;
// Find a key in a StdMap.
private static TValue* FindInMap<TKey, TValue>(StdMap<TKey, TValue>* map, in TKey key)
where TKey : unmanaged, IComparable<TKey>
where TValue : unmanaged
{
if (map == null || map->Count == 0)
return null;
var node = map->Head->Parent;
while (!node->IsNil)
{
switch (key.CompareTo(node->KeyValuePair.Item1))
{
case 0: return &node->KeyValuePair.Item2;
case < 0:
node = node->Left;
break;
default:
node = node->Right;
break;
}
}
return null;
}
// Iterate in tree-order through a map, applying action to each KeyValuePair.
private static void IterateMap<TKey, TValue>(StdMap<TKey, TValue>* map, Action<TKey, TValue> action)
where TKey : unmanaged
where TValue : unmanaged
{
if (map == null || map->Count == 0)
return;
for (var node = map->SmallestValue; !node->IsNil; node = node->Next())
action(node->KeyValuePair.Item1, node->KeyValuePair.Item2);
}
}

View file

@ -0,0 +1,212 @@
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 Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Interop.Loader;
public unsafe class ResourceService : IDisposable
{
private readonly PerformanceTracker _performance;
private readonly ResourceManagerService _resourceManager;
public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager)
{
_performance = performance;
_resourceManager = resourceManager;
SignatureHelper.Initialise(this);
_getResourceSyncHook.Enable();
_getResourceAsyncHook.Enable();
_resourceHandleDestructorHook.Enable();
_incRefHook = Hook<ResourceHandlePrototype>.FromAddress(
(nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef,
ResourceHandleIncRefDetour);
_incRefHook.Enable();
_decRefHook = Hook<ResourceHandleDecRefPrototype>.FromAddress(
(nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef,
ResourceHandleDecRefDetour);
_decRefHook.Enable();
}
public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, ByteString path)
{
var hash = path.Crc32;
return GetResourceHandler(true, (ResourceManager*)_resourceManager.ResourceManagerAddress,
&category, &type, &hash, path.Path, null, false);
}
public void Dispose()
{
_getResourceSyncHook.Dispose();
_getResourceAsyncHook.Dispose();
_resourceHandleDestructorHook.Dispose();
_incRefHook.Dispose();
_decRefHook.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, can be null.</param>
/// <param name="sync">Whether to request the resource synchronously or asynchronously.</param>
/// <param name="returnValue">The returned resource handle. If this is not null, calling original will be skipped. </param>
public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path,
GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue);
/// <summary> <inheritdoc cref="GetResourcePreDelegate"/> <para/>
/// Subscribers should be exception-safe.</summary>
public event GetResourcePreDelegate? ResourceRequested;
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)
{
using var performance = _performance.Measure(PerformanceType.GetResourceHandler);
if (!Utf8GamePath.FromPointer(path, out var gamePath))
{
Penumbra.Log.Error("[ResourceService] Could not create GamePath from resource path.");
return isSync
? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams)
: _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk);
}
ResourceHandle* returnValue = null;
ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, pGetResParams, ref isSync,
ref returnValue);
if (returnValue != null)
return returnValue;
return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk);
}
/// <summary> Call the original GetResource function. </summary>
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, ByteString path,
GetResourceParameters* resourceParameters = null, bool unk = false)
=> sync
? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
resourceParameters)
: _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
resourceParameters,
unk);
#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="returnValue">The return value to use, setting this value will skip calling original.</param>
public delegate void ResourceHandleIncRefDelegate(ResourceHandle* handle, ref nint? returnValue);
/// <summary>
/// <inheritdoc cref="ResourceHandleIncRefDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ResourceHandleIncRefDelegate? ResourceHandleIncRef;
/// <summary>
/// Call the game function that increases the reference counter of a resource handle.
/// </summary>
public nint IncRef(ResourceHandle* handle)
=> _incRefHook.OriginalDisposeSafe(handle);
private readonly Hook<ResourceHandlePrototype> _incRefHook;
private nint ResourceHandleIncRefDetour(ResourceHandle* handle)
{
nint? ret = null;
ResourceHandleIncRef?.Invoke(handle, ref ret);
return ret ?? _incRefHook.OriginalDisposeSafe(handle);
}
#endregion
#region DecRef
/// <summary> Invoked before a resource handle reference count is decremented. </summary>
/// <param name="handle">The resource handle.</param>
/// <param name="returnValue">The return value to use, setting this value will skip calling original.</param>
public delegate void ResourceHandleDecRefDelegate(ResourceHandle* handle, ref byte? returnValue);
/// <summary>
/// <inheritdoc cref="ResourceHandleDecRefDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ResourceHandleDecRefDelegate? ResourceHandleDecRef;
/// <summary>
/// Call the original game function that decreases the reference counter of a resource handle.
/// </summary>
public byte DecRef(ResourceHandle* handle)
=> _decRefHook.OriginalDisposeSafe(handle);
private delegate byte ResourceHandleDecRefPrototype(ResourceHandle* handle);
private readonly Hook<ResourceHandleDecRefPrototype> _decRefHook;
private byte ResourceHandleDecRefDetour(ResourceHandle* handle)
{
byte? ret = null;
ResourceHandleDecRef?.Invoke(handle, ref ret);
return ret ?? _decRefHook.OriginalDisposeSafe(handle);
}
#endregion
#region Destructor
/// <summary> Invoked before a resource handle is destructed. </summary>
/// <param name="handle">The resource handle.</param>
public delegate void ResourceHandleDtorDelegate(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(handle);
return _resourceHandleDestructorHook.OriginalDisposeSafe(handle);
}
#endregion
}

View file

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Loader;
public unsafe class TexMdlService
{
/// <summary> Custom ulong flag to signal our files as opposed to SE files. </summary>
public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF);
/// <summary>
/// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files,
/// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes.
/// </summary>
public IReadOnlySet<ulong> CustomFileCrc
=> _customFileCrc;
public TexMdlService()
{
SignatureHelper.Initialise(this);
_checkFileStateHook.Enable();
_loadTexFileExternHook.Enable();
_loadMdlFileExternHook.Enable();
}
/// <summary> Add CRC64 if the given file is a model or texture file and has an associated path. </summary>
public void AddCrc(ResourceType type, FullPath? path)
{
if (path.HasValue && type is ResourceType.Mdl or ResourceType.Tex)
_customFileCrc.Add(path.Value.Crc64);
}
/// <summary> Add a fixed CRC64 value. </summary>
public void AddCrc(ulong crc64)
=> _customFileCrc.Add(crc64);
public void Dispose()
{
_checkFileStateHook.Dispose();
_loadTexFileExternHook.Dispose();
_loadMdlFileExternHook.Dispose();
}
private readonly HashSet<ulong> _customFileCrc = new();
private delegate IntPtr CheckFileStatePrototype(IntPtr unk1, ulong crc64);
[Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))]
private readonly Hook<CheckFileStatePrototype> _checkFileStateHook = null!;
/// <summary>
/// The function that checks a files CRC64 to determine whether it is 'protected'.
/// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag.
/// </summary>
private IntPtr CheckFileStateDetour(IntPtr ptr, ulong crc64)
=> _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64);
private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3);
/// <summary> We use the local functions for our own files in the extern hook. </summary>
[Signature(Sigs.LoadTexFileLocal)]
private readonly LoadTexFileLocalDelegate _loadTexFileLocal = null!;
private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2);
/// <summary> We use the local functions for our own files in the extern hook. </summary>
[Signature(Sigs.LoadMdlFileLocal)]
private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!;
private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4);
[Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))]
private readonly Hook<LoadTexFileExternPrototype> _loadTexFileExternHook = null!;
/// <summary> We hook the extern functions to just return the local one if given the custom flag as last argument. </summary>
private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr)
=> ptr.Equals(CustomFileFlag)
? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3)
: _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr);
public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3);
[Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))]
private readonly Hook<LoadMdlFileExternPrototype> _loadMdlFileExternHook = null!;
/// <summary> We hook the extern functions to just return the local one if given the custom flag as last argument. </summary>
private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr)
=> ptr.Equals(CustomFileFlag)
? _loadMdlFileLocal.Invoke(resourceHandle, unk1, unk2)
: _loadMdlFileExternHook.Original(resourceHandle, unk1, unk2, ptr);
}