Change resolving to possibly work correctly for all materials and load specific materials for each collection.

This commit is contained in:
Ottermandias 2022-03-24 22:01:39 +01:00
parent b6ed27e235
commit 1e5776a481
16 changed files with 408 additions and 172 deletions

View file

@ -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
{

View 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 ] ),
};
}
}

View file

@ -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;

View file

@ -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,23 +145,51 @@ 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 )'|' )
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 );
FileLoaded?.Invoke( gamePath.Path, ret != 0, false );
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;
}
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.
// 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';
// Use the SE ReadFile function.
ret = ReadFile( resourceManager, fileDescriptor, priority, isSync );
FileLoaded?.Invoke( gamePath.Path, ret != 0, true );
}
var ret = ReadFile( resourceManager, fileDescriptor, priority, isSync );
FileLoaded?.Invoke( gamePath, ret != 0, true );
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 )
{
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path );
return ( resolved, null );
}
// 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()
{

View file

@ -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()
{

View file

@ -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;
}

View file

@ -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()

View file

@ -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;
}

View file

@ -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()

View file

@ -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;

View file

@ -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;
@ -21,13 +23,11 @@ public partial class MetaManager
private readonly ModCollection _collection;
private static int _imcManagerCount;
private static ResourceLoader.ResourceLoadCustomizationDelegate? _previousDelegate;
public MetaManagerImc( ModCollection collection )
{
_collection = collection;
_previousDelegate = Penumbra.ResourceLoader.ResourceLoadCustomization;
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,11 +115,7 @@ public partial class MetaManager
{
if( --_imcManagerCount == 0 )
{
if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcLoadHandler )
{
Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate;
}
Penumbra.ResourceLoader.ResourceLoadCustomization -= ImcLoadHandler;
Penumbra.ResourceLoader.ResourceLoaded -= ImcResourceHandler;
}
}
@ -127,34 +123,34 @@ public partial class MetaManager
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 )

View file

@ -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;

View file

@ -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 )

View file

@ -109,6 +109,7 @@ public class Penumbra : IDalamudPlugin
{
ResourceLoader.EnableFullLogging();
}
ResidentResources.Reload();
}
public bool Enable()

View file

@ -88,9 +88,12 @@ public partial class SettingsInterface
if( ImGui.IsItemClicked() )
{
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 ) );
}

View file

@ -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;
}
}