mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Change resolving to possibly work correctly for all materials and load specific materials for each collection.
This commit is contained in:
parent
b6ed27e235
commit
1e5776a481
16 changed files with 408 additions and 172 deletions
|
|
@ -130,10 +130,13 @@ public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< U
|
||||||
=> Path.Dispose();
|
=> Path.Dispose();
|
||||||
|
|
||||||
public bool IsRooted()
|
public bool IsRooted()
|
||||||
=> Path.Length >= 1 && ( Path[ 0 ] == '/' || Path[ 0 ] == '\\' )
|
=> IsRooted( Path );
|
||||||
|| Path.Length >= 2
|
|
||||||
&& ( Path[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' )
|
public static bool IsRooted( Utf8String path )
|
||||||
&& Path[ 1 ] == ':';
|
=> 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
|
public class Utf8GamePathConverter : JsonConverter
|
||||||
{
|
{
|
||||||
|
|
|
||||||
110
Penumbra.GameData/Enums/ResourceType.cs
Normal file
110
Penumbra.GameData/Enums/ResourceType.cs
Normal file
|
|
@ -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 ] ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,8 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
using FFXIVClientStructs.STD;
|
using FFXIVClientStructs.STD;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.Interop.Resolver;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Loader;
|
namespace Penumbra.Interop.Loader;
|
||||||
|
|
||||||
|
|
@ -25,7 +27,7 @@ public unsafe partial class ResourceLoader
|
||||||
public FullPath ManipulatedPath;
|
public FullPath ManipulatedPath;
|
||||||
public ResourceCategory Category;
|
public ResourceCategory Category;
|
||||||
public object? ResolverInfo;
|
public object? ResolverInfo;
|
||||||
public uint Extension;
|
public ResourceType Extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly SortedDictionary< FullPath, DebugData > _debugList = new();
|
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
|
// 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 manager = *ResourceManager;
|
||||||
var catIdx = ( uint )cat >> 0x18;
|
var catIdx = ( uint )cat >> 0x18;
|
||||||
cat = ( ResourceCategory )( ushort )cat;
|
cat = ( ResourceCategory )( ushort )cat;
|
||||||
var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat;
|
var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat;
|
||||||
var extMap = FindInMap( ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )category->CategoryMaps[ catIdx ],
|
var extMap = FindInMap( ( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* )category->CategoryMaps[ catIdx ],
|
||||||
ext );
|
( uint )ext );
|
||||||
if( extMap == null )
|
if( extMap == null )
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using FileMode = Penumbra.Interop.Structs.FileMode;
|
using FileMode = Penumbra.Interop.Structs.FileMode;
|
||||||
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
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.
|
// 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.
|
// 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,
|
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" )]
|
[Signature( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00", DetourName = "GetResourceSyncDetour" )]
|
||||||
public Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!;
|
public Hook< GetResourceSyncPrototype > GetResourceSyncHook = null!;
|
||||||
|
|
||||||
public delegate ResourceHandle* GetResourceAsyncPrototype( ResourceManager* resourceManager, ResourceCategory* pCategoryId,
|
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" )]
|
[Signature( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00", DetourName = "GetResourceAsyncDetour" )]
|
||||||
public Hook< GetResourceAsyncPrototype > GetResourceAsyncHook = null!;
|
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 )
|
int* resourceHash, byte* path, void* unk )
|
||||||
=> GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, unk, false );
|
=> 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 )
|
int* resourceHash, byte* path, void* unk, bool isUnk )
|
||||||
=> GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
=> GetResourceHandler( false, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
||||||
|
|
||||||
private ResourceHandle* CallOriginalHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId,
|
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
|
=> isSync
|
||||||
? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk )
|
? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk )
|
||||||
: GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
: 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 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 )
|
int* resourceHash, byte* path, void* unk, bool isUnk )
|
||||||
{
|
{
|
||||||
if( !Utf8GamePath.FromPointer( path, out var gamePath ) )
|
if( !Utf8GamePath.FromPointer( path, out var gamePath ) )
|
||||||
|
|
@ -67,7 +70,7 @@ public unsafe partial class ResourceLoader
|
||||||
ResourceRequested?.Invoke( gamePath, isSync );
|
ResourceRequested?.Invoke( gamePath, isSync );
|
||||||
|
|
||||||
// If no replacements are being made, we still want to be able to trigger the event.
|
// 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 );
|
PathResolved?.Invoke( gamePath, resolvedPath, data );
|
||||||
if( resolvedPath == null )
|
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.
|
// 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,
|
public delegate byte ReadFileDelegate( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority,
|
||||||
bool isSync );
|
bool isSync );
|
||||||
|
|
@ -111,23 +145,51 @@ public unsafe partial class ResourceLoader
|
||||||
return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
||||||
}
|
}
|
||||||
|
|
||||||
var valid = Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false );
|
if( !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( valid && ResourceLoadCustomization != null && gamePath.Path[ 0 ] == ( byte )'|' )
|
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 )'|' )
|
||||||
{
|
{
|
||||||
ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync );
|
return DefaultLoadResource( gamePath.Path, resourceManager, fileDescriptor, priority, isSync );
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// 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 = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
ret = DefaultLoadResource( split[ 2 ], resourceManager, fileDescriptor, priority, isSync );
|
||||||
FileLoaded?.Invoke( gamePath.Path, ret != 0, false );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// 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 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.
|
// 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,
|
// We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations,
|
||||||
|
|
@ -147,26 +209,17 @@ public unsafe partial class ResourceLoader
|
||||||
fdPtr[ gamePath.Length ] = '\0';
|
fdPtr[ gamePath.Length ] = '\0';
|
||||||
|
|
||||||
// Use the SE ReadFile function.
|
// Use the SE ReadFile function.
|
||||||
ret = ReadFile( resourceManager, fileDescriptor, priority, isSync );
|
var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync );
|
||||||
FileLoaded?.Invoke( gamePath.Path, ret != 0, true );
|
FileLoaded?.Invoke( gamePath, ret != 0, true );
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customize file loading for any GamePaths that start with "|".
|
// Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack.
|
||||||
public delegate byte ResourceLoadCustomizationDelegate( Utf8GamePath gamePath, ResourceManager* resourceManager,
|
internal byte DefaultLoadResource( Utf8String gamePath, ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority,
|
||||||
SeFileDescriptor* fileDescriptor, int priority, bool isSync );
|
bool isSync )
|
||||||
|
=> Utf8GamePath.IsRooted( gamePath )
|
||||||
public ResourceLoadCustomizationDelegate? ResourceLoadCustomization;
|
? DefaultRootedResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync )
|
||||||
|
: DefaultResourceLoad( gamePath, resourceManager, fileDescriptor, priority, isSync );
|
||||||
|
|
||||||
// Use the default method of path replacement.
|
|
||||||
public static (FullPath?, object?) DefaultReplacer( Utf8GamePath path )
|
|
||||||
{
|
|
||||||
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path );
|
|
||||||
return ( resolved, null );
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisposeHooks()
|
private void DisposeHooks()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.Interop.Structs;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Loader;
|
namespace Penumbra.Interop.Loader;
|
||||||
|
|
||||||
|
|
@ -104,7 +107,7 @@ public unsafe partial class ResourceLoader : IDisposable
|
||||||
// Event fired whenever a resource is returned.
|
// Event fired whenever a resource is returned.
|
||||||
// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource.
|
// 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.
|
// 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 );
|
object? resolveData );
|
||||||
|
|
||||||
public event ResourceLoadedDelegate? ResourceLoaded;
|
public event ResourceLoadedDelegate? ResourceLoaded;
|
||||||
|
|
@ -117,7 +120,19 @@ public unsafe partial class ResourceLoader : IDisposable
|
||||||
public event FileLoadedDelegate? FileLoaded;
|
public event FileLoadedDelegate? FileLoaded;
|
||||||
|
|
||||||
// Customization point to control how path resolving is handled.
|
// 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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
|
|
@ -92,7 +93,7 @@ public unsafe partial class PathResolver
|
||||||
internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new();
|
internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new();
|
||||||
|
|
||||||
// This map links files to their corresponding collection, if it is non-default.
|
// 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;
|
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.
|
// 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 )
|
if( PathCollections.ContainsKey( path ) || path.IsOwned )
|
||||||
{
|
|
||||||
PathCollections.Remove( path );
|
|
||||||
}
|
|
||||||
else if( PathCollections.ContainsKey( path ) || path.IsOwned )
|
|
||||||
{
|
{
|
||||||
PathCollections[ path ] = collection;
|
PathCollections[ path ] = collection;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
|
||||||
|
|
@ -19,7 +21,7 @@ public unsafe partial class PathResolver
|
||||||
|
|
||||||
private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle )
|
private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle )
|
||||||
{
|
{
|
||||||
LoadMtrlTexHelper( mtrlResourceHandle );
|
LoadMtrlHelper( mtrlResourceHandle );
|
||||||
var ret = LoadMtrlTexHook!.Original( mtrlResourceHandle );
|
var ret = LoadMtrlTexHook!.Original( mtrlResourceHandle );
|
||||||
_mtrlCollection = null;
|
_mtrlCollection = null;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -31,7 +33,7 @@ public unsafe partial class PathResolver
|
||||||
|
|
||||||
private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle )
|
private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle )
|
||||||
{
|
{
|
||||||
LoadMtrlShpkHelper( mtrlResourceHandle );
|
LoadMtrlHelper( mtrlResourceHandle );
|
||||||
var ret = LoadMtrlShpkHook!.Original( mtrlResourceHandle );
|
var ret = LoadMtrlShpkHook!.Original( mtrlResourceHandle );
|
||||||
_mtrlCollection = null;
|
_mtrlCollection = null;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -39,7 +41,7 @@ public unsafe partial class PathResolver
|
||||||
|
|
||||||
private ModCollection? _mtrlCollection;
|
private ModCollection? _mtrlCollection;
|
||||||
|
|
||||||
private void LoadMtrlShpkHelper( IntPtr mtrlResourceHandle )
|
private void LoadMtrlHelper( IntPtr mtrlResourceHandle )
|
||||||
{
|
{
|
||||||
if( mtrlResourceHandle == IntPtr.Zero )
|
if( mtrlResourceHandle == IntPtr.Zero )
|
||||||
{
|
{
|
||||||
|
|
@ -51,27 +53,10 @@ public unsafe partial class PathResolver
|
||||||
_mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null;
|
_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.
|
// 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;
|
collection = _mtrlCollection;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -81,16 +66,51 @@ public unsafe partial class PathResolver
|
||||||
return false;
|
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()
|
private void EnableMtrlHooks()
|
||||||
{
|
{
|
||||||
LoadMtrlShpkHook?.Enable();
|
LoadMtrlShpkHook?.Enable();
|
||||||
LoadMtrlTexHook?.Enable();
|
LoadMtrlTexHook?.Enable();
|
||||||
|
Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisableMtrlHooks()
|
private void DisableMtrlHooks()
|
||||||
{
|
{
|
||||||
LoadMtrlShpkHook?.Disable();
|
LoadMtrlShpkHook?.Disable();
|
||||||
LoadMtrlTexHook?.Disable();
|
LoadMtrlTexHook?.Disable();
|
||||||
|
Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeMtrlHooks()
|
private void DisposeMtrlHooks()
|
||||||
|
|
|
||||||
|
|
@ -138,12 +138,6 @@ public unsafe partial class PathResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
var gamePath = new Utf8String( ( byte* )path );
|
var gamePath = new Utf8String( ( byte* )path );
|
||||||
if( collection == Penumbra.CollectionManager.DefaultCollection )
|
|
||||||
{
|
|
||||||
SetCollection( gamePath, null );
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetCollection( gamePath, collection );
|
SetCollection( gamePath, collection );
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Interop.Loader;
|
using Penumbra.Interop.Loader;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Resolver;
|
namespace Penumbra.Interop.Resolver;
|
||||||
|
|
@ -14,9 +16,6 @@ public partial class PathResolver : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ResourceLoader _loader;
|
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 )
|
public PathResolver( ResourceLoader loader )
|
||||||
{
|
{
|
||||||
_loader = loader;
|
_loader = loader;
|
||||||
|
|
@ -28,22 +27,18 @@ public partial class PathResolver : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// The modified resolver that handles game path resolving.
|
// 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,
|
// 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.
|
// 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.
|
// 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 )
|
if( !nonDefault )
|
||||||
{
|
{
|
||||||
collection = Penumbra.CollectionManager.DefaultCollection;
|
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.
|
// Resolve using character/default collection first, otherwise forced, as usual.
|
||||||
var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath );
|
var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath );
|
||||||
|
|
@ -53,12 +48,8 @@ public partial class PathResolver : IDisposable
|
||||||
if( resolved == null )
|
if( resolved == null )
|
||||||
{
|
{
|
||||||
// We also need to handle defaulted materials against a non-default collection.
|
// We also need to handle defaulted materials against a non-default collection.
|
||||||
if( nonDefault && gamePath.Path.EndsWith( 'm', 't', 'r', 'l' ) )
|
HandleMtrlCollection( collection, gamePath.Path.ToString(), nonDefault, type, resolved, out data );
|
||||||
{
|
return true;
|
||||||
SetCollection( gamePath.Path, collection );
|
|
||||||
}
|
|
||||||
|
|
||||||
return ( null, collection );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
collection = Penumbra.CollectionManager.ForcedCollection;
|
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
|
// 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.
|
// so that the functions loading tex and shpk can find that path and use its collection.
|
||||||
if( nonDefault && resolved.Value.Extension == ".mtrl" )
|
HandleMtrlCollection( collection, resolved.Value.FullName, nonDefault, type, resolved, out data );
|
||||||
{
|
return true;
|
||||||
SetCollection( resolved.Value.InternalName, nonDefault ? collection : null );
|
|
||||||
}
|
|
||||||
|
|
||||||
return ( resolved, collection );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Enable()
|
public void Enable()
|
||||||
|
|
@ -84,8 +71,7 @@ public partial class PathResolver : IDisposable
|
||||||
EnableDataHooks();
|
EnableDataHooks();
|
||||||
EnableMetaHooks();
|
EnableMetaHooks();
|
||||||
|
|
||||||
_oldResolver = _loader.ResolvePath;
|
_loader.ResolvePathCustomization += CharacterResolver;
|
||||||
_loader.ResolvePath = CharacterResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Disable()
|
public void Disable()
|
||||||
|
|
@ -96,7 +82,7 @@ public partial class PathResolver : IDisposable
|
||||||
DisableDataHooks();
|
DisableDataHooks();
|
||||||
DisableMetaHooks();
|
DisableMetaHooks();
|
||||||
|
|
||||||
_loader.ResolvePath = _oldResolver;
|
_loader.ResolvePathCustomization -= CharacterResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.Interop.Resolver;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Structs;
|
namespace Penumbra.Interop.Structs;
|
||||||
|
|
||||||
|
|
@ -45,7 +47,7 @@ public unsafe struct ResourceHandle
|
||||||
public ResourceCategory Category;
|
public ResourceCategory Category;
|
||||||
|
|
||||||
[FieldOffset( 0x0C )]
|
[FieldOffset( 0x0C )]
|
||||||
public uint FileType;
|
public ResourceType FileType;
|
||||||
|
|
||||||
[FieldOffset( 0x10 )]
|
[FieldOffset( 0x10 )]
|
||||||
public uint Id;
|
public uint Id;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ using System.Diagnostics;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Interop.Loader;
|
using Penumbra.Interop.Loader;
|
||||||
|
using Penumbra.Interop.Resolver;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.Meta.Files;
|
using Penumbra.Meta.Files;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
@ -21,13 +23,11 @@ public partial class MetaManager
|
||||||
|
|
||||||
private readonly ModCollection _collection;
|
private readonly ModCollection _collection;
|
||||||
private static int _imcManagerCount;
|
private static int _imcManagerCount;
|
||||||
private static ResourceLoader.ResourceLoadCustomizationDelegate? _previousDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public MetaManagerImc( ModCollection collection )
|
public MetaManagerImc( ModCollection collection )
|
||||||
{
|
{
|
||||||
_collection = collection;
|
_collection = collection;
|
||||||
_previousDelegate = Penumbra.ResourceLoader.ResourceLoadCustomization;
|
|
||||||
SetupDelegate();
|
SetupDelegate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@ public partial class MetaManager
|
||||||
{
|
{
|
||||||
if( _imcManagerCount++ == 0 )
|
if( _imcManagerCount++ == 0 )
|
||||||
{
|
{
|
||||||
Penumbra.ResourceLoader.ResourceLoadCustomization = ImcLoadHandler;
|
Penumbra.ResourceLoader.ResourceLoadCustomization += ImcLoadHandler;
|
||||||
Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler;
|
Penumbra.ResourceLoader.ResourceLoaded += ImcResourceHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,11 +115,7 @@ public partial class MetaManager
|
||||||
{
|
{
|
||||||
if( --_imcManagerCount == 0 )
|
if( --_imcManagerCount == 0 )
|
||||||
{
|
{
|
||||||
if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcLoadHandler )
|
Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler;
|
||||||
{
|
|
||||||
Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler;
|
Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,34 +123,34 @@ public partial class MetaManager
|
||||||
private FullPath CreateImcPath( Utf8GamePath path )
|
private FullPath CreateImcPath( Utf8GamePath path )
|
||||||
=> new($"|{_collection.Name}|{path}");
|
=> new($"|{_collection.Name}|{path}");
|
||||||
|
|
||||||
private static unsafe byte ImcLoadHandler( Utf8GamePath gamePath, ResourceManager* resourceManager,
|
private static unsafe bool ImcLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager,
|
||||||
SeFileDescriptor* fileDescriptor, int priority, bool isSync )
|
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret )
|
||||||
{
|
{
|
||||||
var split = gamePath.Path.Split( ( byte )'|', 2, true );
|
ret = 0;
|
||||||
fileDescriptor->ResourceHandle->FileNameData = split[ 1 ].Path;
|
if( fileDescriptor->ResourceHandle->FileType != ResourceType.Imc )
|
||||||
fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length;
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
||||||
if( Penumbra.CollectionManager.ByName( split[ 0 ].ToString(), out var collection )
|
if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection )
|
||||||
&& collection.Cache != null
|
&& collection.Cache != null
|
||||||
&& collection.Cache.MetaManipulations.Imc.Files.TryGetValue(
|
&& 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 );
|
collection.Name );
|
||||||
file.Replace( fileDescriptor->ResourceHandle );
|
file.Replace( fileDescriptor->ResourceHandle );
|
||||||
file.ChangesSinceLoad = false;
|
file.ChangesSinceLoad = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path;
|
return true;
|
||||||
fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length;
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData )
|
private static unsafe void ImcResourceHandler( ResourceHandle* resource, Utf8GamePath gamePath, FullPath? _2, object? resolveData )
|
||||||
{
|
{
|
||||||
// Only check imcs.
|
// Only check imcs.
|
||||||
if( resource->FileType != 0x00696D63
|
if( resource->FileType != ResourceType.Imc
|
||||||
|| resolveData is not ModCollection collection
|
|| resolveData is not ModCollection collection
|
||||||
|| collection.Cache == null
|
|| collection.Cache == null
|
||||||
|| !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file )
|
|| !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file )
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ public delegate void CollectionChangeDelegate( ModCollection? oldCollection, Mod
|
||||||
string? characterName = null );
|
string? characterName = null );
|
||||||
|
|
||||||
// Contains all collections and respective functions, as well as the collection settings.
|
// 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;
|
private readonly ModManager _manager;
|
||||||
|
|
||||||
|
|
@ -40,10 +40,20 @@ public class CollectionManager : IDisposable
|
||||||
=> ByName( ModCollection.DefaultCollection )!;
|
=> ByName( ModCollection.DefaultCollection )!;
|
||||||
|
|
||||||
public ModCollection? ByName( string name )
|
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 )
|
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.
|
// Is invoked after the collections actually changed.
|
||||||
public event CollectionChangeDelegate? CollectionChanged;
|
public event CollectionChangeDelegate? CollectionChanged;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
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.
|
// The ModManager handles the basic mods installed to the mod directory.
|
||||||
// It also contains the CollectionManager that handles all collections.
|
// It also contains the CollectionManager that handles all collections.
|
||||||
public class ModManager
|
public class ModManager : IEnumerable< ModData >
|
||||||
{
|
{
|
||||||
public DirectoryInfo BasePath { get; private set; } = null!;
|
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
|
public IReadOnlyList< ModData > Mods
|
||||||
=> ModsInternal;
|
=> _mods;
|
||||||
|
|
||||||
|
public IEnumerator< ModData > GetEnumerator()
|
||||||
|
=> _mods.GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> GetEnumerator();
|
||||||
|
|
||||||
public ModFolder StructuredMods { get; } = ModFileSystem.Root;
|
public ModFolder StructuredMods { get; } = ModFileSystem.Root;
|
||||||
|
|
||||||
|
|
@ -37,6 +47,9 @@ public class ModManager
|
||||||
|
|
||||||
public bool Valid { get; private set; }
|
public bool Valid { get; private set; }
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
=> _mods.Count;
|
||||||
|
|
||||||
public Configuration Config
|
public Configuration Config
|
||||||
=> Penumbra.Config;
|
=> Penumbra.Config;
|
||||||
|
|
||||||
|
|
@ -116,7 +129,7 @@ public class ModManager
|
||||||
|
|
||||||
foreach( var (folder, path) in Config.ModSortOrder.ToArray() )
|
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 );
|
changes |= SetSortOrderPath( mod, path );
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +148,7 @@ public class ModManager
|
||||||
|
|
||||||
public void DiscoverMods()
|
public void DiscoverMods()
|
||||||
{
|
{
|
||||||
ModsInternal.Clear();
|
_mods.Clear();
|
||||||
BasePath.Refresh();
|
BasePath.Refresh();
|
||||||
|
|
||||||
StructuredMods.SubFolders.Clear();
|
StructuredMods.SubFolders.Clear();
|
||||||
|
|
@ -150,7 +163,7 @@ public class ModManager
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModsInternal.Add( mod );
|
_mods.Add( mod );
|
||||||
}
|
}
|
||||||
|
|
||||||
SetModStructure();
|
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 )
|
if( idx >= 0 )
|
||||||
{
|
{
|
||||||
var mod = ModsInternal[ idx ];
|
var mod = _mods[ idx ];
|
||||||
mod.SortOrder.ParentFolder.RemoveMod( mod );
|
mod.SortOrder.ParentFolder.RemoveMod( mod );
|
||||||
ModsInternal.RemoveAt( idx );
|
_mods.RemoveAt( idx );
|
||||||
ModChange?.Invoke( ModChangeType.Removed, idx, mod );
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModsInternal.Add( mod );
|
_mods.Add( mod );
|
||||||
ModChange?.Invoke( ModChangeType.Added, ModsInternal.Count - 1, 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 )
|
public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false )
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
{
|
{
|
||||||
ResourceLoader.EnableFullLogging();
|
ResourceLoader.EnableFullLogging();
|
||||||
}
|
}
|
||||||
|
ResidentResources.Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Enable()
|
public bool Enable()
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,12 @@ public partial class SettingsInterface
|
||||||
if( ImGui.IsItemClicked() )
|
if( ImGui.IsItemClicked() )
|
||||||
{
|
{
|
||||||
var data = Interop.Structs.ResourceHandle.GetData( ( Interop.Structs.ResourceHandle* )r );
|
var data = Interop.Structs.ResourceHandle.GetData( ( Interop.Structs.ResourceHandle* )r );
|
||||||
|
if( data != null )
|
||||||
|
{
|
||||||
var length = ( int )Interop.Structs.ResourceHandle.GetLength( ( Interop.Structs.ResourceHandle* )r );
|
var length = ( int )Interop.Structs.ResourceHandle.GetLength( ( Interop.Structs.ResourceHandle* )r );
|
||||||
ImGui.SetClipboardText( string.Join( " ",
|
ImGui.SetClipboardText( string.Join( " ",
|
||||||
new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) );
|
new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) );
|
||||||
|
}
|
||||||
//ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) );
|
//ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,4 +61,35 @@ public static class ArrayExtensions
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue