mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +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();
|
||||
|
||||
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
|
||||
{
|
||||
|
|
|
|||
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.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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ public class Penumbra : IDalamudPlugin
|
|||
{
|
||||
ResourceLoader.EnableFullLogging();
|
||||
}
|
||||
ResidentResources.Reload();
|
||||
}
|
||||
|
||||
public bool Enable()
|
||||
|
|
|
|||
|
|
@ -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 ) );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue