mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Add Byte String stuff, remove Services, cleanup and refactor interop stuff, disable path resolver for the moment
This commit is contained in:
parent
0e8f839471
commit
c3454f1d16
65 changed files with 4707 additions and 3371 deletions
94
Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs
Normal file
94
Penumbra.GameData/ByteString/ByteStringFunctions.Case.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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" );
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
96
Penumbra.GameData/ByteString/FullPath.cs
Normal file
96
Penumbra.GameData/ByteString/FullPath.cs
Normal 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;
|
||||
}
|
||||
159
Penumbra.GameData/ByteString/NewGamePath.cs
Normal file
159
Penumbra.GameData/ByteString/NewGamePath.cs
Normal 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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Penumbra.GameData/ByteString/NewRelPath.cs
Normal file
133
Penumbra.GameData/ByteString/NewRelPath.cs
Normal 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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
Penumbra.GameData/ByteString/Utf8String.Access.cs
Normal file
75
Penumbra.GameData/ByteString/Utf8String.Access.cs
Normal 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;
|
||||
}
|
||||
127
Penumbra.GameData/ByteString/Utf8String.Comparison.cs
Normal file
127
Penumbra.GameData/ByteString/Utf8String.Comparison.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
214
Penumbra.GameData/ByteString/Utf8String.Construction.cs
Normal file
214
Penumbra.GameData/ByteString/Utf8String.Construction.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
142
Penumbra.GameData/ByteString/Utf8String.Manipulation.cs
Normal file
142
Penumbra.GameData/ByteString/Utf8String.Manipulation.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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 );
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 >();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +155,7 @@ namespace Penumbra.Importer
|
|||
{
|
||||
try
|
||||
{
|
||||
if( !Service< MetaDefaults >.Get().CheckAgainstDefault( manipulation ) )
|
||||
if( Penumbra.MetaDefaults.CheckAgainstDefault( manipulation ) )
|
||||
{
|
||||
Manipulations.Add( manipulation );
|
||||
}
|
||||
|
|
|
|||
75
Penumbra/Interop/CharacterUtility.cs
Normal file
75
Penumbra/Interop/CharacterUtility.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
36
Penumbra/Interop/ResidentResourceManager.cs
Normal file
36
Penumbra/Interop/ResidentResourceManager.cs
Normal 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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
205
Penumbra/Interop/ResourceLoader.Debug.cs
Normal file
205
Penumbra/Interop/ResourceLoader.Debug.cs
Normal 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" )}." );
|
||||
}
|
||||
168
Penumbra/Interop/ResourceLoader.Replacement.cs
Normal file
168
Penumbra/Interop/ResourceLoader.Replacement.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
104
Penumbra/Interop/ResourceLoader.TexMdl.cs
Normal file
104
Penumbra/Interop/ResourceLoader.TexMdl.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
87
Penumbra/Interop/Structs/CharacterUtility.cs
Normal file
87
Penumbra/Interop/Structs/CharacterUtility.cs
Normal 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.
|
||||
}
|
||||
11
Penumbra/Interop/Structs/FileMode.cs
Normal file
11
Penumbra/Interop/Structs/FileMode.cs
Normal 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,
|
||||
}
|
||||
67
Penumbra/Interop/Structs/ResourceHandle.cs
Normal file
67
Penumbra/Interop/Structs/ResourceHandle.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
21
Penumbra/Interop/Structs/SeFileDescriptor.cs
Normal file
21
Penumbra/Interop/Structs/SeFileDescriptor.cs
Normal 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; //
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ namespace Penumbra.Mods
|
|||
Cache.UpdateMetaManipulations();
|
||||
if( activeCollection )
|
||||
{
|
||||
Service< ResidentResources >.Get().ReloadResidentResources();
|
||||
Penumbra.ResidentResources.Reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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; //
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
131
Penumbra/UI/MenuTabs/TabDebug.Model.cs
Normal file
131
Penumbra/UI/MenuTabs/TabDebug.Model.cs
Normal 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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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" ) )
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.Util;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue