diff --git a/Penumbra.GameData/Util/Functions.cs b/Penumbra.GameData/Util/Functions.cs new file mode 100644 index 00000000..91dc7799 --- /dev/null +++ b/Penumbra.GameData/Util/Functions.cs @@ -0,0 +1,63 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.GameData.Util; + +public static class Functions +{ + public static ulong ComputeCrc64( string name ) + { + if( name.Length == 0 ) + { + return 0; + } + + var lastSlash = name.LastIndexOf( '/' ); + if( lastSlash == -1 ) + { + return Lumina.Misc.Crc32.Get( name ); + } + + var folder = name[ ..lastSlash ]; + var file = name[ ( lastSlash + 1 ).. ]; + return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); + } + + public static ulong ComputeCrc64( ReadOnlySpan< byte > name ) + { + if( name.Length == 0 ) + { + return 0; + } + + var lastSlash = name.LastIndexOf( ( byte )'/' ); + if( lastSlash == -1 ) + { + return Lumina.Misc.Crc32.Get( name ); + } + + var folder = name[ ..lastSlash ]; + var file = name[ ( lastSlash + 1 ).. ]; + return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); + } + + [DllImport( "msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe IntPtr memcpy( byte* dest, byte* src, int count ); + + public static unsafe void MemCpyUnchecked( byte* dest, byte* src, int count ) + => memcpy( dest, src, count ); + + + [DllImport( "msvcrt.dll", EntryPoint = "memcmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe int memcmp( byte* b1, byte* b2, int count ); + + public static unsafe int MemCmpUnchecked( byte* ptr1, byte* ptr2, int count ) + => memcmp( ptr1, ptr2, count ); + + + [DllImport( "msvcrt.dll", EntryPoint = "_memicmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe int memicmp( byte* b1, byte* b2, int count ); + + public static unsafe int MemCmpCaseInsensitiveUnchecked( byte* ptr1, byte* ptr2, int count ) + => memicmp( ptr1, ptr2, count ); +} \ No newline at end of file diff --git a/Penumbra.GameData/Util/GamePath.cs b/Penumbra.GameData/Util/GamePath.cs index 002740f1..4c670605 100644 --- a/Penumbra.GameData/Util/GamePath.cs +++ b/Penumbra.GameData/Util/GamePath.cs @@ -1,105 +1,620 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Penumbra.GameData.Util +namespace Penumbra.GameData.Util; + +public static unsafe class ByteStringFunctions { - public readonly struct GamePath : IComparable + public class NullTerminator { - public const int MaxGamePathLength = 256; + public readonly byte* NullBytePtr; - private readonly string _path; - - private GamePath( string path, bool _ ) - => _path = path; - - public GamePath( string? path ) + public NullTerminator() { - if( path != null && path.Length < MaxGamePathLength ) - { - _path = Lower( Trim( ReplaceSlash( path ) ) ); - } - else - { - _path = string.Empty; - } + NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 ); + *NullBytePtr = 0; } - public GamePath( FileInfo file, DirectoryInfo baseDir ) - => _path = CheckPre( file, baseDir ) ? Lower( Trim( ReplaceSlash( Substring( file, baseDir ) ) ) ) : ""; - - private static bool CheckPre( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxGamePathLength; - - private static string Substring( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.Substring( baseDir.FullName.Length ); - - private static string ReplaceSlash( string path ) - => path.Replace( '\\', '/' ); - - private static string Trim( string path ) - => path.TrimStart( '/' ); - - private static string Lower( string path ) - => path.ToLowerInvariant(); - - public static GamePath GenerateUnchecked( string path ) - => new( path, true ); - - public static GamePath GenerateUncheckedLower( string path ) - => new( Lower( path ), true ); - - public static implicit operator string( GamePath gamePath ) - => gamePath._path; - - public static explicit operator GamePath( string gamePath ) - => new( gamePath ); - - public bool Empty - => _path.Length == 0; - - public string Filename() - { - var idx = _path.LastIndexOf( "/", StringComparison.Ordinal ); - return idx == -1 ? _path : idx == _path.Length - 1 ? "" : _path.Substring( idx + 1 ); - } - - public int CompareTo( object? rhs ) - { - return rhs switch - { - string path => string.Compare( _path, path, StringComparison.InvariantCulture ), - GamePath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), - _ => -1, - }; - } - - public override string ToString() - => _path; + ~NullTerminator() + => Marshal.FreeHGlobal( ( IntPtr )NullBytePtr ); } - public class GamePathConverter : JsonConverter - { - public override bool CanConvert( Type objectType ) - => objectType == typeof( GamePath ); + private static readonly byte[] LowerCaseBytes = Enumerable.Range( 0, 256 ) + .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) + .ToArray(); - public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + public static byte* FromString( string s, out int length ) + { + length = Encoding.UTF8.GetByteCount( s ); + var path = ( byte* )Marshal.AllocHGlobal( length + 1 ); + fixed( char* ptr = s ) { - var token = JToken.Load( reader ); - return token.ToObject< GamePath >(); + Encoding.UTF8.GetBytes( ptr, length, path, length + 1 ); } - public override bool CanWrite - => true; + path[ length ] = 0; + return path; + } - public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + public static byte* CopyPath( byte* path, int length ) + { + var ret = ( byte* )Marshal.AllocHGlobal( length + 1 ); + Functions.MemCpyUnchecked( ret, path, length ); + ret[ length ] = 0; + return ret; + } + + public static int CheckLength( byte* path ) + { + var end = path + int.MaxValue; + for( var it = path; it < end; ++it ) { - if( value != null ) + if( *it == 0 ) { - var v = ( GamePath )value; - serializer.Serialize( writer, v.ToString() ); + return ( int )( it - path ); } } + + throw new ArgumentOutOfRangeException( "Null-terminated path too long" ); + } + + public static int Compare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength == rhsLength ) + { + return lhs == rhs ? 0 : Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); + } + + if( lhsLength < rhsLength ) + { + var cmp = Functions.MemCmpUnchecked( lhs, rhs, lhsLength ); + return cmp != 0 ? cmp : -1; + } + + var cmp2 = Functions.MemCmpUnchecked( lhs, rhs, rhsLength ); + return cmp2 != 0 ? cmp2 : 1; + } + + public static int Compare( byte* lhs, int lhsLength, byte* rhs ) + { + var end = lhs + lhsLength; + for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) + { + if( *rhs == 0 ) + { + return 1; + } + + var diff = *tmp - *rhs; + if( diff != 0 ) + { + return diff; + } + } + + return 0; + } + + public static int Compare( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) + { + var end = lhs + maxLength; + for( var tmp = lhs; tmp < end; ++tmp, ++rhs ) + { + if( *lhs == 0 ) + { + return *rhs == 0 ? 0 : -1; + } + + if( *rhs == 0 ) + { + return 1; + } + + var diff = *tmp - *rhs; + if( diff != 0 ) + { + return diff; + } + } + + return 0; + } + + + public static bool Equals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength != rhsLength ) + { + return false; + } + + if( lhs == rhs || lhsLength == 0 ) + { + return true; + } + + return Functions.MemCmpUnchecked( lhs, rhs, lhsLength ) == 0; + } + + private static bool Equal( byte* lhs, int lhsLength, byte* rhs ) + => Compare( lhs, lhsLength, rhs ) == 0; + + private static bool Equal( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) + => Compare( lhs, rhs, maxLength ) == 0; + + + public static int AsciiCaselessCompare( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength == rhsLength ) + { + return lhs == rhs ? 0 : Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); + } + + if( lhsLength < rhsLength ) + { + var cmp = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ); + return cmp != 0 ? cmp : -1; + } + + var cmp2 = Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, rhsLength ); + return cmp2 != 0 ? cmp2 : 1; + } + + public static bool AsciiCaselessEquals( byte* lhs, int lhsLength, byte* rhs, int rhsLength ) + { + if( lhsLength != rhsLength ) + { + return false; + } + + if( lhs == rhs || lhsLength == 0 ) + { + return true; + } + + return Functions.MemCmpCaseInsensitiveUnchecked( lhs, rhs, lhsLength ) == 0; + } + + public static void AsciiToLowerInPlace( byte* path, int length ) + { + for( var i = 0; i < length; ++i ) + { + path[ i ] = LowerCaseBytes[ path[ i ] ]; + } + } + + public static byte* AsciiToLower( byte* path, int length ) + { + var ptr = ( byte* )Marshal.AllocHGlobal( length + 1 ); + ptr[ length ] = 0; + for( var i = 0; i < length; ++i ) + { + ptr[ i ] = LowerCaseBytes[ path[ i ] ]; + } + + return ptr; + } + + public static bool IsLowerCase( byte* path, int length ) + { + for( var i = 0; i < length; ++i ) + { + if( path[ i ] != LowerCaseBytes[ path[ i ] ] ) + { + return false; + } + } + + return true; + } +} + +public unsafe class AsciiString : IEnumerable< byte >, IEquatable< AsciiString >, IComparable< AsciiString > +{ + private static readonly ByteStringFunctions.NullTerminator Null = new(); + + [Flags] + private enum Flags : byte + { + IsOwned = 0x01, + IsNullTerminated = 0x02, + LowerCaseChecked = 0x04, + IsLowerCase = 0x08, + } + + public readonly IntPtr Path; + public readonly ulong Crc64; + public readonly int Length; + private Flags _flags; + + public bool IsNullTerminated + { + get => _flags.HasFlag( Flags.IsNullTerminated ); + init => _flags = value ? _flags | Flags.IsNullTerminated : _flags & ~ Flags.IsNullTerminated; + } + + public bool IsOwned + { + get => _flags.HasFlag( Flags.IsOwned ); + init => _flags = value ? _flags | Flags.IsOwned : _flags & ~Flags.IsOwned; + } + + public bool IsLowerCase + { + get + { + if( _flags.HasFlag( Flags.LowerCaseChecked ) ) + { + return _flags.HasFlag( Flags.IsLowerCase ); + } + + _flags |= Flags.LowerCaseChecked; + var ret = ByteStringFunctions.IsLowerCase( Ptr, Length ); + if( ret ) + { + _flags |= Flags.IsLowerCase; + } + + return ret; + } + } + + public bool IsEmpty + => Length == 0; + + public AsciiString() + { + Path = ( IntPtr )Null.NullBytePtr; + Length = 0; + IsNullTerminated = true; + IsOwned = false; + _flags |= Flags.LowerCaseChecked | Flags.IsLowerCase; + Crc64 = 0; + } + + public static bool FromString( string? path, out AsciiString ret, bool toLower = false ) + { + if( string.IsNullOrEmpty( path ) ) + { + ret = Empty; + return true; + } + + var p = ByteStringFunctions.FromString( path, out var l ); + if( l != path.Length ) + { + ret = Empty; + return false; + } + + if( toLower ) + { + ByteStringFunctions.AsciiToLowerInPlace( p, l ); + } + + ret = new AsciiString( p, l, true, true, toLower ? true : null ); + return true; + } + + public static AsciiString FromStringUnchecked( string? path, bool? isLower ) + { + if( string.IsNullOrEmpty( path ) ) + { + return Empty; + } + + var p = ByteStringFunctions.FromString( path, out var l ); + return new AsciiString( p, l, true, true, isLower ); + } + + public AsciiString( byte* path ) + : this( path, ByteStringFunctions.CheckLength( path ), true, false ) + { } + + protected AsciiString( byte* path, int length, bool isNullTerminated, bool isOwned, bool? isLower = null ) + { + Length = length; + Path = ( IntPtr )path; + IsNullTerminated = isNullTerminated; + IsOwned = isOwned; + Crc64 = Functions.ComputeCrc64( Span ); + if( isLower != null ) + { + _flags |= Flags.LowerCaseChecked; + if( isLower.Value ) + { + _flags |= Flags.IsLowerCase; + } + } + } + + public ReadOnlySpan< byte > Span + => new(( void* )Path, Length); + + private byte* Ptr + => ( byte* )Path; + + public override string ToString() + => Encoding.ASCII.GetString( Ptr, Length ); + + public IEnumerator< byte > GetEnumerator() + { + for( var i = 0; i < Length; ++i ) + { + yield return Span[ i ]; + } + } + + ~AsciiString() + { + if( IsOwned ) + { + Marshal.FreeHGlobal( Path ); + } + } + + public bool Equals( AsciiString? other ) + { + if( ReferenceEquals( null, other ) ) + { + return false; + } + + if( ReferenceEquals( this, other ) ) + { + return true; + } + + return Crc64 == other.Crc64 && ByteStringFunctions.Equals( Ptr, Length, other.Ptr, other.Length ); + } + + public override int GetHashCode() + => Crc64.GetHashCode(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int CompareTo( AsciiString? other ) + { + if( ReferenceEquals( this, other ) ) + { + return 0; + } + + if( ReferenceEquals( null, other ) ) + { + return 1; + } + + return ByteStringFunctions.Compare( Ptr, Length, other.Ptr, other.Length ); + } + + private bool? IsLowerInternal + => _flags.HasFlag( Flags.LowerCaseChecked ) ? _flags.HasFlag( Flags.IsLowerCase ) : null; + + public AsciiString Clone() + => new(ByteStringFunctions.CopyPath( Ptr, Length ), Length, true, true, IsLowerInternal); + + public AsciiString Substring( int from ) + => from < Length + ? new AsciiString( Ptr + from, Length - from, IsNullTerminated, false, IsLowerInternal ) + : Empty; + + public AsciiString Substring( int from, int length ) + { + Debug.Assert( from >= 0 ); + if( from >= Length ) + { + return Empty; + } + + var maxLength = Length - from; + return length < maxLength + ? new AsciiString( Ptr + from, length, false, false, IsLowerInternal ) + : new AsciiString( Ptr + from, maxLength, true, false, IsLowerInternal ); + } + + public int IndexOf( byte b, int from = 0 ) + { + var end = Ptr + Length; + for( var tmp = Ptr + from; tmp < end; ++tmp ) + { + if( *tmp == b ) + { + return ( int )( tmp - Ptr ); + } + } + + return -1; + } + + public int LastIndexOf( byte b, int to = 0 ) + { + var end = Ptr + to; + for( var tmp = Ptr + Length - 1; tmp >= end; --tmp ) + { + if( *tmp == b ) + { + return ( int )( tmp - Ptr ); + } + } + + return -1; + } + + + public static readonly AsciiString Empty = new(); +} + +public readonly struct NewGamePath +{ + public const int MaxGamePathLength = 256; + + private readonly AsciiString _string; + + private NewGamePath( AsciiString s ) + => _string = s; + + + public static readonly NewGamePath Empty = new(AsciiString.Empty); + + public static NewGamePath FromStringUnchecked( string? s, bool? isLower ) + => new(AsciiString.FromStringUnchecked( s, isLower )); + + public static bool FromString( string? s, out NewGamePath path, bool toLower = false ) + { + path = Empty; + if( s.IsNullOrEmpty() ) + { + return true; + } + + var substring = s.Replace( '\\', '/' ); + substring.TrimStart( '/' ); + if( substring.Length > MaxGamePathLength ) + { + return false; + } + + if( substring.Length == 0 ) + { + return true; + } + + if( !AsciiString.FromString( substring, out var ascii, toLower ) ) + { + return false; + } + + path = new NewGamePath( ascii ); + return true; + } + + public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewGamePath path, bool toLower = false ) + { + path = Empty; + if( !file.FullName.StartsWith( baseDir.FullName ) ) + { + return false; + } + + var substring = file.FullName[ baseDir.FullName.Length.. ]; + return FromString( substring, out path, toLower ); + } + + public AsciiString Filename() + { + var idx = _string.LastIndexOf( ( byte )'/' ); + return idx == -1 ? _string : _string.Substring( idx + 1 ); + } + + public override string ToString() + => _string.ToString(); +} + +public readonly struct GamePath : IComparable +{ + public const int MaxGamePathLength = 256; + + private readonly string _path; + + private GamePath( string path, bool _ ) + => _path = path; + + public GamePath( string? path ) + { + if( path != null && path.Length < MaxGamePathLength ) + { + _path = Lower( Trim( ReplaceSlash( path ) ) ); + } + else + { + _path = string.Empty; + } + } + + public GamePath( FileInfo file, DirectoryInfo baseDir ) + => _path = CheckPre( file, baseDir ) ? Lower( Trim( ReplaceSlash( Substring( file, baseDir ) ) ) ) : ""; + + private static bool CheckPre( FileInfo file, DirectoryInfo baseDir ) + => file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxGamePathLength; + + private static string Substring( FileInfo file, DirectoryInfo baseDir ) + => file.FullName.Substring( baseDir.FullName.Length ); + + private static string ReplaceSlash( string path ) + => path.Replace( '\\', '/' ); + + private static string Trim( string path ) + => path.TrimStart( '/' ); + + private static string Lower( string path ) + => path.ToLowerInvariant(); + + public static GamePath GenerateUnchecked( string path ) + => new(path, true); + + public static GamePath GenerateUncheckedLower( string path ) + => new(Lower( path ), true); + + public static implicit operator string( GamePath gamePath ) + => gamePath._path; + + public static explicit operator GamePath( string gamePath ) + => new(gamePath); + + public bool Empty + => _path.Length == 0; + + public string Filename() + { + var idx = _path.LastIndexOf( "/", StringComparison.Ordinal ); + return idx == -1 ? _path : idx == _path.Length - 1 ? "" : _path[ ( idx + 1 ).. ]; + } + + public int CompareTo( object? rhs ) + { + return rhs switch + { + string path => string.Compare( _path, path, StringComparison.InvariantCulture ), + GamePath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), + _ => -1, + }; + } + + public override string ToString() + => _path; +} + +public class GamePathConverter : JsonConverter +{ + public override bool CanConvert( Type objectType ) + => objectType == typeof( GamePath ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + var token = JToken.Load( reader ); + return token.ToObject< GamePath >(); + } + + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + if( value != null ) + { + var v = ( GamePath )value; + serializer.Serialize( writer, v.ToString() ); + } } } \ No newline at end of file diff --git a/Penumbra/Interop/PathResolver.cs b/Penumbra/Interop/PathResolver.cs index 6c683863..1fce700d 100644 --- a/Penumbra/Interop/PathResolver.cs +++ b/Penumbra/Interop/PathResolver.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Dalamud.Hooking; @@ -6,81 +8,123 @@ using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.Util; +using String = FFXIVClientStructs.STD.String; namespace Penumbra.Interop; public unsafe class PathResolver : IDisposable { - public delegate IntPtr ResolveMdlPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); + public delegate IntPtr ResolveMdlImcPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); public delegate IntPtr ResolveMtrlPath( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ); + public delegate void LoadMtrlTex( IntPtr mtrlResourceHandle ); - [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41" )] - public Hook< ResolveMdlPath >? ResolveMdlPathHook; + [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41", + DetourName = "ResolveMdlPathDetour" )] + public Hook< ResolveMdlImcPath >? ResolveMdlPathHook; - [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F" )] - public Hook? ResolveMtrlPathHook; + [Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F", + DetourName = "ResolveMtrlPathDetour" )] + public Hook< ResolveMtrlPath >? ResolveMtrlPathHook; + + [Signature( "40 ?? 48 83 ?? ?? 4D 8B ?? 48 8B ?? 41", DetourName = "ResolveImcPathDetour" )] + public Hook< ResolveMdlImcPath >? ResolveImcPathHook; + + [Signature( "4C 8B ?? ?? 89 ?? ?? ?? 89 ?? ?? 55 57 41 ?? 41" )] + public Hook< LoadMtrlTex >? LoadMtrlTexHook; private global::Dalamud.Game.ClientState.Objects.Types.GameObject? FindParent( IntPtr drawObject ) => Dalamud.Objects.FirstOrDefault( a => ( ( GameObject* )a.Address )->DrawObject == ( DrawObject* )drawObject ); private readonly byte[] _data = new byte[512]; - private unsafe IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) - { - var ret = ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ); - var n = Marshal.PtrToStringAnsi( ret )!; - var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; - PluginLog.Information( $"{drawObject:X} {path:X} {unk3:X} {unk4}\n{n}\n{name}" ); - if( Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( name, out var collection ) ) - { - var replacement = collection.ResolveSwappedOrReplacementPath( GamePath.GenerateUncheckedLower( n ) ); - if( replacement != null ) - { - for( var i = 0; i < replacement.Length; ++i ) - { - _data[ i ] = ( byte )replacement[ i ]; - } + public static Dictionary< string, ModCollection > Dict = new(); - _data[ replacement.Length ] = 0; - fixed( byte* data = _data ) + private IntPtr WriteData( string characterName, string path ) + { + _data[ 0 ] = ( byte )'|'; + var i = 1; + foreach( var c in characterName ) + { + _data[ i++ ] = ( byte )c; + } + + _data[ i++ ] = ( byte )'|'; + + foreach( var c in path ) + { + _data[ i++ ] = ( byte )c; + } + + _data[ i ] = 0; + fixed( byte* data = _data ) + { + return ( IntPtr )data; + } + } + + private void LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) + { + var handle = ( ResourceHandle* )mtrlResourceHandle; + var mtrlName = handle->FileName.ToString(); + if( Dict.TryGetValue( mtrlName, out var collection ) ) + { + var numTex = *( byte* )( mtrlResourceHandle + 0xFA ); + if( numTex != 0 ) + { + PluginLog.Information( $"{mtrlResourceHandle:X} -> {mtrlName} ({collection.Name}), {numTex} Texes" ); + var texSpace = *( byte** )( mtrlResourceHandle + 0xD0 ); + for( var i = 0; i < numTex; ++i ) { - return ( IntPtr )data; + var texStringPtr = ( IntPtr )( *( ulong* )( mtrlResourceHandle + 0xE0 ) + *( ushort* )( texSpace + 8 + i * 16 ) ); + var texString = Marshal.PtrToStringAnsi( texStringPtr ) ?? string.Empty; + PluginLog.Information( $"{texStringPtr:X}: {texString}" ); + Dict[ texString ] = collection; } } } - return ret; + LoadMtrlTexHook!.Original( mtrlResourceHandle ); } + private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) + { + if( path == IntPtr.Zero ) + { + return path; + } + + var n = Marshal.PtrToStringAnsi( path ); + if( n == null ) + { + return path; + } + + var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; + PluginLog.Information( $"{drawObject:X} {path:X}\n{n}\n{name}" ); + if( Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( name, out var value ) ) + { + Dict[ n ] = value; + } + else + { + Dict.Remove( n ); + } + + return path; + } + + private unsafe IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ) ); + + private unsafe IntPtr ResolveImcPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + => ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); + private unsafe IntPtr ResolveMtrlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ) - { - var ret = ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ); - var n = Marshal.PtrToStringAnsi( ret )!; - var name = FindParent( drawObject )?.Name.ToString() ?? string.Empty; - PluginLog.Information( $"{drawObject:X} {path:X} {unk3:X} {unk4} {unk5:X}\n{n}\n{name}" ); - if( Service.Get().Collections.CharacterCollection.TryGetValue( name, out var collection ) ) - { - var replacement = collection.ResolveSwappedOrReplacementPath( GamePath.GenerateUncheckedLower( n ) ); - if( replacement != null ) - { - for( var i = 0; i < replacement.Length; ++i ) - { - _data[i] = ( byte )replacement[i]; - } - - _data[replacement.Length] = 0; - fixed( byte* data = _data ) - { - return ( IntPtr )data; - } - } - } - - return ret; - } + => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); public PathResolver() { @@ -92,17 +136,23 @@ public unsafe class PathResolver : IDisposable { ResolveMdlPathHook?.Enable(); ResolveMtrlPathHook?.Enable(); + ResolveImcPathHook?.Enable(); + LoadMtrlTexHook?.Enable(); } public void Disable() { ResolveMdlPathHook?.Disable(); ResolveMtrlPathHook?.Disable(); + ResolveImcPathHook?.Disable(); + LoadMtrlTexHook?.Disable(); } public void Dispose() { ResolveMdlPathHook?.Dispose(); ResolveMtrlPathHook?.Dispose(); + ResolveImcPathHook?.Dispose(); + LoadMtrlTexHook?.Dispose(); } } \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 6f8fbb49..09e9e160 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.RegularExpressions; using Dalamud.Hooking; using Dalamud.Logging; +using ImGuiNET; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.Structs; @@ -223,8 +224,10 @@ public class ResourceLoader : IDisposable } file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; - var gameFsPath = GamePath.GenerateUncheckedLower( file ); - var replacementPath = modManager.ResolveSwappedOrReplacementPath( gameFsPath ); + var gameFsPath = GamePath.GenerateUncheckedLower( file ); + var replacementPath = PathResolver.Dict.TryGetValue( file, out var collection ) + ? collection.ResolveSwappedOrReplacementPath( gameFsPath ) + : modManager.ResolveSwappedOrReplacementPath( gameFsPath ); if( LogAllFiles && ( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) ) { PluginLog.Information( "[GetResourceHandler] {0}", file ); @@ -236,6 +239,8 @@ public class ResourceLoader : IDisposable return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); } + if (collection != null) + PathResolver.Dict[ replacementPath ] = collection; var path = Encoding.ASCII.GetBytes( replacementPath ); var bPath = stackalloc byte[path.Length + 1]; @@ -261,15 +266,42 @@ public class ResourceLoader : IDisposable } var gameFsPath = Marshal.PtrToStringAnsi( new IntPtr( pFileDesc->ResourceHandle->FileName() ) ); - - var isRooted = Path.IsPathRooted( gameFsPath ); - - if( gameFsPath == null || gameFsPath.Length >= 260 || !isRooted ) + if( gameFsPath is not { Length: < 260 } ) { return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; } - PluginLog.Debug( "loading modded file: {GameFsPath}", gameFsPath ); + + //var collection = gameFsPath.StartsWith( '|' ); + //if( collection ) + //{ + // var end = gameFsPath.IndexOf( '|', 1 ); + // if( end < 0 ) + // { + // PluginLog.Error( $"Unterminated Collection Name {gameFsPath}" ); + // return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; + // } + // + // var name = gameFsPath[ 1..end ]; + // gameFsPath = gameFsPath[ ( end + 1 ).. ]; + // PluginLog.Debug( "Loading file for {Name}: {GameFsPath}", name, gameFsPath ); + // + // if( !Path.IsPathRooted( gameFsPath ) ) + // { + // var encoding = Encoding.UTF8.GetBytes( gameFsPath ); + // Marshal.Copy( encoding, 0, new IntPtr( pFileDesc->ResourceHandle->FileName() ), encoding.Length ); + // pFileDesc->ResourceHandle->FileName()[ encoding.Length ] = 0; + // pFileDesc->ResourceHandle->FileNameLength -= name.Length + 2; + // return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; + // } + //} + //else + if( !Path.IsPathRooted( gameFsPath ) ) + { + return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; + } + + PluginLog.Debug( "Loading modded file: {GameFsPath}", gameFsPath ); pFileDesc->FileMode = FileMode.LoadUnpackedResource; @@ -282,8 +314,7 @@ public class ResourceLoader : IDisposable Marshal.Copy( utfPath, 0, new IntPtr( fd + 0x21 ), utfPath.Length ); pFileDesc->FileDescriptor = fd; - var ret = ReadFile( pFileHandler, pFileDesc, priority, isSync ); - return ret; + return ReadFile( pFileHandler, pFileDesc, priority, isSync ); } public void Enable() diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 1f3ad8de..39f89602 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -58,8 +58,21 @@ public partial class SettingsInterface ImGui.SetClipboardText( address ); } + ref var name = ref node->KeyValuePair.Item2.Value->FileName; ImGui.TableNextColumn(); - ImGui.Text( node->KeyValuePair.Item2.Value->FileName.ToString() ); + if( name.Capacity > 15 ) + { + ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); + } + else + { + fixed( byte* ptr = name.Buffer ) + { + ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); + } + } + + //ImGui.Text( node->KeyValuePair.Item2.Value->FileName.ToString() ); ImGui.TableNextColumn(); ImGui.Text( node->KeyValuePair.Item2.Value->RefCount.ToString() ); node = node->Next();