diff --git a/Penumbra.GameData/ByteString/Utf8GamePath.cs b/Penumbra.GameData/ByteString/Utf8GamePath.cs index afd5d12b..79386002 100644 --- a/Penumbra.GameData/ByteString/Utf8GamePath.cs +++ b/Penumbra.GameData/ByteString/Utf8GamePath.cs @@ -130,10 +130,13 @@ public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< U => Path.Dispose(); public bool IsRooted() - => Path.Length >= 1 && ( Path[ 0 ] == '/' || Path[ 0 ] == '\\' ) - || Path.Length >= 2 - && ( Path[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' ) - && Path[ 1 ] == ':'; + => IsRooted( Path ); + + public static bool IsRooted( Utf8String path ) + => path.Length >= 1 && ( path[ 0 ] == '/' || path[ 0 ] == '\\' ) + || path.Length >= 2 + && ( path[ 0 ] >= 'A' && path[ 0 ] <= 'Z' || path[ 0 ] >= 'a' && path[ 0 ] <= 'z' ) + && path[ 1 ] == ':'; public class Utf8GamePathConverter : JsonConverter { diff --git a/Penumbra.GameData/Enums/ResourceType.cs b/Penumbra.GameData/Enums/ResourceType.cs new file mode 100644 index 00000000..dedaf3c2 --- /dev/null +++ b/Penumbra.GameData/Enums/ResourceType.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using Penumbra.GameData.ByteString; + +namespace Penumbra.GameData.Enums; + +public enum ResourceType : uint +{ + Aet = 0x00616574, + Amb = 0x00616D62, + Atch = 0x61746368, + Atex = 0x61746578, + Avfx = 0x61766678, + Awt = 0x00617774, + Cmp = 0x00636D70, + Dic = 0x00646963, + Eid = 0x00656964, + Envb = 0x656E7662, + Eqdp = 0x65716470, + Eqp = 0x00657170, + Essb = 0x65737362, + Est = 0x00657374, + Exd = 0x00657864, + Exh = 0x00657868, + Exl = 0x0065786C, + Fdt = 0x00666474, + Gfd = 0x00676664, + Ggd = 0x00676764, + Gmp = 0x00676D70, + Gzd = 0x00677A64, + Imc = 0x00696D63, + Lcb = 0x006C6362, + Lgb = 0x006C6762, + Luab = 0x6C756162, + Lvb = 0x006C7662, + Mdl = 0x006D646C, + Mlt = 0x006D6C74, + Mtrl = 0x6D74726C, + Obsb = 0x6F627362, + Pap = 0x00706170, + Pbd = 0x00706264, + Pcb = 0x00706362, + Phyb = 0x70687962, + Plt = 0x00706C74, + Scd = 0x00736364, + Sgb = 0x00736762, + Shcd = 0x73686364, + Shpk = 0x7368706B, + Sklb = 0x736B6C62, + Skp = 0x00736B70, + Stm = 0x0073746D, + Svb = 0x00737662, + Tera = 0x74657261, + Tex = 0x00746578, + Tmb = 0x00746D62, + Ugd = 0x00756764, + Uld = 0x00756C64, + Waoe = 0x77616F65, + Wtd = 0x00777464, +} + +public static class ResourceTypeExtensions +{ + public static ResourceType FromBytes( byte a1, byte a2, byte a3 ) + => ( ResourceType )( ( ( uint )ByteStringFunctions.AsciiToLower( a1 ) << 16 ) + | ( ( uint )ByteStringFunctions.AsciiToLower( a2 ) << 8 ) + | ByteStringFunctions.AsciiToLower( a3 ) ); + + public static ResourceType FromBytes( byte a1, byte a2, byte a3, byte a4 ) + => ( ResourceType )( ( ( uint )ByteStringFunctions.AsciiToLower( a1 ) << 24 ) + | ( ( uint )ByteStringFunctions.AsciiToLower( a2 ) << 16 ) + | ( ( uint )ByteStringFunctions.AsciiToLower( a3 ) << 8 ) + | ByteStringFunctions.AsciiToLower( a4 ) ); + + public static ResourceType FromBytes( char a1, char a2, char a3 ) + => FromBytes( ( byte )a1, ( byte )a2, ( byte )a3 ); + + public static ResourceType FromBytes( char a1, char a2, char a3, char a4 ) + => FromBytes( ( byte )a1, ( byte )a2, ( byte )a3, ( byte )a4 ); + + public static ResourceType FromString( string path ) + { + var ext = Path.GetExtension( path.AsSpan() ); + ext = ext.Length == 0 ? path.AsSpan() : ext[ 1.. ]; + + return ext.Length switch + { + 0 => 0, + 1 => ( ResourceType )ext[ ^1 ], + 2 => FromBytes( '\0', ext[ ^2 ], ext[ ^1 ] ), + 3 => FromBytes( ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + _ => FromBytes( ext[ ^4 ], ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + }; + } + + public static ResourceType FromString( Utf8String path ) + { + var extIdx = path.LastIndexOf( ( byte )'.' ); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? Utf8String.Empty : path.Substring( extIdx + 1 ); + + return ext.Length switch + { + 0 => 0, + 1 => ( ResourceType )ext[ ^1 ], + 2 => FromBytes( 0, ext[ ^2 ], ext[ ^1 ] ), + 3 => FromBytes( ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + _ => FromBytes( ext[ ^4 ], ext[ ^3 ], ext[ ^2 ], ext[ ^1 ] ), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index b39a3c57..f43067d0 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -7,6 +7,8 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Resolver; namespace Penumbra.Interop.Loader; @@ -25,7 +27,7 @@ public unsafe partial class ResourceLoader public FullPath ManipulatedPath; public ResourceCategory Category; public object? ResolverInfo; - public uint Extension; + public ResourceType Extension; } private readonly SortedDictionary< FullPath, DebugData > _debugList = new(); @@ -112,14 +114,14 @@ public unsafe partial class ResourceLoader // Find a resource in the resource manager by its category, extension and crc-hash - public static ResourceHandle* FindResource( ResourceCategory cat, uint ext, uint crc32 ) + public static ResourceHandle* FindResource( ResourceCategory cat, ResourceType ext, uint crc32 ) { var manager = *ResourceManager; var catIdx = ( uint )cat >> 0x18; cat = ( ResourceCategory )( ushort )cat; var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat; var extMap = FindInMap( ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )category->CategoryMaps[ catIdx ], - ext ); + ( uint )ext ); if( extMap == null ) { return null; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index a7b5ddfb..7e9089c7 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -1,10 +1,12 @@ using System; using System.Diagnostics; +using System.Linq; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -16,27 +18,27 @@ public unsafe partial class ResourceLoader // 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. public delegate ResourceHandle* GetResourceSyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - uint* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown ); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown ); [Signature( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00", DetourName = "GetResourceSyncDetour" )] public Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!; public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId, - uint* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown, bool isUnknown ); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, void* pUnknown, bool isUnknown ); [Signature( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00", DetourName = "GetResourceAsyncDetour" )] public Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!; - private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + private ResourceHandle* GetResourceSyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, void* unk ) => GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, unk, false ); - private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) => GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); private ResourceHandle* CallOriginalHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - uint* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) + ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) => isSync ? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk ) : GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); @@ -53,7 +55,8 @@ public unsafe partial class ResourceLoader private event Action< Utf8GamePath, FullPath?, object? >? PathResolved; - private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, + ResourceType* resourceType, int* resourceHash, byte* path, void* unk, bool isUnk ) { if( !Utf8GamePath.FromPointer( path, out var gamePath ) ) @@ -67,7 +70,7 @@ public unsafe partial class ResourceLoader ResourceRequested?.Invoke( gamePath, isSync ); // If no replacements are being made, we still want to be able to trigger the event. - var (resolvedPath, data) = DoReplacements ? ResolvePath( gamePath.ToLower() ) : ( null, null ); + var (resolvedPath, data) = ResolvePath( gamePath, *categoryId, *resourceType, *resourceHash ); PathResolved?.Invoke( gamePath, resolvedPath, data ); if( resolvedPath == null ) { @@ -85,6 +88,37 @@ public unsafe partial class ResourceLoader } + // Use the default method of path replacement. + public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) + { + var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path ); + return ( resolved, null ); + } + + // Try all resolve path subscribers or use the default replacer. + private (FullPath?, object?) ResolvePath( Utf8GamePath path, ResourceCategory category, ResourceType resourceType, int resourceHash ) + { + if( !DoReplacements ) + { + return ( null, null ); + } + + path = path.ToLower(); + if( ResolvePathCustomization != null ) + { + foreach( var resolver in ResolvePathCustomization.GetInvocationList() ) + { + if( ( ( ResolvePathDelegate )resolver ).Invoke( path, category, resourceType, resourceHash, out var ret ) ) + { + return ret; + } + } + } + + return DefaultResolver( path ); + } + + // We need to use the ReadFile function to load local, uncompressed files instead of loading them from the SqPacks. public delegate byte ReadFileDelegate( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ); @@ -111,63 +145,82 @@ public unsafe partial class ResourceLoader return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } - var valid = Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ); - byte ret; - // The internal buffer size does not allow for more than 260 characters. - // We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack. - if( !valid || !gamePath.IsRooted() ) + if( !Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ) ) { - if( valid && ResourceLoadCustomization != null && gamePath.Path[ 0 ] == ( byte )'|' ) - { - ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync ); - } - else - { - ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( gamePath.Path, ret != 0, false ); - } - } - else - { - // Specify that we are loading unpacked files from the drive. - // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations, - // but since we only allow ASCII in the game paths, this is just a matter of upcasting. - fileDescriptor->FileMode = FileMode.LoadUnpackedResource; - - var fd = stackalloc byte[0x20 + 2 * gamePath.Length + 0x16]; - fileDescriptor->FileDescriptor = fd; - var fdPtr = ( char* )( fd + 0x21 ); - for( var i = 0; i < gamePath.Length; ++i ) - { - ( &fileDescriptor->Utf16FileName )[ i ] = ( char )gamePath.Path[ i ]; - fdPtr[ i ] = ( char )gamePath.Path[ i ]; - } - - ( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0'; - fdPtr[ gamePath.Length ] = '\0'; - - // Use the SE ReadFile function. - ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); - FileLoaded?.Invoke( gamePath.Path, ret != 0, true ); + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); } + byte ret = 0; + // Paths starting with a '|' are handled separately to allow for special treatment. + // They are expected to also have a closing '|'. + if( ResourceLoadCustomization == null || gamePath.Path[ 0 ] != ( byte )'|' ) + { + return DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync ); + } + + // 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; + var funcFound = ResourceLoadCustomization.GetInvocationList() + .Any( f => ( ( ResourceLoadCustomizationDelegate )f ) + .Invoke( split[ 1 ], split[ 2 ], resourceManager, fileDescriptor, priority, isSync, out ret ) ); + + if( !funcFound ) + { + ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync ); + } + + // Return original resource handle path so that they can be loaded separately. + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; return ret; } - // Customize file loading for any GamePaths that start with "|". - public delegate byte ResourceLoadCustomizationDelegate( Utf8GamePath gamePath, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ); - - public ResourceLoadCustomizationDelegate? ResourceLoadCustomization; - - - // Use the default method of path replacement. - public static (FullPath?, object?) DefaultReplacer( Utf8GamePath path ) + // Load the resource from an SqPack and trigger the FileLoaded event. + private byte DefaultResourceLoad( Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync ) { - var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path ); - return ( resolved, null ); + var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( path, ret != 0, false ); + return ret; } + // Load the resource from a path on the users hard drives. + private byte DefaultRootedResourceLoad( Utf8String gamePath, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + { + // Specify that we are loading unpacked files from the drive. + // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations, + // but since we only allow ASCII in the game paths, this is just a matter of upcasting. + fileDescriptor->FileMode = FileMode.LoadUnpackedResource; + + var fd = stackalloc byte[0x20 + 2 * gamePath.Length + 0x16]; + fileDescriptor->FileDescriptor = fd; + var fdPtr = ( char* )( fd + 0x21 ); + for( var i = 0; i < gamePath.Length; ++i ) + { + ( &fileDescriptor->Utf16FileName )[ i ] = ( char )gamePath.Path[ i ]; + fdPtr[ i ] = ( char )gamePath.Path[ i ]; + } + + ( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0'; + fdPtr[ gamePath.Length ] = '\0'; + + // Use the SE ReadFile function. + var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( gamePath, ret != 0, true ); + return ret; + } + + // Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. + internal byte DefaultLoadResource( Utf8String gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, + bool isSync ) + => Utf8GamePath.IsRooted( gamePath ) + ? DefaultRootedResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ) + : DefaultResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync ); + private void DisposeHooks() { DisableHooks(); diff --git a/Penumbra/Interop/Loader/ResourceLoader.cs b/Penumbra/Interop/Loader/ResourceLoader.cs index f02f0895..ad721b5e 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.cs @@ -1,6 +1,9 @@ using System; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; namespace Penumbra.Interop.Loader; @@ -104,7 +107,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( Structs.ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, + public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolveData ); public event ResourceLoadedDelegate? ResourceLoaded; @@ -117,7 +120,19 @@ public unsafe partial class ResourceLoader : IDisposable public event FileLoadedDelegate? FileLoaded; // Customization point to control how path resolving is handled. - public Func< Utf8GamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer; + // Resolving goes through all subscribed functions in arbitrary order until one returns true, + // or uses default resolving if none return true. + public delegate bool ResolvePathDelegate( Utf8GamePath path, ResourceCategory category, ResourceType type, int hash, + out (FullPath?, object?) ret ); + + public event ResolvePathDelegate? ResolvePathCustomization; + + // Customize file loading for any GamePaths that start with "|". + // Same procedure as above. + public delegate bool ResourceLoadCustomizationDelegate( Utf8String split, Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte retValue ); + + public event ResourceLoadCustomizationDelegate? ResourceLoadCustomization; public void Dispose() { diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index d5804d23..7907b172 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Dalamud.Hooking; @@ -92,7 +93,7 @@ public unsafe partial class PathResolver internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new(); // This map links files to their corresponding collection, if it is non-default. - internal readonly Dictionary< Utf8String, ModCollection > PathCollections = new(); + internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new(); internal GameObject* LastGameObject = null; @@ -225,13 +226,9 @@ public unsafe partial class PathResolver // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - private void SetCollection( Utf8String path, ModCollection? collection ) + private void SetCollection( Utf8String path, ModCollection collection ) { - if( collection == null ) - { - PathCollections.Remove( path ); - } - else if( PathCollections.ContainsKey( path ) || path.IsOwned ) + if( PathCollections.ContainsKey( path ) || path.IsOwned ) { PathCollections[ path ] = collection; } diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 01f1a2d1..ed2ce84c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -1,7 +1,9 @@ using System; using Dalamud.Hooking; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Mods; @@ -19,7 +21,7 @@ public unsafe partial class PathResolver private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) { - LoadMtrlTexHelper( mtrlResourceHandle ); + LoadMtrlHelper( mtrlResourceHandle ); var ret = LoadMtrlTexHook!.Original( mtrlResourceHandle ); _mtrlCollection = null; return ret; @@ -31,7 +33,7 @@ public unsafe partial class PathResolver private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) { - LoadMtrlShpkHelper( mtrlResourceHandle ); + LoadMtrlHelper( mtrlResourceHandle ); var ret = LoadMtrlShpkHook!.Original( mtrlResourceHandle ); _mtrlCollection = null; return ret; @@ -39,7 +41,7 @@ public unsafe partial class PathResolver private ModCollection? _mtrlCollection; - private void LoadMtrlShpkHelper( IntPtr mtrlResourceHandle ) + private void LoadMtrlHelper( IntPtr mtrlResourceHandle ) { if( mtrlResourceHandle == IntPtr.Zero ) { @@ -51,27 +53,10 @@ public unsafe partial class PathResolver _mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; } - private void LoadMtrlTexHelper( IntPtr mtrlResourceHandle ) - { - if( mtrlResourceHandle == IntPtr.Zero ) - { - return; - } - - var mtrl = ( MtrlResource* )mtrlResourceHandle; - if( mtrl->NumTex == 0 ) - { - return; - } - - var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); - _mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null; - } - // Check specifically for shpk and tex files whether we are currently in a material load. - private bool HandleMaterialSubFiles( Utf8GamePath gamePath, out ModCollection? collection ) + private bool HandleMaterialSubFiles( ResourceType type, out ModCollection? collection ) { - if( _mtrlCollection != null && ( gamePath.Path.EndsWith( 't', 'e', 'x' ) || gamePath.Path.EndsWith( 's', 'h', 'p', 'k' ) ) ) + if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) { collection = _mtrlCollection; return true; @@ -81,16 +66,51 @@ public unsafe partial class PathResolver return false; } + // We need to set the correct collection for the actual material path that is loaded + // before actually loading the file. + private bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) + { + ret = 0; + if( fileDescriptor->ResourceHandle->FileType == ResourceType.Mtrl + && Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) ) + { + SetCollection( path, collection ); + } + + ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, isSync ); + PathCollections.TryRemove( path, out _ ); + return true; + } + + // Materials need to be set per collection so they can load their textures independently from each other. + private void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, + out (FullPath?, object?) data ) + { + if( nonDefault && type == ResourceType.Mtrl ) + { + var fullPath = new FullPath( $"|{collection.Name}|{path}" ); + SetCollection( fullPath.InternalName, collection ); + data = ( fullPath, collection ); + } + else + { + data = ( resolved, collection ); + } + } + private void EnableMtrlHooks() { LoadMtrlShpkHook?.Enable(); LoadMtrlTexHook?.Enable(); + Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler; } private void DisableMtrlHooks() { LoadMtrlShpkHook?.Disable(); LoadMtrlTexHook?.Disable(); + Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler; } private void DisposeMtrlHooks() diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index f79f25b6..e6d443ed 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -138,12 +138,6 @@ public unsafe partial class PathResolver } var gamePath = new Utf8String( ( byte* )path ); - if( collection == Penumbra.CollectionManager.DefaultCollection ) - { - SetCollection( gamePath, null ); - return path; - } - SetCollection( gamePath, collection ); return path; } diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index ae58a64e..c9543189 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,6 +1,8 @@ using System; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; namespace Penumbra.Interop.Resolver; @@ -14,9 +16,6 @@ public partial class PathResolver : IDisposable { private readonly ResourceLoader _loader; - // Keep track of the last path resolver to be able to restore it. - private Func< Utf8GamePath, (FullPath?, object?) > _oldResolver = null!; - public PathResolver( ResourceLoader loader ) { _loader = loader; @@ -28,22 +27,18 @@ public partial class PathResolver : IDisposable } // The modified resolver that handles game path resolving. - private (FullPath?, object?) CharacterResolver( Utf8GamePath gamePath ) + private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, object?) data ) { // Check if the path was marked for a specific collection, // or if it is a file loaded by a material, and if we are currently in a material load. // If not use the default collection. - var nonDefault = HandleMaterialSubFiles( gamePath, out var collection ) || PathCollections.TryGetValue( gamePath.Path, out collection ); + // We can remove paths after they have actually been loaded. + // A potential next request will add the path anew. + var nonDefault = HandleMaterialSubFiles( type, out var collection ) || PathCollections.TryRemove( gamePath.Path, out collection ); if( !nonDefault ) { collection = Penumbra.CollectionManager.DefaultCollection; } - else - { - // We can remove paths after they have actually been loaded. - // A potential next request will add the path anew. - PathCollections.Remove( gamePath.Path ); - } // Resolve using character/default collection first, otherwise forced, as usual. var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); @@ -53,12 +48,8 @@ public partial class PathResolver : IDisposable if( resolved == null ) { // We also need to handle defaulted materials against a non-default collection. - if( nonDefault && gamePath.Path.EndsWith( 'm', 't', 'r', 'l' ) ) - { - SetCollection( gamePath.Path, collection ); - } - - return ( null, collection ); + HandleMtrlCollection( collection, gamePath.Path.ToString(), nonDefault, type, resolved, out data ); + return true; } collection = Penumbra.CollectionManager.ForcedCollection; @@ -66,12 +57,8 @@ public partial class PathResolver : IDisposable // Since mtrl files load their files separately, we need to add the new, resolved path // so that the functions loading tex and shpk can find that path and use its collection. - if( nonDefault && resolved.Value.Extension == ".mtrl" ) - { - SetCollection( resolved.Value.InternalName, nonDefault ? collection : null ); - } - - return ( resolved, collection ); + HandleMtrlCollection( collection, resolved.Value.FullName, nonDefault, type, resolved, out data ); + return true; } public void Enable() @@ -84,8 +71,7 @@ public partial class PathResolver : IDisposable EnableDataHooks(); EnableMetaHooks(); - _oldResolver = _loader.ResolvePath; - _loader.ResolvePath = CharacterResolver; + _loader.ResolvePathCustomization += CharacterResolver; } public void Disable() @@ -96,7 +82,7 @@ public partial class PathResolver : IDisposable DisableDataHooks(); DisableMetaHooks(); - _loader.ResolvePath = _oldResolver; + _loader.ResolvePathCustomization -= CharacterResolver; } public void Dispose() diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 447fa361..88f38dca 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,6 +1,8 @@ using System; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Resolver; namespace Penumbra.Interop.Structs; @@ -45,7 +47,7 @@ public unsafe struct ResourceHandle public ResourceCategory Category; [FieldOffset( 0x0C )] - public uint FileType; + public ResourceType FileType; [FieldOffset( 0x10 )] public uint Id; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index abe86fe5..fadd9545 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -4,7 +4,9 @@ using System.Diagnostics; using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; +using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -19,15 +21,13 @@ public partial class MetaManager public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); public readonly Dictionary< ImcManipulation, Mod.Mod > Manipulations = new(); - private readonly ModCollection _collection; - private static int _imcManagerCount; - private static ResourceLoader.ResourceLoadCustomizationDelegate? _previousDelegate; + private readonly ModCollection _collection; + private static int _imcManagerCount; public MetaManagerImc( ModCollection collection ) { - _collection = collection; - _previousDelegate = Penumbra.ResourceLoader.ResourceLoadCustomization; + _collection = collection; SetupDelegate(); } @@ -105,7 +105,7 @@ public partial class MetaManager { if( _imcManagerCount++ == 0 ) { - Penumbra.ResourceLoader.ResourceLoadCustomization = ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoadCustomization += ImcLoadHandler; Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler; } } @@ -115,46 +115,42 @@ public partial class MetaManager { if( --_imcManagerCount == 0 ) { - if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcLoadHandler ) - { - Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate; - } - - Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; + Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler; + Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler; } } private FullPath CreateImcPath( Utf8GamePath path ) => new($"|{_collection.Name}|{path}"); - private static unsafe byte ImcLoadHandler( Utf8GamePath gamePath, ResourceManager* resourceManager, - SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager, + SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret ) { - var split = gamePath.Path.Split( ( byte )'|', 2, true ); - fileDescriptor->ResourceHandle->FileNameData = split[ 1 ].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length; + ret = 0; + if( fileDescriptor->ResourceHandle->FileType != ResourceType.Imc ) + { + return false; + } - var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); - if( Penumbra.CollectionManager.ByName( split[ 0 ].ToString(), out var collection ) + ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) && collection.Cache != null && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( - Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) + Utf8GamePath.FromSpan( path.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, + PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, collection.Name ); file.Replace( fileDescriptor->ResourceHandle ); file.ChangesSinceLoad = false; } - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; - fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; - return ret; + return true; } private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData ) { // Only check imcs. - if( resource->FileType != 0x00696D63 + if( resource->FileType != ResourceType.Imc || resolveData is not ModCollection collection || collection.Cache == null || !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file ) diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 7f6e5468..b99d9657 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -22,7 +22,7 @@ public delegate void CollectionChangeDelegate( ModCollection? oldCollection, Mod string? characterName = null ); // Contains all collections and respective functions, as well as the collection settings. -public class CollectionManager : IDisposable +public sealed class CollectionManager : IDisposable { private readonly ModManager _manager; @@ -40,10 +40,20 @@ public class CollectionManager : IDisposable => ByName( ModCollection.DefaultCollection )!; public ModCollection? ByName( string name ) - => Collections.Find( c => c.Name == name ); + => name.Length > 0 + ? Collections.Find( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ) ) + : ModCollection.Empty; public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - => Collections.FindFirst( c => c.Name == name, out collection ); + { + if( name.Length > 0 ) + { + return Collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); + } + + collection = ModCollection.Empty; + return true; + } // Is invoked after the collections actually changed. public event CollectionChangeDelegate? CollectionChanged; diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index ba58e522..a23a9a32 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -21,14 +22,23 @@ public delegate void ModChangeDelegate( ModChangeType type, int modIndex, ModDat // The ModManager handles the basic mods installed to the mod directory. // It also contains the CollectionManager that handles all collections. -public class ModManager +public class ModManager : IEnumerable< ModData > { public DirectoryInfo BasePath { get; private set; } = null!; - private List< ModData > ModsInternal { get; init; } = new(); + private readonly List< ModData > _mods = new(); + + public ModData this[ int idx ] + => _mods[ idx ]; public IReadOnlyList< ModData > Mods - => ModsInternal; + => _mods; + + public IEnumerator< ModData > GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); public ModFolder StructuredMods { get; } = ModFileSystem.Root; @@ -37,6 +47,9 @@ public class ModManager public bool Valid { get; private set; } + public int Count + => _mods.Count; + public Configuration Config => Penumbra.Config; @@ -116,7 +129,7 @@ public class ModManager foreach( var (folder, path) in Config.ModSortOrder.ToArray() ) { - if( path.Length > 0 && ModsInternal.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) + if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) { changes |= SetSortOrderPath( mod, path ); } @@ -135,7 +148,7 @@ public class ModManager public void DiscoverMods() { - ModsInternal.Clear(); + _mods.Clear(); BasePath.Refresh(); StructuredMods.SubFolders.Clear(); @@ -150,7 +163,7 @@ public class ModManager continue; } - ModsInternal.Add( mod ); + _mods.Add( mod ); } SetModStructure(); @@ -173,12 +186,12 @@ public class ModManager } } - var idx = ModsInternal.FindIndex( m => m.BasePath.Name == modFolder.Name ); + var idx = _mods.FindIndex( m => m.BasePath.Name == modFolder.Name ); if( idx >= 0 ) { - var mod = ModsInternal[ idx ]; + var mod = _mods[ idx ]; mod.SortOrder.ParentFolder.RemoveMod( mod ); - ModsInternal.RemoveAt( idx ); + _mods.RemoveAt( idx ); ModChange?.Invoke( ModChangeType.Removed, idx, mod ); } } @@ -199,15 +212,15 @@ public class ModManager } } - if( ModsInternal.Any( m => m.BasePath.Name == modFolder.Name ) ) + if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) { return -1; } - ModsInternal.Add( mod ); - ModChange?.Invoke( ModChangeType.Added, ModsInternal.Count - 1, mod ); + _mods.Add( mod ); + ModChange?.Invoke( ModChangeType.Added, _mods.Count - 1, mod ); - return ModsInternal.Count - 1; + return _mods.Count - 1; } public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index bdea9ecd..8517d5c5 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -109,6 +109,7 @@ public class Penumbra : IDalamudPlugin { ResourceLoader.EnableFullLogging(); } + ResidentResources.Reload(); } public bool Enable() diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index af8f613f..f8f8d9a4 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -87,10 +87,13 @@ public partial class SettingsInterface if( ImGui.IsItemClicked() ) { - var data = Interop.Structs.ResourceHandle.GetData( ( Interop.Structs.ResourceHandle* )r ); - var length = ( int )Interop.Structs.ResourceHandle.GetLength( ( Interop.Structs.ResourceHandle* )r ); - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + var data = Interop.Structs.ResourceHandle.GetData( ( Interop.Structs.ResourceHandle* )r ); + if( data != null ) + { + var length = ( int )Interop.Structs.ResourceHandle.GetLength( ( Interop.Structs.ResourceHandle* )r ); + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + } //ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) ); } diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index acae0203..a5a8bb82 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -23,7 +23,7 @@ public static class ArrayExtensions { for( var i = 0; i < array.Count; ++i ) { - if( needle!.Equals( array[i] ) ) + if( needle!.Equals( array[ i ] ) ) { return i; } @@ -61,4 +61,35 @@ public static class ArrayExtensions result = default; return false; } + + public static bool Move< T >( this IList< T > list, int idx1, int idx2 ) + { + idx1 = Math.Clamp( idx1, 0, list.Count - 1 ); + idx2 = Math.Clamp( idx2, 0, list.Count - 1 ); + if( idx1 == idx2 ) + { + return false; + } + + var tmp = list[ idx1 ]; + // move element down and shift other elements up + if( idx1 < idx2 ) + { + for( var i = idx1; i < idx2; i++ ) + { + list[ i ] = list[ i + 1 ]; + } + } + // move element up and shift other elements down + else + { + for( var i = idx1; i > idx2; i-- ) + { + list[ i ] = list[ i - 1 ]; + } + } + + list[ idx2 ] = tmp; + return true; + } } \ No newline at end of file