Add Byte String stuff, remove Services, cleanup and refactor interop stuff, disable path resolver for the moment

This commit is contained in:
Ottermandias 2022-03-06 00:40:42 +01:00
parent 0e8f839471
commit c3454f1d16
65 changed files with 4707 additions and 3371 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -155,7 +155,7 @@ namespace Penumbra.Importer
{
try
{
if( !Service< MetaDefaults >.Get().CheckAgainstDefault( manipulation ) )
if( Penumbra.MetaDefaults.CheckAgainstDefault( manipulation ) )
{
Manipulations.Add( manipulation );
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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" )}." );
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<ModManager>.Get().Collections.Collections.Values)
foreach(var collection in Penumbra.ModManager.Collections.Collections.Values)
collection.UpdateSetting(baseDir, meta, true);
}

View file

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

View file

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

View file

@ -145,7 +145,7 @@ namespace Penumbra.Mods
Cache.UpdateMetaManipulations();
if( activeCollection )
{
Service< ResidentResources >.Get().ReloadResidentResources();
Penumbra.ResidentResources.Reload();
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<string, object?>();
var forced = _modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary<string, object?>();
_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 );
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
namespace Penumbra.Util;

View file

@ -1,56 +0,0 @@
using System;
namespace Penumbra.Util
{
/// <summary>
/// Basic service locator
/// </summary>
/// <typeparam name="T">The class you want to store in the service locator</typeparam>
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;
}
}
}