diff --git a/Penumbra/Interop/MetaFileManager.cs b/Penumbra/Interop/MetaFileManager.cs new file mode 100644 index 00000000..c6b5241a --- /dev/null +++ b/Penumbra/Interop/MetaFileManager.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using Penumbra.GameData.ByteString; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop; + +public unsafe class MetaFileManager : IDisposable +{ + public MetaFileManager() + { + SignatureHelper.Initialise( this ); + InitImc(); + } + + public void Dispose() + { + DisposeImc(); + } + + + // Allocate in the games space for file storage. + // We only need this if using any meta file. +#if USE_IMC || USE_CMP || USE_EQDP || USE_EQP || USE_EST || USE_GMP + [Signature( "E8 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 4C 8B C0" )] + public IntPtr GetFileSpaceAddress; +#endif + public IMemorySpace* GetFileSpace() + => ( ( delegate* unmanaged< IMemorySpace* > )GetFileSpaceAddress )(); + + public void* AllocateFileMemory( ulong length, ulong alignment = 0 ) + => GetFileSpace()->Malloc( length, alignment ); + + public void* AllocateFileMemory( int length, int alignment = 0 ) + => AllocateFileMemory( ( ulong )length, ( ulong )alignment ); + + + // We only need this for IMC files, since we need to hook their cleanup function. +#if USE_IMC + [Signature( "48 8D 05 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 89 03", ScanType = ScanType.StaticAddress )] + public IntPtr* DefaultResourceHandleVTable; +#endif + + public delegate void ClearResource( ResourceHandle* resource ); + public Hook< ClearResource > ClearDefaultResourceHook = null!; + + private readonly Dictionary< IntPtr, (IntPtr, int) > _originalImcData = new(); + + // We store the original data of loaded IMCs so that we can restore it before they get destroyed, + // similar to the other meta files, just with arbitrary destruction. + private void ClearDefaultResourceDetour( ResourceHandle* resource ) + { + if( _originalImcData.TryGetValue( ( IntPtr )resource, out var data ) ) + { + PluginLog.Debug( "Restoring data of {$Name:l} (0x{Resource}) to 0x{Data:X} and Length {Length} before deletion.", + Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true, null, null ), ( ulong )resource, ( ulong )data.Item1, data.Item2 ); + resource->SetData( data.Item1, data.Item2 ); + _originalImcData.Remove( ( IntPtr )resource ); + } + + ClearDefaultResourceHook.Original( resource ); + } + + // Called when a new IMC is manipulated to store its data. + [Conditional( "USE_IMC" )] + public void AddImcFile( ResourceHandle* resource, IntPtr data, int length ) + { + PluginLog.Debug( "Storing data 0x{Data:X} of Length {Length} for {$Name:l} (0x{Resource:X}).", ( ulong )data, length, + Utf8String.FromSpanUnsafe( resource->FileNameSpan(), true, null, null ), ( ulong )resource ); + _originalImcData[ ( IntPtr )resource ] = ( data, length ); + } + + // Initialize the hook at VFunc 25, which is called when default resources (and IMC resources do not overwrite it) destroy their data. + [Conditional( "USE_IMC" )] + private void InitImc() + { + ClearDefaultResourceHook = new Hook< ClearResource >( DefaultResourceHandleVTable[ 25 ], ClearDefaultResourceDetour ); + ClearDefaultResourceHook.Enable(); + } + + [Conditional( "USE_IMC" )] + private void DisposeImc() + { + ClearDefaultResourceHook.Disable(); + ClearDefaultResourceHook.Dispose(); + // Restore all IMCs to their default values on dispose. + // This should only be relevant when testing/disabling/reenabling penumbra. + foreach( var (resourcePtr, (data, length)) in _originalImcData ) + { + var resource = ( ResourceHandle* )resourcePtr; + resource->SetData( data, length ); + } + + _originalImcData.Clear(); + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 1a992a89..67be61e4 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; +using Penumbra.Interop; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Files; @@ -68,7 +69,7 @@ public unsafe class ImcFile : MetaBaseFile public readonly Utf8GamePath Path; public readonly int NumParts; - public bool ChangesSinceLoad = true; + public bool ChangesSinceLoad = false; public ReadOnlySpan< ImcEntry > Span => new(( ImcEntry* )( Data + PreambleSize ), ( Length - PreambleSize ) / sizeof( ImcEntry )); @@ -79,17 +80,6 @@ public unsafe class ImcFile : MetaBaseFile private static ushort PartMask( byte* data ) => *( ushort* )( data + 2 ); - private static ImcEntry* DefaultPartPtr( byte* data, int partIdx ) - { - var flag = 1 << partIdx; - if( ( PartMask( data ) & flag ) == 0 ) - { - return null; - } - - return ( ImcEntry* )( data + PreambleSize ) + partIdx; - } - private static ImcEntry* VariantPtr( byte* data, int partIdx, int variantIdx ) { var flag = 1 << partIdx; @@ -140,6 +130,7 @@ public unsafe class ImcFile : MetaBaseFile var newLength = ( ( ( ActualLength - 1 ) >> 7 ) + 1 ) << 7; PluginLog.Verbose( "Resized IMC {Path} from {Length} to {NewLength}.", Path, Length, newLength ); ResizeResources( newLength ); + ChangesSinceLoad = true; } var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); @@ -173,8 +164,7 @@ public unsafe class ImcFile : MetaBaseFile return false; } - *variantPtr = entry; - ChangesSinceLoad = true; + *variantPtr = entry; return true; } @@ -187,8 +177,6 @@ public unsafe class ImcFile : MetaBaseFile Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); Functions.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); } - - ChangesSinceLoad = true; } public ImcFile( Utf8GamePath path ) @@ -226,7 +214,7 @@ public unsafe class ImcFile : MetaBaseFile } } - public void Replace( ResourceHandle* resource ) + public void Replace( ResourceHandle* resource, bool firstTime ) { var (data, length) = resource->GetData(); if( data == IntPtr.Zero ) @@ -234,18 +222,10 @@ public unsafe class ImcFile : MetaBaseFile return; } - var requiredLength = ActualLength; resource->SetData( ( IntPtr )Data, Length ); - if( length >= requiredLength ) + if( firstTime ) { - Functions.MemCpyUnchecked( ( void* )data, Data, requiredLength ); - Functions.MemSet( ( byte* )data + requiredLength, 0, length - requiredLength ); - return; + Penumbra.MetaFileManager.AddImcFile( resource, data, length ); } - - MemoryHelper.GameFree( ref data, ( ulong )length ); - var file = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )requiredLength ); - Functions.MemCpyUnchecked( file, Data, requiredLength ); - resource->SetData( ( IntPtr )file, requiredLength ); } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index e6e79740..303499a5 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -24,7 +24,7 @@ public unsafe class MetaBaseFile : IDisposable protected void AllocateData( int length ) { Length = length; - Data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )length ); + Data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( length ); if( length > 0 ) { GC.AddMemoryPressure( length ); @@ -53,7 +53,7 @@ public unsafe class MetaBaseFile : IDisposable return; } - var data = ( byte* )MemoryHelper.GameAllocateDefault( ( ulong )newLength ); + var data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( ( ulong )newLength ); if( newLength > Length ) { Functions.MemCpyUnchecked( data, Data, Length ); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index a4f32b76..c921049f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -145,7 +145,7 @@ public partial class MetaManager { PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, collection.Name ); - file.Replace( fileDescriptor->ResourceHandle ); + file.Replace( fileDescriptor->ResourceHandle, true); file.ChangesSinceLoad = false; } @@ -165,7 +165,7 @@ public partial class MetaManager PluginLog.Debug( "File {GamePath:l} was already loaded but IMC in collection {Collection:l} was changed, so reloaded.", gamePath, collection.Name ); - file.Replace( resource ); + file.Replace( resource, false ); file.ChangesSinceLoad = false; } }