From d03a3168b0590b3d31f68f56b0b813b21df0a00f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Mar 2022 21:48:51 +0100 Subject: [PATCH] Fix IMC handling. --- .../Interop/Loader/ResourceLoader.Debug.cs | 25 ++++++------ .../Loader/ResourceLoader.Replacement.cs | 4 +- Penumbra/Interop/Loader/ResourceLoader.cs | 2 +- Penumbra/Interop/ObjectReloader.cs | 15 ++++---- Penumbra/Interop/Structs/ResourceHandle.cs | 17 ++++++++- Penumbra/Meta/Files/ImcFile.cs | 14 ++++--- Penumbra/Meta/Manager/MetaManager.Imc.cs | 38 ++++++++++++++----- Penumbra/Mods/CollectionManager.cs | 3 +- 8 files changed, 77 insertions(+), 41 deletions(-) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index 6e876ab4..b39a3c57 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -19,13 +19,13 @@ public unsafe partial class ResourceLoader // Gather some debugging data about penumbra-loaded objects. public struct DebugData { - public ResourceHandle* OriginalResource; - public ResourceHandle* ManipulatedResource; - public Utf8GamePath OriginalPath; - public FullPath ManipulatedPath; - public ResourceCategory Category; - public object? ResolverInfo; - public uint Extension; + public Structs.ResourceHandle* OriginalResource; + public Structs.ResourceHandle* ManipulatedResource; + public Utf8GamePath OriginalPath; + public FullPath ManipulatedPath; + public ResourceCategory Category; + public object? ResolverInfo; + public uint Extension; } private readonly SortedDictionary< FullPath, DebugData > _debugList = new(); @@ -44,7 +44,8 @@ public unsafe partial class ResourceLoader ResourceLoaded -= AddModifiedDebugInfo; } - private void AddModifiedDebugInfo( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolverInfo ) + private void AddModifiedDebugInfo( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + object? resolverInfo ) { if( manipulatedPath == null ) { @@ -55,7 +56,7 @@ public unsafe partial class ResourceLoader var originalResource = FindResource( handle->Category, handle->FileType, crc ); _debugList[ manipulatedPath.Value ] = new DebugData() { - OriginalResource = originalResource, + OriginalResource = ( Structs.ResourceHandle* )originalResource, ManipulatedResource = handle, Category = handle->Category, Extension = handle->FileType, @@ -172,8 +173,8 @@ public unsafe partial class ResourceLoader { _deleteList.Add( ( data.ManipulatedPath, data with { - OriginalResource = regularResource, - ManipulatedResource = modifiedResource, + OriginalResource = ( Structs.ResourceHandle* )regularResource, + ManipulatedResource = ( Structs.ResourceHandle* )modifiedResource, } ) ); } } @@ -195,7 +196,7 @@ public unsafe partial class ResourceLoader private static void LogPath( Utf8GamePath path, bool synchronous ) => PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); - private static void LogResource( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ ) + private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ ) { var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index b9473345..a7b5ddfb 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -72,7 +72,7 @@ public unsafe partial class ResourceLoader if( resolvedPath == null ) { var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); - ResourceLoaded?.Invoke( retUnmodified, gamePath, null, data ); + ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retUnmodified, gamePath, null, data ); return retUnmodified; } @@ -80,7 +80,7 @@ public unsafe partial class ResourceLoader *resourceHash = resolvedPath.Value.InternalName.Crc32; path = resolvedPath.Value.InternalName.Path; var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); - ResourceLoaded?.Invoke( retModified, gamePath, resolvedPath.Value, data ); + ResourceLoaded?.Invoke( ( Structs.ResourceHandle* )retModified, gamePath, resolvedPath.Value, data ); return retModified; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index 161bbd07..98457acc 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -105,7 +105,7 @@ public unsafe partial class ResourceLoader : IDisposable // 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 and is user-defined. - public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + public delegate void ResourceLoadedDelegate( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolveData ); public event ResourceLoadedDelegate? ResourceLoaded; diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 56a40584..6d345858 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Enums; @@ -33,11 +34,11 @@ public unsafe class ObjectReloader : IDisposable public static DrawState* ActorDrawState( GameObject actor ) => ( DrawState* )( actor.Address + 0x0104 ); - private static delegate*< IntPtr, void > GetDisableDraw( GameObject actor ) - => ( ( delegate*< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]; + private static void DisableDraw( GameObject actor ) + => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]( actor.Address ); - private static delegate*< IntPtr, void > GetEnableDraw( GameObject actor ) - => ( ( delegate*< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]; + private static void EnableDraw( GameObject actor ) + => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); public ObjectReloader( ModManager mods ) => _mods = mods; @@ -57,14 +58,14 @@ public unsafe class ObjectReloader : IDisposable _changedSettings = false; } - private unsafe void WriteInvisible( GameObject actor, int actorIdx ) + private void WriteInvisible( GameObject actor, int actorIdx ) { _currentObjectStartState = *ActorDrawState( actor ); *ActorDrawState( actor ) |= DrawState.Invisibility; if( _inGPose ) { - GetDisableDraw( actor )( actor.Address ); + DisableDraw( actor ); } } @@ -95,7 +96,7 @@ public unsafe class ObjectReloader : IDisposable if( _inGPose ) { - GetEnableDraw( actor )( actor.Address ); + EnableDraw( actor ); } } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 9493a2eb..447fa361 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.System.Resource; namespace Penumbra.Interop.Structs; @@ -40,18 +41,30 @@ public unsafe struct ResourceHandle [FieldOffset( 0x00 )] public void** VTable; + [FieldOffset( 0x08 )] + public ResourceCategory Category; + + [FieldOffset( 0x0C )] + public uint FileType; + + [FieldOffset( 0x10 )] + public uint Id; + [FieldOffset( 0x48 )] public byte* FileNameData; [FieldOffset( 0x58 )] public int FileNameLength; + [FieldOffset( 0xAC )] + public uint RefCount; + // May return null. public static byte* GetData( ResourceHandle* handle ) - => ( ( delegate*< ResourceHandle*, byte* > )handle->VTable[ 23 ] )( handle ); + => ( ( delegate* unmanaged< ResourceHandle*, byte* > )handle->VTable[ 23 ] )( handle ); public static ulong GetLength( ResourceHandle* handle ) - => ( ( delegate*< ResourceHandle*, ulong > )handle->VTable[ 17 ] )( handle ); + => ( ( delegate* unmanaged< ResourceHandle*, ulong > )handle->VTable[ 17 ] )( handle ); // Only use these if you know what you are doing. diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index f3ce0a4a..7f2e27fc 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Numerics; using Dalamud.Logging; using Dalamud.Memory; @@ -8,7 +7,6 @@ using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Interop.Structs; -using Penumbra.Meta.Manipulations; namespace Penumbra.Meta.Files; @@ -68,8 +66,9 @@ public unsafe class ImcFile : MetaBaseFile public int Count => CountInternal( Data ); - public readonly int NumParts; public readonly Utf8GamePath Path; + public readonly int NumParts; + public bool ChangesSinceLoad = true; private static int CountInternal( byte* data ) => *( ushort* )data; @@ -179,7 +178,8 @@ public unsafe class ImcFile : MetaBaseFile return false; } - *variantPtr = entry; + *variantPtr = entry; + ChangesSinceLoad = true; return true; } @@ -192,6 +192,8 @@ 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 ) @@ -209,9 +211,8 @@ public unsafe class ImcFile : MetaBaseFile fixed( byte* ptr = file.Data ) { NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); - AllocateData( file.Data.Length + sizeof( ImcEntry ) * 100 * NumParts ); + AllocateData( file.Data.Length ); Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); - Functions.MemSet( Data + file.Data.Length, 0, sizeof( ImcEntry ) * 100 * NumParts ); } } @@ -239,6 +240,7 @@ public unsafe class ImcFile : MetaBaseFile } var requiredLength = ActualLength; + resource->SetData( (IntPtr) Data, Length ); if( length >= requiredLength ) { Functions.MemCpyUnchecked( ( void* )data, Data, requiredLength ); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 8c4fb7c2..8dee907f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.Interop.Loader; @@ -57,7 +58,6 @@ public partial class MetaManager public unsafe bool ApplyMod( ImcManipulation m, Mod.Mod mod ) { - const uint imcExt = 0x00696D63; #if USE_IMC if( !Manipulations.TryAdd( m, mod ) ) { @@ -82,12 +82,6 @@ public partial class MetaManager _collection.Cache.ResolvedFiles[ path ] = fullPath; } - var resource = ResourceLoader.FindResource( ResourceCategory.Chara, imcExt, ( uint )path.Path.Crc32 ); - if( resource != null ) - { - file.Replace( ( ResourceHandle* )resource ); - } - return true; #else return false; @@ -109,22 +103,25 @@ public partial class MetaManager [Conditional( "USE_IMC" )] private unsafe void SetupDelegate() { - Penumbra.ResourceLoader.ResourceLoadCustomization = ImcHandler; + Penumbra.ResourceLoader.ResourceLoadCustomization = ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; } [Conditional( "USE_IMC" )] private unsafe void RestoreDelegate() { - if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcHandler ) + if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcLoadHandler ) { Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate; } + + Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; } private FullPath CreateImcPath( Utf8GamePath path ) => new($"|{_collection.Name}|{path}"); - private static unsafe byte ImcHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, + private static unsafe byte ImcLoadHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { var split = gamePath.Path.Split( ( byte )'|', 2, true ); @@ -137,12 +134,33 @@ public partial class MetaManager && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) { + PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", gamePath, + collection.Name ); file.Replace( fileDescriptor->ResourceHandle ); + file.ChangesSinceLoad = false; } fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; return ret; } + + private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData ) + { + // Only check imcs. + if( resource->FileType != 0x00696D63 + || resolveData is not ModCollection collection + || collection.Cache == null + || !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file ) + || !file.ChangesSinceLoad ) + { + return; + } + + 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.ChangesSinceLoad = false; + } } } \ No newline at end of file diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index fa3835da..cbada433 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -25,7 +26,7 @@ public class CollectionManager private readonly ModManager _manager; public string CollectionChangedTo { get; private set; } = string.Empty; - public Dictionary< string, ModCollection > Collections { get; } = new(); + public Dictionary< string, ModCollection > Collections { get; } = new(StringComparer.InvariantCultureIgnoreCase); public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty;