diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs new file mode 100644 index 00000000..e1e0970c --- /dev/null +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs @@ -0,0 +1,94 @@ +using System.Linq; +using System.Runtime.InteropServices; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public static unsafe partial class ByteStringFunctions +{ + private static readonly byte[] AsciiLowerCaseBytes = Enumerable.Range( 0, 256 ) + .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) + .ToArray(); + + // Convert a byte to its ASCII-lowercase version. + public static byte AsciiToLower( byte b ) + => AsciiLowerCaseBytes[ b ]; + + // Check if a byte is ASCII-lowercase. + public static bool AsciiIsLower( byte b ) + => AsciiToLower( b ) == b; + + // Check if a byte array of given length is ASCII-lowercase. + public static bool IsAsciiLowerCase( byte* path, int length ) + { + var end = path + length; + for( ; path < end; ++path ) + { + if( *path != AsciiLowerCaseBytes[*path] ) + { + return false; + } + } + + return true; + } + + // Compare two byte arrays of given lengths ASCII-case-insensitive. + 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; + } + + // Check two byte arrays of given lengths for ASCII-case-insensitive equality. + 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; + } + + // Check if a byte array of given length consists purely of ASCII characters. + public static bool IsAscii( byte* path, int length ) + { + var length8 = length / 8; + var end8 = ( ulong* )path + length8; + for( var ptr8 = ( ulong* )path; ptr8 < end8; ++ptr8 ) + { + if( ( *ptr8 & 0x8080808080808080ul ) != 0 ) + { + return false; + } + } + + var end = path + length; + for( path += length8 * 8; path < end; ++path ) + { + if( *path > 127 ) + { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs new file mode 100644 index 00000000..3e7382c9 --- /dev/null +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Comparison.cs @@ -0,0 +1,95 @@ +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public static unsafe partial class ByteStringFunctions +{ + // Lexicographically compare two byte arrays of given length. + 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; + } + + // Lexicographically compare one byte array of given length with a null-terminated byte array of unknown length. + 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; + } + + // Lexicographically compare two null-terminated byte arrays of unknown length not larger than maxLength. + 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; + } + + // Check two byte arrays of given length for equality. + 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; + } + + // Check one byte array of given length for equality against a null-terminated byte array of unknown length. + private static bool Equal( byte* lhs, int lhsLength, byte* rhs ) + => Compare( lhs, lhsLength, rhs ) == 0; + + // Check two null-terminated byte arrays of unknown length not larger than maxLength for equality. + private static bool Equal( byte* lhs, byte* rhs, int maxLength = int.MaxValue ) + => Compare( lhs, rhs, maxLength ) == 0; +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs new file mode 100644 index 00000000..ca4cadd0 --- /dev/null +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Construction.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public static unsafe partial class ByteStringFunctions +{ + // Used for static null-terminators. + public class NullTerminator + { + public readonly byte* NullBytePtr; + + public NullTerminator() + { + NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 ); + *NullBytePtr = 0; + } + + ~NullTerminator() + => Marshal.FreeHGlobal( ( IntPtr )NullBytePtr ); + } + + // Convert a C# unicode-string to an unmanaged UTF8-byte array and return the pointer. + // If the length would exceed the given maxLength, return a nullpointer instead. + public static byte* Utf8FromString( string s, out int length, int maxLength = int.MaxValue ) + { + length = Encoding.UTF8.GetByteCount( s ); + if( length >= maxLength ) + { + return null; + } + + var path = ( byte* )Marshal.AllocHGlobal( length + 1 ); + fixed( char* ptr = s ) + { + Encoding.UTF8.GetBytes( ptr, length, path, length + 1 ); + } + + path[ length ] = 0; + return path; + } + + // Create a copy of a given string and return the pointer. + public static byte* CopyString( byte* path, int length ) + { + var ret = ( byte* )Marshal.AllocHGlobal( length + 1 ); + Functions.MemCpyUnchecked( ret, path, length ); + ret[ length ] = 0; + return ret; + } + + // Check the length of a null-terminated byte array no longer than the given maxLength. + public static int CheckLength( byte* path, int maxLength = int.MaxValue ) + { + var end = path + maxLength; + for( var it = path; it < end; ++it ) + { + if( *it == 0 ) + { + return ( int )( it - path ); + } + } + + throw new ArgumentOutOfRangeException( "Null-terminated path too long" ); + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs b/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs new file mode 100644 index 00000000..7d21593a --- /dev/null +++ b/Penumbra.GameData/ByteString/ByteStringFunctions.Manipulation.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.GameData.ByteString; + +public static unsafe partial class ByteStringFunctions +{ + // Replace all occurrences of from in a byte array of known length with to. + public static int Replace( byte* ptr, int length, byte from, byte to ) + { + var end = ptr + length; + var numReplaced = 0; + for( ; ptr < end; ++ptr ) + { + if( *ptr == from ) + { + *ptr = to; + ++numReplaced; + } + } + + return numReplaced; + } + + // Convert a byte array of given length to ASCII-lowercase. + public static void AsciiToLowerInPlace( byte* path, int length ) + { + for( var i = 0; i < length; ++i ) + { + path[ i ] = AsciiLowerCaseBytes[ path[ i ] ]; + } + } + + // Copy a byte array and convert the copy to ASCII-lowercase. + 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 ] = AsciiLowerCaseBytes[ path[ i ] ]; + } + + return ptr; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/FullPath.cs b/Penumbra.GameData/ByteString/FullPath.cs new file mode 100644 index 00000000..f9c679dc --- /dev/null +++ b/Penumbra.GameData/ByteString/FullPath.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public readonly struct FullPath : IComparable, IEquatable< FullPath > +{ + public readonly string FullName; + public readonly Utf8String InternalName; + public readonly ulong Crc64; + + + public FullPath( DirectoryInfo baseDir, NewRelPath relPath ) + : this( Path.Combine( baseDir.FullName, relPath.ToString() ) ) + { } + + public FullPath( FileInfo file ) + : this( file.FullName ) + { } + + public FullPath( string s ) + { + FullName = s; + InternalName = Utf8String.FromString( FullName, out var name, true ) ? name : Utf8String.Empty; + Crc64 = Functions.ComputeCrc64( InternalName.Span ); + } + + public bool Exists + => File.Exists( FullName ); + + public string Extension + => Path.GetExtension( FullName ); + + public string Name + => Path.GetFileName( FullName ); + + public bool ToGamePath( DirectoryInfo dir, out NewGamePath path ) + { + path = NewGamePath.Empty; + if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) ) + { + return false; + } + + var substring = InternalName.Substring( dir.FullName.Length + 1 ); + + path = new NewGamePath( substring.Replace( ( byte )'\\', ( byte )'/' ) ); + return true; + } + + public bool ToRelPath( DirectoryInfo dir, out NewRelPath path ) + { + path = NewRelPath.Empty; + if( !FullName.StartsWith( dir.FullName ) ) + { + return false; + } + + var substring = InternalName.Substring( dir.FullName.Length + 1 ); + + path = new NewRelPath( substring ); + return true; + } + + public int CompareTo( object? obj ) + => obj switch + { + FullPath p => InternalName.CompareTo( p.InternalName ), + FileInfo f => string.Compare( FullName, f.FullName, StringComparison.InvariantCultureIgnoreCase ), + Utf8String u => InternalName.CompareTo( u ), + string s => string.Compare( FullName, s, StringComparison.InvariantCultureIgnoreCase ), + _ => -1, + }; + + public bool Equals( FullPath other ) + { + if( Crc64 != other.Crc64 ) + { + return false; + } + + if( FullName.Length == 0 || other.FullName.Length == 0 ) + { + return true; + } + + return InternalName.Equals( other.InternalName ); + } + + public override int GetHashCode() + => InternalName.Crc32; + + public override string ToString() + => FullName; +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/NewGamePath.cs b/Penumbra.GameData/ByteString/NewGamePath.cs new file mode 100644 index 00000000..1685f15a --- /dev/null +++ b/Penumbra.GameData/ByteString/NewGamePath.cs @@ -0,0 +1,159 @@ +using System; +using System.IO; +using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Penumbra.GameData.ByteString; + +// NewGamePath wrap some additional validity checking around Utf8String, +// provide some filesystem helpers, and conversion to Json. +[JsonConverter( typeof( NewGamePathConverter ) )] +public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< NewGamePath >, IDisposable +{ + public const int MaxGamePathLength = 256; + + public readonly Utf8String Path; + public static readonly NewGamePath Empty = new(Utf8String.Empty); + + internal NewGamePath( Utf8String s ) + => Path = s; + + public int Length + => Path.Length; + + public bool IsEmpty + => Path.IsEmpty; + + public NewGamePath ToLower() + => new(Path.AsciiToLower()); + + public static unsafe bool FromPointer( byte* ptr, out NewGamePath path, bool lower = false ) + { + var utf = new Utf8String( ptr ); + return ReturnChecked( utf, out path, lower ); + } + + public static bool FromSpan( ReadOnlySpan< byte > data, out NewGamePath path, bool lower = false ) + { + var utf = Utf8String.FromSpanUnsafe( data, false, null, null ); + return ReturnChecked( utf, out path, lower ); + } + + // Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one. + // Does not check for initial slashes either, since they are assumed to be by choice. + // Checks for maxlength, ASCII and lowercase. + private static bool ReturnChecked( Utf8String utf, out NewGamePath path, bool lower = false ) + { + path = Empty; + if( !utf.IsAscii || utf.Length > MaxGamePathLength ) + { + return false; + } + + path = new NewGamePath( lower ? utf.AsciiToLower() : utf ); + return true; + } + + public NewGamePath Clone() + => new(Path.Clone()); + + 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( !Utf8String.FromString( substring, out var ascii, toLower ) || !ascii.IsAscii ) + { + 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 Utf8String Filename() + { + var idx = Path.LastIndexOf( ( byte )'/' ); + return idx == -1 ? Path : Path.Substring( idx + 1 ); + } + + public Utf8String Extension() + { + var idx = Path.LastIndexOf( ( byte )'.' ); + return idx == -1 ? Utf8String.Empty : Path.Substring( idx ); + } + + public bool Equals( NewGamePath other ) + => Path.Equals( other.Path ); + + public override int GetHashCode() + => Path.GetHashCode(); + + public int CompareTo( NewGamePath other ) + => Path.CompareTo( other.Path ); + + public override string ToString() + => Path.ToString(); + + public void Dispose() + => 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 ] == ':'; + + private class NewGamePathConverter : JsonConverter + { + public override bool CanConvert( Type objectType ) + => objectType == typeof( NewGamePath ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + var token = JToken.Load( reader ).ToString(); + return FromString( token, out var p, true ) + ? p + : throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewGamePath )}." ); + } + + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + if( value is NewGamePath p ) + { + serializer.Serialize( writer, p.ToString() ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/NewRelPath.cs b/Penumbra.GameData/ByteString/NewRelPath.cs new file mode 100644 index 00000000..25b6f9e0 --- /dev/null +++ b/Penumbra.GameData/ByteString/NewRelPath.cs @@ -0,0 +1,133 @@ +using System; +using System.IO; +using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Penumbra.GameData.ByteString; + +[JsonConverter( typeof( NewRelPathConverter ) )] +public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRelPath >, IDisposable +{ + public const int MaxRelPathLength = 250; + + public readonly Utf8String Path; + public static readonly NewRelPath Empty = new(Utf8String.Empty); + + internal NewRelPath( Utf8String path ) + => Path = path; + + public static bool FromString( string? s, out NewRelPath path ) + { + path = Empty; + if( s.IsNullOrEmpty() ) + { + return true; + } + + var substring = s!.Replace( '/', '\\' ); + substring.TrimStart( '\\' ); + if( substring.Length > MaxRelPathLength ) + { + return false; + } + + if( substring.Length == 0 ) + { + return true; + } + + if( !Utf8String.FromString( substring, out var ascii ) || !ascii.IsAscii ) + { + return false; + } + + path = new NewRelPath( ascii ); + return true; + } + + public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewRelPath path ) + { + path = Empty; + if( !file.FullName.StartsWith( baseDir.FullName ) ) + { + return false; + } + + var substring = file.FullName[ baseDir.FullName.Length.. ]; + return FromString( substring, out path ); + } + + public static bool FromFile( FullPath file, DirectoryInfo baseDir, out NewRelPath path ) + { + path = Empty; + if( !file.FullName.StartsWith( baseDir.FullName ) ) + { + return false; + } + + var substring = file.FullName[ baseDir.FullName.Length.. ]; + return FromString( substring, out path ); + } + + public NewRelPath( NewGamePath gamePath ) + => Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' ); + + public unsafe NewGamePath ToGamePath( int skipFolders = 0 ) + { + var idx = 0; + while( skipFolders > 0 ) + { + idx = Path.IndexOf( ( byte )'\\', idx ) + 1; + --skipFolders; + if( idx <= 0 ) + { + return NewGamePath.Empty; + } + } + + var length = Path.Length - idx; + var ptr = ByteStringFunctions.CopyString( Path.Path + idx, length ); + ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' ); + ByteStringFunctions.AsciiToLowerInPlace( ptr, length ); + var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true ); + return new NewGamePath( utf ); + } + + public int CompareTo( NewRelPath rhs ) + => Path.CompareTo( rhs.Path ); + + public bool Equals( NewRelPath other ) + => Path.Equals( other.Path ); + + public override string ToString() + => Path.ToString(); + + public void Dispose() + => Path.Dispose(); + + private class NewRelPathConverter : JsonConverter + { + public override bool CanConvert( Type objectType ) + => objectType == typeof( NewRelPath ); + + public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) + { + var token = JToken.Load( reader ).ToString(); + return FromString( token, out var p ) + ? p + : throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewRelPath )}." ); + } + + public override bool CanWrite + => true; + + public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) + { + if( value is NewRelPath p ) + { + serializer.Serialize( writer, p.ToString() ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Access.cs b/Penumbra.GameData/ByteString/Utf8String.Access.cs new file mode 100644 index 00000000..a74dd672 --- /dev/null +++ b/Penumbra.GameData/ByteString/Utf8String.Access.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Penumbra.GameData.ByteString; + +// Utf8String is a wrapper around unsafe byte strings. +// It may be used to store owned strings in unmanaged space, +// as well as refer to unowned strings. +// Unowned strings may change their value and thus become corrupt, +// so they should never be stored, just used locally or with great care. +// The string keeps track of whether it is owned or not, it also can keep track +// of some other information, like the string being pure ASCII, ASCII-lowercase or null-terminated. +// Owned strings are always null-terminated. +// Any constructed string will compute its own CRC32-value (as long as the string itself is not changed). +public sealed unsafe partial class Utf8String : IEnumerable< byte > +{ + // We keep information on some of the state of the Utf8String in specific bits. + // This costs some potential max size, but that is not relevant for our case. + // Except for destruction/dispose, or if the non-owned pointer changes values, + // the CheckedFlag, AsciiLowerCaseFlag and AsciiFlag are the only things that are mutable. + private const uint NullTerminatedFlag = 0x80000000; + private const uint OwnedFlag = 0x40000000; + private const uint AsciiCheckedFlag = 0x04000000; + private const uint AsciiFlag = 0x08000000; + private const uint AsciiLowerCheckedFlag = 0x10000000; + private const uint AsciiLowerFlag = 0x20000000; + private const uint FlagMask = 0x03FFFFFF; + + public bool IsNullTerminated + => ( _length & NullTerminatedFlag ) != 0; + + public bool IsOwned + => ( _length & OwnedFlag ) != 0; + + public bool IsAscii + => CheckAscii(); + + public bool IsAsciiLowerCase + => CheckAsciiLower(); + + public byte* Path + => _path; + + public int Crc32 + => _crc32; + + public int Length + => ( int )( _length & FlagMask ); + + public bool IsEmpty + => Length == 0; + + public ReadOnlySpan< byte > Span + => new(_path, Length); + + public byte this[ int idx ] + => ( uint )idx < Length ? _path[ idx ] : throw new IndexOutOfRangeException(); + + public IEnumerator< byte > GetEnumerator() + { + for( var i = 0; i < Length; ++i ) + { + yield return Span[ i ]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + // Only not readonly due to dispose. + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() + => _crc32; +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Comparison.cs b/Penumbra.GameData/ByteString/Utf8String.Comparison.cs new file mode 100644 index 00000000..c2ca9488 --- /dev/null +++ b/Penumbra.GameData/ByteString/Utf8String.Comparison.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; + +namespace Penumbra.GameData.ByteString; + +public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, IComparable< Utf8String > +{ + public bool Equals( Utf8String? other ) + { + if( ReferenceEquals( null, other ) ) + { + return false; + } + + if( ReferenceEquals( this, other ) ) + { + return true; + } + + return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length ); + } + + public bool EqualsCi( Utf8String? other ) + { + if( ReferenceEquals( null, other ) ) + { + return false; + } + + if( ReferenceEquals( this, other ) ) + { + return true; + } + + if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) ) + { + return _crc32 == other._crc32 && ByteStringFunctions.Equals( _path, Length, other._path, other.Length ); + } + + return ByteStringFunctions.AsciiCaselessEquals( _path, Length, other._path, other.Length ); + } + + public int CompareTo( Utf8String? other ) + { + if( ReferenceEquals( this, other ) ) + { + return 0; + } + + if( ReferenceEquals( null, other ) ) + { + return 1; + } + + return ByteStringFunctions.Compare( _path, Length, other._path, other.Length ); + } + + public int CompareToCi( Utf8String? other ) + { + if( ReferenceEquals( null, other ) ) + { + return 0; + } + + if( ReferenceEquals( this, other ) ) + { + return 1; + } + + if( ( IsAsciiLowerInternal ?? false ) && ( other.IsAsciiLowerInternal ?? false ) ) + { + return ByteStringFunctions.Compare( _path, Length, other._path, other.Length ); + } + + return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length ); + } + + public bool StartsWith( params char[] chars ) + { + if( chars.Length > Length ) + { + return false; + } + + var ptr = _path; + return chars.All( t => *ptr++ == ( byte )t ); + } + + public bool EndsWith( params char[] chars ) + { + if( chars.Length > Length ) + { + return false; + } + + var ptr = _path + Length - chars.Length; + return chars.All( c => *ptr++ == ( byte )c ); + } + + public int IndexOf( byte b, int from = 0 ) + { + var end = _path + Length; + for( var tmp = _path + from; tmp < end; ++tmp ) + { + if( *tmp == b ) + { + return ( int )( tmp - _path ); + } + } + + return -1; + } + + public int LastIndexOf( byte b, int to = 0 ) + { + var end = _path + to; + for( var tmp = _path + Length - 1; tmp >= end; --tmp ) + { + if( *tmp == b ) + { + return ( int )( tmp - _path ); + } + } + + return -1; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Construction.cs b/Penumbra.GameData/ByteString/Utf8String.Construction.cs new file mode 100644 index 00000000..96ec7313 --- /dev/null +++ b/Penumbra.GameData/ByteString/Utf8String.Construction.cs @@ -0,0 +1,214 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public sealed unsafe partial class Utf8String : IDisposable +{ + // statically allocated null-terminator for empty strings to point to. + private static readonly ByteStringFunctions.NullTerminator Null = new(); + + public static readonly Utf8String Empty = new(); + + // actual data members. + private byte* _path; + private uint _length; + private int _crc32; + + // Create an empty string. + public Utf8String() + { + _path = Null.NullBytePtr; + _length |= AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag | AsciiFlag; + _crc32 = 0; + } + + // Create a temporary Utf8String from a byte pointer. + // This computes CRC, checks for ASCII and AsciiLower and assumes Null-Termination. + public Utf8String( byte* path ) + { + var length = Functions.ComputeCrc32AsciiLowerAndSize( path, out var crc32, out var lower, out var ascii ); + Setup( path, length, crc32, true, false, lower, ascii ); + } + + // Construct a temporary Utf8String from a given byte string of known size. + // Other known attributes can also be provided and are not computed. + // Can throw ArgumentOutOfRange if length is higher than max length. + // The Crc32 will be computed. + public static Utf8String FromByteStringUnsafe( byte* path, int length, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) + => FromSpanUnsafe( new ReadOnlySpan< byte >( path, length ), isNullTerminated, isLower, isAscii ); + + // Same as above, just with a span. + public static Utf8String FromSpanUnsafe( ReadOnlySpan< byte > path, bool isNullTerminated, bool? isLower = null, bool? isAscii = false ) + { + fixed( byte* ptr = path ) + { + return new Utf8String().Setup( ptr, path.Length, null, isNullTerminated, false, isLower, isAscii ); + } + } + + // Construct a Utf8String from a given unicode string, possibly converted to ascii lowercase. + // Only returns false if the length exceeds the max length. + public static bool FromString( string? path, out Utf8String ret, bool toAsciiLower = false ) + { + if( string.IsNullOrEmpty( path ) ) + { + ret = Empty; + return true; + } + + var p = ByteStringFunctions.Utf8FromString( path, out var l, ( int )FlagMask ); + if( p == null ) + { + ret = Empty; + return false; + } + + if( toAsciiLower ) + { + ByteStringFunctions.AsciiToLowerInPlace( p, l ); + } + + ret = new Utf8String().Setup( p, l, null, true, true, toAsciiLower ? true : null, l == path.Length ); + return true; + } + + // Does not check for length and just assumes the isLower state from the second argument. + public static Utf8String FromStringUnsafe( string? path, bool? isLower ) + { + if( string.IsNullOrEmpty( path ) ) + { + return Empty; + } + + var p = ByteStringFunctions.Utf8FromString( path, out var l ); + var ret = new Utf8String().Setup( p, l, null, true, true, isLower, l == path.Length ); + return ret; + } + + // Free memory if the string is owned. + private void ReleaseUnmanagedResources() + { + if( !IsOwned ) + { + return; + } + + Marshal.FreeHGlobal( ( IntPtr )_path ); + GC.RemoveMemoryPressure( Length ); + _length = AsciiCheckedFlag | AsciiFlag | AsciiLowerCheckedFlag | AsciiLowerFlag | NullTerminatedFlag; + _path = Null.NullBytePtr; + _crc32 = 0; + } + + // Manually free memory. Sets the string to an empty string. + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize( this ); + } + + ~Utf8String() + { + ReleaseUnmanagedResources(); + } + + // Setup from all given values. + // Only called from constructors or factory functions in this library. + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + internal Utf8String Setup( byte* path, int length, int? crc32, bool isNullTerminated, bool isOwned, + bool? isLower = null, bool? isAscii = null ) + { + if( length > FlagMask ) + { + throw new ArgumentOutOfRangeException( nameof( length ) ); + } + + _path = path; + _length = ( uint )length; + _crc32 = crc32 ?? ( int )~Lumina.Misc.Crc32.Get( new ReadOnlySpan< byte >( path, length ) ); + if( isNullTerminated ) + { + _length |= NullTerminatedFlag; + } + + if( isOwned ) + { + _length |= OwnedFlag; + } + + if( isLower != null ) + { + _length |= AsciiLowerCheckedFlag; + if( isLower.Value ) + { + _length |= AsciiLowerFlag; + } + } + + if( isAscii != null ) + { + _length |= AsciiCheckedFlag; + if( isAscii.Value ) + { + _length |= AsciiFlag; + } + } + + return this; + } + + private bool CheckAscii() + { + switch( _length & ( AsciiCheckedFlag | AsciiFlag ) ) + { + case AsciiCheckedFlag: return false; + case AsciiCheckedFlag | AsciiFlag: return true; + default: + _length |= AsciiCheckedFlag; + var isAscii = ByteStringFunctions.IsAscii( _path, Length ); + if( isAscii ) + { + _length |= AsciiFlag; + } + + return isAscii; + } + } + + private bool CheckAsciiLower() + { + switch( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) + { + case AsciiLowerCheckedFlag: return false; + case AsciiLowerCheckedFlag | AsciiLowerFlag: return true; + default: + _length |= AsciiLowerCheckedFlag; + var isAsciiLower = ByteStringFunctions.IsAsciiLowerCase( _path, Length ); + if( isAsciiLower ) + { + _length |= AsciiLowerFlag; + } + + return isAsciiLower; + } + } + + private bool? IsAsciiInternal + => ( _length & ( AsciiCheckedFlag | AsciiFlag ) ) switch + { + AsciiCheckedFlag => false, + AsciiFlag => true, + _ => null, + }; + + private bool? IsAsciiLowerInternal + => ( _length & ( AsciiLowerCheckedFlag | AsciiLowerFlag ) ) switch + { + AsciiLowerCheckedFlag => false, + AsciiLowerFlag => true, + _ => null, + }; +} \ No newline at end of file diff --git a/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs new file mode 100644 index 00000000..08a38e91 --- /dev/null +++ b/Penumbra.GameData/ByteString/Utf8String.Manipulation.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Penumbra.GameData.Util; + +namespace Penumbra.GameData.ByteString; + +public sealed unsafe partial class Utf8String +{ + // Create a C# Unicode string from this string. + // If the string is known to be pure ASCII, use that encoding, otherwise UTF8. + public override string ToString() + => Length == 0 + ? string.Empty + : ( _length & AsciiFlag ) != 0 + ? Encoding.ASCII.GetString( _path, Length ) + : Encoding.UTF8.GetString( _path, Length ); + + + // Convert the ascii portion of the string to lowercase. + // Only creates a new string and copy if the string is not already known to be lowercase. + public Utf8String AsciiToLower() + => ( _length & AsciiLowerFlag ) == 0 + ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) + : this; + + // Convert the ascii portion of the string to lowercase. + // Guaranteed to create an owned copy. + public Utf8String AsciiToLowerClone() + => ( _length & AsciiLowerFlag ) == 0 + ? new Utf8String().Setup( ByteStringFunctions.AsciiToLower( _path, Length ), Length, null, true, true, true, IsAsciiInternal ) + : Clone(); + + // Create an owned copy of the given string. + public Utf8String Clone() + { + var ret = new Utf8String(); + ret._length = _length | OwnedFlag | NullTerminatedFlag; + ret._path = ByteStringFunctions.CopyString(Path, Length); + ret._crc32 = Crc32; + return ret; + } + + // Create a non-owning substring from the given position. + // If from is negative or too large, the returned string will be the empty string. + public Utf8String Substring( int from ) + => ( uint )from < Length + ? FromByteStringUnsafe( _path + from, Length - from, IsNullTerminated, IsAsciiLowerInternal, IsAsciiInternal ) + : Empty; + + // Create a non-owning substring from the given position of the given length. + // If from is negative or too large, the returned string will be the empty string. + // If from + length is too large, it will be the same as if length was not specified. + public Utf8String Substring( int from, int length ) + { + var maxLength = Length - ( uint )from; + if( maxLength <= 0 ) + { + return Empty; + } + + return length < maxLength + ? FromByteStringUnsafe( _path + from, length, false, IsAsciiLowerInternal, IsAsciiInternal ) + : Substring( from ); + } + + // Create a owned copy of the string and replace all occurences of from with to in it. + public Utf8String Replace( byte from, byte to ) + { + var length = Length; + var newPtr = ByteStringFunctions.CopyString( _path, length ); + var numReplaced = ByteStringFunctions.Replace( newPtr, length, from, to ); + return new Utf8String().Setup( newPtr, length, numReplaced > 0 ? _crc32 : null, true, true, IsAsciiLowerInternal, IsAsciiInternal ); + } + + // Join a number of strings with a given byte between them. + public static Utf8String Join( byte splitter, params Utf8String[] strings ) + { + var length = strings.Sum( s => s.Length ) + strings.Length; + var data = ( byte* )Marshal.AllocHGlobal( length ); + + var ptr = data; + bool? isLower = ByteStringFunctions.AsciiIsLower( splitter ); + bool? isAscii = splitter < 128; + foreach( var s in strings ) + { + Functions.MemCpyUnchecked( ptr, s.Path, s.Length ); + ptr += s.Length; + *ptr++ = splitter; + isLower = Combine( isLower, s.IsAsciiLowerInternal ); + isAscii &= s.IsAscii; + } + + --length; + data[ length ] = 0; + var ret = FromByteStringUnsafe( data, length, true, isLower, isAscii ); + ret._length |= OwnedFlag; + return ret; + } + + // Split a string and return a list of the substrings delimited by b. + // You can specify the maximum number of splits (if the maximum is reached, the last substring may contain delimiters). + // You can also specify to ignore empty substrings inside delimiters. Those are also not counted for max splits. + public List< Utf8String > Split( byte b, int maxSplits = int.MaxValue, bool removeEmpty = true ) + { + var ret = new List< Utf8String >(); + var start = 0; + for( var idx = IndexOf( b, start ); idx >= 0; idx = IndexOf( b, start ) ) + { + if( start + 1 != idx || !removeEmpty ) + { + ret.Add( Substring( start, idx - start ) ); + } + + start = idx + 1; + if( ret.Count == maxSplits - 1 ) + { + break; + } + } + + ret.Add( Substring( start ) ); + return ret; + } + + private static bool? Combine( bool? val1, bool? val2 ) + { + return ( val1, val2 ) switch + { + (null, null) => null, + (null, true) => null, + (null, false) => false, + (true, null) => null, + (true, true) => true, + (true, false) => false, + (false, null) => false, + (false, true) => false, + (false, false) => false, + }; + } +} \ No newline at end of file diff --git a/Penumbra.GameData/Util/Functions.cs b/Penumbra.GameData/Util/Functions.cs index 91dc7799..5d9ab10e 100644 --- a/Penumbra.GameData/Util/Functions.cs +++ b/Penumbra.GameData/Util/Functions.cs @@ -1,5 +1,7 @@ using System; +using System.Reflection; using System.Runtime.InteropServices; +using ByteStringFunctions = Penumbra.GameData.ByteString.ByteStringFunctions; namespace Penumbra.GameData.Util; @@ -41,10 +43,96 @@ public static class Functions 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 ); + private static readonly uint[] CrcTable = + typeof( Lumina.Misc.Crc32 ).GetField( "CrcTable", BindingFlags.Static | BindingFlags.NonPublic )?.GetValue( null ) as uint[] + ?? throw new Exception( "Could not fetch CrcTable from Lumina." ); - public static unsafe void MemCpyUnchecked( byte* dest, byte* src, int count ) + + public static unsafe int ComputeCrc64LowerAndSize( byte* ptr, out ulong crc64, out int crc32Ret, out bool isLower, out bool isAscii ) + { + var tmp = ptr; + uint crcFolder = 0; + uint crcFile = 0; + var crc32 = uint.MaxValue; + crc64 = 0; + isLower = true; + isAscii = true; + while( true ) + { + var value = *tmp; + if( value == 0 ) + { + break; + } + + if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp ) + { + isLower = false; + } + + if( value > 0x80 ) + { + isAscii = false; + } + + if( value == ( byte )'/' ) + { + crcFolder = crc32; + crcFile = uint.MaxValue; + crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); + } + else + { + crcFile = CrcTable[ ( byte )( crcFolder ^ value ) ] ^ ( crcFolder >> 8 ); + crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); + } + + ++tmp; + } + + var size = ( int )( tmp - ptr ); + crc64 = ~( ( ulong )crcFolder << 32 ) | crcFile; + crc32Ret = ( int )~crc32; + return size; + } + + public static unsafe int ComputeCrc32AsciiLowerAndSize( byte* ptr, out int crc32Ret, out bool isLower, out bool isAscii ) + { + var tmp = ptr; + var crc32 = uint.MaxValue; + isLower = true; + isAscii = true; + while( true ) + { + var value = *tmp; + if( value == 0 ) + { + break; + } + + if( ByteStringFunctions.AsciiToLower( *tmp ) != *tmp ) + { + isLower = false; + } + + if( value > 0x80 ) + { + isAscii = false; + } + + crc32 = CrcTable[ ( byte )( crc32 ^ value ) ] ^ ( crc32 >> 8 ); + ++tmp; + } + + var size = ( int )( tmp - ptr ); + crc32Ret = ( int )~crc32; + return size; + } + + [DllImport( "msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false )] + private static extern unsafe IntPtr memcpy( void* dest, void* src, int count ); + + public static unsafe void MemCpyUnchecked( void* dest, void* src, int count ) => memcpy( dest, src, count ); diff --git a/Penumbra.GameData/Util/GamePath.cs b/Penumbra.GameData/Util/GamePath.cs index 4c670605..ded35038 100644 --- a/Penumbra.GameData/Util/GamePath.cs +++ b/Penumbra.GameData/Util/GamePath.cs @@ -1,526 +1,11 @@ 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; +using JsonSerializer = Newtonsoft.Json.JsonSerializer; namespace Penumbra.GameData.Util; -public static unsafe class ByteStringFunctions -{ - public class NullTerminator - { - public readonly byte* NullBytePtr; - - public NullTerminator() - { - NullBytePtr = ( byte* )Marshal.AllocHGlobal( 1 ); - *NullBytePtr = 0; - } - - ~NullTerminator() - => Marshal.FreeHGlobal( ( IntPtr )NullBytePtr ); - } - - private static readonly byte[] LowerCaseBytes = Enumerable.Range( 0, 256 ) - .Select( i => ( byte )char.ToLowerInvariant( ( char )i ) ) - .ToArray(); - - 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 ) - { - Encoding.UTF8.GetBytes( ptr, length, path, length + 1 ); - } - - path[ length ] = 0; - return path; - } - - 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( *it == 0 ) - { - 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; @@ -532,7 +17,7 @@ public readonly struct GamePath : IComparable public GamePath( string? path ) { - if( path != null && path.Length < MaxGamePathLength ) + if( path is { Length: < MaxGamePathLength } ) { _path = Lower( Trim( ReplaceSlash( path ) ) ); } @@ -617,4 +102,5 @@ public class GamePathConverter : JsonConverter serializer.Serialize( writer, v.ToString() ); } } -} \ No newline at end of file +} + diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 277f4a3a..1f03dfc5 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -3,47 +3,42 @@ using System.Linq; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; -using Penumbra.Mods; -using Penumbra.Util; -namespace Penumbra.Api +namespace Penumbra.Api; + +public class ModsController : WebApiController { - public class ModsController : WebApiController + private readonly Penumbra _penumbra; + + public ModsController( Penumbra penumbra ) + => _penumbra = penumbra; + + [Route( HttpVerbs.Get, "/mods" )] + public object? GetMods() { - private readonly Penumbra _penumbra; + return Penumbra.ModManager.Collections.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new + { + x.Settings.Enabled, + x.Settings.Priority, + x.Data.BasePath.Name, + x.Data.Meta, + BasePath = x.Data.BasePath.FullName, + Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ), + } ) + ?? null; + } - public ModsController( Penumbra penumbra ) - => _penumbra = penumbra; + [Route( HttpVerbs.Post, "/mods" )] + public object CreateMod() + => new { }; - [Route( HttpVerbs.Get, "/mods" )] - public object? GetMods() - { - var modManager = Service< ModManager >.Get(); - return modManager.Collections.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new - { - x.Settings.Enabled, - x.Settings.Priority, - x.Data.BasePath.Name, - x.Data.Meta, - BasePath = x.Data.BasePath.FullName, - Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ), - } ) - ?? null; - } - - [Route( HttpVerbs.Post, "/mods" )] - public object CreateMod() - => new { }; - - [Route( HttpVerbs.Get, "/files" )] - public object GetFiles() - { - var modManager = Service< ModManager >.Get(); - return modManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( - o => ( string )o.Key, - o => o.Value.FullName - ) - ?? new Dictionary< string, string >(); - } + [Route( HttpVerbs.Get, "/files" )] + public object GetFiles() + { + return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( + o => ( string )o.Key, + o => o.Value.FullName + ) + ?? new Dictionary< string, string >(); } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 5190bc07..39bcbd35 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,166 +1,157 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Mods; -using Penumbra.Util; -namespace Penumbra.Api +namespace Penumbra.Api; + +public class PenumbraApi : IDisposable, IPenumbraApi { - public class PenumbraApi : IDisposable, IPenumbraApi + public int ApiVersion { get; } = 3; + private Penumbra? _penumbra; + private Lumina.GameData? _lumina; + + public bool Valid + => _penumbra != null; + + public PenumbraApi( Penumbra penumbra ) { - public int ApiVersion { get; } = 3; - private Penumbra? _penumbra; - private Lumina.GameData? _lumina; + _penumbra = penumbra; + _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() + .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) + ?.GetValue( Dalamud.GameData ); + } - public bool Valid - => _penumbra != null; + public void Dispose() + { + _penumbra = null; + _lumina = null; + } - public PenumbraApi( Penumbra penumbra ) + public event ChangedItemClick? ChangedItemClicked; + public event ChangedItemHover? ChangedItemTooltip; + + internal bool HasTooltip + => ChangedItemTooltip != null; + + internal void InvokeTooltip( object? it ) + => ChangedItemTooltip?.Invoke( it ); + + internal void InvokeClick( MouseButton button, object? it ) + => ChangedItemClicked?.Invoke( button, it ); + + + private void CheckInitialized() + { + if( !Valid ) { - _penumbra = penumbra; - _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() - .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) - ?.GetValue( Dalamud.GameData ); + throw new Exception( "PluginShare is not initialized." ); + } + } + + public void RedrawObject( string name, RedrawType setting ) + { + CheckInitialized(); + + _penumbra!.ObjectReloader.RedrawObject( name, setting ); + } + + public void RedrawObject( GameObject? gameObject, RedrawType setting ) + { + CheckInitialized(); + + _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); + } + + public void RedrawAll( RedrawType setting ) + { + CheckInitialized(); + + _penumbra!.ObjectReloader.RedrawAll( setting ); + } + + private static string ResolvePath( string path, ModManager manager, ModCollection collection ) + { + if( !Penumbra.Config.IsEnabled ) + { + return path; } - public void Dispose() + var gamePath = new GamePath( path ); + var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); + ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); + ret ??= path; + return ret; + } + + public string ResolvePath( string path ) + { + CheckInitialized(); + return ResolvePath( path, Penumbra.ModManager, Penumbra.ModManager.Collections.DefaultCollection ); + } + + public string ResolvePath( string path, string characterName ) + { + CheckInitialized(); + return ResolvePath( path, Penumbra.ModManager, + Penumbra.ModManager.Collections.CharacterCollection.TryGetValue( characterName, out var collection ) + ? collection + : ModCollection.Empty ); + } + + private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + { + CheckInitialized(); + try { - _penumbra = null; - _lumina = null; - } - - public event ChangedItemClick? ChangedItemClicked; - public event ChangedItemHover? ChangedItemTooltip; - - internal bool HasTooltip - => ChangedItemTooltip != null; - - internal void InvokeTooltip( object? it ) - => ChangedItemTooltip?.Invoke( it ); - - internal void InvokeClick( MouseButton button, object? it ) - => ChangedItemClicked?.Invoke( button, it ); - - - private void CheckInitialized() - { - if( !Valid ) + if( Path.IsPathRooted( resolvedPath ) ) { - throw new Exception( "PluginShare is not initialized." ); - } - } - - public void RedrawObject( string name, RedrawType setting ) - { - CheckInitialized(); - - _penumbra!.ObjectReloader.RedrawObject( name, setting ); - } - - public void RedrawObject( GameObject? gameObject, RedrawType setting ) - { - CheckInitialized(); - - _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); - } - - public void RedrawAll( RedrawType setting ) - { - CheckInitialized(); - - _penumbra!.ObjectReloader.RedrawAll( setting ); - } - - private static string ResolvePath( string path, ModManager manager, ModCollection collection ) - { - if( !Penumbra.Config.IsEnabled ) - { - return path; + return _lumina?.GetFileFromDisk< T >( resolvedPath ); } - var gamePath = new GamePath( path ); - var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); - ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); - ret ??= path; - return ret; + return Dalamud.GameData.GetFile< T >( resolvedPath ); } - - public string ResolvePath( string path ) + catch( Exception e ) { - CheckInitialized(); - var modManager = Service< ModManager >.Get(); - return ResolvePath( path, modManager, modManager.Collections.DefaultCollection ); + PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); + return null; } + } - public string ResolvePath( string path, string characterName ) + public T? GetFile< T >( string gamePath ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath ) ); + + public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource + => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + { + CheckInitialized(); + try { - CheckInitialized(); - var modManager = Service< ModManager >.Get(); - return ResolvePath( path, modManager, - modManager.Collections.CharacterCollection.TryGetValue( characterName, out var collection ) - ? collection - : ModCollection.Empty ); + if( !Penumbra.ModManager.Collections.Collections.TryGetValue( collectionName, out var collection ) ) + { + collection = ModCollection.Empty; + } + + if( collection.Cache != null ) + { + return collection.Cache.ChangedItems; + } + + PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); + return new Dictionary< string, object? >(); } - - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + catch( Exception e ) { - CheckInitialized(); - try - { - if( Path.IsPathRooted( resolvedPath ) ) - { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); - } - - return Dalamud.GameData.GetFile< T >( resolvedPath ); - } - catch( Exception e ) - { - PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); - return null; - } - } - - public T? GetFile< T >( string gamePath ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath ) ); - - public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource - => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); - - public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) - { - CheckInitialized(); - try - { - var modManager = Service< ModManager >.Get(); - if( !modManager.Collections.Collections.TryGetValue( collectionName, out var collection ) ) - { - collection = ModCollection.Empty; - } - - if( collection.Cache != null ) - { - return collection.Cache.ChangedItems; - } - - PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); - return new Dictionary< string, object? >(); - - } - catch( Exception e ) - { - PluginLog.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); - throw; - } + PluginLog.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); + throw; } } } \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 9c6142a8..8d360422 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,69 +1,73 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Numerics; using Dalamud.Configuration; using Dalamud.Logging; -namespace Penumbra +namespace Penumbra; + +[Serializable] +public class Configuration : IPluginConfiguration { - [Serializable] - public class Configuration : IPluginConfiguration + private const int CurrentVersion = 1; + + public int Version { get; set; } = CurrentVersion; + + public bool IsEnabled { get; set; } = true; +#if DEBUG + public bool DebugMode { get; set; } = true; +#else + public bool DebugMode { get; set; } = false; +#endif + public bool ScaleModSelector { get; set; } = false; + public bool ShowAdvanced { get; set; } + + public bool DisableFileSystemNotifications { get; set; } + + public bool DisableSoundStreaming { get; set; } = true; + public bool EnableHttpApi { get; set; } + public bool EnablePlayerWatch { get; set; } = false; + public int WaitFrames { get; set; } = 30; + + public string ModDirectory { get; set; } = string.Empty; + public string TempDirectory { get; set; } = string.Empty; + + public string CurrentCollection { get; set; } = "Default"; + public string DefaultCollection { get; set; } = "Default"; + public string ForcedCollection { get; set; } = ""; + + public bool SortFoldersFirst { get; set; } = false; + public bool HasReadCharacterCollectionDesc { get; set; } = false; + + public Dictionary< string, string > CharacterCollections { get; set; } = new(); + public Dictionary< string, string > ModSortOrder { get; set; } = new(); + + public bool InvertModListOrder { internal get; set; } + + public static Configuration Load() { - private const int CurrentVersion = 1; - - public int Version { get; set; } = CurrentVersion; - - public bool IsEnabled { get; set; } = true; - - public bool ScaleModSelector { get; set; } = false; - public bool ShowAdvanced { get; set; } - - public bool DisableFileSystemNotifications { get; set; } - - public bool DisableSoundStreaming { get; set; } = true; - public bool EnableHttpApi { get; set; } - public bool EnablePlayerWatch { get; set; } = false; - public int WaitFrames { get; set; } = 30; - - public string ModDirectory { get; set; } = string.Empty; - public string TempDirectory { get; set; } = string.Empty; - - public string CurrentCollection { get; set; } = "Default"; - public string DefaultCollection { get; set; } = "Default"; - public string ForcedCollection { get; set; } = ""; - - public bool SortFoldersFirst { get; set; } = false; - public bool HasReadCharacterCollectionDesc { get; set; } = false; - - public Dictionary< string, string > CharacterCollections { get; set; } = new(); - public Dictionary< string, string > ModSortOrder { get; set; } = new(); - - public bool InvertModListOrder { internal get; set; } - - public static Configuration Load() + var configuration = Dalamud.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + if( configuration.Version == CurrentVersion ) { - var configuration = Dalamud.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - if( configuration.Version == CurrentVersion ) - { - return configuration; - } - - MigrateConfiguration.Version0To1( configuration ); - configuration.Save(); - return configuration; } - public void Save() + MigrateConfiguration.Version0To1( configuration ); + configuration.Save(); + + return configuration; + } + + public void Save() + { + try { - try - { - Dalamud.PluginInterface.SavePluginConfig( this ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not save plugin configuration:\n{e}" ); - } + Dalamud.PluginInterface.SavePluginConfig( this ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save plugin configuration:\n{e}" ); } } } \ No newline at end of file diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index e801a477..343c128c 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -155,7 +155,7 @@ namespace Penumbra.Importer { try { - if( !Service< MetaDefaults >.Get().CheckAgainstDefault( manipulation ) ) + if( Penumbra.MetaDefaults.CheckAgainstDefault( manipulation ) ) { Manipulations.Add( manipulation ); } diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs new file mode 100644 index 00000000..f73c654f --- /dev/null +++ b/Penumbra/Interop/CharacterUtility.cs @@ -0,0 +1,75 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; + +namespace Penumbra.Interop; + +public unsafe class CharacterUtility : IDisposable +{ + // A static pointer to the CharacterUtility address. + [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", ScanType = ScanType.StaticAddress )] + private readonly Structs.CharacterUtility** _characterUtilityAddress = null; + + // The initial function in which all the character resources get loaded. + public delegate void LoadDataFilesDelegate( Structs.CharacterUtility* characterUtility ); + + [Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" )] + public Hook< LoadDataFilesDelegate > LoadDataFilesHook = null!; + + public Structs.CharacterUtility* Address + => *_characterUtilityAddress; + + public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[Structs.CharacterUtility.NumResources]; + + public CharacterUtility() + { + SignatureHelper.Initialise( this ); + LoadDataFilesHook.Enable(); + } + + // Self-disabling hook to set default resources after loading them. + private void LoadDataFilesDetour( Structs.CharacterUtility* characterUtility ) + { + LoadDataFilesHook.Original( characterUtility ); + LoadDefaultResources(); + PluginLog.Debug( "Character Utility resources loaded and defaults stored, disabling hook." ); + LoadDataFilesHook.Disable(); + } + + // We store the default data of the resources so we can always restore them. + private void LoadDefaultResources() + { + for( var i = 0; i < Structs.CharacterUtility.NumResources; ++i ) + { + var resource = ( Structs.ResourceHandle* )Address->Resources[ i ]; + DefaultResources[ i ] = resource->GetData(); + } + } + + // Set the data of one of the stored resources to a given pointer and length. + public bool SetResource( int idx, IntPtr data, int length ) + { + var resource = ( Structs.ResourceHandle* )Address->Resources[ idx ]; + var ret = resource->SetData( data, length ); + PluginLog.Verbose( "Set resource {Idx} to 0x{NewData:X} ({NewLength} bytes).", idx, ( ulong )data, length ); + return ret; + } + + // Reset the data of one of the stored resources to its default values. + public void ResetResource( int idx ) + { + var resource = ( Structs.ResourceHandle* )Address->Resources[ idx ]; + resource->SetData( DefaultResources[ idx ].Address, DefaultResources[ idx ].Size ); + } + + public void Dispose() + { + for( var i = 0; i < Structs.CharacterUtility.NumResources; ++i ) + { + ResetResource( i ); + } + + LoadDataFilesHook.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/MusicManager.cs b/Penumbra/Interop/MusicManager.cs index 7c46427c..39885eef 100644 --- a/Penumbra/Interop/MusicManager.cs +++ b/Penumbra/Interop/MusicManager.cs @@ -1,46 +1,39 @@ using System; using Dalamud.Logging; -namespace Penumbra.Interop +namespace Penumbra.Interop; + +// Use this to disable streaming of specific soundfiles, +// which will allow replacement of .scd files. +public unsafe class MusicManager { - // Use this to disable streaming of specific soundfiles, - // which will allow replacement of .scd files. - public unsafe class MusicManager + private readonly IntPtr _musicManager; + + public MusicManager() { - private readonly IntPtr _musicManager; - - public MusicManager( ) - { - var framework = Dalamud.Framework.Address.BaseAddress; - - // the wildcard is basically the framework offset we want (lol) - // .text:000000000009051A 48 8B 8E 18 2A 00 00 mov rcx, [rsi+2A18h] - // .text:0000000000090521 39 78 20 cmp [rax+20h], edi - // .text:0000000000090524 0F 94 C2 setz dl - // .text:0000000000090527 45 33 C0 xor r8d, r8d - // .text:000000000009052A E8 41 1C 15 00 call musicInit - var musicInitCallLocation = Dalamud.SigScanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" ); - var musicManagerOffset = *( int* )( musicInitCallLocation + 3 ); - PluginLog.Debug( "Found MusicInitCall location at 0x{Location:X16}. Framework offset for MusicManager is 0x{Offset:X8}", - musicInitCallLocation.ToInt64(), musicManagerOffset ); - _musicManager = *( IntPtr* )( framework + musicManagerOffset ); - PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager.ToInt64() ); - } - - public bool StreamingEnabled - { - get => *( bool* )( _musicManager + 50 ); - private set - { - PluginLog.Debug( value ? "Music streaming enabled." : "Music streaming disabled." ); - *( bool* )( _musicManager + 50 ) = value; - } - } - - public void EnableStreaming() - => StreamingEnabled = true; - - public void DisableStreaming() - => StreamingEnabled = false; + var framework = Dalamud.Framework.Address.BaseAddress; + // The wildcard is the offset in framework to the MusicManager in Framework. + var musicInitCallLocation = Dalamud.SigScanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" ); + var musicManagerOffset = *( int* )( musicInitCallLocation + 3 ); + PluginLog.Debug( "Found MusicInitCall location at 0x{Location:X16}. Framework offset for MusicManager is 0x{Offset:X8}", + musicInitCallLocation.ToInt64(), musicManagerOffset ); + _musicManager = *( IntPtr* )( framework + musicManagerOffset ); + PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager.ToInt64() ); } + + public bool StreamingEnabled + { + get => *( bool* )( _musicManager + 50 ); + private set + { + PluginLog.Debug( value ? "Music streaming enabled." : "Music streaming disabled." ); + *( bool* )( _musicManager + 50 ) = value; + } + } + + public void EnableStreaming() + => StreamingEnabled = true; + + public void DisableStreaming() + => StreamingEnabled = false; } \ No newline at end of file diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index b8000f9e..2b497874 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -354,7 +354,7 @@ namespace Penumbra.Interop { if( actor != null ) { - RedrawObjectIntern( actor.ObjectId, actor.Name.ToString(), settings ); + RedrawObjectIntern( actor.ObjectId, actor.Name.ToString(), RedrawType.WithoutSettings ); // TODO settings ); } } diff --git a/Penumbra/Interop/PathResolver.cs b/Penumbra/Interop/PathResolver.cs index 1fce700d..b94c2e86 100644 --- a/Penumbra/Interop/PathResolver.cs +++ b/Penumbra/Interop/PathResolver.cs @@ -1,158 +1,440 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; using Dalamud.Hooking; 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.ByteString; 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 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", - DetourName = "ResolveMdlPathDetour" )] - public Hook< ResolveMdlImcPath >? ResolveMdlPathHook; - - [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]; - - public static Dictionary< string, ModCollection > Dict = new(); - - 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 ) - { - 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; - } - } - } - - 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 ) - => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); - - public PathResolver() - { - SignatureHelper.Initialise( this ); - Enable(); - } - - public void Enable() - { - ResolveMdlPathHook?.Enable(); - ResolveMtrlPathHook?.Enable(); - ResolveImcPathHook?.Enable(); - LoadMtrlTexHook?.Enable(); - } - - public void Disable() - { - ResolveMdlPathHook?.Disable(); - ResolveMtrlPathHook?.Disable(); - ResolveImcPathHook?.Disable(); - LoadMtrlTexHook?.Disable(); - } - + //public delegate IntPtr ResolveMdlImcPathDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ); + //public delegate IntPtr ResolveMtrlPathDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ); + //public delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); + //public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); + //public delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ); + //public delegate void CharacterBaseDestructorDelegate( IntPtr drawBase ); + // + //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 41 ?? 48 83 ?? ?? 45 8B ?? 49 8B ?? 48 8B ?? 48 8B ?? 41", + // DetourName = "ResolveMdlPathDetour" )] + //public Hook< ResolveMdlImcPathDelegate >? ResolveMdlPathHook; + // + //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 48 83 ?? ?? 49 8B ?? 48 8B ?? 48 8B ?? 41 83 ?? ?? 0F", + // DetourName = "ResolveMtrlPathDetour" )] + //public Hook< ResolveMtrlPathDelegate >? ResolveMtrlPathHook; + // + //[Signature( "40 ?? 48 83 ?? ?? 4D 8B ?? 48 8B ?? 41", DetourName = "ResolveImcPathDetour" )] + //public Hook< ResolveMdlImcPathDelegate >? ResolveImcPathHook; + // + //[Signature( "4C 8B ?? ?? 89 ?? ?? ?? 89 ?? ?? 55 57 41 ?? 41", DetourName = "LoadMtrlTexDetour" )] + //public Hook< LoadMtrlFilesDelegate >? LoadMtrlTexHook; + // + //[Signature( "?? 89 ?? ?? ?? 57 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? ?? 89 ?? ?? ?? ?? ?? ?? 44 ?? ?? ?? ?? ?? ?? ?? 4C", + // DetourName = "LoadMtrlShpkDetour" )] + //public Hook< LoadMtrlFilesDelegate >? LoadMtrlShpkHook; + // + //[Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40" )] + //public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; + // + //[Signature( + // "40 ?? 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? ?? 89 ?? ?? ?? ?? ?? ?? 48 8B ?? 48 8B ?? ?? ?? ?? ?? E8 ?? ?? ?? ?? ?? BB" )] + //public Hook< EnableDrawDelegate >? EnableDrawHook; + // + //[Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30", + // DetourName = "CharacterBaseDestructorDetour" )] + //public Hook< CharacterBaseDestructorDelegate >? CharacterBaseDestructorHook; + // + //public delegate void UpdateModelDelegate( IntPtr drawObject ); + // + //[Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = "UpdateModelsDetour" )] + //public Hook< UpdateModelDelegate >? UpdateModelsHook; + // + //public delegate void SetupConnectorModelAttributesDelegate( IntPtr drawObject, IntPtr unk ); + // + //[Signature( "?? 89 ?? ?? ?? ?? 89 ?? ?? ?? ?? 89 ?? ?? ?? 57 41 ?? 41 ?? 41 ?? 41 ?? 48 83 ?? ?? 8B ?? ?? ?? 4C", + // DetourName = "SetupConnectorModelAttributesDetour" )] + //public Hook< SetupConnectorModelAttributesDelegate >? SetupConnectorModelAttributesHook; + // + //public delegate void SetupModelAttributesDelegate( IntPtr drawObject ); + // + //[Signature( "48 89 6C 24 ?? 56 57 41 54 41 55 41 56 48 83 EC 20", DetourName = "SetupModelAttributesDetour" )] + //public Hook< SetupModelAttributesDelegate >? SetupModelAttributesHook; + // + //[Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C", + // DetourName = "GetSlotEqpFlagIndirectDetour" )] + //public Hook< SetupModelAttributesDelegate >? GetSlotEqpFlagIndirectHook; + // + //public delegate void ApplyVisorStuffDelegate( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ); + // + //[Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "ApplyVisorStuffDetour" )] + //public Hook< ApplyVisorStuffDelegate >? ApplyVisorStuffHook; + // + //private readonly ResourceLoader _loader; + //private readonly ResidentResourceManager _resident; + //internal readonly Dictionary< IntPtr, int > _drawObjectToObject = new(); + //internal readonly Dictionary< Utf8String, ModCollection > _pathCollections = new(); + // + //internal GameObject* _lastGameObject = null; + //internal DrawObject* _lastDrawObject = null; + // + //private bool EqpDataChanged = false; + //private IntPtr DefaultEqpData; + //private int DefaultEqpLength; + // + //private void ApplyVisorStuffDetour( IntPtr drawObject, IntPtr unk1, float unk2, IntPtr unk3, ushort unk4, char unk5 ) + //{ + // PluginLog.Information( $"{drawObject:X} {unk1:X} {unk2} {unk3:X} {unk4} {unk5} {( ulong )FindParent( drawObject ):X}" ); + // ApplyVisorStuffHook!.Original( drawObject, unk1, unk2, unk3, unk4, unk5 ); + //} + // + //private void GetSlotEqpFlagIndirectDetour( IntPtr drawObject ) + //{ + // if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 ) + // { + // return; + // } + // + // ChangeEqp( drawObject ); + // GetSlotEqpFlagIndirectHook!.Original( drawObject ); + // RestoreEqp(); + //} + // + //private void ChangeEqp( IntPtr drawObject ) + //{ + // var parent = FindParent( drawObject ); + // if( parent == null ) + // { + // return; + // } + // + // var name = new Utf8String( parent->Name ); + // if( name.Length == 0 ) + // { + // return; + // } + // + // var charName = name.ToString(); + // if( !Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( charName, out var collection ) ) + // { + // collection = Service< ModManager >.Get().Collections.DefaultCollection; + // } + // + // if( collection.Cache == null ) + // { + // collection = Service< ModManager >.Get().Collections.ForcedCollection; + // } + // + // var data = collection.Cache?.MetaManipulations.EqpData; + // if( data == null || data.Length == 0 ) + // { + // return; + // } + // + // _resident.CharacterUtility->EqpResource->SetData( data ); + // PluginLog.Information( $"Changed eqp data to {collection.Name}." ); + // EqpDataChanged = true; + //} + // + //private void RestoreEqp() + //{ + // if( !EqpDataChanged ) + // { + // return; + // } + // + // _resident.CharacterUtility->EqpResource->SetData( new ReadOnlySpan< byte >( ( void* )DefaultEqpData, DefaultEqpLength ) ); + // PluginLog.Information( $"Changed eqp data back." ); + // EqpDataChanged = false; + //} + // + //private void SetupModelAttributesDetour( IntPtr drawObject ) + //{ + // ChangeEqp( drawObject ); + // SetupModelAttributesHook!.Original( drawObject ); + // RestoreEqp(); + //} + // + //private void UpdateModelsDetour( IntPtr drawObject ) + //{ + // if( *( int* )( drawObject + 0x90c ) == 0 ) + // { + // return; + // } + // + // ChangeEqp( drawObject ); + // UpdateModelsHook!.Original.Invoke( drawObject ); + // RestoreEqp(); + //} + // + //private void SetupConnectorModelAttributesDetour( IntPtr drawObject, IntPtr unk ) + //{ + // ChangeEqp( drawObject ); + // SetupConnectorModelAttributesHook!.Original.Invoke( drawObject, unk ); + // RestoreEqp(); + //} + // + //private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d ) + //{ + // _lastGameObject = ( GameObject* )gameObject; + // EnableDrawHook!.Original.Invoke( gameObject, b, c, d ); + // _lastGameObject = null; + //} + // + //private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d ) + //{ + // var ret = CharacterBaseCreateHook!.Original( a, b, c, d ); + // if( _lastGameObject != null ) + // { + // _drawObjectToObject[ ret ] = _lastGameObject->ObjectIndex; + // } + // + // return ret; + //} + // + //private void CharacterBaseDestructorDetour( IntPtr drawBase ) + //{ + // _drawObjectToObject.Remove( drawBase ); + // CharacterBaseDestructorHook!.Original.Invoke( drawBase ); + //} + // + //private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject ) + //{ + // gameObject = ( GameObject* )( Dalamud.Objects[ gameObjectIdx ]?.Address ?? IntPtr.Zero ); + // if( gameObject != null && gameObject->DrawObject == ( DrawObject* )drawObject ) + // { + // return true; + // } + // + // _drawObjectToObject.Remove( drawObject ); + // return false; + //} + // + //private GameObject* FindParent( IntPtr drawObject ) + //{ + // if( _drawObjectToObject.TryGetValue( drawObject, out var gameObjectIdx ) ) + // { + // if( VerifyEntry( drawObject, gameObjectIdx, out var gameObject ) ) + // { + // return gameObject; + // } + // + // _drawObjectToObject.Remove( drawObject ); + // } + // + // if( _lastGameObject != null && ( _lastGameObject->DrawObject == null || _lastGameObject->DrawObject == ( DrawObject* )drawObject ) ) + // { + // return _lastGameObject; + // } + // + // return null; + //} + // + //private void SetCollection( Utf8String path, ModCollection? collection ) + //{ + // if( collection == null ) + // { + // _pathCollections.Remove( path ); + // } + // else if( _pathCollections.ContainsKey( path ) ) + // { + // _pathCollections[ path ] = collection; + // } + // else + // { + // _pathCollections[ path.Clone() ] = collection; + // } + //} + // + //private void LoadMtrlTexHelper( IntPtr mtrlResourceHandle ) + //{ + // if( mtrlResourceHandle == IntPtr.Zero ) + // { + // return; + // } + // + // var numTex = *( byte* )( mtrlResourceHandle + 0xFA ); + // if( numTex == 0 ) + // { + // return; + // } + // + // var handle = ( Structs.ResourceHandle* )mtrlResourceHandle; + // var mtrlName = Utf8String.FromSpanUnsafe( handle->FileNameSpan(), true, null, true ); + // var collection = _pathCollections.TryGetValue( mtrlName, out var c ) ? c : null; + // var texSpace = *( byte** )( mtrlResourceHandle + 0xD0 ); + // for( var i = 0; i < numTex; ++i ) + // { + // var texStringPtr = ( byte* )( *( ulong* )( mtrlResourceHandle + 0xE0 ) + *( ushort* )( texSpace + 8 + i * 16 ) ); + // var texString = new Utf8String( texStringPtr ); + // SetCollection( texString, collection ); + // } + //} + // + //private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle ) + //{ + // LoadMtrlTexHelper( mtrlResourceHandle ); + // return LoadMtrlTexHook!.Original( mtrlResourceHandle ); + //} + // + //private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) + // => LoadMtrlShpkHook!.Original( mtrlResourceHandle ); + // + //private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) + //{ + // if( path == IntPtr.Zero ) + // { + // return path; + // } + // + // var p = new Utf8String( ( byte* )path ); + // + // var parent = FindParent( drawObject ); + // if( parent == null ) + // { + // return path; + // } + // + // var name = new Utf8String( parent->Name ); + // if( name.Length == 0 ) + // { + // return path; + // } + // + // var charName = name.ToString(); + // var gamePath = new Utf8String( ( byte* )path ); + // if( !Service< ModManager >.Get().Collections.CharacterCollection.TryGetValue( charName, out var collection ) ) + // { + // SetCollection( gamePath, null ); + // return path; + // } + // + // SetCollection( gamePath, collection ); + // return path; + //} + // + //private IntPtr ResolveMdlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + //{ + // ChangeEqp( drawObject ); + // var ret = ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, unk4 ) ); + // RestoreEqp(); + // return ret; + //} + // + //private IntPtr ResolveImcPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 ) + // => ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) ); + // + //private IntPtr ResolveMtrlPathDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, IntPtr unk5 ) + // => ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) ); + // + //public PathResolver( ResourceLoader loader, ResidentResourceManager resident ) + //{ + // _loader = loader; + // _resident = resident; + // SignatureHelper.Initialise( this ); + // var data = _resident.CharacterUtility->EqpResource->GetData(); + // fixed( byte* ptr = data ) + // { + // DefaultEqpData = ( IntPtr )ptr; + // } + // + // DefaultEqpLength = data.Length; + // Enable(); + // foreach( var gameObject in Dalamud.Objects ) + // { + // var ptr = ( GameObject* )gameObject.Address; + // if( ptr->IsCharacter() && ptr->DrawObject != null ) + // { + // _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ptr->ObjectIndex; + // } + // } + //} + // + // + //private (FullPath?, object?) CharacterReplacer( NewGamePath path ) + //{ + // var modManager = Service< ModManager >.Get(); + // var gamePath = new GamePath( path.ToString() ); + // var nonDefault = _pathCollections.TryGetValue( path.Path, out var collection ); + // if( !nonDefault ) + // { + // collection = modManager.Collections.DefaultCollection; + // } + // + // var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); + // if( resolved == null ) + // { + // resolved = modManager.Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); + // if( resolved == null ) + // { + // return ( null, collection ); + // } + // + // collection = modManager.Collections.ForcedCollection; + // } + // + // var fullPath = new FullPath( resolved ); + // if( nonDefault ) + // { + // SetCollection( fullPath.InternalName, nonDefault ? collection : null ); + // } + // + // return ( fullPath, collection ); + //} + // + //public void Enable() + //{ + // ResolveMdlPathHook?.Enable(); + // ResolveMtrlPathHook?.Enable(); + // ResolveImcPathHook?.Enable(); + // LoadMtrlTexHook?.Enable(); + // LoadMtrlShpkHook?.Enable(); + // EnableDrawHook?.Enable(); + // CharacterBaseCreateHook?.Enable(); + // _loader.ResolvePath = CharacterReplacer; + // CharacterBaseDestructorHook?.Enable(); + // SetupConnectorModelAttributesHook?.Enable(); + // UpdateModelsHook?.Enable(); + // SetupModelAttributesHook?.Enable(); + // ApplyVisorStuffHook?.Enable(); + //} + // + //public void Disable() + //{ + // _loader.ResolvePath = ResourceLoader.DefaultReplacer; + // ResolveMdlPathHook?.Disable(); + // ResolveMtrlPathHook?.Disable(); + // ResolveImcPathHook?.Disable(); + // LoadMtrlTexHook?.Disable(); + // LoadMtrlShpkHook?.Disable(); + // EnableDrawHook?.Disable(); + // CharacterBaseCreateHook?.Disable(); + // CharacterBaseDestructorHook?.Disable(); + // SetupConnectorModelAttributesHook?.Disable(); + // UpdateModelsHook?.Disable(); + // SetupModelAttributesHook?.Disable(); + // ApplyVisorStuffHook?.Disable(); + //} + // public void Dispose() { - ResolveMdlPathHook?.Dispose(); - ResolveMtrlPathHook?.Dispose(); - ResolveImcPathHook?.Dispose(); - LoadMtrlTexHook?.Dispose(); + // Disable(); + // ResolveMdlPathHook?.Dispose(); + // ResolveMtrlPathHook?.Dispose(); + // ResolveImcPathHook?.Dispose(); + // LoadMtrlTexHook?.Dispose(); + // LoadMtrlShpkHook?.Dispose(); + // EnableDrawHook?.Dispose(); + // CharacterBaseCreateHook?.Dispose(); + // CharacterBaseDestructorHook?.Dispose(); + // SetupConnectorModelAttributesHook?.Dispose(); + // UpdateModelsHook?.Dispose(); + // SetupModelAttributesHook?.Dispose(); + // ApplyVisorStuffHook?.Dispose(); } } \ No newline at end of file diff --git a/Penumbra/Interop/ResidentResourceManager.cs b/Penumbra/Interop/ResidentResourceManager.cs new file mode 100644 index 00000000..dbe49df1 --- /dev/null +++ b/Penumbra/Interop/ResidentResourceManager.cs @@ -0,0 +1,36 @@ +using Dalamud.Logging; +using Dalamud.Utility.Signatures; + +namespace Penumbra.Interop; + +public unsafe class ResidentResourceManager +{ + // Some attach and physics files are stored in the resident resource manager, and we need to manually trigger a reload of them to get them to apply. + public delegate void* ResidentResourceDelegate( void* residentResourceManager ); + + [Signature( "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A" )] + public ResidentResourceDelegate LoadPlayerResources = null!; + + [Signature( "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08" )] + public ResidentResourceDelegate UnloadPlayerResources = null!; + + // A static pointer to the resident resource manager address. + [Signature( "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05", ScanType = ScanType.StaticAddress )] + private readonly void** _residentResourceManagerAddress = null; + + public void* Address + => *_residentResourceManagerAddress; + + public ResidentResourceManager() + { + SignatureHelper.Initialise( this ); + } + + // Reload certain player resources by force. + public void Reload() + { + PluginLog.Debug( "Reload of resident resources triggered." ); + UnloadPlayerResources.Invoke( Address ); + LoadPlayerResources.Invoke( Address ); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/ResidentResources.cs b/Penumbra/Interop/ResidentResources.cs deleted file mode 100644 index e1d43b62..00000000 --- a/Penumbra/Interop/ResidentResources.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Dalamud.Logging; -using Penumbra.Structs; -using Penumbra.Util; -using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; - -namespace Penumbra.Interop -{ - public class ResidentResources - { - private const int NumResources = 85; - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* LoadPlayerResourcesPrototype( IntPtr pResourceManager ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* UnloadPlayerResourcesPrototype( IntPtr pResourceManager ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* LoadCharacterResourcesPrototype( CharacterUtility* pCharacterResourceManager ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* UnloadCharacterResourcePrototype( IntPtr resource ); - - - public LoadPlayerResourcesPrototype LoadPlayerResources { get; } - public UnloadPlayerResourcesPrototype UnloadPlayerResources { get; } - public LoadCharacterResourcesPrototype LoadDataFiles { get; } - public UnloadCharacterResourcePrototype UnloadCharacterResource { get; } - - // Object addresses - private readonly IntPtr _residentResourceManagerAddress; - - public IntPtr ResidentResourceManager - => Marshal.ReadIntPtr( _residentResourceManagerAddress ); - - private readonly IntPtr _characterUtilityAddress; - - public unsafe CharacterUtility* CharacterUtility - => ( CharacterUtility* )Marshal.ReadIntPtr( _characterUtilityAddress ).ToPointer(); - - public ResidentResources() - { - var module = Dalamud.SigScanner.Module.BaseAddress.ToInt64(); - var loadPlayerResourcesAddress = - Dalamud.SigScanner.ScanText( - "E8 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? BA ?? ?? ?? ?? 41 B8 ?? ?? ?? ?? 48 8B 48 30 48 8B 01 FF 50 10 48 85 C0 74 0A" ); - GeneralUtil.PrintDebugAddress( "LoadPlayerResources", loadPlayerResourcesAddress ); - - var unloadPlayerResourcesAddress = - Dalamud.SigScanner.ScanText( - "41 55 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4C 8B E9 48 83 C1 08" ); - GeneralUtil.PrintDebugAddress( "UnloadPlayerResources", unloadPlayerResourcesAddress ); - - var loadDataFilesAddress = Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" ); - GeneralUtil.PrintDebugAddress( "LoadDataFiles", loadDataFilesAddress ); - - var unloadCharacterResourceAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? FF 4C 89 37 48 83 C7 08 48 83 ED 01 75 ?? 48 8B CB" ); - GeneralUtil.PrintDebugAddress( "UnloadCharacterResource", unloadCharacterResourceAddress ); - - _residentResourceManagerAddress = Dalamud.SigScanner.GetStaticAddressFromSig( "0F 44 FE 48 8B 0D ?? ?? ?? ?? 48 85 C9 74 05" ); - GeneralUtil.PrintDebugAddress( "ResidentResourceManager", _residentResourceManagerAddress ); - - _characterUtilityAddress = - Dalamud.SigScanner.GetStaticAddressFromSig( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" ); - GeneralUtil.PrintDebugAddress( "CharacterUtility", _characterUtilityAddress ); - - LoadPlayerResources = Marshal.GetDelegateForFunctionPointer< LoadPlayerResourcesPrototype >( loadPlayerResourcesAddress ); - UnloadPlayerResources = Marshal.GetDelegateForFunctionPointer< UnloadPlayerResourcesPrototype >( unloadPlayerResourcesAddress ); - LoadDataFiles = Marshal.GetDelegateForFunctionPointer< LoadCharacterResourcesPrototype >( loadDataFilesAddress ); - UnloadCharacterResource = - Marshal.GetDelegateForFunctionPointer< UnloadCharacterResourcePrototype >( unloadCharacterResourceAddress ); - } - - // Forces the reload of a specific set of 85 files, notably containing the eqp, eqdp, gmp and est tables, by filename. - public unsafe void ReloadResidentResources() - { - ReloadCharacterResources(); - - UnloadPlayerResources( ResidentResourceManager ); - LoadPlayerResources( ResidentResourceManager ); - } - - public unsafe string ResourceToPath( byte* resource ) - => Marshal.PtrToStringAnsi( new IntPtr( *( char** )( resource + 9 * 8 ) ) )!; - - private unsafe void ReloadCharacterResources() - { - var oldResources = new IntPtr[NumResources]; - var resources = new IntPtr( &CharacterUtility->Resources ); - var pResources = ( void** )resources.ToPointer(); - - Marshal.Copy( resources, oldResources, 0, NumResources ); - - LoadDataFiles( CharacterUtility ); - - for( var i = 0; i < NumResources; i++ ) - { - var handle = ( ResourceHandle* )oldResources[ i ]; - if( oldResources[ i ].ToPointer() == pResources[ i ] ) - { - PluginLog.Verbose( $"Unchanged resource: {ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}" ); - ( ( ResourceHandle* )oldResources[ i ] )->DecRef(); - continue; - } - - PluginLog.Debug( "Freeing " - + $"{ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}, replaced with " - + $"{ResourceToPath( ( byte* )pResources[ i ] )}" ); - - UnloadCharacterResource( oldResources[ i ] ); - - // Temporary fix against crashes? - if( handle->RefCount <= 0 ) - { - handle->RefCount = 1; - handle->IncRef(); - handle->RefCount = 1; - } - } - } - } -} diff --git a/Penumbra/Interop/ResourceLoader.Debug.cs b/Penumbra/Interop/ResourceLoader.Debug.cs new file mode 100644 index 00000000..dcfd7f97 --- /dev/null +++ b/Penumbra/Interop/ResourceLoader.Debug.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.STD; +using Penumbra.GameData.ByteString; + +namespace Penumbra.Interop; + +public unsafe partial class ResourceLoader +{ + // A static pointer to the SE Resource Manager + [Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0", ScanType = ScanType.StaticAddress, UseFlags = SignatureUseFlags.Pointer )] + public static ResourceManager** ResourceManager; + + // Gather some debugging data about penumbra-loaded objects. + public struct DebugData + { + public ResourceHandle* OriginalResource; + public ResourceHandle* ManipulatedResource; + public NewGamePath OriginalPath; + public FullPath ManipulatedPath; + public ResourceCategory Category; + public object? ResolverInfo; + public uint Extension; + } + + private readonly SortedDictionary< FullPath, DebugData > _debugList = new(); + private readonly List< (FullPath, DebugData?) > _deleteList = new(); + + public IReadOnlyDictionary< FullPath, DebugData > DebugList + => _debugList; + + public void EnableDebug() + { + ResourceLoaded += AddModifiedDebugInfo; + } + + public void DisableDebug() + { + ResourceLoaded -= AddModifiedDebugInfo; + } + + private void AddModifiedDebugInfo( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, object? resolverInfo ) + { + if( manipulatedPath == null ) + { + return; + } + + var crc = ( uint )originalPath.Path.Crc32; + var originalResource = ( *ResourceManager )->FindResourceHandle( &handle->Category, &handle->FileType, &crc ); + _debugList[ manipulatedPath.Value ] = new DebugData() + { + OriginalResource = originalResource, + ManipulatedResource = handle, + Category = handle->Category, + Extension = handle->FileType, + OriginalPath = originalPath.Clone(), + ManipulatedPath = manipulatedPath.Value, + ResolverInfo = resolverInfo, + }; + } + + // Find a key in a StdMap. + private static TValue* FindInMap< TKey, TValue >( StdMap< TKey, TValue >* map, in TKey key ) + where TKey : unmanaged, IComparable< TKey > + where TValue : unmanaged + { + if( map == null || map->Count == 0 ) + { + return null; + } + + var node = map->Head->Parent; + while( !node->IsNil ) + { + switch( key.CompareTo( node->KeyValuePair.Item1 ) ) + { + case 0: return &node->KeyValuePair.Item2; + case < 0: + node = node->Left; + break; + default: + node = node->Right; + break; + } + } + + return null; + } + + // Iterate in tree-order through a map, applying action to each KeyValuePair. + private static void IterateMap< TKey, TValue >( StdMap< TKey, TValue >* map, Action< TKey, TValue > action ) + where TKey : unmanaged + where TValue : unmanaged + { + if( map == null || map->Count == 0 ) + { + return; + } + + for( var node = map->SmallestValue; !node->IsNil; node = node->Next() ) + { + action( node->KeyValuePair.Item1, node->KeyValuePair.Item2 ); + } + } + + + // Find a resource in the resource manager by its category, extension and crc-hash + public static ResourceHandle* FindResource( ResourceCategory cat, uint ext, uint crc32 ) + { + var manager = *ResourceManager; + var category = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )cat; + var extMap = FindInMap( category->MainMap, ext ); + if( extMap == null ) + { + return null; + } + + var ret = FindInMap( extMap->Value, crc32 ); + return ret == null ? null : ret->Value; + } + + public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph ); + public delegate void ResourceMapAction( uint ext, StdMap< uint, Pointer< ResourceHandle > >* graph ); + public delegate void ResourceAction( uint crc32, ResourceHandle* graph ); + + // Iteration functions through the resource manager. + public static void IterateGraphs( ExtMapAction action ) + { + var manager = *ResourceManager; + foreach( var resourceType in Enum.GetValues< ResourceCategory >().SkipLast( 1 ) ) + { + var graph = ( ResourceGraph.CategoryContainer* )manager->ResourceGraph->ContainerArray + ( int )resourceType; + action( resourceType, graph->MainMap ); + } + } + + public static void IterateExtMap( StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map, ResourceMapAction action ) + => IterateMap( map, ( ext, m ) => action( ext, m.Value ) ); + + public static void IterateResourceMap( StdMap< uint, Pointer< ResourceHandle > >* map, ResourceAction action ) + => IterateMap( map, ( crc, r ) => action( crc, r.Value ) ); + + public static void IterateResources( ResourceAction action ) + { + IterateGraphs( ( _, extMap ) + => IterateExtMap( extMap, ( _, resourceMap ) + => IterateResourceMap( resourceMap, action ) ) ); + } + + public void UpdateDebugInfo() + { + var manager = *ResourceManager; + _deleteList.Clear(); + foreach( var data in _debugList.Values ) + { + var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 ); + var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 ); + if( modifiedResource == null ) + { + _deleteList.Add( ( data.ManipulatedPath, null ) ); + } + else if( regularResource != data.OriginalResource || modifiedResource != data.ManipulatedResource ) + { + _deleteList.Add( ( data.ManipulatedPath, data with + { + OriginalResource = regularResource, + ManipulatedResource = modifiedResource, + } ) ); + } + } + + foreach( var (path, data) in _deleteList ) + { + if( data == null ) + { + _debugList.Remove( path ); + } + else + { + _debugList[ path ] = data.Value; + } + } + } + + // Logging functions for EnableLogging. + private static void LogPath( NewGamePath path, bool synchronous ) + => PluginLog.Information( $"Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); + + private static void LogResource( ResourceHandle* handle, NewGamePath path, FullPath? manipulatedPath, object? _ ) + { + var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); + PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); + } + + private static void LogLoadedFile( Utf8String path, bool success, bool custom ) + => PluginLog.Information( success + ? $"Loaded {path} from {( custom ? "local files" : "SqPack" )}" + : $"Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); +} \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.Replacement.cs b/Penumbra/Interop/ResourceLoader.Replacement.cs new file mode 100644 index 00000000..4792ec3f --- /dev/null +++ b/Penumbra/Interop/ResourceLoader.Replacement.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Util; +using Penumbra.Interop.Structs; +using Penumbra.Mods; +using Penumbra.Util; +using FileMode = Penumbra.Interop.Structs.FileMode; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; + +namespace Penumbra.Interop; + +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 ); + + [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 ); + + [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, + int* resourceHash, byte* path, void* unk ) + => GetResourceHandler( true, resourceManager, categoryId, resourceType, resourceHash, path, unk, false ); + + private ResourceHandle* GetResourceAsyncDetour( ResourceManager* resourceManager, ResourceCategory* categoryId, uint* 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 ) + => isSync + ? GetResourceSyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk ) + : GetResourceAsyncHook.Original( resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + + + [Conditional( "DEBUG" )] + private static void CompareHash( int local, int game, NewGamePath path ) + { + if( local != game ) + { + PluginLog.Warning( "Hash function appears to have changed. {Hash1:X8} vs {Hash2:X8} for {Path}.", game, local, path ); + } + } + + private event Action< NewGamePath, FullPath?, object? >? PathResolved; + + private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, + int* resourceHash, byte* path, void* unk, bool isUnk ) + { + if( !NewGamePath.FromPointer( path, out var gamePath ) ) + { + PluginLog.Error( "Could not create GamePath from resource path." ); + return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + } + + CompareHash( gamePath.Path.Crc32, *resourceHash, gamePath ); + + 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 ); + PathResolved?.Invoke( gamePath, resolvedPath, data ); + if( resolvedPath == null ) + { + var retUnmodified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + ResourceLoaded?.Invoke( retUnmodified, gamePath, null, data ); + return retUnmodified; + } + + // Replace the hash and path with the correct one for the replacement. + *resourceHash = resolvedPath.Value.InternalName.Crc32; + path = resolvedPath.Value.InternalName.Path; + var retModified = CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); + ResourceLoaded?.Invoke( retModified, gamePath, resolvedPath.Value, data ); + return retModified; + } + + + // 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 ); + + [Signature( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" )] + public ReadFileDelegate ReadFile = null!; + + // We hook ReadSqPack to redirect rooted files to ReadFile. + public delegate byte ReadSqPackPrototype( ResourceManager* resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync ); + + [Signature( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3", DetourName = "ReadSqPackDetour" )] + public Hook< ReadSqPackPrototype > ReadSqPackHook = null!; + + private byte ReadSqPackDetour( ResourceManager* resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync ) + { + if( !DoReplacements ) + { + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + } + + if( fileDescriptor == null || fileDescriptor->ResourceHandle == null ) + { + PluginLog.Error( "Failure to load file from SqPack: invalid File Descriptor." ); + return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + } + + var valid = NewGamePath.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() ) + { + ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( gamePath.Path, ret != 0, false ); + } + else + { + // Specify that we are loading unpacked files from the drive. + // We need to copy the actual file path in UTF16 (Windows-Unicode) on two locations, + // but since we only allow ASCII in the game paths, this is just a matter of upcasting. + fileDescriptor->FileMode = FileMode.LoadUnpackedResource; + + var fd = stackalloc byte[0x20 + 2 * gamePath.Length + 0x16]; + fileDescriptor->FileDescriptor = fd; + var fdPtr = ( char* )( fd + 0x21 ); + for( var i = 0; i < gamePath.Length; ++i ) + { + ( &fileDescriptor->Utf16FileName )[ i ] = ( char )gamePath.Path[ i ]; + fdPtr[ i ] = ( char )gamePath.Path[ i ]; + } + + ( &fileDescriptor->Utf16FileName )[ gamePath.Length ] = '\0'; + fdPtr[ gamePath.Length ] = '\0'; + + // Use the SE ReadFile function. + ret = ReadFile( resourceManager, fileDescriptor, priority, isSync ); + FileLoaded?.Invoke( gamePath.Path, ret != 0, true ); + } + + return ret; + } + + // Use the default method of path replacement. + public static (FullPath?, object?) DefaultReplacer( NewGamePath path ) + { + var gamePath = new GamePath( path.ToString() ); + var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( gamePath ); + return resolved != null ? ( new FullPath( resolved ), null ) : ( null, null ); + } + + private void DisposeHooks() + { + DisableHooks(); + ReadSqPackHook.Dispose(); + GetResourceSyncHook.Dispose(); + GetResourceAsyncHook.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.TexMdl.cs b/Penumbra/Interop/ResourceLoader.TexMdl.cs new file mode 100644 index 00000000..d5616055 --- /dev/null +++ b/Penumbra/Interop/ResourceLoader.TexMdl.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Util; +using Penumbra.Util; + +namespace Penumbra.Interop; + +// Since 6.0, Mdl and Tex Files require special treatment, probably due to datamining protection. +public unsafe partial class ResourceLoader +{ + // Custom ulong flag to signal our files as opposed to SE files. + public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); + + // We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, + // i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. + private readonly HashSet< ulong > _customFileCrc = new(); + + public IReadOnlySet< ulong > CustomFileCrc + => _customFileCrc; + + + // The function that checks a files CRC64 to determine whether it is 'protected'. + // We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. + public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc64 ); + + [Signature( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24", DetourName = "CheckFileStateDetour" )] + public Hook< CheckFileStatePrototype > CheckFileStateHook = null!; + + private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) + => _customFileCrc.Contains( crc64 ) ? CustomFileFlag : CheckFileStateHook.Original( ptr, crc64 ); + + + // We use the local functions for our own files in the extern hook. + public delegate byte LoadTexFileLocalDelegate( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3 ); + + [Signature( "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20" )] + public LoadTexFileLocalDelegate LoadTexFileLocal = null!; + + public delegate byte LoadMdlFileLocalPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2 ); + + [Signature( "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00" )] + public LoadMdlFileLocalPrototype LoadMdlFileLocal = null!; + + + // We hook the extern functions to just return the local one if given the custom flag as last argument. + public delegate byte LoadTexFileExternPrototype( ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); + + [Signature( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8", DetourName = "LoadTexFileExternDetour" )] + public Hook< LoadTexFileExternPrototype > LoadTexFileExternHook = null!; + + private byte LoadTexFileExternDetour( ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) + => ptr.Equals( CustomFileFlag ) + ? LoadTexFileLocal.Invoke( resourceHandle, unk1, unk2, unk3 ) + : LoadTexFileExternHook.Original( resourceHandle, unk1, unk2, unk3, ptr ); + + public delegate byte LoadMdlFileExternPrototype( ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3 ); + + + [Signature( "E8 ?? ?? ?? ?? EB 02 B0 F1", DetourName = "LoadMdlFileExternDetour" )] + public Hook< LoadMdlFileExternPrototype > LoadMdlFileExternHook = null!; + + private byte LoadMdlFileExternDetour( ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) + => ptr.Equals( CustomFileFlag ) + ? LoadMdlFileLocal.Invoke( resourceHandle, unk1, unk2 ) + : LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr ); + + + private void AddCrc( NewGamePath _, FullPath? path, object? _2 ) + { + if( path is { Extension: ".mdl" or ".tex" } p ) + { + _customFileCrc.Add( p.Crc64 ); + } + } + + private void EnableTexMdlTreatment() + { + PathResolved += AddCrc; + CheckFileStateHook.Enable(); + LoadTexFileExternHook.Enable(); + LoadMdlFileExternHook.Enable(); + } + + private void DisableTexMdlTreatment() + { + PathResolved -= AddCrc; + _customFileCrc.Clear(); + _customFileCrc.TrimExcess(); + CheckFileStateHook.Disable(); + LoadTexFileExternHook.Disable(); + LoadMdlFileExternHook.Disable(); + } + + private void DisposeTexMdlTreatment() + { + CheckFileStateHook.Dispose(); + LoadTexFileExternHook.Dispose(); + LoadMdlFileExternHook.Dispose(); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 09e9e160..562d2cfb 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -1,374 +1,128 @@ using System; -using System.IO; -using System.Runtime.InteropServices; -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; -using Penumbra.Util; -using FileMode = Penumbra.Structs.FileMode; +using Dalamud.Utility.Signatures; +using Penumbra.GameData.ByteString; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop; -public class ResourceLoader : IDisposable +public unsafe partial class ResourceLoader : IDisposable { - public Penumbra Penumbra { get; set; } + // Toggle whether replacing paths is active, independently of hook and event state. + public bool DoReplacements { get; private set; } - public bool IsEnabled { get; set; } + // Hooks are required for everything, even events firing. + public bool HooksEnabled { get; private set; } - public Crc32 Crc32 { get; } + // This Logging just logs all file requests, returns and loads to the Dalamud log. + // Events can be used to make smarter logging. + public bool IsLoggingEnabled { get; private set; } - public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); - - // Delegate prototypes - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate byte ReadFilePrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate byte ReadSqpackPrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType - , uint* pResourceHash, char* pPath, void* pUnknown ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType - , uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate byte LoadTexFileExternPrototype( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate byte LoadTexFileLocalPrototype( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3 ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate byte LoadMdlFileExternPrototype( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr unk3 ); - - [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate byte LoadMdlFileLocalPrototype( IntPtr resourceHandle, IntPtr unk1, bool unk2 ); - - // Hooks - public Hook< GetResourceSyncPrototype >? GetResourceSyncHook { get; private set; } - public Hook< GetResourceAsyncPrototype >? GetResourceAsyncHook { get; private set; } - public Hook< ReadSqpackPrototype >? ReadSqpackHook { get; private set; } - public Hook< CheckFileStatePrototype >? CheckFileStateHook { get; private set; } - public Hook< LoadTexFileExternPrototype >? LoadTexFileExternHook { get; private set; } - public Hook< LoadMdlFileExternPrototype >? LoadMdlFileExternHook { get; private set; } - - // Unmanaged functions - public ReadFilePrototype? ReadFile { get; private set; } - public LoadTexFileLocalPrototype? LoadTexFileLocal { get; private set; } - public LoadMdlFileLocalPrototype? LoadMdlFileLocal { get; private set; } - - public bool LogAllFiles = false; - public Regex? LogFileFilter = null; - - - public ResourceLoader( Penumbra penumbra ) + public void EnableLogging() { - Penumbra = penumbra; - Crc32 = new Crc32(); - } - - public unsafe void Init() - { - var readFileAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3 BA 05" ); - GeneralUtil.PrintDebugAddress( "ReadFile", readFileAddress ); - - var readSqpackAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? EB 05 E8 ?? ?? ?? ?? 84 C0 0F 84 ?? 00 00 00 4C 8B C3" ); - GeneralUtil.PrintDebugAddress( "ReadSqPack", readSqpackAddress ); - - var getResourceSyncAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00" ); - GeneralUtil.PrintDebugAddress( "GetResourceSync", getResourceSyncAddress ); - - var getResourceAsyncAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? 00 48 8B D8 EB ?? F0 FF 83 ?? ?? 00 00" ); - GeneralUtil.PrintDebugAddress( "GetResourceAsync", getResourceAsyncAddress ); - - var checkFileStateAddress = Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 48 85 c0 74 ?? 45 0f b6 ce 48 89 44 24" ); - GeneralUtil.PrintDebugAddress( "CheckFileState", checkFileStateAddress ); - - var loadTexFileLocalAddress = - Dalamud.SigScanner.ScanText( "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 49 8B F0 44 88 4C 24 20" ); - GeneralUtil.PrintDebugAddress( "LoadTexFileLocal", loadTexFileLocalAddress ); - - var loadTexFileExternAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8" ); - GeneralUtil.PrintDebugAddress( "LoadTexFileExtern", loadTexFileExternAddress ); - - var loadMdlFileLocalAddress = - Dalamud.SigScanner.ScanText( "40 55 53 56 57 41 56 41 57 48 8D 6C 24 D1 48 81 EC 98 00 00 00" ); - GeneralUtil.PrintDebugAddress( "LoadMdlFileLocal", loadMdlFileLocalAddress ); - - var loadMdlFileExternAddress = - Dalamud.SigScanner.ScanText( "E8 ?? ?? ?? ?? EB 02 B0 F1" ); - GeneralUtil.PrintDebugAddress( "LoadMdlFileExtern", loadMdlFileExternAddress ); - - ReadSqpackHook = new Hook< ReadSqpackPrototype >( readSqpackAddress, ReadSqpackHandler ); - GetResourceSyncHook = new Hook< GetResourceSyncPrototype >( getResourceSyncAddress, GetResourceSyncHandler ); - GetResourceAsyncHook = new Hook< GetResourceAsyncPrototype >( getResourceAsyncAddress, GetResourceAsyncHandler ); - - ReadFile = Marshal.GetDelegateForFunctionPointer< ReadFilePrototype >( readFileAddress ); - LoadTexFileLocal = Marshal.GetDelegateForFunctionPointer< LoadTexFileLocalPrototype >( loadTexFileLocalAddress ); - LoadMdlFileLocal = Marshal.GetDelegateForFunctionPointer< LoadMdlFileLocalPrototype >( loadMdlFileLocalAddress ); - - CheckFileStateHook = new Hook< CheckFileStatePrototype >( checkFileStateAddress, CheckFileStateDetour ); - LoadTexFileExternHook = new Hook< LoadTexFileExternPrototype >( loadTexFileExternAddress, LoadTexFileExternDetour ); - LoadMdlFileExternHook = new Hook< LoadMdlFileExternPrototype >( loadMdlFileExternAddress, LoadMdlFileExternDetour ); - } - - private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) - { - var modManager = Service< ModManager >.Get(); - return true || modManager.CheckCrc64( crc64 ) ? CustomFileFlag : CheckFileStateHook!.Original( ptr, crc64 ); - } - - private byte LoadTexFileExternDetour( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) - => ptr.Equals( CustomFileFlag ) - ? LoadTexFileLocal!.Invoke( resourceHandle, unk1, unk2, unk3 ) - : LoadTexFileExternHook!.Original( resourceHandle, unk1, unk2, unk3, ptr ); - - private byte LoadMdlFileExternDetour( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) - => ptr.Equals( CustomFileFlag ) - ? LoadMdlFileLocal!.Invoke( resourceHandle, unk1, unk2 ) - : LoadMdlFileExternHook!.Original( resourceHandle, unk1, unk2, ptr ); - - private unsafe void* GetResourceSyncHandler( - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown - ) - => GetResourceHandler( true, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, false ); - - private unsafe void* GetResourceAsyncHandler( - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) - => GetResourceHandler( false, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - - private unsafe void* CallOriginalHandler( - bool isSync, - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) - { - if( isSync ) - { - if( GetResourceSyncHook == null ) - { - PluginLog.Error( "[GetResourceHandler] GetResourceSync is null." ); - return null; - } - - return GetResourceSyncHook.Original( pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown ); - } - - if( GetResourceAsyncHook == null ) - { - PluginLog.Error( "[GetResourceHandler] GetResourceAsync is null." ); - return null; - } - - return GetResourceAsyncHook.Original( pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - - private unsafe void* GetResourceHandler( - bool isSync, - IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown - ) - { - string file; - var modManager = Service< ModManager >.Get(); - - if( !Penumbra.Config.IsEnabled || modManager == null ) - { - if( LogAllFiles ) - { - file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; - if( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) - { - PluginLog.Information( "[GetResourceHandler] {0}", file ); - } - } - - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - - file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; - 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 ); - } - - // path must be < 260 because statically defined array length :( - if( replacementPath == null ) - { - 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]; - Marshal.Copy( path, 0, new IntPtr( bPath ), path.Length ); - pPath = ( char* )bPath; - - Crc32.Init(); - Crc32.Update( path ); - *pResourceHash = Crc32.Checksum; - - PluginLog.Verbose( "[GetResourceHandler] resolved {GamePath} to {NewPath}", gameFsPath, replacementPath ); - - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - - - private unsafe byte ReadSqpackHandler( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ) - { - if( ReadFile == null || pFileDesc == null || pFileDesc->ResourceHandle == null ) - { - PluginLog.Error( "THIS SHOULD NOT HAPPEN" ); - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - - var gameFsPath = Marshal.PtrToStringAnsi( new IntPtr( pFileDesc->ResourceHandle->FileName() ) ); - if( gameFsPath is not { Length: < 260 } ) - { - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - - - //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; - - // note: must be utf16 - var utfPath = Encoding.Unicode.GetBytes( gameFsPath ); - - Marshal.Copy( utfPath, 0, new IntPtr( &pFileDesc->UtfFileName ), utfPath.Length ); - - var fd = stackalloc byte[0x20 + utfPath.Length + 0x16]; - Marshal.Copy( utfPath, 0, new IntPtr( fd + 0x21 ), utfPath.Length ); - - pFileDesc->FileDescriptor = fd; - return ReadFile( pFileHandler, pFileDesc, priority, isSync ); - } - - public void Enable() - { - if( IsEnabled ) + if( IsLoggingEnabled ) { return; } - if( ReadSqpackHook == null - || GetResourceSyncHook == null - || GetResourceAsyncHook == null - || CheckFileStateHook == null - || LoadTexFileExternHook == null - || LoadMdlFileExternHook == null ) + IsLoggingEnabled = true; + ResourceRequested += LogPath; + ResourceLoaded += LogResource; + FileLoaded += LogLoadedFile; + EnableHooks(); + } + + public void DisableLogging() + { + if( !IsLoggingEnabled ) { - PluginLog.Error( "[GetResourceHandler] Could not activate hooks because at least one was not set." ); return; } - ReadSqpackHook.Enable(); + IsLoggingEnabled = false; + ResourceRequested -= LogPath; + ResourceLoaded -= LogResource; + FileLoaded -= LogLoadedFile; + } + + public void EnableReplacements() + { + if( DoReplacements ) + { + return; + } + + DoReplacements = true; + EnableTexMdlTreatment(); + EnableHooks(); + } + + public void DisableReplacements() + { + if( !DoReplacements ) + { + return; + } + + DoReplacements = false; + DisableTexMdlTreatment(); + } + + public void EnableHooks() + { + if( HooksEnabled ) + { + return; + } + + HooksEnabled = true; + ReadSqPackHook.Enable(); GetResourceSyncHook.Enable(); GetResourceAsyncHook.Enable(); - CheckFileStateHook.Enable(); - LoadTexFileExternHook.Enable(); - LoadMdlFileExternHook.Enable(); - - IsEnabled = true; } - public void Disable() + public void DisableHooks() { - if( !IsEnabled ) + if( !HooksEnabled ) { return; } - ReadSqpackHook?.Disable(); - GetResourceSyncHook?.Disable(); - GetResourceAsyncHook?.Disable(); - CheckFileStateHook?.Disable(); - LoadTexFileExternHook?.Disable(); - LoadMdlFileExternHook?.Disable(); - IsEnabled = false; + HooksEnabled = false; + ReadSqPackHook.Disable(); + GetResourceSyncHook.Disable(); + GetResourceAsyncHook.Disable(); } + public ResourceLoader( Penumbra _ ) + { + SignatureHelper.Initialise( this ); + } + + // Event fired whenever a resource is requested. + public delegate void ResourceRequestedDelegate( NewGamePath path, bool synchronous ); + public event ResourceRequestedDelegate? ResourceRequested; + + // 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( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, + object? resolveData ); + + public event ResourceLoadedDelegate? ResourceLoaded; + + + // Event fired whenever a resource is newly loaded. + // Success indicates the return value of the loading function (which does not imply that the resource was actually successfully loaded) + // custom is true if the file was loaded from local files instead of the default SqPacks. + public delegate void FileLoadedDelegate( Utf8String path, bool success, bool custom ); + public event FileLoadedDelegate? FileLoaded; + + // Customization point to control how path resolving is handled. + public Func< NewGamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer; + public void Dispose() { - Disable(); - ReadSqpackHook?.Dispose(); - GetResourceSyncHook?.Dispose(); - GetResourceAsyncHook?.Dispose(); - CheckFileStateHook?.Dispose(); - LoadTexFileExternHook?.Dispose(); - LoadMdlFileExternHook?.Dispose(); + DisposeHooks(); + DisposeTexMdlTreatment(); } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs new file mode 100644 index 00000000..35f274c4 --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -0,0 +1,87 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct CharacterUtility +{ + public const int NumResources = 85; + public const int EqpIdx = 0; + public const int GmpIdx = 1; + public const int HumanCmpIdx = 63; + public const int FaceEstIdx = 64; + public const int HairEstIdx = 65; + public const int BodyEstIdx = 66; + public const int HeadEstIdx = 67; + + public static int EqdpIdx( ushort raceCode, bool accessory ) + => ( accessory ? 28 : 0 ) + + raceCode switch + { + 0101 => 2, + 0201 => 3, + 0301 => 4, + 0401 => 5, + 0501 => 6, + 0601 => 7, + 0701 => 8, + 0801 => 9, + 0901 => 10, + 1001 => 11, + 1101 => 12, + 1201 => 13, + 1301 => 14, + 1401 => 15, + 1501 => 16, + 1601 => 17, // Does not exist yet + 1701 => 18, + 1801 => 19, + 0104 => 20, + 0204 => 21, + 0504 => 22, + 0604 => 23, + 0704 => 24, + 0804 => 25, + 1304 => 26, + 1404 => 27, + 9104 => 28, + 9204 => 29, + _ => throw new ArgumentException(), + }; + + [FieldOffset( 0 )] + public void* VTable; + + [FieldOffset( 8 )] + public fixed ulong Resources[NumResources]; + + [FieldOffset( 8 + EqpIdx * 8 )] + public ResourceHandle* EqpResource; + + [FieldOffset( 8 + GmpIdx * 8 )] + public ResourceHandle* GmpResource; + + public ResourceHandle* Resource( int idx ) + => ( ResourceHandle* )Resources[ idx ]; + + public ResourceHandle* EqdpResource( ushort raceCode, bool accessory ) + => Resource( EqdpIdx( raceCode, accessory ) ); + + [FieldOffset( 8 + HumanCmpIdx * 8 )] + public ResourceHandle* HumanCmpResource; + + [FieldOffset( 8 + FaceEstIdx * 8 )] + public ResourceHandle* FaceEstResource; + + [FieldOffset( 8 + HairEstIdx * 8 )] + public ResourceHandle* HairEstResource; + + [FieldOffset( 8 + BodyEstIdx * 8 )] + public ResourceHandle* BodyEstResource; + + [FieldOffset( 8 + HeadEstIdx * 8 )] + public ResourceHandle* HeadEstResource; + + // not included resources have no known use case. +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/FileMode.cs b/Penumbra/Interop/Structs/FileMode.cs new file mode 100644 index 00000000..21270176 --- /dev/null +++ b/Penumbra/Interop/Structs/FileMode.cs @@ -0,0 +1,11 @@ +namespace Penumbra.Interop.Structs; + +public enum FileMode : uint +{ + LoadUnpackedResource = 0, + LoadFileResource = 1, // Shit in My Games uses this + + // some shit here, the game does some jump if its < 0xA for other files for some reason but there's no impl, probs debug? + LoadIndexResource = 0xA, // load index/index2 + LoadSqPackResource = 0xB, +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs new file mode 100644 index 00000000..d0f05f07 --- /dev/null +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -0,0 +1,67 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct ResourceHandle +{ + [StructLayout( LayoutKind.Explicit )] + public struct DataIndirection + { + [FieldOffset( 0x10 )] + public byte* DataPtr; + + [FieldOffset( 0x28 )] + public ulong DataLength; + } + + public const int SsoSize = 15; + + public byte* FileName() + { + if( FileNameLength > SsoSize ) + { + return FileNameData; + } + + fixed( byte** name = &FileNameData ) + { + return ( byte* )name; + } + } + + public ReadOnlySpan< byte > FileNameSpan() + => new(FileName(), FileNameLength); + + [FieldOffset( 0x48 )] + public byte* FileNameData; + + [FieldOffset( 0x58 )] + public int FileNameLength; + + [FieldOffset( 0xB0 )] + public DataIndirection* Data; + + [FieldOffset( 0xB8 )] + public uint DataLength; + + + public (IntPtr Data, int Length) GetData() + => Data != null + ? ( ( IntPtr )Data->DataPtr, ( int )Data->DataLength ) + : ( IntPtr.Zero, 0 ); + + public bool SetData( IntPtr data, int length ) + { + if( Data == null ) + { + return false; + } + + Data->DataPtr = length != 0 ? ( byte* )data : null; + Data->DataLength = ( ulong )length; + DataLength = ( uint )length; + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/SeFileDescriptor.cs b/Penumbra/Interop/Structs/SeFileDescriptor.cs new file mode 100644 index 00000000..c83f3796 --- /dev/null +++ b/Penumbra/Interop/Structs/SeFileDescriptor.cs @@ -0,0 +1,21 @@ +using System.Runtime.InteropServices; +using Penumbra.Structs; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct SeFileDescriptor +{ + [FieldOffset( 0x00 )] + public FileMode FileMode; + + [FieldOffset( 0x30 )] + public void* FileDescriptor; // + + [FieldOffset( 0x50 )] + public ResourceHandle* ResourceHandle; // + + + [FieldOffset( 0x70 )] + public char Utf16FileName; // +} \ No newline at end of file diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index e0722424..f6c84358 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -4,231 +4,231 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; +using Penumbra.GameData.ByteString; using Penumbra.Importer; using Penumbra.Meta.Files; using Penumbra.Mod; using Penumbra.Structs; using Penumbra.Util; -namespace Penumbra.Meta +namespace Penumbra.Meta; + +// Corresponds meta manipulations of any kind with the settings for a mod. +// DefaultData contains all manipulations that are active regardless of option groups. +// GroupData contains a mapping of Group -> { Options -> {Manipulations} }. +public class MetaCollection { - // Corresponds meta manipulations of any kind with the settings for a mod. - // DefaultData contains all manipulations that are active regardless of option groups. - // GroupData contains a mapping of Group -> { Options -> {Manipulations} }. - public class MetaCollection + public List< MetaManipulation > DefaultData = new(); + public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new(); + + + // Store total number of manipulations for some ease of access. + [JsonIgnore] + internal int Count; + + + // Return an enumeration of all active meta manipulations for a given mod with given settings. + public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta ) { - public List< MetaManipulation > DefaultData = new(); - public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new(); - - - // Store total number of manipulations for some ease of access. - [JsonIgnore] - internal int Count; - - - // Return an enumeration of all active meta manipulations for a given mod with given settings. - public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta ) + if( Count == DefaultData.Count ) { - if( Count == DefaultData.Count ) + return DefaultData; + } + + IEnumerable< MetaManipulation > ret = DefaultData; + + foreach( var group in modMeta.Groups ) + { + if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) ) { - return DefaultData; + continue; } - IEnumerable< MetaManipulation > ret = DefaultData; - - foreach( var group in modMeta.Groups ) + if( group.Value.SelectionType == SelectType.Single ) { - if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) ) + var settingName = group.Value.Options[ setting ].OptionName; + if( metas.TryGetValue( settingName, out var meta ) ) { - continue; + ret = ret.Concat( meta ); } - - if( group.Value.SelectionType == SelectType.Single ) + } + else + { + for( var i = 0; i < group.Value.Options.Count; ++i ) { - var settingName = group.Value.Options[ setting ].OptionName; + var flag = 1 << i; + if( ( setting & flag ) == 0 ) + { + continue; + } + + var settingName = group.Value.Options[ i ].OptionName; if( metas.TryGetValue( settingName, out var meta ) ) { ret = ret.Concat( meta ); } } - else - { - for( var i = 0; i < group.Value.Options.Count; ++i ) - { - var flag = 1 << i; - if( ( setting & flag ) == 0 ) - { - continue; - } - - var settingName = group.Value.Options[ i ].OptionName; - if( metas.TryGetValue( settingName, out var meta ) ) - { - ret = ret.Concat( meta ); - } - } - } } - - return ret; } - // Check that the collection is still basically valid, - // i.e. keep it sorted, and verify that the options stored by name are all still part of the mod, - // and that the contained manipulations are still valid and non-default manipulations. - public bool Validate( ModMeta modMeta ) + return ret; + } + + // Check that the collection is still basically valid, + // i.e. keep it sorted, and verify that the options stored by name are all still part of the mod, + // and that the contained manipulations are still valid and non-default manipulations. + public bool Validate( ModMeta modMeta ) + { + var defaultFiles = Penumbra.MetaDefaults; + SortLists(); + foreach( var group in GroupData ) { - var defaultFiles = Service< MetaDefaults >.Get(); - SortLists(); - foreach( var group in GroupData ) + if( !modMeta.Groups.TryGetValue( group.Key, out var options ) ) { - if( !modMeta.Groups.TryGetValue( group.Key, out var options ) ) + return false; + } + + foreach( var option in group.Value ) + { + if( options.Options.All( o => o.OptionName != option.Key ) ) { return false; } - foreach( var option in group.Value ) + if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) { - if( options.Options.All( o => o.OptionName != option.Key ) ) - { - return false; - } - - if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) - { - return false; - } + return false; } } - - return DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); - } - - // Re-sort all manipulations. - private void SortLists() - { - DefaultData.Sort(); - foreach( var list in GroupData.Values.SelectMany( g => g.Values ) ) - { - list.Sort(); - } } - // Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default. - // Creates the option group and the option if necessary. - private void AddMeta( string group, string option, TexToolsMeta meta ) + return DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); + } + + // Re-sort all manipulations. + private void SortLists() + { + DefaultData.Sort(); + foreach( var list in GroupData.Values.SelectMany( g => g.Values ) ) { - if( meta.Manipulations.Count == 0 ) - { - return; - } + list.Sort(); + } + } - if( group.Length == 0 ) - { - DefaultData.AddRange( meta.Manipulations ); - } - else if( option.Length == 0 ) - { } - else if( !GroupData.TryGetValue( group, out var options ) ) - { - GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, meta.Manipulations.ToList() } } ); - } - else if( !options.TryGetValue( option, out var list ) ) - { - options.Add( option, meta.Manipulations.ToList() ); - } - else - { - list.AddRange( meta.Manipulations ); - } - - Count += meta.Manipulations.Count; + // Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default. + // Creates the option group and the option if necessary. + private void AddMeta( string group, string option, TexToolsMeta meta ) + { + if( meta.Manipulations.Count == 0 ) + { + return; } - // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, - // combining them with the given ModMeta. - public void Update( IEnumerable< FullPath > files, DirectoryInfo basePath, ModMeta modMeta ) + if( group.Length == 0 ) { - DefaultData.Clear(); - GroupData.Clear(); - Count = 0; - foreach( var file in files ) - { - TexToolsMeta metaData = file.Extension.ToLowerInvariant() switch - { - ".meta" => new TexToolsMeta( File.ReadAllBytes( file.FullName ) ), - ".rgsp" => TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ), - _ => TexToolsMeta.Invalid, - }; - - if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 ) - { - continue; - } - - var path = new RelPath( file, basePath ); - var foundAny = false; - foreach( var group in modMeta.Groups ) - { - foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) - { - foundAny = true; - AddMeta( group.Key, option.OptionName, metaData ); - } - } - - if( !foundAny ) - { - AddMeta( string.Empty, string.Empty, metaData ); - } - } - - SortLists(); + DefaultData.AddRange( meta.Manipulations ); + } + else if( option.Length == 0 ) + { } + else if( !GroupData.TryGetValue( group, out var options ) ) + { + GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, meta.Manipulations.ToList() } } ); + } + else if( !options.TryGetValue( option, out var list ) ) + { + options.Add( option, meta.Manipulations.ToList() ); + } + else + { + list.AddRange( meta.Manipulations ); } - public static FileInfo FileName( DirectoryInfo basePath ) - => new( Path.Combine( basePath.FullName, "metadata_manipulations.json" ) ); + Count += meta.Manipulations.Count; + } - public void SaveToFile( FileInfo file ) + // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, + // combining them with the given ModMeta. + public void Update( IEnumerable< FullPath > files, DirectoryInfo basePath, ModMeta modMeta ) + { + DefaultData.Clear(); + GroupData.Clear(); + Count = 0; + foreach( var file in files ) { - try + var metaData = file.Extension.ToLowerInvariant() switch { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( file.FullName, text ); + ".meta" => new TexToolsMeta( File.ReadAllBytes( file.FullName ) ), + ".rgsp" => TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ), + _ => TexToolsMeta.Invalid, + }; + + if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 ) + { + continue; } - catch( Exception e ) + + var path = new RelPath( file, basePath ); + var foundAny = false; + foreach( var group in modMeta.Groups ) { - PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" ); + foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) + { + foundAny = true; + AddMeta( group.Key, option.OptionName, metaData ); + } + } + + if( !foundAny ) + { + AddMeta( string.Empty, string.Empty, metaData ); } } - public static MetaCollection? LoadFromFile( FileInfo file ) + SortLists(); + } + + public static FileInfo FileName( DirectoryInfo basePath ) + => new(Path.Combine( basePath.FullName, "metadata_manipulations.json" )); + + public void SaveToFile( FileInfo file ) + { + try { - if( !file.Exists ) + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( file.FullName, text ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" ); + } + } + + public static MetaCollection? LoadFromFile( FileInfo file ) + { + if( !file.Exists ) + { + return null; + } + + try + { + var text = File.ReadAllText( file.FullName ); + + var collection = JsonConvert.DeserializeObject< MetaCollection >( text, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); + + if( collection != null ) { - return null; + collection.Count = collection.DefaultData.Count + + collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count ); } - try - { - var text = File.ReadAllText( file.FullName ); - - var collection = JsonConvert.DeserializeObject< MetaCollection >( text, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); - - if( collection != null ) - { - collection.Count = collection.DefaultData.Count - + collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count ); - } - - return collection; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" ); - return null; - } + return collection; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" ); + return null; } } } \ No newline at end of file diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index cfb88ce2..6ff76118 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -4,182 +4,185 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Lumina.Data.Files; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Interop; using Penumbra.Meta.Files; using Penumbra.Util; -namespace Penumbra.Meta +namespace Penumbra.Meta; + +public class MetaManager : IDisposable { - public class MetaManager : IDisposable + internal class FileInformation { - internal class FileInformation + public readonly object Data; + public bool Changed; + public FullPath? CurrentFile; + public byte[] ByteData = Array.Empty< byte >(); + + public FileInformation( object data ) + => Data = data; + + public void Write( DirectoryInfo dir, GamePath originalPath ) { - public readonly object Data; - public bool Changed; - public FullPath? CurrentFile; - - public FileInformation( object data ) - => Data = data; - - public void Write( DirectoryInfo dir, GamePath originalPath ) + ByteData = Data switch { - var data = Data switch - { - EqdpFile eqdp => eqdp.WriteBytes(), - EqpFile eqp => eqp.WriteBytes(), - GmpFile gmp => gmp.WriteBytes(), - EstFile est => est.WriteBytes(), - ImcFile imc => imc.WriteBytes(), - CmpFile cmp => cmp.WriteBytes(), - _ => throw new NotImplementedException(), - }; - DisposeFile( CurrentFile ); - CurrentFile = new FullPath(TempFile.WriteNew( dir, data, $"_{originalPath.Filename()}" )); - Changed = false; - } + EqdpFile eqdp => eqdp.WriteBytes(), + EqpFile eqp => eqp.WriteBytes(), + GmpFile gmp => gmp.WriteBytes(), + EstFile est => est.WriteBytes(), + ImcFile imc => imc.WriteBytes(), + CmpFile cmp => cmp.WriteBytes(), + _ => throw new NotImplementedException(), + }; + DisposeFile( CurrentFile ); + CurrentFile = new FullPath( TempFile.WriteNew( dir, ByteData, $"_{originalPath.Filename()}" ) ); + Changed = false; + } + } + + public const string TmpDirectory = "penumbrametatmp"; + + private readonly DirectoryInfo _dir; + private readonly Dictionary< GamePath, FullPath > _resolvedFiles; + + private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); + private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); + + public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations + => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); + + public IEnumerable< (GamePath, FullPath) > Files + => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) + .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); + + public int Count + => _currentManipulations.Count; + + public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod ) + => _currentManipulations.TryGetValue( manip, out mod! ); + + public byte[] EqpData = Array.Empty< byte >(); + + private static void DisposeFile( FullPath? file ) + { + if( !( file?.Exists ?? false ) ) + { + return; } - public const string TmpDirectory = "penumbrametatmp"; - - private readonly MetaDefaults _default; - private readonly DirectoryInfo _dir; - private readonly ResidentResources _resourceManagement; - private readonly Dictionary< GamePath, FullPath > _resolvedFiles; - - private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); - private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); - - public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations - => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); - - public IEnumerable< (GamePath, FullPath) > Files - => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) - .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); - - public int Count - => _currentManipulations.Count; - - public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod ) - => _currentManipulations.TryGetValue( manip, out mod! ); - - private static void DisposeFile( FullPath? file ) + try { - if( !( file?.Exists ?? false ) ) - { - return; - } + File.Delete( file.Value.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete temporary file \"{file.Value.FullName}\":\n{e}" ); + } + } + public void Reset( bool reload = true ) + { + foreach( var file in _currentFiles ) + { + _resolvedFiles.Remove( file.Key ); + DisposeFile( file.Value.CurrentFile ); + } + + _currentManipulations.Clear(); + _currentFiles.Clear(); + ClearDirectory(); + if( reload ) + { + Penumbra.ResidentResources.Reload(); + } + } + + public void Dispose() + => Reset(); + + private static void ClearDirectory( DirectoryInfo modDir ) + { + modDir.Refresh(); + if( modDir.Exists ) + { try { - File.Delete( file.Value.FullName ); + Directory.Delete( modDir.FullName, true ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete temporary file \"{file.Value.FullName}\":\n{e}" ); - } - } - - public void Reset( bool reload = true ) - { - foreach( var file in _currentFiles ) - { - _resolvedFiles.Remove( file.Key ); - DisposeFile( file.Value.CurrentFile ); - } - - _currentManipulations.Clear(); - _currentFiles.Clear(); - ClearDirectory(); - if( reload ) - { - _resourceManagement.ReloadResidentResources(); - } - } - - public void Dispose() - => Reset(); - - private static void ClearDirectory( DirectoryInfo modDir ) - { - modDir.Refresh(); - if( modDir.Exists ) - { - try - { - Directory.Delete( modDir.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not clear temporary metafile directory \"{modDir.FullName}\":\n{e}" ); - } - } - } - - private void ClearDirectory() - => ClearDirectory( _dir ); - - public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) - { - _resolvedFiles = resolvedFiles; - _default = Service< MetaDefaults >.Get(); - _resourceManagement = Service< ResidentResources >.Get(); - _dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) ); - ClearDirectory(); - } - - public void WriteNewFiles() - { - if( _currentFiles.Any() ) - { - Directory.CreateDirectory( _dir.FullName ); - } - - foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) - { - kvp.Value.Write( _dir, kvp.Key ); - _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value; - } - } - - public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) - { - if( _currentManipulations.ContainsKey( m ) ) - { - return false; - } - - _currentManipulations.Add( m, mod ); - var gamePath = m.CorrespondingFilename(); - try - { - if( !_currentFiles.TryGetValue( gamePath, out var file ) ) - { - file = new FileInformation( _default.CreateNewFile( m ) ?? throw new IOException() ) - { - Changed = true, - CurrentFile = null, - }; - _currentFiles[ gamePath ] = file; - } - - file.Changed |= m.Type switch - { - MetaType.Eqp => m.Apply( ( EqpFile )file.Data ), - MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ), - MetaType.Gmp => m.Apply( ( GmpFile )file.Data ), - MetaType.Est => m.Apply( ( EstFile )file.Data ), - MetaType.Imc => m.Apply( ( ImcFile )file.Data ), - MetaType.Rsp => m.Apply( ( CmpFile )file.Data ), - _ => throw new NotImplementedException(), - }; - return true; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not obtain default file for manipulation {m.CorrespondingFilename()}:\n{e}" ); - return false; + PluginLog.Error( $"Could not clear temporary metafile directory \"{modDir.FullName}\":\n{e}" ); } } } + + private void ClearDirectory() + => ClearDirectory( _dir ); + + public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) + { + _resolvedFiles = resolvedFiles; + _dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) ); + ClearDirectory(); + } + + public void WriteNewFiles() + { + if( _currentFiles.Any() ) + { + Directory.CreateDirectory( _dir.FullName ); + } + + foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) + { + kvp.Value.Write( _dir, kvp.Key ); + _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value; + if( kvp.Value.Data is EqpFile ) + { + EqpData = kvp.Value.ByteData; + } + } + } + + public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) + { + if( _currentManipulations.ContainsKey( m ) ) + { + return false; + } + + _currentManipulations.Add( m, mod ); + var gamePath = m.CorrespondingFilename(); + try + { + if( !_currentFiles.TryGetValue( gamePath, out var file ) ) + { + file = new FileInformation( Penumbra.MetaDefaults.CreateNewFile( m ) ?? throw new IOException() ) + { + Changed = true, + CurrentFile = null, + }; + _currentFiles[ gamePath ] = file; + } + + file.Changed |= m.Type switch + { + MetaType.Eqp => m.Apply( ( EqpFile )file.Data ), + MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ), + MetaType.Gmp => m.Apply( ( GmpFile )file.Data ), + MetaType.Est => m.Apply( ( EstFile )file.Data ), + MetaType.Imc => m.Apply( ( ImcFile )file.Data ), + MetaType.Rsp => m.Apply( ( CmpFile )file.Data ), + _ => throw new NotImplementedException(), + }; + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not obtain default file for manipulation {m.CorrespondingFilename()}:\n{e}" ); + return false; + } + } } \ No newline at end of file diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index dc3e1510..a8f33f8c 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -65,9 +65,8 @@ namespace Penumbra.Mod private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) { - var manager = Service< ModManager >.Get(); - manager.AddMod( newDir ); - var newMod = manager.Mods[ newDir.Name ]; + Penumbra.ModManager.AddMod( newDir ); + var newMod = Penumbra.ModManager.Mods[ newDir.Name ]; newMod.Move( newSortOrder ); newMod.ComputeChangedItems(); ModFileSystem.InvokeChange(); @@ -516,11 +515,11 @@ namespace Penumbra.Mod if( group.Options.Any() ) { - meta.Groups.Add( groupDir.Name, @group ); + meta.Groups.Add( groupDir.Name, group ); } } - foreach(var collection in Service.Get().Collections.Collections.Values) + foreach(var collection in Penumbra.ModManager.Collections.Collections.Values) collection.UpdateSetting(baseDir, meta, true); } diff --git a/Penumbra/Mod/ModResources.cs b/Penumbra/Mod/ModResources.cs index d47851a5..590cda79 100644 --- a/Penumbra/Mod/ModResources.cs +++ b/Penumbra/Mod/ModResources.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Penumbra.GameData.ByteString; using Penumbra.Meta; -using Penumbra.Util; namespace Penumbra.Mod; diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 4a71de10..70145b03 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -42,9 +42,8 @@ public class CollectionManager if( ActiveCollection.Cache?.MetaManipulations.Count > 0 || newActive.Cache?.MetaManipulations.Count > 0 ) { - var resourceManager = Service< ResidentResources >.Get(); ActiveCollection = newActive; - resourceManager.ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } else { @@ -115,7 +114,7 @@ public class CollectionManager if( reloadMeta && ActiveCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) { - Service< ResidentResources >.Get().ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } } @@ -223,8 +222,7 @@ public class CollectionManager if( !CollectionChangedTo.Any() ) { ActiveCollection = c; - var resourceManager = Service< ResidentResources >.Get(); - resourceManager.ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } DefaultCollection = c; @@ -244,8 +242,7 @@ public class CollectionManager if( CollectionChangedTo == characterName && CharacterCollection.TryGetValue( characterName, out var collection ) ) { ActiveCollection = c; - var resourceManager = Service< ResidentResources >.Get(); - resourceManager.ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } CharacterCollection[ characterName ] = c; diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index 0a6153d9..92f8275a 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -145,7 +145,7 @@ namespace Penumbra.Mods Cache.UpdateMetaManipulations(); if( activeCollection ) { - Service< ResidentResources >.Get().ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } } } diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 4322b73d..c7d7cc1c 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using Dalamud.Logging; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; @@ -186,8 +187,9 @@ public class ModCollectionCache { foreach( var (file, paths) in option.OptionFiles ) { - var fullPath = new FullPath( mod.Data.BasePath, file ); - var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); + var fullPath = new FullPath( mod.Data.BasePath, + NewRelPath.FromString( file.ToString(), out var p ) ? p : NewRelPath.Empty ); // TODO + var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); if( idx < 0 ) { AddMissingFile( fullPath ); @@ -255,7 +257,14 @@ public class ModCollectionCache var file = mod.Data.Resources.ModFiles[ i ]; if( file.Exists ) { - AddFile( mod, file.ToGamePath( mod.Data.BasePath ), file ); + if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) ) + { + AddFile( mod, new GamePath( gamePath.ToString() ), file ); // TODO + } + else + { + PluginLog.Warning( $"Could not convert {file} in {mod.Data.BasePath.FullName} to GamePath." ); + } } else { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 14329de8..cabae47d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -18,18 +18,44 @@ using System.Linq; namespace Penumbra; +public class Penumbra2 // : IDalamudPlugin +{ + public string Name + => "Penumbra"; + + private const string CommandName = "/penumbra"; + + public static Configuration Config { get; private set; } = null!; + public static ResourceLoader ResourceLoader { get; private set; } = null!; + + public void Dispose() + { + ResourceLoader.Dispose(); + } +} + public class Penumbra : IDalamudPlugin { - public string Name { get; } = "Penumbra"; - public string PluginDebugTitleStr { get; } = "Penumbra - Debug Build"; + public string Name + => "Penumbra"; + + public string PluginDebugTitleStr + => "Penumbra - Debug Build"; private const string CommandName = "/penumbra"; public static Configuration Config { get; private set; } = null!; public static IPlayerWatcher PlayerWatcher { get; private set; } = null!; + public static ResidentResourceManager ResidentResources { get; private set; } = null!; + public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static MetaDefaults MetaDefaults { get; private set; } = null!; + public static ModManager ModManager { get; private set; } = null!; + + public ResourceLoader ResourceLoader { get; } - public PathResolver PathResolver { get; } + + //public PathResolver PathResolver { get; } public SettingsInterface SettingsInterface { get; } public MusicManager MusicManager { get; } public ObjectReloader ObjectReloader { get; } @@ -39,8 +65,6 @@ public class Penumbra : IDalamudPlugin private WebServer? _webServer; - private readonly ModManager _modManager; - public Penumbra( DalamudPluginInterface pluginInterface ) { Dalamud.Initialize( pluginInterface ); @@ -53,27 +77,29 @@ public class Penumbra : IDalamudPlugin MusicManager.DisableStreaming(); } - var gameUtils = Service< ResidentResources >.Set(); - PathResolver = new PathResolver(); - PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); - Service< MetaDefaults >.Set(); - _modManager = Service< ModManager >.Set(); - - _modManager.DiscoverMods(); - - ObjectReloader = new ObjectReloader( _modManager, Config.WaitFrames ); - - ResourceLoader = new ResourceLoader( this ); + ResidentResources = new ResidentResourceManager(); + CharacterUtility = new CharacterUtility(); + MetaDefaults = new MetaDefaults(); + ResourceLoader = new ResourceLoader( this ); + ModManager = new ModManager(); + ModManager.DiscoverMods(); + //PathResolver = new PathResolver( ResourceLoader, gameUtils ); + PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects ); + ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", } ); - ResourceLoader.Init(); - ResourceLoader.Enable(); + ResourceLoader.EnableReplacements(); + ResourceLoader.EnableLogging(); + if( Config.DebugMode ) + { + ResourceLoader.EnableDebug(); + } - gameUtils.ReloadResidentResources(); + ResidentResources.Reload(); Api = new PenumbraApi( this ); Ipc = new PenumbraIpc( pluginInterface, Api ); @@ -106,7 +132,7 @@ public class Penumbra : IDalamudPlugin } Config.IsEnabled = true; - Service< ResidentResources >.Get().ReloadResidentResources(); + ResidentResources.Reload(); if( Config.EnablePlayerWatch ) { PlayerWatcher.SetStatus( true ); @@ -125,7 +151,7 @@ public class Penumbra : IDalamudPlugin } Config.IsEnabled = false; - Service< ResidentResources >.Get().ReloadResidentResources(); + ResidentResources.Reload(); if( Config.EnablePlayerWatch ) { PlayerWatcher.SetStatus( false ); @@ -192,7 +218,7 @@ public class Penumbra : IDalamudPlugin Dalamud.Commands.RemoveHandler( CommandName ); - PathResolver.Dispose(); + //PathResolver.Dispose(); ResourceLoader.Dispose(); ShutdownWebServer(); @@ -205,7 +231,7 @@ public class Penumbra : IDalamudPlugin var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) ? ModCollection.Empty - : _modManager.Collections.Collections.Values.FirstOrDefault( c + : ModManager.Collections.Collections.Values.FirstOrDefault( c => string.Equals( c.Name, collectionName, StringComparison.InvariantCultureIgnoreCase ) ); if( collection == null ) { @@ -216,24 +242,24 @@ public class Penumbra : IDalamudPlugin switch( type ) { case "default": - if( collection == _modManager.Collections.DefaultCollection ) + if( collection == ModManager.Collections.DefaultCollection ) { Dalamud.Chat.Print( $"{collection.Name} already is the default collection." ); return false; } - _modManager.Collections.SetDefaultCollection( collection ); + ModManager.Collections.SetDefaultCollection( collection ); Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); SettingsInterface.ResetDefaultCollection(); return true; case "forced": - if( collection == _modManager.Collections.ForcedCollection ) + if( collection == ModManager.Collections.ForcedCollection ) { Dalamud.Chat.Print( $"{collection.Name} already is the forced collection." ); return false; } - _modManager.Collections.SetForcedCollection( collection ); + ModManager.Collections.SetForcedCollection( collection ); Dalamud.Chat.Print( $"Set {collection.Name} as forced collection." ); SettingsInterface.ResetForcedCollection(); return true; @@ -256,9 +282,9 @@ public class Penumbra : IDalamudPlugin { case "reload": { - Service< ModManager >.Get().DiscoverMods(); + ModManager.DiscoverMods(); Dalamud.Chat.Print( - $"Reloaded Penumbra mods. You have {_modManager.Mods.Count} mods." + $"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods." ); break; } diff --git a/Penumbra/Structs/CharacterUtility.cs b/Penumbra/Structs/CharacterUtility.cs deleted file mode 100644 index 2459e2d6..00000000 --- a/Penumbra/Structs/CharacterUtility.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Penumbra.Structs -{ - [StructLayout( LayoutKind.Sequential )] - public unsafe struct CharacterUtility - { - public void* VTable; - - public IntPtr Resources; // Size: 85, I hate C# - } -} \ No newline at end of file diff --git a/Penumbra/Structs/FileMode.cs b/Penumbra/Structs/FileMode.cs deleted file mode 100644 index 13235521..00000000 --- a/Penumbra/Structs/FileMode.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Penumbra.Structs -{ - public enum FileMode : uint - { - LoadUnpackedResource = 0, - LoadFileResource = 1, // Shit in My Games uses this - - // some shit here, the game does some jump if its < 0xA for other files for some reason but there's no impl, probs debug? - LoadIndexResource = 0xA, // load index/index2 - LoadSqPackResource = 0xB, - } -} \ No newline at end of file diff --git a/Penumbra/Structs/ResourceHandle.cs b/Penumbra/Structs/ResourceHandle.cs deleted file mode 100644 index 3318bb99..00000000 --- a/Penumbra/Structs/ResourceHandle.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Penumbra.Structs -{ - [StructLayout( LayoutKind.Explicit )] - public unsafe struct ResourceHandle - { - public const int SsoSize = 15; - - public byte* FileName() - { - if( FileNameLength > SsoSize ) - { - return _fileName; - } - - fixed( byte** name = &_fileName ) - { - return ( byte* )name; - } - } - - [FieldOffset( 0x48 )] - private byte* _fileName; - - [FieldOffset( 0x58 )] - public int FileNameLength; - } -} \ No newline at end of file diff --git a/Penumbra/Structs/SeFileDescriptor.cs b/Penumbra/Structs/SeFileDescriptor.cs deleted file mode 100644 index dc22b81b..00000000 --- a/Penumbra/Structs/SeFileDescriptor.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Penumbra.Structs -{ - [StructLayout( LayoutKind.Explicit )] - public unsafe struct SeFileDescriptor - { - [FieldOffset( 0x00 )] - public FileMode FileMode; - - [FieldOffset( 0x30 )] - public void* FileDescriptor; // - - [FieldOffset( 0x50 )] - public ResourceHandle* ResourceHandle; // - - - [FieldOffset( 0x70 )] - public byte UtfFileName; // - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuBar.cs b/Penumbra/UI/MenuBar.cs deleted file mode 100644 index 5c3c6e41..00000000 --- a/Penumbra/UI/MenuBar.cs +++ /dev/null @@ -1,61 +0,0 @@ -using ImGuiNET; -using Penumbra.UI.Custom; - -namespace Penumbra.UI -{ - public partial class SettingsInterface - { - private class MenuBar - { - private const string MenuLabel = "Penumbra"; - private const string MenuItemToggle = "Toggle UI"; - private const string SlashCommand = "/penumbra"; - private const string MenuItemRediscover = "Rediscover Mods"; - private const string MenuItemHide = "Hide Menu Bar"; - -#if DEBUG - private bool _showDebugBar = true; -#else - private const bool _showDebugBar = false; -#endif - - private readonly SettingsInterface _base; - - public MenuBar( SettingsInterface ui ) - => _base = ui; - - public void Draw() - { - if( !_showDebugBar || !ImGui.BeginMainMenuBar() ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndMainMenuBar ); - - if( !ImGui.BeginMenu( MenuLabel ) ) - { - return; - } - - raii.Push( ImGui.EndMenu ); - - if( ImGui.MenuItem( MenuItemToggle, SlashCommand, _base._menu.Visible ) ) - { - _base.FlipVisibility(); - } - - if( ImGui.MenuItem( MenuItemRediscover ) ) - { - _base.ReloadMods(); - } -#if DEBUG - if( ImGui.MenuItem( MenuItemHide ) ) - { - _showDebugBar = false; - } -#endif - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs index 45c85437..9e45038d 100644 --- a/Penumbra/UI/MenuTabs/TabChangedItems.cs +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -1,70 +1,65 @@ using System.Collections.Generic; using System.Linq; using ImGuiNET; -using Penumbra.Mods; using Penumbra.UI.Custom; -using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabChangedItems { - private class TabChangedItems + private const string LabelTab = "Changed Items"; + private readonly SettingsInterface _base; + + private string _filter = string.Empty; + private string _filterLower = string.Empty; + + public TabChangedItems( SettingsInterface ui ) + => _base = ui; + + public void Draw() { - private const string LabelTab = "Changed Items"; - private readonly ModManager _modManager; - private readonly SettingsInterface _base; - - private string _filter = string.Empty; - private string _filterLower = string.Empty; - - public TabChangedItems( SettingsInterface ui ) + if( !ImGui.BeginTabItem( LabelTab ) ) { - _base = ui; - _modManager = Service< ModManager >.Get(); + return; } - public void Draw() + var modManager = Penumbra.ModManager; + var items = modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var forced = modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##ChangedItemsFilter", "Filter...", ref _filter, 64 ) ) { - if( !ImGui.BeginTabItem( LabelTab ) ) - { - return; - } - var items = _modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary(); - var forced = _modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary(); + _filterLower = _filter.ToLowerInvariant(); + } - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, AutoFillSize ) ) + { + return; + } - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##ChangedItemsFilter", "Filter...", ref _filter, 64 ) ) - { - _filterLower = _filter.ToLowerInvariant(); - } + raii.Push( ImGui.EndTable ); - if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, AutoFillSize ) ) - { - return; - } + var list = items.AsEnumerable(); + if( forced.Count > 0 ) + { + list = list.Concat( forced ).OrderBy( kvp => kvp.Key ); + } - raii.Push( ImGui.EndTable ); + if( _filter.Any() ) + { + list = list.Where( kvp => kvp.Key.ToLowerInvariant().Contains( _filterLower ) ); + } - var list = items.AsEnumerable(); - if( forced.Count > 0 ) - { - list = list.Concat( forced ).OrderBy( kvp => kvp.Key ); - } - - if( _filter.Any() ) - { - list = list.Where( kvp => kvp.Key.ToLowerInvariant().Contains( _filterLower ) ); - } - - foreach( var (name, data) in list ) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - _base.DrawChangedItem( name, data, ImGui.GetStyle().ScrollbarSize ); - } + foreach( var (name, data) in list ) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + _base.DrawChangedItem( name, data, ImGui.GetStyle().ScrollbarSize ); } } } diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index cee1bd71..e2e6bdac 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -19,7 +19,6 @@ public partial class SettingsInterface { private const string CharacterCollectionHelpPopup = "Character Collection Information"; private readonly Selector _selector; - private readonly ModManager _manager; private string _collectionNames = null!; private string _collectionNamesWithNone = null!; private ModCollection[] _collections = null!; @@ -32,7 +31,7 @@ public partial class SettingsInterface private void UpdateNames() { - _collections = _manager.Collections.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); + _collections = Penumbra.ModManager.Collections.Collections.Values.Prepend( ModCollection.Empty ).ToArray(); _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; _collectionNamesWithNone = "None\0" + _collectionNames; UpdateIndices(); @@ -52,18 +51,18 @@ public partial class SettingsInterface } private void UpdateIndex() - => _currentCollectionIndex = GetIndex( _manager.Collections.CurrentCollection ) - 1; + => _currentCollectionIndex = GetIndex( Penumbra.ModManager.Collections.CurrentCollection ) - 1; public void UpdateForcedIndex() - => _currentForcedIndex = GetIndex( _manager.Collections.ForcedCollection ); + => _currentForcedIndex = GetIndex( Penumbra.ModManager.Collections.ForcedCollection ); public void UpdateDefaultIndex() - => _currentDefaultIndex = GetIndex( _manager.Collections.DefaultCollection ); + => _currentDefaultIndex = GetIndex( Penumbra.ModManager.Collections.DefaultCollection ); private void UpdateCharacterIndices() { _currentCharacterIndices.Clear(); - foreach( var kvp in _manager.Collections.CharacterCollection ) + foreach( var kvp in Penumbra.ModManager.Collections.CharacterCollection ) { _currentCharacterIndices[ kvp.Key ] = GetIndex( kvp.Value ); } @@ -80,16 +79,16 @@ public partial class SettingsInterface public TabCollections( Selector selector ) { _selector = selector; - _manager = Service< ModManager >.Get(); UpdateNames(); } private void CreateNewCollection( Dictionary< string, ModSettings > settings ) { - if( _manager.Collections.AddCollection( _newCollectionName, settings ) ) + var manager = Penumbra.ModManager; + if( manager.Collections.AddCollection( _newCollectionName, settings ) ) { UpdateNames(); - SetCurrentCollection( _manager.Collections.Collections[ _newCollectionName ], true ); + SetCurrentCollection( manager.Collections.Collections[ _newCollectionName ], true ); } _newCollectionName = string.Empty; @@ -99,9 +98,10 @@ public partial class SettingsInterface { if( ImGui.Button( "Clean Settings" ) ) { - var changes = ModFunctions.CleanUpCollection( _manager.Collections.CurrentCollection.Settings, - _manager.BasePath.EnumerateDirectories() ); - _manager.Collections.CurrentCollection.UpdateSettings( changes ); + var manager = Penumbra.ModManager; + var changes = ModFunctions.CleanUpCollection( manager.Collections.CurrentCollection.Settings, + manager.BasePath.EnumerateDirectories() ); + manager.Collections.CurrentCollection.UpdateSettings( changes ); } ImGuiCustom.HoverTooltip( @@ -126,9 +126,10 @@ public partial class SettingsInterface var hover = ImGui.IsItemHovered(); ImGui.SameLine(); + var manager = Penumbra.ModManager; if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) { - CreateNewCollection( _manager.Collections.CurrentCollection.Settings ); + CreateNewCollection( manager.Collections.CurrentCollection.Settings ); } hover |= ImGui.IsItemHovered(); @@ -139,13 +140,13 @@ public partial class SettingsInterface ImGui.SetTooltip( "Please enter a name before creating a collection." ); } - var deleteCondition = _manager.Collections.Collections.Count > 1 - && _manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection; + var deleteCondition = manager.Collections.Collections.Count > 1 + && manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection; ImGui.SameLine(); if( ImGuiCustom.DisableButton( "Delete Current Collection", deleteCondition ) ) { - _manager.Collections.RemoveCollection( _manager.Collections.CurrentCollection.Name ); - SetCurrentCollection( _manager.Collections.CurrentCollection, true ); + manager.Collections.RemoveCollection( manager.Collections.CurrentCollection.Name ); + SetCurrentCollection( manager.Collections.CurrentCollection, true ); UpdateNames(); } @@ -168,7 +169,7 @@ public partial class SettingsInterface return; } - _manager.Collections.SetCurrentCollection( _collections[ idx + 1 ] ); + Penumbra.ModManager.Collections.SetCurrentCollection( _collections[ idx + 1 ] ); _currentCollectionIndex = idx; _selector.Cache.TriggerListReset(); if( _selector.Mod != null ) @@ -207,7 +208,7 @@ public partial class SettingsInterface ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( "##Default Collection", ref index, _collectionNamesWithNone ) && index != _currentDefaultIndex ) { - _manager.Collections.SetDefaultCollection( _collections[ index ] ); + Penumbra.ModManager.Collections.SetDefaultCollection( _collections[ index ] ); _currentDefaultIndex = index; } @@ -224,17 +225,18 @@ public partial class SettingsInterface { var index = _currentForcedIndex; ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, _manager.Collections.CharacterCollection.Count == 0 ); + var manager = Penumbra.ModManager; + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, manager.Collections.CharacterCollection.Count == 0 ); if( ImGui.Combo( "##Forced Collection", ref index, _collectionNamesWithNone ) - && index != _currentForcedIndex - && _manager.Collections.CharacterCollection.Count > 0 ) + && index != _currentForcedIndex + && manager.Collections.CharacterCollection.Count > 0 ) { - _manager.Collections.SetForcedCollection( _collections[ index ] ); + manager.Collections.SetForcedCollection( _collections[ index ] ); _currentForcedIndex = index; } style.Pop(); - if( _manager.Collections.CharacterCollection.Count == 0 && ImGui.IsItemHovered() ) + if( manager.Collections.CharacterCollection.Count == 0 && ImGui.IsItemHovered() ) { ImGui.SetTooltip( "Forced Collections only provide value if you have at least one Character Collection. There is no need to set one until then." ); @@ -260,7 +262,7 @@ public partial class SettingsInterface if( ImGuiCustom.DisableButton( "Create New Character Collection", _newCharacterName.Length > 0 && Penumbra.Config.HasReadCharacterCollectionDesc ) ) { - _manager.Collections.CreateCharacterCollection( _newCharacterName ); + Penumbra.ModManager.Collections.CreateCharacterCollection( _newCharacterName ); _currentCharacterIndices[ _newCharacterName ] = 0; _newCharacterName = string.Empty; } @@ -342,14 +344,15 @@ public partial class SettingsInterface DrawDefaultCollectionSelector(); DrawForcedCollectionSelector(); - foreach( var name in _manager.Collections.CharacterCollection.Keys.ToArray() ) + var manager = Penumbra.ModManager; + foreach( var name in manager.Collections.CharacterCollection.Keys.ToArray() ) { var idx = _currentCharacterIndices[ name ]; var tmp = idx; ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); if( ImGui.Combo( $"##{name}collection", ref tmp, _collectionNamesWithNone ) && idx != tmp ) { - _manager.Collections.SetCharacterCollection( name, _collections[ tmp ] ); + manager.Collections.SetCharacterCollection( name, _collections[ tmp ] ); _currentCharacterIndices[ name ] = tmp; } @@ -360,7 +363,7 @@ public partial class SettingsInterface using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.FramePadding, Vector2.One * ImGuiHelpers.GlobalScale * 1.5f ); if( ImGui.Button( $"{FontAwesomeIcon.Trash.ToIconString()}##{name}" ) ) { - _manager.Collections.RemoveCharacterCollection( name ); + manager.Collections.RemoveCharacterCollection( name ); } style.Pop(); diff --git a/Penumbra/UI/MenuTabs/TabDebug.Model.cs b/Penumbra/UI/MenuTabs/TabDebug.Model.cs new file mode 100644 index 00000000..527a265d --- /dev/null +++ b/Penumbra/UI/MenuTabs/TabDebug.Model.cs @@ -0,0 +1,131 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using ImGuiNET; +using Penumbra.UI.Custom; + +namespace Penumbra.UI; + +public partial class SettingsInterface +{ + [StructLayout( LayoutKind.Explicit )] + private unsafe struct RenderModel + { + [FieldOffset( 0x18 )] + public RenderModel* PreviousModel; + + [FieldOffset( 0x20 )] + public RenderModel* NextModel; + + [FieldOffset( 0x30 )] + public ResourceHandle* ResourceHandle; + + [FieldOffset( 0x40 )] + public Skeleton* Skeleton; + + [FieldOffset( 0x58 )] + public void** BoneList; + + [FieldOffset( 0x60 )] + public int BoneListCount; + + [FieldOffset( 0x68 )] + private void* UnkDXBuffer1; + + [FieldOffset( 0x70 )] + private void* UnkDXBuffer2; + + [FieldOffset( 0x78 )] + private void* UnkDXBuffer3; + + [FieldOffset( 0x90 )] + public void** Materials; + + [FieldOffset( 0x98 )] + public int MaterialCount; + } + + [StructLayout( LayoutKind.Explicit )] + private unsafe struct Material + { + [FieldOffset( 0x10 )] + public ResourceHandle* ResourceHandle; + + [FieldOffset( 0x28 )] + public void* MaterialData; + + [FieldOffset( 0x48 )] + public Texture* Tex1; + + [FieldOffset( 0x60 )] + public Texture* Tex2; + + [FieldOffset( 0x78 )] + public Texture* Tex3; + } + + private static unsafe void DrawPlayerModelInfo() + { + var player = Dalamud.ClientState.LocalPlayer; + var name = player?.Name.ToString() ?? "NULL"; + if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) + { + return; + } + + var model = ( CharacterBase* )( ( Character* )player.Address )->GameObject.GetDrawObject(); + if( model == null ) + { + return; + } + + if( !ImGui.BeginTable( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Slot" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Imc Ptr" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Imc File" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Model Ptr" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Model File" ); + + for( var i = 0; i < model->SlotCount; ++i ) + { + var imc = ( ResourceHandle* )model->IMCArray[ i ]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text( $"Slot {i}" ); + ImGui.TableNextColumn(); + ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); + ImGui.TableNextColumn(); + if( imc != null ) + { + ImGui.Text( imc->FileName.ToString() ); + } + + var mdl = ( RenderModel* )model->ModelArray[ i ]; + ImGui.TableNextColumn(); + ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); + if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) + { + continue; + } + + ImGui.TableNextColumn(); + if( mdl != null ) + { + ImGui.Text( mdl->ResourceHandle->FileName.ToString() ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 49961160..758a46ea 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; +using System.Drawing.Text; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Types; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.FFXIV.Client.System.String; using ImGuiNET; using Penumbra.Api; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; @@ -16,6 +20,8 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; +using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; +using Utf8String = Penumbra.GameData.ByteString.Utf8String; namespace Penumbra.UI; @@ -143,7 +149,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - var manager = Service< ModManager >.Get(); + var manager = Penumbra.ModManager; PrintValue( "Active Collection", manager.Collections.ActiveCollection.Name ); PrintValue( " has Cache", ( manager.Collections.ActiveCollection.Cache != null ).ToString() ); PrintValue( "Current Collection", manager.Collections.CurrentCollection.Name ); @@ -164,7 +170,7 @@ public partial class SettingsInterface PrintValue( "Mod Manager Temp Path Exists", manager.TempPath != null ? Directory.Exists( manager.TempPath.FullName ).ToString() : false.ToString() ); PrintValue( "Mod Manager Temp Path IsWritable", manager.TempWritable.ToString() ); - PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); + //PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); } private void DrawDebugTabRedraw() @@ -281,7 +287,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - foreach( var collection in Service< ModManager >.Get().Collections.Collections.Values.Where( c => c.Cache != null ) ) + foreach( var collection in Penumbra.ModManager.Collections.Collections.Values.Where( c => c.Cache != null ) ) { var manip = collection.Cache!.MetaManipulations; var files = ( Dictionary< GamePath, MetaManager.FileInformation >? )manip.GetType() @@ -363,8 +369,7 @@ public partial class SettingsInterface return; } - var manager = Service< ModManager >.Get(); - var cache = manager.Collections.CurrentCollection.Cache; + var cache = Penumbra.ModManager.Collections.CurrentCollection.Cache; if( cache == null || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX ) ) { return; @@ -385,6 +390,86 @@ public partial class SettingsInterface } } + private unsafe void DrawDebugTabReplacedResources() + { + if( !ImGui.CollapsingHeader( "Replaced Resources##Debug" ) ) + { + return; + } + + _penumbra.ResourceLoader.UpdateDebugInfo(); + + if( _penumbra.ResourceLoader.DebugList.Count == 0 + || !ImGui.BeginTable( "##ReplacedResourcesDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) + { + return; + } + + using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + foreach( var data in _penumbra.ResourceLoader.DebugList.Values.ToArray() ) + { + var refCountManip = data.ManipulatedResource == null ? 0 : data.ManipulatedResource->RefCount; + var refCountOrig = data.OriginalResource == null ? 0 : data.OriginalResource->RefCount; + ImGui.TableNextColumn(); + ImGui.Text( data.ManipulatedPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )data.ManipulatedResource ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( refCountManip.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( data.OriginalPath.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )data.OriginalResource ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( refCountOrig.ToString() ); + } + } + + private unsafe void DrawPathResolverDebug() + { + if( !ImGui.CollapsingHeader( "Path Resolver##Debug" ) ) + { + return; + } + + //if( ImGui.TreeNodeEx( "Draw Object to Object" ) ) + //{ + // using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + // if( ImGui.BeginTable( "###DrawObjectResolverTable", 4, ImGuiTableFlags.SizingFixedFit ) ) + // { + // end.Push( ImGui.EndTable ); + // foreach( var (ptr, idx) in _penumbra.PathResolver._drawObjectToObject ) + // { + // ImGui.TableNextColumn(); + // ImGui.Text( ptr.ToString( "X" ) ); + // ImGui.TableNextColumn(); + // ImGui.Text( idx.ToString() ); + // ImGui.TableNextColumn(); + // ImGui.Text( Dalamud.Objects[ idx ]?.Address.ToString() ?? "NULL" ); + // ImGui.TableNextColumn(); + // ImGui.Text( Dalamud.Objects[ idx ]?.Name.ToString() ?? "NULL" ); + // } + // } + //} + // + //if( ImGui.TreeNodeEx( "Path Collections" ) ) + //{ + // using var end = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + // if( ImGui.BeginTable( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ) ) + // { + // end.Push( ImGui.EndTable ); + // foreach( var (path, collection) in _penumbra.PathResolver._pathCollections ) + // { + // ImGui.TableNextColumn(); + // ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); + // ImGui.TableNextColumn(); + // ImGui.Text( collection.Name ); + // } + // } + //} + } + private void DrawDebugTab() { if( !ImGui.BeginTabItem( "Debug Tab" ) ) @@ -396,8 +481,16 @@ public partial class SettingsInterface DrawDebugTabGeneral(); ImGui.NewLine(); + DrawDebugTabReplacedResources(); + ImGui.NewLine(); + DrawResourceProblems(); + ImGui.NewLine(); DrawDebugTabMissingFiles(); ImGui.NewLine(); + DrawPlayerModelInfo(); + ImGui.NewLine(); + DrawPathResolverDebug(); + ImGui.NewLine(); DrawDebugTabRedraw(); ImGui.NewLine(); DrawDebugTabPlayers(); diff --git a/Penumbra/UI/MenuTabs/TabDebugModels.cs b/Penumbra/UI/MenuTabs/TabDebugModels.cs deleted file mode 100644 index 67765caf..00000000 --- a/Penumbra/UI/MenuTabs/TabDebugModels.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using ImGuiNET; -using Lumina.Models.Models; -using Penumbra.UI.Custom; -using DalamudCharacter = Dalamud.Game.ClientState.Objects.Types.Character; - -namespace Penumbra.UI -{ - public partial class SettingsInterface - { - [StructLayout( LayoutKind.Explicit )] - private unsafe struct RenderModel - { - [FieldOffset(0x18)] - public RenderModel* PreviousModel; - [FieldOffset( 0x20 )] - public RenderModel* NextModel; - - [FieldOffset( 0x30 )] - public ResourceHandle* ResourceHandle; - - [FieldOffset( 0x40 )] - public Skeleton* Skeleton; - - [FieldOffset( 0x58 )] - public void** BoneList; - [FieldOffset( 0x60 )] - public int BoneListCount; - - [FieldOffset( 0x68 )] - private void* UnkDXBuffer1; - - [FieldOffset( 0x70 )] - private void* UnkDXBuffer2; - - [FieldOffset( 0x78 )] - private void* UnkDXBuffer3; - - [FieldOffset( 0x90 )] - public void** Materials; - - [FieldOffset( 0x98 )] - public int MaterialCount; - } - - [StructLayout( LayoutKind.Explicit )] - private unsafe struct Material - { - [FieldOffset(0x10)] - public ResourceHandle* ResourceHandle; - [FieldOffset(0x28)] - public void* MaterialData; - - [FieldOffset( 0x48 )] - public Texture* Tex1; - [FieldOffset( 0x60 )] - public Texture* Tex2; - [FieldOffset( 0x78 )] - public Texture* Tex3; - } - - private static unsafe void DrawPlayerModelInfo( DalamudCharacter character ) - { - var name = character.Name.ToString(); - if( !ImGui.CollapsingHeader( $"{name}##Draw" ) ) - { - return; - } - - var model = ( CharacterBase* )( ( Character* )character.Address )->GameObject.GetDrawObject(); - if( model == null ) - { - return; - } - - if( !ImGui.BeginTable( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Slot" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc File" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model File" ); - - for( var i = 0; i < model->SlotCount; ++i ) - { - var imc = ( ResourceHandle* )model->IMCArray[ i ]; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text( $"Slot {i}" ); - ImGui.TableNextColumn(); - ImGui.Text( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); - ImGui.TableNextColumn(); - if( imc != null ) - { - ImGui.Text( imc->FileName.ToString() ); - } - - var mdl = ( RenderModel* )model->ModelArray[ i ]; - ImGui.TableNextColumn(); - ImGui.Text( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); - if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) - { - continue; - } - - - ImGui.TableNextColumn(); - if( mdl != null ) - { - ImGui.Text( mdl->ResourceHandle->FileName.ToString() ); - } - } - } - - private void DrawPlayerModelTab() - { - if( !ImGui.BeginTabItem( "Model Debug" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - var player = Dalamud.ClientState.LocalPlayer; - if( player == null ) - { - return; - } - - DrawPlayerModelInfo( player ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 63edcebf..ff2606f8 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -3,215 +3,210 @@ using System.IO; using System.Linq; using Dalamud.Interface; using ImGuiNET; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.UI.Custom; -using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabEffective { - private class TabEffective + private const string LabelTab = "Effective Changes"; + + private string _gamePathFilter = string.Empty; + private string _gamePathFilterLower = string.Empty; + private string _filePathFilter = string.Empty; + private string _filePathFilterLower = string.Empty; + + private readonly float _leftTextLength = + ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40; + + private float _arrowLength = 0; + + private static void DrawLine( string path, string name ) { - private const string LabelTab = "Effective Changes"; - private readonly ModManager _modManager; + ImGui.TableNextColumn(); + ImGuiCustom.CopyOnClickSelectable( path ); - private string _gamePathFilter = string.Empty; - private string _gamePathFilterLower = string.Empty; - private string _filePathFilter = string.Empty; - private string _filePathFilterLower = string.Empty; + ImGui.TableNextColumn(); + ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); + ImGui.SameLine(); + ImGuiCustom.CopyOnClickSelectable( name ); + } - private readonly float _leftTextLength = - ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40; - - private float _arrowLength = 0; - - public TabEffective() - => _modManager = Service< ModManager >.Get(); - - - private static void DrawLine( string path, string name ) + private void DrawFilters() + { + if( _arrowLength == 0 ) { - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( path ); - - ImGui.TableNextColumn(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.SameLine(); - ImGuiCustom.CopyOnClickSelectable( name ); + using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + _arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; } - private void DrawFilters() + ImGui.SetNextItemWidth( _leftTextLength * ImGuiHelpers.GlobalScale ); + if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) { - if( _arrowLength == 0 ) + _gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); + } + + ImGui.SameLine( ( _leftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) + { + _filePathFilterLower = _filePathFilter.ToLowerInvariant(); + } + } + + private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp ) + { + if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); + } + + private bool CheckFilters( KeyValuePair< GamePath, GamePath > kvp ) + { + if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Value.ToString().Contains( _filePathFilterLower ); + } + + private bool CheckFilters( (string, string, string) kvp ) + { + if( _gamePathFilter.Any() && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilterLower ) ) + { + return false; + } + + return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); + } + + private void DrawFilteredRows( ModCollectionCache? active, ModCollectionCache? forced ) + { + void DrawFileLines( ModCollectionCache cache ) + { + foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) { - using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); - _arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; + DrawLine( gp, fp.FullName ); } - ImGui.SetNextItemWidth( _leftTextLength * ImGuiHelpers.GlobalScale ); - if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) + foreach( var (gp, fp) in cache.SwappedFiles.Where( CheckFilters ) ) { - _gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); + DrawLine( gp, fp ); } - ImGui.SameLine( ( _leftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) + foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations + .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) ) + .Where( CheckFilters ) ) { - _filePathFilterLower = _filePathFilter.ToLowerInvariant(); + DrawLine( mp, mod ); } } - private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp ) + if( active != null ) { - if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) - { - return false; - } - - return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); + DrawFileLines( active ); } - private bool CheckFilters( KeyValuePair< GamePath, GamePath > kvp ) + if( forced != null ) { - if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) - { - return false; - } + DrawFileLines( forced ); + } + } - return !_filePathFilter.Any() || kvp.Value.ToString().Contains( _filePathFilterLower ); + public void Draw() + { + if( !ImGui.BeginTabItem( LabelTab ) ) + { + return; } - private bool CheckFilters( (string, string, string) kvp ) - { - if( _gamePathFilter.Any() && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilterLower ) ) - { - return false; - } + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); + DrawFilters(); + + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; + + var modManager = Penumbra.ModManager; + var activeCollection = modManager.Collections.ActiveCollection.Cache; + var forcedCollection = modManager.Collections.ForcedCollection.Cache; + + var (activeResolved, activeSwap, activeMeta) = activeCollection != null + ? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count ) + : ( 0, 0, 0 ); + var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null + ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count ) + : ( 0, 0, 0 ); + var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta; + if( totalLines == 0 ) + { + return; } - private void DrawFilteredRows( ModCollectionCache? active, ModCollectionCache? forced ) + if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) { - void DrawFileLines( ModCollectionCache cache ) + raii.Push( ImGui.EndTable ); + ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale ); + + if( _filePathFilter.Any() || _gamePathFilter.Any() ) { - foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) + DrawFilteredRows( activeCollection, forcedCollection ); + } + else + { + ImGuiListClipperPtr clipper; + unsafe { - DrawLine( gp, fp.FullName ); + clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); } - foreach( var (gp, fp) in cache.SwappedFiles.Where( CheckFilters ) ) + clipper.Begin( totalLines ); + + + while( clipper.Step() ) { - DrawLine( gp, fp ); - } - - foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations - .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) ) - .Where( CheckFilters ) ) - { - DrawLine( mp, mod ); - } - } - - if( active != null ) - { - DrawFileLines( active ); - } - - if( forced != null ) - { - DrawFileLines( forced ); - } - } - - public void Draw() - { - if( !ImGui.BeginTabItem( LabelTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - DrawFilters(); - - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - - var activeCollection = _modManager.Collections.ActiveCollection.Cache; - var forcedCollection = _modManager.Collections.ForcedCollection.Cache; - - var (activeResolved, activeSwap, activeMeta) = activeCollection != null - ? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count ) - : ( 0, 0, 0 ); - var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null - ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count ) - : ( 0, 0, 0 ); - var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta; - if( totalLines == 0 ) - { - return; - } - - if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) - { - raii.Push( ImGui.EndTable ); - ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale ); - - if( _filePathFilter.Any() || _gamePathFilter.Any() ) - { - DrawFilteredRows( activeCollection, forcedCollection ); - } - else - { - ImGuiListClipperPtr clipper; - unsafe + for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) { - clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); - } - - clipper.Begin( totalLines ); - - - while( clipper.Step() ) - { - for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ ) + var row = actualRow; + ImGui.TableNextRow(); + if( row < activeResolved ) { - var row = actualRow; - ImGui.TableNextRow(); - if( row < activeResolved ) - { - var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); - DrawLine( gamePath, file.FullName ); - } - else if( ( row -= activeResolved ) < activeSwap ) - { - var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row ); - DrawLine( gamePath, swap ); - } - else if( ( row -= activeSwap ) < activeMeta ) - { - var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); - } - else if( ( row -= activeMeta ) < forcedResolved ) - { - var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); - DrawLine( gamePath, file.FullName ); - } - else if( ( row -= forcedResolved ) < forcedSwap ) - { - var (gamePath, swap) = forcedCollection!.SwappedFiles.ElementAt( row ); - DrawLine( gamePath, swap ); - } - else - { - row -= forcedSwap; - var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); - DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); - } + var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); + DrawLine( gamePath, file.FullName ); + } + else if( ( row -= activeResolved ) < activeSwap ) + { + var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row ); + DrawLine( gamePath, swap ); + } + else if( ( row -= activeSwap ) < activeMeta ) + { + var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); + } + else if( ( row -= activeMeta ) < forcedResolved ) + { + var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row ); + DrawLine( gamePath, file.FullName ); + } + else if( ( row -= forcedResolved ) < forcedSwap ) + { + var (gamePath, swap) = forcedCollection!.SwappedFiles.ElementAt( row ); + DrawLine( gamePath, swap ); + } + else + { + row -= forcedSwap; + var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row ); + DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); } } } @@ -219,5 +214,4 @@ namespace Penumbra.UI } } } -} - +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabImport.cs b/Penumbra/UI/MenuTabs/TabImport.cs index 6680947b..4b49f88a 100644 --- a/Penumbra/UI/MenuTabs/TabImport.cs +++ b/Penumbra/UI/MenuTabs/TabImport.cs @@ -8,188 +8,182 @@ using System.Windows.Forms; using Dalamud.Logging; using ImGuiNET; using Penumbra.Importer; -using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabImport { - private class TabImport + private const string LabelTab = "Import Mods"; + private const string LabelImportButton = "Import TexTools Modpacks"; + private const string LabelFileDialog = "Pick one or more modpacks."; + private const string LabelFileImportRunning = "Import in progress..."; + private const string FileTypeFilter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*"; + private const string TooltipModpack1 = "Writing modpack to disk before extracting..."; + + private const uint ColorRed = 0xFF0000C8; + private const uint ColorYellow = 0xFF00C8C8; + + private static readonly Vector2 ImportBarSize = new(-1, 0); + + private bool _isImportRunning; + private string _errorMessage = string.Empty; + private TexToolsImport? _texToolsImport; + private readonly SettingsInterface _base; + + public readonly HashSet< string > NewMods = new(); + + public TabImport( SettingsInterface ui ) + => _base = ui; + + public bool IsImporting() + => _isImportRunning; + + private void RunImportTask() { - private const string LabelTab = "Import Mods"; - private const string LabelImportButton = "Import TexTools Modpacks"; - private const string LabelFileDialog = "Pick one or more modpacks."; - private const string LabelFileImportRunning = "Import in progress..."; - private const string FileTypeFilter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*"; - private const string TooltipModpack1 = "Writing modpack to disk before extracting..."; - - private const uint ColorRed = 0xFF0000C8; - private const uint ColorYellow = 0xFF00C8C8; - - private static readonly Vector2 ImportBarSize = new( -1, 0 ); - - private bool _isImportRunning; - private string _errorMessage = string.Empty; - private TexToolsImport? _texToolsImport; - private readonly SettingsInterface _base; - private readonly ModManager _manager; - - public readonly HashSet< string > NewMods = new(); - - public TabImport( SettingsInterface ui ) + _isImportRunning = true; + Task.Run( async () => { - _base = ui; - _manager = Service< ModManager >.Get(); - } - - public bool IsImporting() - => _isImportRunning; - - private void RunImportTask() - { - _isImportRunning = true; - Task.Run( async () => + try { - try + var picker = new OpenFileDialog { - var picker = new OpenFileDialog + Multiselect = true, + Filter = FileTypeFilter, + CheckFileExists = true, + Title = LabelFileDialog, + }; + + var result = await picker.ShowDialogAsync(); + + if( result == DialogResult.OK ) + { + _errorMessage = string.Empty; + + foreach( var fileName in picker.FileNames ) { - Multiselect = true, - Filter = FileTypeFilter, - CheckFileExists = true, - Title = LabelFileDialog, - }; + PluginLog.Information( $"-> {fileName} START" ); - var result = await picker.ShowDialogAsync(); - - if( result == DialogResult.OK ) - { - _errorMessage = string.Empty; - - foreach( var fileName in picker.FileNames ) + try { - PluginLog.Information( $"-> {fileName} START" ); - - try + _texToolsImport = new TexToolsImport( Penumbra.ModManager.BasePath ); + var dir = _texToolsImport.ImportModPack( new FileInfo( fileName ) ); + if( dir.Name.Any() ) { - _texToolsImport = new TexToolsImport( _manager.BasePath ); - var dir = _texToolsImport.ImportModPack( new FileInfo( fileName ) ); - if( dir.Name.Any() ) - { - NewMods.Add( dir.Name ); - } + NewMods.Add( dir.Name ); + } - PluginLog.Information( $"-> {fileName} OK!" ); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); - _errorMessage = ex.Message; - } + PluginLog.Information( $"-> {fileName} OK!" ); } - - var directory = _texToolsImport?.ExtractedDirectory; - _texToolsImport = null; - _base.ReloadMods(); - if( directory != null ) + catch( Exception ex ) { - _base._menu.InstalledTab.Selector.SelectModOnUpdate( directory.Name ); + PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); + _errorMessage = ex.Message; } } + + var directory = _texToolsImport?.ExtractedDirectory; + _texToolsImport = null; + _base.ReloadMods(); + if( directory != null ) + { + _base._menu.InstalledTab.Selector.SelectModOnUpdate( directory.Name ); + } } - catch( Exception e ) - { - PluginLog.Error( $"Error opening file picker dialogue:\n{e}" ); - } + } + catch( Exception e ) + { + PluginLog.Error( $"Error opening file picker dialogue:\n{e}" ); + } - _isImportRunning = false; - } ); - } + _isImportRunning = false; + } ); + } - private void DrawImportButton() + private void DrawImportButton() + { + if( !Penumbra.ModManager.Valid ) { - if( !_manager.Valid ) - { - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); - ImGui.Button( LabelImportButton ); - style.Pop(); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); + ImGui.Button( LabelImportButton ); + style.Pop(); - using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); - ImGui.Text( "Can not import since the mod directory path is not valid." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() ); - color.Pop(); - - ImGui.Text( "Please set the mod directory in the settings tab." ); - ImGui.Text( "This folder should preferably be close to the root directory of your (preferably SSD) drive, for example" ); - color.Push( ImGuiCol.Text, ColorYellow ); - ImGui.Text( " D:\\ffxivmods" ); - color.Pop(); - ImGui.Text( "You can return to this tab once you've done that." ); - } - else if( ImGui.Button( LabelImportButton ) ) - { - RunImportTask(); - } - } - - private void DrawImportProgress() - { - ImGui.Button( LabelFileImportRunning ); - - if( _texToolsImport == null ) - { - return; - } - - switch( _texToolsImport.State ) - { - case ImporterState.None: break; - case ImporterState.WritingPackToDisk: - ImGui.Text( TooltipModpack1 ); - break; - case ImporterState.ExtractingModFiles: - { - var str = - $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; - - ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); - break; - } - case ImporterState.Done: break; - default: throw new ArgumentOutOfRangeException(); - } - } - - private void DrawFailedImportMessage() - { using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); - ImGui.Text( $"One or more of your modpacks failed to import:\n\t\t{_errorMessage}" ); + ImGui.Text( "Can not import since the mod directory path is not valid." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() ); + color.Pop(); + + ImGui.Text( "Please set the mod directory in the settings tab." ); + ImGui.Text( "This folder should preferably be close to the root directory of your (preferably SSD) drive, for example" ); + color.Push( ImGuiCol.Text, ColorYellow ); + ImGui.Text( " D:\\ffxivmods" ); + color.Pop(); + ImGui.Text( "You can return to this tab once you've done that." ); + } + else if( ImGui.Button( LabelImportButton ) ) + { + RunImportTask(); + } + } + + private void DrawImportProgress() + { + ImGui.Button( LabelFileImportRunning ); + + if( _texToolsImport == null ) + { + return; } - public void Draw() + switch( _texToolsImport.State ) { - if( !ImGui.BeginTabItem( LabelTab ) ) + case ImporterState.None: break; + case ImporterState.WritingPackToDisk: + ImGui.Text( TooltipModpack1 ); + break; + case ImporterState.ExtractingModFiles: { - return; - } + var str = + $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); + break; + } + case ImporterState.Done: break; + default: throw new ArgumentOutOfRangeException(); + } + } - if( !_isImportRunning ) - { - DrawImportButton(); - } - else - { - DrawImportProgress(); - } + private void DrawFailedImportMessage() + { + using var color = ImGuiRaii.PushColor( ImGuiCol.Text, ColorRed ); + ImGui.Text( $"One or more of your modpacks failed to import:\n\t\t{_errorMessage}" ); + } - if( _errorMessage.Any() ) - { - DrawFailedImportMessage(); - } + public void Draw() + { + if( !ImGui.BeginTabItem( LabelTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + if( !_isImportRunning ) + { + DrawImportButton(); + } + else + { + DrawImportProgress(); + } + + if( _errorMessage.Any() ) + { + DrawFailedImportMessage(); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs index c4ed131c..beecf9b6 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs @@ -1,42 +1,37 @@ using System.Collections.Generic; using ImGuiNET; -using Penumbra.Mods; using Penumbra.UI.Custom; -using Penumbra.Util; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private class TabInstalled { - private class TabInstalled + private const string LabelTab = "Installed Mods"; + + public readonly Selector Selector; + public readonly ModPanel ModPanel; + + public TabInstalled( SettingsInterface ui, HashSet< string > newMods ) { - private const string LabelTab = "Installed Mods"; + Selector = new Selector( ui, newMods ); + ModPanel = new ModPanel( ui, Selector, newMods ); + } - private readonly ModManager _modManager; - public readonly Selector Selector; - public readonly ModPanel ModPanel; - - public TabInstalled( SettingsInterface ui, HashSet< string > newMods ) + public void Draw() + { + var ret = ImGui.BeginTabItem( LabelTab ); + if( !ret ) { - Selector = new Selector( ui, newMods ); - ModPanel = new ModPanel( ui, Selector, newMods ); - _modManager = Service< ModManager >.Get(); + return; } - public void Draw() - { - var ret = ImGui.BeginTabItem( LabelTab ); - if( !ret ) - { - return; - } + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - Selector.Draw(); - ImGui.SameLine(); - ModPanel.Draw(); - } + Selector.Draw(); + ImGui.SameLine(); + ModPanel.Draw(); } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index cf079a24..cabba206 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -5,6 +5,7 @@ using Dalamud.Interface; using ImGuiNET; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta; @@ -15,693 +16,691 @@ using Penumbra.UI.Custom; using Penumbra.Util; using ImGui = ImGuiNET.ImGui; -namespace Penumbra.UI +namespace Penumbra.UI; + +public partial class SettingsInterface { - public partial class SettingsInterface + private partial class PluginDetails { - private partial class PluginDetails + private const string LabelPluginDetails = "PenumbraPluginDetails"; + private const string LabelAboutTab = "About"; + private const string LabelChangedItemsTab = "Changed Items"; + private const string LabelChangedItemsHeader = "##changedItems"; + private const string LabelConflictsTab = "Mod Conflicts"; + private const string LabelConflictsHeader = "##conflicts"; + private const string LabelFileSwapTab = "File Swaps"; + private const string LabelFileSwapHeader = "##fileSwaps"; + private const string LabelFileListTab = "Files"; + private const string LabelFileListHeader = "##fileList"; + private const string LabelGroupSelect = "##groupSelect"; + private const string LabelOptionSelect = "##optionSelect"; + private const string LabelConfigurationTab = "Configuration"; + + private const string TooltipFilesTab = + "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" + + "Yellow files are restricted to some options."; + + private const float OptionSelectionWidth = 140f; + private const float CheckMarkSize = 50f; + private const uint ColorDarkGreen = 0xFF00A000; + private const uint ColorGreen = 0xFF00C800; + private const uint ColorYellow = 0xFF00C8C8; + private const uint ColorDarkRed = 0xFF0000A0; + private const uint ColorRed = 0xFF0000C8; + + + private bool _editMode; + private int _selectedGroupIndex; + private OptionGroup? _selectedGroup; + private int _selectedOptionIndex; + private Option? _selectedOption; + private string _currentGamePaths = ""; + + private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; + + private readonly Selector _selector; + private readonly SettingsInterface _base; + + private void SelectGroup( int idx ) { - private const string LabelPluginDetails = "PenumbraPluginDetails"; - private const string LabelAboutTab = "About"; - private const string LabelChangedItemsTab = "Changed Items"; - private const string LabelChangedItemsHeader = "##changedItems"; - private const string LabelConflictsTab = "Mod Conflicts"; - private const string LabelConflictsHeader = "##conflicts"; - private const string LabelFileSwapTab = "File Swaps"; - private const string LabelFileSwapHeader = "##fileSwaps"; - private const string LabelFileListTab = "Files"; - private const string LabelFileListHeader = "##fileList"; - private const string LabelGroupSelect = "##groupSelect"; - private const string LabelOptionSelect = "##optionSelect"; - private const string LabelConfigurationTab = "Configuration"; - - private const string TooltipFilesTab = - "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" - + "Yellow files are restricted to some options."; - - private const float OptionSelectionWidth = 140f; - private const float CheckMarkSize = 50f; - private const uint ColorDarkGreen = 0xFF00A000; - private const uint ColorGreen = 0xFF00C800; - private const uint ColorYellow = 0xFF00C8C8; - private const uint ColorDarkRed = 0xFF0000A0; - private const uint ColorRed = 0xFF0000C8; - - - private bool _editMode; - private int _selectedGroupIndex; - private OptionGroup? _selectedGroup; - private int _selectedOptionIndex; - private Option? _selectedOption; - private string _currentGamePaths = ""; - - private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; - - private readonly Selector _selector; - private readonly SettingsInterface _base; - private readonly ModManager _modManager; - - private void SelectGroup( int idx ) + // Not using the properties here because we need it to be not null forgiving in this case. + var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0; + _selectedGroupIndex = idx; + if( _selectedGroupIndex >= numGroups ) { - // Not using the properties here because we need it to be not null forgiving in this case. - var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0; - _selectedGroupIndex = idx; - if( _selectedGroupIndex >= numGroups ) - { - _selectedGroupIndex = 0; - } - - if( numGroups > 0 ) - { - _selectedGroup = Meta.Groups.ElementAt( _selectedGroupIndex ).Value; - } - else - { - _selectedGroup = null; - } + _selectedGroupIndex = 0; } - private void SelectGroup() - => SelectGroup( _selectedGroupIndex ); - - private void SelectOption( int idx ) + if( numGroups > 0 ) { - _selectedOptionIndex = idx; - if( _selectedOptionIndex >= _selectedGroup?.Options.Count ) - { - _selectedOptionIndex = 0; - } + _selectedGroup = Meta.Groups.ElementAt( _selectedGroupIndex ).Value; + } + else + { + _selectedGroup = null; + } + } - if( _selectedGroup?.Options.Count > 0 ) - { - _selectedOption = ( ( OptionGroup )_selectedGroup ).Options[ _selectedOptionIndex ]; - } - else - { - _selectedOption = null; - } + private void SelectGroup() + => SelectGroup( _selectedGroupIndex ); + + private void SelectOption( int idx ) + { + _selectedOptionIndex = idx; + if( _selectedOptionIndex >= _selectedGroup?.Options.Count ) + { + _selectedOptionIndex = 0; } - private void SelectOption() - => SelectOption( _selectedOptionIndex ); + if( _selectedGroup?.Options.Count > 0 ) + { + _selectedOption = ( ( OptionGroup )_selectedGroup ).Options[ _selectedOptionIndex ]; + } + else + { + _selectedOption = null; + } + } - public void ResetState() + private void SelectOption() + => SelectOption( _selectedOptionIndex ); + + public void ResetState() + { + _fullFilenameList = null; + SelectGroup(); + SelectOption(); + } + + public PluginDetails( SettingsInterface ui, Selector s ) + { + _base = ui; + _selector = s; + ResetState(); + } + + // This is only drawn when we have a mod selected, so we can forgive nulls. + private Mod.Mod Mod + => _selector.Mod!; + + private ModMeta Meta + => Mod.Data.Meta; + + private void Save() + { + Penumbra.ModManager.Collections.CurrentCollection.Save(); + } + + private void DrawAboutTab() + { + if( !_editMode && Meta.Description.Length == 0 ) + { + return; + } + + if( !ImGui.BeginTabItem( LabelAboutTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + var desc = Meta.Description; + var flags = _editMode + ? ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CtrlEnterForNewLine + : ImGuiInputTextFlags.ReadOnly; + + if( _editMode ) + { + if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, + AutoFillSize, flags ) ) + { + Meta.Description = desc; + _selector.SaveCurrentMod(); + } + + ImGuiCustom.HoverTooltip( TooltipAboutEdit ); + } + else + { + ImGui.TextWrapped( desc ); + } + } + + private void DrawChangedItemsTab() + { + if( Mod.Data.ChangedItems.Count == 0 || !ImGui.BeginTabItem( LabelChangedItemsTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + if( !ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndListBox ); + foreach( var (name, data) in Mod.Data.ChangedItems ) + { + _base.DrawChangedItem( name, data ); + } + } + + private void DrawConflictTab() + { + if( !Mod.Cache.Conflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + ImGui.SetNextItemWidth( -1 ); + if( !ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndListBox ); + using var indent = ImGuiRaii.PushIndent( 0 ); + foreach( var (mod, (files, manipulations)) in Mod.Cache.Conflicts ) + { + if( ImGui.Selectable( mod.Data.Meta.Name ) ) + { + _selector.SelectModByDir( mod.Data.BasePath.Name ); + } + + ImGui.SameLine(); + ImGui.Text( $"(Priority {mod.Settings.Priority})" ); + + indent.Push( 15f ); + foreach( var file in files ) + { + ImGui.Selectable( file ); + } + + foreach( var manip in manipulations ) + { + ImGui.Text( manip.IdentifierString() ); + } + + indent.Pop( 15f ); + } + } + + private void DrawFileSwapTab() + { + if( _editMode ) + { + DrawFileSwapTabEdit(); + return; + } + + if( !Meta.FileSwaps.Any() || !ImGui.BeginTabItem( LabelFileSwapTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; + + ImGui.SetNextItemWidth( -1 ); + if( !ImGui.BeginTable( LabelFileSwapHeader, 3, flags, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndTable ); + + foreach( var (source, target) in Meta.FileSwaps ) + { + ImGui.TableNextColumn(); + ImGuiCustom.CopyOnClickSelectable( source ); + + ImGui.TableNextColumn(); + ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); + + ImGui.TableNextColumn(); + ImGuiCustom.CopyOnClickSelectable( target ); + + ImGui.TableNextRow(); + } + } + + private void UpdateFilenameList() + { + if( _fullFilenameList != null ) + { + return; + } + + _fullFilenameList = Mod.Data.Resources.ModFiles + .Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray(); + + if( Meta.Groups.Count == 0 ) + { + return; + } + + for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) + { + foreach( var group in Meta.Groups.Values ) + { + var inAll = true; + foreach( var option in group.Options ) + { + if( option.OptionFiles.ContainsKey( _fullFilenameList[ i ].relName ) ) + { + _fullFilenameList[ i ].color = ColorYellow; + } + else + { + inAll = false; + } + } + + if( inAll && group.SelectionType == SelectType.Single ) + { + _fullFilenameList[ i ].color = ColorGreen; + } + } + } + } + + private void DrawFileListTab() + { + if( !ImGui.BeginTabItem( LabelFileListTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + ImGuiCustom.HoverTooltip( TooltipFilesTab ); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) + { + raii.Push( ImGui.EndListBox ); + UpdateFilenameList(); + using var colorRaii = new ImGuiRaii.Color(); + foreach( var (name, _, color, _) in _fullFilenameList! ) + { + colorRaii.Push( ImGuiCol.Text, color ); + ImGui.Selectable( name.FullName ); + colorRaii.Pop(); + } + } + else { _fullFilenameList = null; - SelectGroup(); - SelectOption(); } + } - public PluginDetails( SettingsInterface ui, Selector s ) + private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) + { + removeFolders = 0; + var defaultIndex = + gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) ); + if( defaultIndex < 0 ) { - _base = ui; - _selector = s; - ResetState(); - _modManager = Service< ModManager >.Get(); - } - - // This is only drawn when we have a mod selected, so we can forgive nulls. - private Mod.Mod Mod - => _selector.Mod!; - - private ModMeta Meta - => Mod.Data.Meta; - - private void Save() - { - _modManager.Collections.CurrentCollection.Save(); - } - - private void DrawAboutTab() - { - if( !_editMode && Meta.Description.Length == 0 ) - { - return; - } - - if( !ImGui.BeginTabItem( LabelAboutTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - var desc = Meta.Description; - var flags = _editMode - ? ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CtrlEnterForNewLine - : ImGuiInputTextFlags.ReadOnly; - - if( _editMode ) - { - if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, - AutoFillSize, flags ) ) - { - Meta.Description = desc; - _selector.SaveCurrentMod(); - } - - ImGuiCustom.HoverTooltip( TooltipAboutEdit ); - } - else - { - ImGui.TextWrapped( desc ); - } - } - - private void DrawChangedItemsTab() - { - if( Mod.Data.ChangedItems.Count == 0 || !ImGui.BeginTabItem( LabelChangedItemsTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - if( !ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - foreach( var (name, data) in Mod.Data.ChangedItems ) - { - _base.DrawChangedItem( name, data ); - } - } - - private void DrawConflictTab() - { - if( !Mod.Cache.Conflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - ImGui.SetNextItemWidth( -1 ); - if( !ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - using var indent = ImGuiRaii.PushIndent( 0 ); - foreach( var (mod, (files, manipulations)) in Mod.Cache.Conflicts ) - { - if( ImGui.Selectable( mod.Data.Meta.Name ) ) - { - _selector.SelectModByDir( mod.Data.BasePath.Name ); - } - - ImGui.SameLine(); - ImGui.Text( $"(Priority {mod.Settings.Priority})" ); - - indent.Push( 15f ); - foreach( var file in files ) - { - ImGui.Selectable( file ); - } - - foreach( var manip in manipulations ) - { - ImGui.Text( manip.IdentifierString() ); - } - - indent.Pop( 15f ); - } - } - - private void DrawFileSwapTab() - { - if( _editMode ) - { - DrawFileSwapTabEdit(); - return; - } - - if( !Meta.FileSwaps.Any() || !ImGui.BeginTabItem( LabelFileSwapTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - - ImGui.SetNextItemWidth( -1 ); - if( !ImGui.BeginTable( LabelFileSwapHeader, 3, flags, AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndTable ); - - foreach( var (source, target) in Meta.FileSwaps ) - { - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( source ); - - ImGui.TableNextColumn(); - ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); - - ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( target ); - - ImGui.TableNextRow(); - } - } - - private void UpdateFilenameList() - { - if( _fullFilenameList != null ) - { - return; - } - - _fullFilenameList = Mod.Data.Resources.ModFiles - .Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray(); - - if( Meta.Groups.Count == 0 ) - { - return; - } - - for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) - { - foreach( var group in Meta.Groups.Values ) - { - var inAll = true; - foreach( var option in group.Options ) - { - if( option.OptionFiles.ContainsKey( _fullFilenameList[ i ].relName ) ) - { - _fullFilenameList[ i ].color = ColorYellow; - } - else - { - inAll = false; - } - } - - if( inAll && group.SelectionType == SelectType.Single ) - { - _fullFilenameList[ i ].color = ColorGreen; - } - } - } - } - - private void DrawFileListTab() - { - if( !ImGui.BeginTabItem( LabelFileListTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - ImGuiCustom.HoverTooltip( TooltipFilesTab ); - - ImGui.SetNextItemWidth( -1 ); - if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) - { - raii.Push( ImGui.EndListBox ); - UpdateFilenameList(); - using var colorRaii = new ImGuiRaii.Color(); - foreach( var (name, _, color, _) in _fullFilenameList! ) - { - colorRaii.Push( ImGuiCol.Text, color ); - ImGui.Selectable( name.FullName ); - colorRaii.Pop(); - } - } - else - { - _fullFilenameList = null; - } - } - - private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) - { - removeFolders = 0; - var defaultIndex = - gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) ); - if( defaultIndex < 0 ) - { - return defaultIndex; - } - - string path = gamePaths[ defaultIndex ]; - if( path.Length == TextDefaultGamePath.Length ) - { - return defaultIndex; - } - - if( path[ TextDefaultGamePath.Length ] != '-' - || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) ) - { - return -1; - } - return defaultIndex; } - private void HandleSelectedFilesButton( bool remove ) + string path = gamePaths[ defaultIndex ]; + if( path.Length == TextDefaultGamePath.Length ) { - if( _selectedOption == null ) - { - return; - } - - var option = ( Option )_selectedOption; - - var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray(); - if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 ) - { - return; - } - - var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); - var changed = false; - for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) - { - if( !_fullFilenameList![ i ].selected ) - { - continue; - } - - _fullFilenameList![ i ].selected = false; - var relName = _fullFilenameList[ i ].relName; - if( defaultIndex >= 0 ) - { - gamePaths[ defaultIndex ] = relName.ToGamePath( removeFolders ); - } - - if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) - { - if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 ) - { - changed = true; - } - - if( setPaths.Count == 0 && option.OptionFiles.Remove( relName ) ) - { - changed = true; - } - } - else - { - changed = gamePaths - .Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) ); - } - } - - if( changed ) - { - _fullFilenameList = null; - _selector.SaveCurrentMod(); - // Since files may have changed, we need to recompute effective files. - foreach( var collection in _modManager.Collections.Collections.Values - .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) - { - collection.CalculateEffectiveFileList( _modManager.TempPath, false, - collection == _modManager.Collections.ActiveCollection ); - } - - // If the mod is enabled in the current collection, its conflicts may have changed. - if( Mod!.Settings.Enabled ) - { - _selector.Cache.TriggerFilterReset(); - } - } + return defaultIndex; } - private void DrawAddToGroupButton() + if( path[ TextDefaultGamePath.Length ] != '-' + || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) ) { - if( ImGui.Button( ButtonAddToGroup ) ) - { - HandleSelectedFilesButton( false ); - } + return -1; } - private void DrawRemoveFromGroupButton() + return defaultIndex; + } + + private void HandleSelectedFilesButton( bool remove ) + { + if( _selectedOption == null ) { - if( ImGui.Button( ButtonRemoveFromGroup ) ) - { - HandleSelectedFilesButton( true ); - } + return; } - private void DrawGamePathInput() + var option = ( Option )_selectedOption; + + var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray(); + if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 ) { - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, - 128 ); - ImGuiCustom.HoverTooltip( TooltipGamePathsEdit ); + return; } - private void DrawGroupRow() + var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); + var changed = false; + for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) { - if( _selectedGroup == null ) + if( !_fullFilenameList![ i ].selected ) { - SelectGroup(); + continue; } - if( _selectedOption == null ) + _fullFilenameList![ i ].selected = false; + var relName = _fullFilenameList[ i ].relName; + if( defaultIndex >= 0 ) { - SelectOption(); + gamePaths[ defaultIndex ] = relName.ToGamePath( removeFolders ); } - if( !DrawEditGroupSelector() ) + if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) { - return; - } - - ImGui.SameLine(); - if( !DrawEditOptionSelector() ) - { - return; - } - - ImGui.SameLine(); - DrawAddToGroupButton(); - ImGui.SameLine(); - DrawRemoveFromGroupButton(); - ImGui.SameLine(); - DrawGamePathInput(); - } - - private void DrawFileAndGamePaths( int idx ) - { - void Selectable( uint colorNormal, uint colorReplace ) - { - var loc = _fullFilenameList![ idx ].color; - if( loc == colorNormal ) + if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 ) { - loc = colorReplace; + changed = true; } - using var colors = ImGuiRaii.PushColor( ImGuiCol.Text, loc ); - ImGui.Selectable( _fullFilenameList[ idx ].name.FullName, ref _fullFilenameList[ idx ].selected ); - } - - const float indentWidth = 30f; - if( _selectedOption == null ) - { - Selectable( 0, ColorGreen ); - return; - } - - var fileName = _fullFilenameList![ idx ].relName; - var optionFiles = ( ( Option )_selectedOption ).OptionFiles; - if( optionFiles.TryGetValue( fileName, out var gamePaths ) ) - { - Selectable( 0, ColorGreen ); - - using var indent = ImGuiRaii.PushIndent( indentWidth ); - var tmpPaths = gamePaths.ToArray(); - foreach( var gamePath in tmpPaths ) + if( setPaths.Count == 0 && option.OptionFiles.Remove( relName ) ) { - string tmp = gamePath; - if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) - && tmp != gamePath ) - { - gamePaths.Remove( gamePath ); - if( tmp.Length > 0 ) - { - gamePaths.Add( new GamePath( tmp ) ); - } - else if( gamePaths.Count == 0 ) - { - optionFiles.Remove( fileName ); - } - - _selector.SaveCurrentMod(); - _selector.ReloadCurrentMod(); - } + changed = true; } } else { - Selectable( ColorYellow, ColorRed ); + changed = gamePaths + .Aggregate( changed, ( current, gamePath ) => current | option.AddFile( relName, gamePath ) ); } } - private void DrawMultiSelectorCheckBox( OptionGroup group, int idx, int flag, string label ) + if( changed ) { - var enabled = ( flag & ( 1 << idx ) ) != 0; - var oldEnabled = enabled; - if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) + _fullFilenameList = null; + _selector.SaveCurrentMod(); + // Since files may have changed, we need to recompute effective files. + var modManager = Penumbra.ModManager; + foreach( var collection in modManager.Collections.Collections.Values + .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) { - Mod.Settings.Settings[ group.GroupName ] ^= 1 << idx; - Save(); - // If the mod is enabled, recalculate files and filters. - if( Mod.Settings.Enabled ) + collection.CalculateEffectiveFileList( modManager.TempPath, false, + collection == modManager.Collections.ActiveCollection ); + } + + // If the mod is enabled in the current collection, its conflicts may have changed. + if( Mod!.Settings.Enabled ) + { + _selector.Cache.TriggerFilterReset(); + } + } + } + + private void DrawAddToGroupButton() + { + if( ImGui.Button( ButtonAddToGroup ) ) + { + HandleSelectedFilesButton( false ); + } + } + + private void DrawRemoveFromGroupButton() + { + if( ImGui.Button( ButtonRemoveFromGroup ) ) + { + HandleSelectedFilesButton( true ); + } + } + + private void DrawGamePathInput() + { + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, + 128 ); + ImGuiCustom.HoverTooltip( TooltipGamePathsEdit ); + } + + private void DrawGroupRow() + { + if( _selectedGroup == null ) + { + SelectGroup(); + } + + if( _selectedOption == null ) + { + SelectOption(); + } + + if( !DrawEditGroupSelector() ) + { + return; + } + + ImGui.SameLine(); + if( !DrawEditOptionSelector() ) + { + return; + } + + ImGui.SameLine(); + DrawAddToGroupButton(); + ImGui.SameLine(); + DrawRemoveFromGroupButton(); + ImGui.SameLine(); + DrawGamePathInput(); + } + + private void DrawFileAndGamePaths( int idx ) + { + void Selectable( uint colorNormal, uint colorReplace ) + { + var loc = _fullFilenameList![ idx ].color; + if( loc == colorNormal ) + { + loc = colorReplace; + } + + using var colors = ImGuiRaii.PushColor( ImGuiCol.Text, loc ); + ImGui.Selectable( _fullFilenameList[ idx ].name.FullName, ref _fullFilenameList[ idx ].selected ); + } + + const float indentWidth = 30f; + if( _selectedOption == null ) + { + Selectable( 0, ColorGreen ); + return; + } + + var fileName = _fullFilenameList![ idx ].relName; + var optionFiles = ( ( Option )_selectedOption ).OptionFiles; + if( optionFiles.TryGetValue( fileName, out var gamePaths ) ) + { + Selectable( 0, ColorGreen ); + + using var indent = ImGuiRaii.PushIndent( indentWidth ); + var tmpPaths = gamePaths.ToArray(); + foreach( var gamePath in tmpPaths ) + { + string tmp = gamePath; + if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) + && tmp != gamePath ) { - _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); - } - } - } - - private void DrawMultiSelector( OptionGroup group ) - { - if( group.Options.Count == 0 ) - { - return; - } - - ImGuiCustom.BeginFramedGroup( group.GroupName ); - using var raii = ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); - for( var i = 0; i < group.Options.Count; ++i ) - { - DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ], - $"{group.Options[ i ].OptionName}##{group.GroupName}" ); - } - } - - private void DrawSingleSelector( OptionGroup group ) - { - if( group.Options.Count < 2 ) - { - return; - } - - var code = Mod.Settings.Settings[ group.GroupName ]; - if( ImGui.Combo( group.GroupName, ref code - , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) - && code != Mod.Settings.Settings[ group.GroupName ] ) - { - Mod.Settings.Settings[ group.GroupName ] = code; - Save(); - // If the mod is enabled, recalculate files and filters. - if( Mod.Settings.Enabled ) - { - _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); - } - } - } - - private void DrawGroupSelectors() - { - foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) - { - DrawSingleSelector( g ); - } - - foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) - { - DrawMultiSelector( g ); - } - } - - private void DrawConfigurationTab() - { - if( !_editMode && !Meta.HasGroupsWithConfig || !ImGui.BeginTabItem( LabelConfigurationTab ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - if( _editMode ) - { - DrawGroupSelectorsEdit(); - } - else - { - DrawGroupSelectors(); - } - } - - private void DrawMetaManipulationsTab() - { - if( !_editMode && Mod.Data.Resources.MetaManipulations.Count == 0 || !ImGui.BeginTabItem( "Meta Manipulations" ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - - if( !ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) ) - { - return; - } - - raii.Push( ImGui.EndListBox ); - - var manips = Mod.Data.Resources.MetaManipulations; - var changes = false; - if( _editMode || manips.DefaultData.Count > 0 ) - { - if( ImGui.CollapsingHeader( "Default" ) ) - { - changes = DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData, ref manips.Count ); - } - } - - foreach( var (groupName, group) in manips.GroupData ) - { - foreach( var (optionName, option) in group ) - { - if( ImGui.CollapsingHeader( $"{groupName} - {optionName}" ) ) + gamePaths.Remove( gamePath ); + if( tmp.Length > 0 ) { - changes |= DrawMetaManipulationsTable( $"##{groupName}{optionName}manips", option, ref manips.Count ); + gamePaths.Add( new GamePath( tmp ) ); } + else if( gamePaths.Count == 0 ) + { + optionFiles.Remove( fileName ); + } + + _selector.SaveCurrentMod(); + _selector.ReloadCurrentMod(); } } - - if( changes ) - { - Mod.Data.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( Mod.Data.BasePath ) ); - Mod.Data.Resources.SetManipulations( Meta, Mod.Data.BasePath, false ); - _selector.ReloadCurrentMod( true, false ); - } } - - public void Draw( bool editMode ) + else { - _editMode = editMode; - if( !ImGui.BeginTabBar( LabelPluginDetails ) ) - { - return; - } - - using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabBar ); - DrawAboutTab(); - DrawChangedItemsTab(); - - DrawConfigurationTab(); - if( _editMode ) - { - DrawFileListTabEdit(); - } - else - { - DrawFileListTab(); - } - - DrawFileSwapTab(); - DrawMetaManipulationsTab(); - DrawConflictTab(); + Selectable( ColorYellow, ColorRed ); } } + + private void DrawMultiSelectorCheckBox( OptionGroup group, int idx, int flag, string label ) + { + var enabled = ( flag & ( 1 << idx ) ) != 0; + var oldEnabled = enabled; + if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) + { + Mod.Settings.Settings[ group.GroupName ] ^= 1 << idx; + Save(); + // If the mod is enabled, recalculate files and filters. + if( Mod.Settings.Enabled ) + { + _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); + } + } + } + + private void DrawMultiSelector( OptionGroup group ) + { + if( group.Options.Count == 0 ) + { + return; + } + + ImGuiCustom.BeginFramedGroup( group.GroupName ); + using var raii = ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup ); + for( var i = 0; i < group.Options.Count; ++i ) + { + DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ], + $"{group.Options[ i ].OptionName}##{group.GroupName}" ); + } + } + + private void DrawSingleSelector( OptionGroup group ) + { + if( group.Options.Count < 2 ) + { + return; + } + + var code = Mod.Settings.Settings[ group.GroupName ]; + if( ImGui.Combo( group.GroupName, ref code + , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) + && code != Mod.Settings.Settings[ group.GroupName ] ) + { + Mod.Settings.Settings[ group.GroupName ] = code; + Save(); + // If the mod is enabled, recalculate files and filters. + if( Mod.Settings.Enabled ) + { + _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); + } + } + } + + private void DrawGroupSelectors() + { + foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) + { + DrawSingleSelector( g ); + } + + foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) + { + DrawMultiSelector( g ); + } + } + + private void DrawConfigurationTab() + { + if( !_editMode && !Meta.HasGroupsWithConfig || !ImGui.BeginTabItem( LabelConfigurationTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + if( _editMode ) + { + DrawGroupSelectorsEdit(); + } + else + { + DrawGroupSelectors(); + } + } + + private void DrawMetaManipulationsTab() + { + if( !_editMode && Mod.Data.Resources.MetaManipulations.Count == 0 || !ImGui.BeginTabItem( "Meta Manipulations" ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + if( !ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndListBox ); + + var manips = Mod.Data.Resources.MetaManipulations; + var changes = false; + if( _editMode || manips.DefaultData.Count > 0 ) + { + if( ImGui.CollapsingHeader( "Default" ) ) + { + changes = DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData, ref manips.Count ); + } + } + + foreach( var (groupName, group) in manips.GroupData ) + { + foreach( var (optionName, option) in group ) + { + if( ImGui.CollapsingHeader( $"{groupName} - {optionName}" ) ) + { + changes |= DrawMetaManipulationsTable( $"##{groupName}{optionName}manips", option, ref manips.Count ); + } + } + } + + if( changes ) + { + Mod.Data.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( Mod.Data.BasePath ) ); + Mod.Data.Resources.SetManipulations( Meta, Mod.Data.BasePath, false ); + _selector.ReloadCurrentMod( true, false ); + } + } + + public void Draw( bool editMode ) + { + _editMode = editMode; + if( !ImGui.BeginTabBar( LabelPluginDetails ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabBar ); + DrawAboutTab(); + DrawChangedItemsTab(); + + DrawConfigurationTab(); + if( _editMode ) + { + DrawFileListTabEdit(); + } + else + { + DrawFileListTab(); + } + + DrawFileSwapTab(); + DrawMetaManipulationsTab(); + DrawConflictTab(); + } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs index 8248e91b..3c8a5feb 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs @@ -112,7 +112,7 @@ namespace Penumbra.UI var groupName = group.GroupName; if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) { - if( _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) + if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) { _selector.Cache.TriggerFilterReset(); } @@ -165,7 +165,7 @@ namespace Penumbra.UI { if( newName.Length == 0 ) { - _modManager.RemoveModOption( i, group, Mod.Data ); + Penumbra.ModManager.RemoveModOption( i, group, Mod.Data ); } else if( newName != opt.OptionName ) { @@ -189,7 +189,7 @@ namespace Penumbra.UI var groupName = group.GroupName; if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - if( _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) + if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) { _selector.Cache.TriggerFilterReset(); } @@ -221,7 +221,7 @@ namespace Penumbra.UI { if( newName.Length == 0 ) { - _modManager.RemoveModOption( code, group, Mod.Data ); + Penumbra.ModManager.RemoveModOption( code, group, Mod.Data ); } else { @@ -267,7 +267,7 @@ namespace Penumbra.UI if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); + Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); // Adds empty group, so can not change filters. } } @@ -280,7 +280,7 @@ namespace Penumbra.UI if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); + Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); // Adds empty group, so can not change filters. } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index 4c9a7ad2..4d3148ae 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -229,7 +229,7 @@ namespace Penumbra.UI if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) { using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); - var defaults = ( EqpEntry )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( EqpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var attributes = Eqp.EqpAttributes[ id.Slot ]; foreach( var flag in attributes ) @@ -254,7 +254,7 @@ namespace Penumbra.UI private bool DrawGmpRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( GmpEntry )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( GmpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].GmpIdentifier; var val = list[ manipIdx ].GmpValue; @@ -364,7 +364,7 @@ namespace Penumbra.UI private bool DrawEqdpRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( EqdpEntry )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( EqdpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].EqdpIdentifier; var val = list[ manipIdx ].EqdpValue; @@ -401,7 +401,7 @@ namespace Penumbra.UI private bool DrawEstRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( ushort )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( ushort )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].EstIdentifier; var val = list[ manipIdx ].EstValue; @@ -433,7 +433,7 @@ namespace Penumbra.UI private bool DrawImcRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( ImcFile.ImageChangeData )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( ImcFile.ImageChangeData )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].ImcIdentifier; var val = list[ manipIdx ].ImcValue; @@ -492,7 +492,7 @@ namespace Penumbra.UI private bool DrawRspRow( int manipIdx, IList< MetaManipulation > list ) { - var defaults = ( float )Service< MetaDefaults >.Get().GetDefaultValue( list[ manipIdx ] )!; + var defaults = ( float )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!; var ret = false; var id = list[ manipIdx ].RspIdentifier; var val = list[ manipIdx ].RspValue; @@ -694,7 +694,7 @@ namespace Penumbra.UI && newManip != null && list.All( m => m.Identifier != newManip.Value.Identifier ) ) { - var def = Service< MetaDefaults >.Get().GetDefaultValue( newManip.Value ); + var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value ); if( def != null ) { var manip = newManip.Value.Type switch diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index 8be04f51..10b85e3d 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -50,7 +50,6 @@ public partial class SettingsInterface private readonly SettingsInterface _base; private readonly Selector _selector; - private readonly ModManager _modManager; private readonly HashSet< string > _newMods; public readonly PluginDetails Details; @@ -68,7 +67,6 @@ public partial class SettingsInterface _newMods = newMods; Details = new PluginDetails( _base, _selector ); _currentWebsite = Meta?.Website ?? ""; - _modManager = Service< ModManager >.Get(); } private Mod.Mod? Mod @@ -79,11 +77,12 @@ public partial class SettingsInterface private void DrawName() { - var name = Meta!.Name; - if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && _modManager.RenameMod( name, Mod!.Data ) ) + var name = Meta!.Name; + var modManager = Penumbra.ModManager; + if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) ) { _selector.SelectModOnUpdate( Mod.Data.BasePath.Name ); - if( !_modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) + if( !modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) ) { Mod.Data.Rename( name ); } @@ -286,7 +285,7 @@ public partial class SettingsInterface { ImGui.OpenPopup( LabelOverWriteDir ); } - else if( _modManager.RenameModFolder( Mod.Data, newDir ) ) + else if( Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) { _selector.ReloadCurrentMod(); ImGui.CloseCurrentPopup(); @@ -301,12 +300,12 @@ public partial class SettingsInterface if( sourceUri.Equals( targetUri ) ) { var tmpFolder = new DirectoryInfo( TempFile.TempFileName( dir.Parent! ).FullName ); - if( _modManager.RenameModFolder( Mod.Data, tmpFolder ) ) + if( Penumbra.ModManager.RenameModFolder( Mod.Data, tmpFolder ) ) { - if( !_modManager.RenameModFolder( Mod.Data, newDir ) ) + if( !Penumbra.ModManager.RenameModFolder( Mod.Data, newDir ) ) { PluginLog.Error( "Could not recapitalize folder after renaming, reverting rename." ); - _modManager.RenameModFolder( Mod.Data, dir ); + Penumbra.ModManager.RenameModFolder( Mod.Data, dir ); } _selector.ReloadCurrentMod(); @@ -364,17 +363,14 @@ public partial class SettingsInterface ImGui.Text( $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); - if( ImGui.Button( "Yes", buttonSize ) ) + if( ImGui.Button( "Yes", buttonSize ) && MergeFolderInto( dir, newDir ) ) { - if( MergeFolderInto( dir, newDir ) ) - { - Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir, false ); + Penumbra.ModManager.RenameModFolder( Mod.Data, newDir, false ); - _selector.SelectModOnUpdate( _newName ); + _selector.SelectModOnUpdate( _newName ); - closeParent = true; - ImGui.CloseCurrentPopup(); - } + closeParent = true; + ImGui.CloseCurrentPopup(); } ImGui.SameLine(); @@ -580,7 +576,7 @@ public partial class SettingsInterface DrawMaterialChangeRow(); - DrawSortOrder( Mod!.Data, _modManager, _selector ); + DrawSortOrder( Mod!.Data, Penumbra.ModManager, _selector ); } public void Draw() diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index dbecb883..a435a6f2 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -101,7 +101,7 @@ public partial class SettingsInterface ImGui.CloseCurrentPopup(); var mod = Mod; Cache.RemoveMod( mod ); - _modManager.DeleteMod( mod.Data.BasePath ); + Penumbra.ModManager.DeleteMod( mod.Data.BasePath ); ModFileSystem.InvokeChange(); ClearSelection(); } @@ -166,7 +166,7 @@ public partial class SettingsInterface var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); modMeta.SaveToFile( metaFile ); - _modManager.AddMod( newDir ); + Penumbra.ModManager.AddMod( newDir ); ModFileSystem.InvokeChange(); SelectModOnUpdate( newDir.Name ); } @@ -464,7 +464,7 @@ public partial class SettingsInterface return; } - if( _index >= 0 && _modManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) + if( _index >= 0 && Penumbra.ModManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) { SelectModOnUpdate( Mod.Data.BasePath.Name ); _base._menu.InstalledTab.ModPanel.Details.ResetState(); @@ -526,11 +526,11 @@ public partial class SettingsInterface } Cache.TriggerFilterReset(); - var collection = _modManager.Collections.CurrentCollection; + var collection = Penumbra.ModManager.Collections.CurrentCollection; if( collection.Cache != null ) { - collection.CalculateEffectiveFileList( _modManager.TempPath, metaManips, - collection == _modManager.Collections.ActiveCollection ); + collection.CalculateEffectiveFileList( Penumbra.ModManager.TempPath, metaManips, + collection == Penumbra.ModManager.Collections.ActiveCollection ); } collection.Save(); @@ -597,7 +597,6 @@ public partial class SettingsInterface private partial class Selector { private readonly SettingsInterface _base; - private readonly ModManager _modManager; public readonly ModListCache Cache; private float _selectorScalingFactor = 1; @@ -605,14 +604,13 @@ public partial class SettingsInterface public Selector( SettingsInterface ui, IReadOnlySet< string > newMods ) { _base = ui; - _modManager = Service< ModManager >.Get(); - Cache = new ModListCache( _modManager, newMods ); + Cache = new ModListCache( Penumbra.ModManager, newMods ); } private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) { if( collection == ModCollection.Empty - || collection == _modManager.Collections.CurrentCollection ) + || collection == Penumbra.ModManager.Collections.CurrentCollection ) { using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); ImGui.Button( label, Vector2.UnitX * size ); @@ -641,10 +639,10 @@ public partial class SettingsInterface - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2, 5f ); ImGui.SameLine(); - DrawCollectionButton( "Default", "default", buttonSize, _modManager.Collections.DefaultCollection ); + DrawCollectionButton( "Default", "default", buttonSize, Penumbra.ModManager.Collections.DefaultCollection ); ImGui.SameLine(); - DrawCollectionButton( "Forced", "forced", buttonSize, _modManager.Collections.ForcedCollection ); + DrawCollectionButton( "Forced", "forced", buttonSize, Penumbra.ModManager.Collections.ForcedCollection ); ImGui.SameLine(); ImGui.SetNextItemWidth( comboSize ); @@ -655,7 +653,7 @@ public partial class SettingsInterface private void DrawFolderContent( ModFolder folder, ref int idx ) { // Collection may be manipulated. - foreach( var item in folder.GetItems( _modManager.Config.SortFoldersFirst ).ToArray() ) + foreach( var item in folder.GetItems( Penumbra.ModManager.Config.SortFoldersFirst ).ToArray() ) { if( item is ModFolder sub ) { @@ -785,7 +783,7 @@ public partial class SettingsInterface style.Push( ImGuiStyleVar.IndentSpacing, 12.5f ); var modIndex = 0; - DrawFolderContent( _modManager.StructuredMods, ref modIndex ); + DrawFolderContent( Penumbra.ModManager.StructuredMods, ref modIndex ); style.Pop(); } diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index 39f89602..f46ad018 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -1,9 +1,12 @@ +using System; +using System.Linq; using System.Numerics; using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using ImGuiNET; +using Penumbra.Interop; using Penumbra.UI.Custom; namespace Penumbra.UI; @@ -21,16 +24,22 @@ public partial class SettingsInterface : $"({type:X8}) {( char )byte1}{( char )byte2}{( char )byte3}{( char )byte4} - {count}###{label}{type}Debug"; } - private unsafe void DrawResourceMap( string label, StdMap< uint, Pointer< ResourceHandle > >* typeMap ) + private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) { - if( typeMap == null || !ImGui.TreeNodeEx( label ) ) + if( map == null ) + { + return; + } + + var label = GetNodeLabel( ( uint )category, ext, map->Count ); + if( !ImGui.TreeNodeEx( label ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); - if( typeMap->Count == 0 || !ImGui.BeginTable( $"##{label}_table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) + if( map->Count == 0 || !ImGui.BeginTable( $"##{label}_table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) { return; } @@ -44,21 +53,19 @@ public partial class SettingsInterface ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale ); ImGui.TableHeadersRow(); - var node = typeMap->SmallestValue; - while( !node->IsNil ) + ResourceLoader.IterateResourceMap( map, ( hash, r ) => { - ImGui.TableNextRow(); ImGui.TableNextColumn(); - ImGui.Text( $"0x{node->KeyValuePair.Item1:X8}" ); + ImGui.Text( $"0x{hash:X8}" ); ImGui.TableNextColumn(); - var address = $"0x{( ulong )node->KeyValuePair.Item2.Value:X}"; + var address = $"0x{( ulong )r:X}"; ImGui.Text( address ); if( ImGui.IsItemClicked() ) { ImGui.SetClipboardText( address ); } - ref var name = ref node->KeyValuePair.Item2.Value->FileName; + ref var name = ref r->FileName; ImGui.TableNextColumn(); if( name.Capacity > 15 ) { @@ -72,30 +79,73 @@ public partial class SettingsInterface } } - //ImGui.Text( node->KeyValuePair.Item2.Value->FileName.ToString() ); + if( ImGui.IsItemClicked() ) + { + var data = ( ( Interop.Structs.ResourceHandle* )r )->GetData(); + ImGui.SetClipboardText( string.Join( " ", + new ReadOnlySpan< byte >( ( byte* )data.Data, data.Length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); + //ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) ); + } + ImGui.TableNextColumn(); - ImGui.Text( node->KeyValuePair.Item2.Value->RefCount.ToString() ); - node = node->Next(); - } + ImGui.Text( r->RefCount.ToString() ); + } ); } - private unsafe void DrawCategoryContainer( ResourceCategory category, ResourceGraph.CategoryContainer container ) + private unsafe void DrawCategoryContainer( ResourceCategory category, + StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map ) { - var map = container.MainMap; if( map == null || !ImGui.TreeNodeEx( $"({( uint )category:D2}) {category} - {map->Count}###{( uint )category}Debug" ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop ); + ResourceLoader.IterateExtMap( map, ( ext, map ) => DrawResourceMap( category, ext, map ) ); + } - var node = map->SmallestValue; - while( !node->IsNil ) + + private static unsafe void DrawResourceProblems() + { + if( !ImGui.CollapsingHeader( "Resource Problems##ResourceManager" ) + || !ImGui.BeginTable( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) { - DrawResourceMap( GetNodeLabel( ( uint )category, node->KeyValuePair.Item1, node->KeyValuePair.Item2.Value->Count ), - node->KeyValuePair.Item2.Value ); - node = node->Next(); + return; } + + using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + ResourceLoader.IterateResources( ( _, r ) => + { + if( r->RefCount < 10000 ) + { + return; + } + + ImGui.TableNextColumn(); + ImGui.Text( r->Category.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( r->FileType.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( r->Id.ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( ( ( ulong )r ).ToString( "X" ) ); + ImGui.TableNextColumn(); + ImGui.Text( r->RefCount.ToString() ); + ImGui.TableNextColumn(); + ref var name = ref r->FileName; + if( name.Capacity > 15 ) + { + ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); + } + else + { + fixed( byte* ptr = name.Buffer ) + { + ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); + } + } + } ); } private unsafe void DrawResourceManagerTab() @@ -107,7 +157,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - var resourceHandler = *( ResourceManager** )Dalamud.SigScanner.GetStaticAddressFromSig( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 32 C0" ); + var resourceHandler = *ResourceLoader.ResourceManager; if( resourceHandler == null ) { @@ -120,20 +170,6 @@ public partial class SettingsInterface return; } - DrawCategoryContainer( ResourceCategory.Common, resourceHandler->ResourceGraph->CommonContainer ); - DrawCategoryContainer( ResourceCategory.BgCommon, resourceHandler->ResourceGraph->BgCommonContainer ); - DrawCategoryContainer( ResourceCategory.Bg, resourceHandler->ResourceGraph->BgContainer ); - DrawCategoryContainer( ResourceCategory.Cut, resourceHandler->ResourceGraph->CutContainer ); - DrawCategoryContainer( ResourceCategory.Chara, resourceHandler->ResourceGraph->CharaContainer ); - DrawCategoryContainer( ResourceCategory.Shader, resourceHandler->ResourceGraph->ShaderContainer ); - DrawCategoryContainer( ResourceCategory.Ui, resourceHandler->ResourceGraph->UiContainer ); - DrawCategoryContainer( ResourceCategory.Sound, resourceHandler->ResourceGraph->SoundContainer ); - DrawCategoryContainer( ResourceCategory.Vfx, resourceHandler->ResourceGraph->VfxContainer ); - DrawCategoryContainer( ResourceCategory.UiScript, resourceHandler->ResourceGraph->UiScriptContainer ); - DrawCategoryContainer( ResourceCategory.Exd, resourceHandler->ResourceGraph->ExdContainer ); - DrawCategoryContainer( ResourceCategory.GameScript, resourceHandler->ResourceGraph->GameScriptContainer ); - DrawCategoryContainer( ResourceCategory.Music, resourceHandler->ResourceGraph->MusicContainer ); - DrawCategoryContainer( ResourceCategory.SqpackTest, resourceHandler->ResourceGraph->SqpackTestContainer ); - DrawCategoryContainer( ResourceCategory.Debug, resourceHandler->ResourceGraph->DebugContainer ); + ResourceLoader.IterateGraphs( DrawCategoryContainer ); } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 112c8136..884a99fa 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -3,12 +3,12 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; -using System.Text.RegularExpressions; using Dalamud.Interface; using Dalamud.Interface.Components; -using Dalamud.Logging; using ImGuiNET; +using Penumbra.GameData.ByteString; using Penumbra.Interop; +using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -71,7 +71,8 @@ public partial class SettingsInterface + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); ImGui.SameLine(); - DrawOpenDirectoryButton( 0, _base._modManager.BasePath, _base._modManager.Valid ); + var modManager = Penumbra.ModManager; + DrawOpenDirectoryButton( 0, modManager.BasePath, modManager.Valid ); ImGui.EndGroup(); if( _config.ModDirectory == _newModDirectory || !_newModDirectory.Any() ) @@ -82,7 +83,7 @@ public partial class SettingsInterface if( save || DrawPressEnterWarning( _config.ModDirectory ) ) { _base._menu.InstalledTab.Selector.ClearSelection(); - _base._modManager.DiscoverMods( _newModDirectory ); + modManager.DiscoverMods( _newModDirectory ); _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); _newModDirectory = _config.ModDirectory; } @@ -99,7 +100,8 @@ public partial class SettingsInterface + "A directory 'penumbrametatmp' will be created as a sub-directory to the specified directory.\n" + "If none is specified (i.e. this is blank) this directory will be created in the root directory instead.\n" ); ImGui.SameLine(); - DrawOpenDirectoryButton( 1, _base._modManager.TempPath, _base._modManager.TempWritable ); + var modManager = Penumbra.ModManager; + DrawOpenDirectoryButton( 1, modManager.TempPath, modManager.TempWritable ); ImGui.EndGroup(); if( _newTempDirectory == _config.TempDirectory ) @@ -109,7 +111,7 @@ public partial class SettingsInterface if( save || DrawPressEnterWarning( _config.TempDirectory ) ) { - _base._modManager.SetTempDirectory( _newTempDirectory ); + modManager.SetTempDirectory( _newTempDirectory ); _newTempDirectory = _config.TempDirectory; } } @@ -119,7 +121,7 @@ public partial class SettingsInterface if( ImGui.Button( "Rediscover Mods" ) ) { _base._menu.InstalledTab.Selector.ClearSelection(); - _base._modManager.DiscoverMods(); + Penumbra.ModManager.DiscoverMods(); _base._menu.InstalledTab.Selector.Cache.TriggerListReset(); } @@ -208,26 +210,26 @@ public partial class SettingsInterface private void DrawLogLoadedFilesBox() { - ImGui.Checkbox( "Log Loaded Files", ref _base._penumbra.ResourceLoader.LogAllFiles ); - ImGui.SameLine(); - var regex = _base._penumbra.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; - var tmp = regex; - ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); - if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) - { - try - { - var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null; - _base._penumbra.ResourceLoader.LogFileFilter = newRegex; - } - catch( Exception e ) - { - PluginLog.Debug( "Could not create regex:\n{Exception}", e ); - } - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Log all loaded files that match the given Regex to the PluginLog." ); + //ImGui.Checkbox( "Log Loaded Files", ref _base._penumbra.ResourceLoader.LogAllFiles ); + //ImGui.SameLine(); + //var regex = _base._penumbra.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; + //var tmp = regex; + //ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth ); + //if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) + //{ + // try + // { + // var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null; + // _base._penumbra.ResourceLoader.LogFileFilter = newRegex; + // } + // catch( Exception e ) + // { + // PluginLog.Debug( "Could not create regex:\n{Exception}", e ); + // } + //} + // + //ImGui.SameLine(); + //ImGuiComponents.HelpMarker( "Log all loaded files that match the given Regex to the PluginLog." ); } private void DrawDisableNotificationsBox() @@ -307,7 +309,7 @@ public partial class SettingsInterface { if( ImGui.Button( "Reload Resident Resources" ) ) { - Service< ResidentResources >.Get().ReloadResidentResources(); + Penumbra.ResidentResources.Reload(); } ImGui.SameLine(); @@ -325,6 +327,11 @@ public partial class SettingsInterface DrawReloadResourceButton(); } + public static unsafe void Text( Utf8String s ) + { + ImGuiNative.igTextUnformatted( ( byte* )s.Path, ( byte* )s.Path + s.Length ); + } + public void Draw() { if( !ImGui.BeginTabItem( "Settings" ) ) diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 20eaff02..7f0f74d6 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -1,7 +1,5 @@ using System; using System.Numerics; -using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.UI; @@ -15,17 +13,13 @@ public partial class SettingsInterface : IDisposable private readonly Penumbra _penumbra; private readonly ManageModsButton _manageModsButton; - private readonly MenuBar _menuBar; private readonly SettingsMenu _menu; - private readonly ModManager _modManager; public SettingsInterface( Penumbra penumbra ) { _penumbra = penumbra; _manageModsButton = new ManageModsButton( this ); - _menuBar = new MenuBar( this ); _menu = new SettingsMenu( this ); - _modManager = Service< ModManager >.Get(); Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; Dalamud.PluginInterface.UiBuilder.Draw += Draw; @@ -51,31 +45,31 @@ public partial class SettingsInterface : IDisposable public void Draw() { - _menuBar.Draw(); _menu.Draw(); } private void ReloadMods() { _menu.InstalledTab.Selector.ClearSelection(); - _modManager.DiscoverMods( Penumbra.Config.ModDirectory ); + Penumbra.ModManager.DiscoverMods( Penumbra.Config.ModDirectory ); _menu.InstalledTab.Selector.Cache.TriggerListReset(); } private void SaveCurrentCollection( bool recalculateMeta ) { - var current = _modManager.Collections.CurrentCollection; + var current = Penumbra.ModManager.Collections.CurrentCollection; current.Save(); RecalculateCurrent( recalculateMeta ); } private void RecalculateCurrent( bool recalculateMeta ) { - var current = _modManager.Collections.CurrentCollection; + var modManager = Penumbra.ModManager; + var current = modManager.Collections.CurrentCollection; if( current.Cache != null ) { - current.CalculateEffectiveFileList( _modManager.TempPath, recalculateMeta, - current == _modManager.Collections.ActiveCollection ); + current.CalculateEffectiveFileList( modManager.TempPath, recalculateMeta, + current == modManager.Collections.ActiveCollection ); _menu.InstalledTab.Selector.Cache.TriggerFilterReset(); } } diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index 12c07617..1bb8628e 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -74,7 +74,7 @@ public partial class SettingsInterface CollectionsTab.Draw(); _importTab.Draw(); - if( Service< ModManager >.Get().Valid && !_importTab.IsImporting() ) + if( Penumbra.ModManager.Valid && !_importTab.IsImporting() ) { _browserTab.Draw(); InstalledTab.Draw(); diff --git a/Penumbra/Util/FullPath.cs b/Penumbra/Util/FullPath.cs deleted file mode 100644 index 829bc5bb..00000000 --- a/Penumbra/Util/FullPath.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.IO; -using Penumbra.GameData.Util; - -namespace Penumbra.Util; - -public readonly struct FullPath : IComparable, IEquatable< FullPath > -{ - public readonly string FullName; - public readonly string InternalName; - public readonly ulong Crc64; - - public FullPath( DirectoryInfo baseDir, RelPath relPath ) - { - FullName = Path.Combine( baseDir.FullName, relPath ); - InternalName = FullName.Replace( '\\', '/' ).ToLowerInvariant().Trim(); - Crc64 = ComputeCrc64( InternalName ); - } - - public FullPath( FileInfo file ) - { - FullName = file.FullName; - InternalName = FullName.Replace( '\\', '/' ).ToLowerInvariant().Trim(); - Crc64 = ComputeCrc64( InternalName ); - } - - public bool Exists - => File.Exists( FullName ); - - public string Extension - => Path.GetExtension( FullName ); - - public string Name - => Path.GetFileName( FullName ); - - public GamePath ToGamePath( DirectoryInfo dir ) - => FullName.StartsWith(dir.FullName) ? GamePath.GenerateUnchecked( InternalName[(dir.FullName.Length+1)..]) : GamePath.GenerateUnchecked( string.Empty ); - - private 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 int CompareTo( object? obj ) - => obj switch - { - FullPath p => string.Compare( InternalName, p.InternalName, StringComparison.InvariantCulture ), - FileInfo f => string.Compare( FullName, f.FullName, StringComparison.InvariantCultureIgnoreCase ), - _ => -1, - }; - - public bool Equals( FullPath other ) - { - if( Crc64 != other.Crc64 ) - { - return false; - } - - if( FullName.Length == 0 || other.FullName.Length == 0 ) - { - return true; - } - - return InternalName.Equals( other.InternalName ); - } - - public override int GetHashCode() - => Crc64.GetHashCode(); - - public override string ToString() - => FullName; -} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 8cb65a70..53429d70 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using Dalamud.Logging; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Files; using Penumbra.Mod; diff --git a/Penumbra/Util/RelPath.cs b/Penumbra/Util/RelPath.cs index 6d6ea369..f4ce0021 100644 --- a/Penumbra/Util/RelPath.cs +++ b/Penumbra/Util/RelPath.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; namespace Penumbra.Util; diff --git a/Penumbra/Util/Service.cs b/Penumbra/Util/Service.cs deleted file mode 100644 index 5391db22..00000000 --- a/Penumbra/Util/Service.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; - -namespace Penumbra.Util -{ - /// - /// Basic service locator - /// - /// The class you want to store in the service locator - public static class Service< T > where T : class - { - private static T? _object; - - public static void Set( T obj ) - { - // ReSharper disable once JoinNullCheckWithUsage - if( obj == null ) - { - throw new ArgumentNullException( $"{nameof( obj )} is null!" ); - } - - _object = obj; - } - - public static T Set() - { - _object = Activator.CreateInstance< T >(); - - return _object; - } - - public static T Set( params object[] args ) - { - var obj = ( T? )Activator.CreateInstance( typeof( T ), args ); - - // ReSharper disable once JoinNullCheckWithUsage - if( obj == null ) - { - throw new Exception( "what he fuc" ); - } - - _object = obj; - - return obj; - } - - public static T Get() - { - if( _object == null ) - { - throw new InvalidOperationException( $"{nameof( T )} hasn't been registered!" ); - } - - return _object; - } - } -} \ No newline at end of file