mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-19 22:37:46 +01:00
Rename interop folders
This commit is contained in:
parent
49f1e2dcde
commit
56286e0123
17 changed files with 1 additions and 1 deletions
177
Penumbra/Interop/ResourceLoading/CreateFileWHook.cs
Normal file
177
Penumbra/Interop/ResourceLoading/CreateFileWHook.cs
Normal 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 );
|
||||
//}
|
||||
}
|
||||
90
Penumbra/Interop/ResourceLoading/FileReadService.cs
Normal file
90
Penumbra/Interop/ResourceLoading/FileReadService.cs
Normal 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;
|
||||
}
|
||||
228
Penumbra/Interop/ResourceLoading/ResourceLoader.cs
Normal file
228
Penumbra/Interop/ResourceLoading/ResourceLoader.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
114
Penumbra/Interop/ResourceLoading/ResourceManagerService.cs
Normal file
114
Penumbra/Interop/ResourceLoading/ResourceManagerService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
212
Penumbra/Interop/ResourceLoading/ResourceService.cs
Normal file
212
Penumbra/Interop/ResourceLoading/ResourceService.cs
Normal 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
|
||||
}
|
||||
100
Penumbra/Interop/ResourceLoading/TexMdlService.cs
Normal file
100
Penumbra/Interop/ResourceLoading/TexMdlService.cs
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue