Metamanipulations seemingly working.

This commit is contained in:
Ottermandias 2022-03-14 15:23:00 +01:00
parent 707570615c
commit 6f527a1dbc
26 changed files with 1637 additions and 1237 deletions

View file

@ -108,7 +108,7 @@ public sealed unsafe partial class Utf8String
var start = 0; var start = 0;
for( var idx = IndexOf( b, start ); idx >= 0; idx = IndexOf( b, start ) ) for( var idx = IndexOf( b, start ); idx >= 0; idx = IndexOf( b, start ) )
{ {
if( start + 1 != idx || !removeEmpty ) if( start != idx || !removeEmpty )
{ {
ret.Add( Substring( start, idx - start ) ); ret.Add( Substring( start, idx - start ) );
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Logging; using Dalamud.Logging;
using Lumina.Data.Files; using Lumina.Data.Files;
@ -10,6 +11,7 @@ using Penumbra.GameData.Util;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Util; using Penumbra.Util;
using ImcFile = Penumbra.Meta.Files.ImcFile;
namespace Penumbra.Importer; namespace Penumbra.Importer;
@ -254,22 +256,38 @@ public class TexToolsMeta
using var reader = new BinaryReader( new MemoryStream( data ) ); using var reader = new BinaryReader( new MemoryStream( data ) );
var values = reader.ReadStructures< ImcEntry >( num ); var values = reader.ReadStructures< ImcEntry >( num );
ushort i = 0; ushort i = 0;
if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory ) try
{ {
// TODO check against default. if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory )
foreach( var value in values )
{ {
ImcManipulations.Add(new ImcManipulation(info.EquipSlot, i, info.PrimaryId, value) ); var def = new ImcFile( new ImcManipulation( info.EquipSlot, i, info.PrimaryId, new ImcEntry() ).GamePath() );
++i; var partIdx = ImcFile.PartIndex( info.EquipSlot );
foreach( var value in values )
{
if( !value.Equals( def.GetEntry( partIdx, i ) ) )
{
ImcManipulations.Add( new ImcManipulation( info.EquipSlot, i, info.PrimaryId, value ) );
}
++i;
}
}
else
{
var def = new ImcFile( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i,
new ImcEntry() ).GamePath() );
foreach( var value in values.Where( v => true || !v.Equals( def.GetEntry( 0, i ) ) ) )
{
ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i,
value ) );
++i;
}
} }
} }
else catch( Exception e )
{ {
foreach( var value in values ) PluginLog.Error( "Could not compute IMC manipulation. This is in all likelihood due to TexTools corrupting your index files.\n"
{ + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" );
ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, value ) );
++i;
}
} }
} }
@ -296,7 +314,7 @@ public class TexToolsMeta
var headerSize = reader.ReadUInt32(); var headerSize = reader.ReadUInt32();
var headerStart = reader.ReadUInt32(); var headerStart = reader.ReadUInt32();
reader.BaseStream.Seek( headerStart, SeekOrigin.Begin ); reader.BaseStream.Seek( headerStart, SeekOrigin.Begin );
List< (MetaManipulation.Type type, uint offset, int size) > entries = new(); List< (MetaManipulation.Type type, uint offset, int size) > entries = new();
for( var i = 0; i < numHeaders; ++i ) for( var i = 0; i < numHeaders; ++i )
{ {
@ -307,7 +325,7 @@ public class TexToolsMeta
entries.Add( ( type, offset, size ) ); entries.Add( ( type, offset, size ) );
reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin ); reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin );
} }
byte[]? ReadEntry( MetaManipulation.Type type ) byte[]? ReadEntry( MetaManipulation.Type type )
{ {
var idx = entries.FindIndex( t => t.type == type ); var idx = entries.FindIndex( t => t.type == type );
@ -315,11 +333,11 @@ public class TexToolsMeta
{ {
return null; return null;
} }
reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin ); reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin );
return reader.ReadBytes( entries[ idx ].size ); return reader.ReadBytes( entries[ idx ].size );
} }
DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) ); DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) );
DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) ); DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) );
DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) ); DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) );
@ -373,32 +391,34 @@ public class TexToolsMeta
void Add( RspAttribute attribute, float value ) void Add( RspAttribute attribute, float value )
{ {
var def = CmpFile.GetDefault( subRace, attribute ); var def = CmpFile.GetDefault( subRace, attribute );
if (value != def) if( value != def )
ret!.RspManipulations.Add(new RspManipulation(subRace, attribute, value)); {
ret!.RspManipulations.Add( new RspManipulation( subRace, attribute, value ) );
}
} }
if( gender == 1 ) if( gender == 1 )
{ {
Add(RspAttribute.FemaleMinSize, br.ReadSingle() ); Add( RspAttribute.FemaleMinSize, br.ReadSingle() );
Add(RspAttribute.FemaleMaxSize, br.ReadSingle() ); Add( RspAttribute.FemaleMaxSize, br.ReadSingle() );
Add(RspAttribute.FemaleMinTail, br.ReadSingle() ); Add( RspAttribute.FemaleMinTail, br.ReadSingle() );
Add(RspAttribute.FemaleMaxTail, br.ReadSingle() ); Add( RspAttribute.FemaleMaxTail, br.ReadSingle() );
Add(RspAttribute.BustMinX, br.ReadSingle() ); Add( RspAttribute.BustMinX, br.ReadSingle() );
Add(RspAttribute.BustMinY, br.ReadSingle() ); Add( RspAttribute.BustMinY, br.ReadSingle() );
Add(RspAttribute.BustMinZ, br.ReadSingle() ); Add( RspAttribute.BustMinZ, br.ReadSingle() );
Add(RspAttribute.BustMaxX, br.ReadSingle() ); Add( RspAttribute.BustMaxX, br.ReadSingle() );
Add(RspAttribute.BustMaxY, br.ReadSingle() ); Add( RspAttribute.BustMaxY, br.ReadSingle() );
Add(RspAttribute.BustMaxZ, br.ReadSingle() ); Add( RspAttribute.BustMaxZ, br.ReadSingle() );
} }
else else
{ {
Add(RspAttribute.MaleMinSize, br.ReadSingle() ); Add( RspAttribute.MaleMinSize, br.ReadSingle() );
Add(RspAttribute.MaleMaxSize, br.ReadSingle() ); Add( RspAttribute.MaleMaxSize, br.ReadSingle() );
Add(RspAttribute.MaleMinTail, br.ReadSingle() ); Add( RspAttribute.MaleMinTail, br.ReadSingle() );
Add(RspAttribute.MaleMaxTail, br.ReadSingle() ); Add( RspAttribute.MaleMaxTail, br.ReadSingle() );
} }
return ret; return ret;
} }
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
@ -20,7 +21,33 @@ public unsafe class CharacterUtility : IDisposable
public Structs.CharacterUtility* Address public Structs.CharacterUtility* Address
=> *_characterUtilityAddress; => *_characterUtilityAddress;
public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[Structs.CharacterUtility.NumRelevantResources]; // The relevant indices depend on which meta manipulations we allow for.
// The defines are set in the project configuration.
public static readonly int[] RelevantIndices
= Array.Empty< int >()
#if USE_EQP
.Append( Structs.CharacterUtility.EqpIdx )
#endif
#if USE_GMP
.Append( Structs.CharacterUtility.GmpIdx )
#endif
#if USE_EQDP
.Concat( Enumerable.Range( Structs.CharacterUtility.EqdpStartIdx, Structs.CharacterUtility.NumEqdpFiles ) )
#endif
#if USE_CMP
.Append( Structs.CharacterUtility.HumanCmpIdx )
#endif
#if USE_EST
.Concat( Enumerable.Range( Structs.CharacterUtility.FaceEstIdx, 4 ) )
#endif
.ToArray();
private static readonly int[] ReverseIndices
= Enumerable.Range( 0, Structs.CharacterUtility.NumResources )
.Select( i => Array.IndexOf( RelevantIndices, i ) ).ToArray();
public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[RelevantIndices.Length];
public CharacterUtility() public CharacterUtility()
{ {
@ -48,9 +75,9 @@ public unsafe class CharacterUtility : IDisposable
// We store the default data of the resources so we can always restore them. // We store the default data of the resources so we can always restore them.
private void LoadDefaultResources() private void LoadDefaultResources()
{ {
for( var i = 0; i < Structs.CharacterUtility.NumRelevantResources; ++i ) for( var i = 0; i < RelevantIndices.Length; ++i )
{ {
var resource = ( Structs.ResourceHandle* )Address->Resources[ i ]; var resource = ( Structs.ResourceHandle* )Address->Resources[ RelevantIndices[ i ] ];
DefaultResources[ i ] = resource->GetData(); DefaultResources[ i ] = resource->GetData();
} }
} }
@ -65,19 +92,25 @@ public unsafe class CharacterUtility : IDisposable
} }
// Reset the data of one of the stored resources to its default values. // Reset the data of one of the stored resources to its default values.
public void ResetResource( int idx ) public void ResetResource( int fileIdx )
{ {
var resource = ( Structs.ResourceHandle* )Address->Resources[ idx ]; var (data, size) = DefaultResources[ ReverseIndices[ fileIdx ] ];
resource->SetData( DefaultResources[ idx ].Address, DefaultResources[ idx ].Size ); var resource = ( Structs.ResourceHandle* )Address->Resources[ fileIdx ];
resource->SetData( data, size );
}
// Return all relevant resources to the default resource.
public void ResetAll()
{
foreach( var idx in RelevantIndices )
{
ResetResource( idx );
}
} }
public void Dispose() public void Dispose()
{ {
for( var i = 0; i < Structs.CharacterUtility.NumRelevantResources; ++i ) ResetAll();
{
ResetResource( i );
}
LoadDataFilesHook.Dispose(); LoadDataFilesHook.Dispose();
} }
} }

View file

@ -117,12 +117,15 @@ public unsafe partial class ResourceLoader
// We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack. // We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack.
if( !valid || !gamePath.IsRooted() ) if( !valid || !gamePath.IsRooted() )
{ {
ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); if( valid && ResourceLoadCustomization != null && gamePath.Path[ 0 ] == ( byte )'|' )
FileLoaded?.Invoke( gamePath.Path, ret != 0, false ); {
} ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync );
else if( ResourceLoadCustomization != null && gamePath.Path[0] == (byte) '|' ) }
{ else
ret = ResourceLoadCustomization.Invoke( gamePath, resourceManager, fileDescriptor, priority, isSync ); {
ret = ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
FileLoaded?.Invoke( gamePath.Path, ret != 0, false );
}
} }
else else
{ {

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -7,49 +8,52 @@ namespace Penumbra.Interop.Structs;
[StructLayout( LayoutKind.Explicit )] [StructLayout( LayoutKind.Explicit )]
public unsafe struct CharacterUtility public unsafe struct CharacterUtility
{ {
public const int NumResources = 85; public static readonly int[] EqdpIndices
public const int NumRelevantResources = 68; = Enumerable.Range( EqdpStartIdx, NumEqdpFiles ).ToArray();
public const int EqpIdx = 0;
public const int GmpIdx = 1; public const int NumResources = 85;
public const int HumanCmpIdx = 63; public const int EqpIdx = 0;
public const int FaceEstIdx = 64; public const int GmpIdx = 1;
public const int HairEstIdx = 65; public const int HumanCmpIdx = 63;
public const int BodyEstIdx = 66; public const int FaceEstIdx = 64;
public const int HeadEstIdx = 67; public const int HairEstIdx = 65;
public const int NumEqdpFiles = 2 * 28; public const int BodyEstIdx = 66;
public const int HeadEstIdx = 67;
public const int EqdpStartIdx = 2;
public const int NumEqdpFiles = 2 * 28;
public static int EqdpIdx( GenderRace raceCode, bool accessory ) public static int EqdpIdx( GenderRace raceCode, bool accessory )
=> ( accessory ? 28 : 0 ) => ( accessory ? NumEqdpFiles / 2 : 0 )
+ ( int )raceCode switch + ( int )raceCode switch
{ {
0101 => 2, 0101 => EqdpStartIdx,
0201 => 3, 0201 => EqdpStartIdx + 1,
0301 => 4, 0301 => EqdpStartIdx + 2,
0401 => 5, 0401 => EqdpStartIdx + 3,
0501 => 6, 0501 => EqdpStartIdx + 4,
0601 => 7, 0601 => EqdpStartIdx + 5,
0701 => 8, 0701 => EqdpStartIdx + 6,
0801 => 9, 0801 => EqdpStartIdx + 7,
0901 => 10, 0901 => EqdpStartIdx + 8,
1001 => 11, 1001 => EqdpStartIdx + 9,
1101 => 12, 1101 => EqdpStartIdx + 10,
1201 => 13, 1201 => EqdpStartIdx + 11,
1301 => 14, 1301 => EqdpStartIdx + 12,
1401 => 15, 1401 => EqdpStartIdx + 13,
1501 => 16, 1501 => EqdpStartIdx + 14,
1601 => 17, // Does not exist yet 1601 => EqdpStartIdx + 15, // Does not exist yet
1701 => 18, 1701 => EqdpStartIdx + 16,
1801 => 19, 1801 => EqdpStartIdx + 17,
0104 => 20, 0104 => EqdpStartIdx + 18,
0204 => 21, 0204 => EqdpStartIdx + 19,
0504 => 22, 0504 => EqdpStartIdx + 20,
0604 => 23, 0604 => EqdpStartIdx + 21,
0704 => 24, 0704 => EqdpStartIdx + 22,
0804 => 25, 0804 => EqdpStartIdx + 23,
1304 => 26, 1304 => EqdpStartIdx + 24,
1404 => 27, 1404 => EqdpStartIdx + 25,
9104 => 28, 9104 => EqdpStartIdx + 26,
9204 => 29, 9204 => EqdpStartIdx + 27,
_ => throw new ArgumentException(), _ => throw new ArgumentException(),
}; };

View file

@ -47,7 +47,8 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
} }
return *( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx ); var x = new ReadOnlySpan< ushort >( ( ushort* )Data, Length / 2 );
return ( EqdpEntry )( *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) );
} }
set set
{ {
@ -56,7 +57,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
} }
*( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx ) = value; *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) = ( ushort )value;
} }
} }
@ -65,9 +66,12 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
var def = ( byte* )DefaultData.Data; var def = ( byte* )DefaultData.Data;
Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize );
var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize );
var dataBasePtr = ( byte* )( controlPtr + BlockCount ); var dataBasePtr = controlPtr + BlockCount;
var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount ); var myDataPtrStart = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount );
var myDataPtr = myDataPtrStart;
var myControlPtr = ( ushort* )( Data + IdentifierSize + PreambleSize );
var x = new ReadOnlySpan< ushort >( ( ushort* )Data, Length / 2 );
for( var i = 0; i < BlockCount; ++i ) for( var i = 0; i < BlockCount; ++i )
{ {
if( controlPtr[ i ] == CollapsedBlock ) if( controlPtr[ i ] == CollapsedBlock )
@ -76,11 +80,16 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
} }
else else
{ {
var y = new ReadOnlySpan< ushort >( dataBasePtr + controlPtr[ i ], BlockSize );
var z = new ReadOnlySpan< ushort >( myDataPtr, BlockSize );
Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize ); Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize );
} }
myDataPtr += BlockSize; myControlPtr[ i ] = ( ushort )( i * BlockSize );
myDataPtr += BlockSize;
} }
Functions.MemSet( myDataPtr, 0, Length - ( int )( ( byte* )myDataPtr - Data ) );
} }
public void Reset( IEnumerable< int > entries ) public void Reset( IEnumerable< int > entries )
@ -102,7 +111,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize; DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize;
var fullLength = DataOffset + totalBlockCount * totalBlockSize; var fullLength = DataOffset + totalBlockCount * totalBlockSize;
fullLength += ( FileAlignment - ( Length & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 ); fullLength += ( FileAlignment - ( fullLength & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 );
AllocateData( fullLength ); AllocateData( fullLength );
Reset(); Reset();
} }
@ -128,8 +137,9 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
return 0; return 0;
} }
var blockData = ( EqdpEntry* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block ); var blockData = ( ushort* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2 );
return *( blockData + blockIdx % blockSize ); var x = new ReadOnlySpan< ushort >( blockData, blockSize );
return (EqdpEntry) (*( blockData + setIdx % blockSize ));
} }
public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx ) public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx )

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
@ -24,17 +25,17 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
public ulong ControlBlock public ulong ControlBlock
=> *( ulong* )Data; => *( ulong* )Data;
protected T Get< T >( int idx ) where T : unmanaged protected ulong GetInternal( int idx )
{ {
return idx switch return idx switch
{ {
>= Count => throw new IndexOutOfRangeException(), >= Count => throw new IndexOutOfRangeException(),
<= 1 => *( ( T* )Data + 1 ), <= 1 => *( ( ulong* )Data + 1 ),
_ => *( ( T* )Data + idx ), _ => *( ( ulong* )Data + idx ),
}; };
} }
protected void Set< T >( int idx, T value ) where T : unmanaged protected void SetInternal( int idx, ulong value )
{ {
idx = idx switch idx = idx switch
{ {
@ -43,7 +44,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
_ => idx, _ => idx,
}; };
*( ( T* )Data + idx ) = value; *( ( ulong* )Data + idx ) = value;
} }
protected virtual void SetEmptyBlock( int idx ) protected virtual void SetEmptyBlock( int idx )
@ -53,21 +54,24 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
public sealed override void Reset() public sealed override void Reset()
{ {
var ptr = ( byte* )DefaultData.Data; var ptr = ( byte* )DefaultData.Data;
var controlBlock = *( ulong* )ptr; var controlBlock = *( ulong* )ptr;
*( ulong* )Data = ulong.MaxValue; var expandedBlocks = 0;
for( var i = 0; i < 64; ++i ) for( var i = 0; i < NumBlocks; ++i )
{ {
var collapsed = ( ( controlBlock >> i ) & 1 ) == 0; var collapsed = ( ( controlBlock >> i ) & 1 ) == 0;
if( !collapsed ) if( !collapsed )
{ {
Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + i * BlockSize * EntrySize, BlockSize * EntrySize ); Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, BlockSize * EntrySize );
expandedBlocks++;
} }
else else
{ {
SetEmptyBlock( i ); SetEmptyBlock( i );
} }
} }
*( ulong* )Data = ulong.MaxValue;
} }
public ExpandedEqpGmpBase( bool gmp ) public ExpandedEqpGmpBase( bool gmp )
@ -77,7 +81,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
Reset(); Reset();
} }
protected static T GetDefault< T >( int fileIdx, int setIdx, T def ) where T : unmanaged protected static ulong GetDefaultInternal( int fileIdx, int setIdx, ulong def )
{ {
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address; var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address;
if( setIdx == 0 ) if( setIdx == 0 )
@ -100,7 +104,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
var count = BitOperations.PopCount( control & ( blockBit - 1 ) ); var count = BitOperations.PopCount( control & ( blockBit - 1 ) );
var idx = setIdx % BlockSize; var idx = setIdx % BlockSize;
var ptr = ( T* )data + BlockSize * count + idx; var ptr = ( ulong* )data + BlockSize * count + idx;
return *ptr; return *ptr;
} }
} }
@ -113,12 +117,12 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase
public EqpEntry this[ int idx ] public EqpEntry this[ int idx ]
{ {
get => Get< EqpEntry >( idx ); get => ( EqpEntry )GetInternal( idx );
set => Set( idx, value ); set => SetInternal( idx, ( ulong )value );
} }
public static EqpEntry GetDefault( int setIdx ) public static EqpEntry GetDefault( int setIdx )
=> GetDefault( CharacterUtility.EqpIdx, setIdx, Eqp.DefaultEntry ); => ( EqpEntry )GetDefaultInternal( CharacterUtility.EqpIdx, setIdx, ( ulong )Eqp.DefaultEntry );
protected override unsafe void SetEmptyBlock( int idx ) protected override unsafe void SetEmptyBlock( int idx )
{ {
@ -147,12 +151,12 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase
public GmpEntry this[ int idx ] public GmpEntry this[ int idx ]
{ {
get => Get< GmpEntry >( idx ); get => ( GmpEntry )GetInternal( idx );
set => Set( idx, value ); set => SetInternal( idx, ( ulong )value );
} }
public static GmpEntry GetDefault( int setIdx ) public static GmpEntry GetDefault( int setIdx )
=> GetDefault( CharacterUtility.GmpIdx, setIdx, GmpEntry.Default ); => ( GmpEntry )GetDefaultInternal( CharacterUtility.GmpIdx, setIdx, ( ulong )GmpEntry.Default );
public void Reset( IEnumerable< int > entries ) public void Reset( IEnumerable< int > entries )
{ {

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections;
using System.Numerics; using System.Numerics;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Memory; using Dalamud.Memory;
@ -7,6 +8,7 @@ using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta.Files; namespace Penumbra.Meta.Files;
@ -64,41 +66,57 @@ public unsafe class ImcFile : MetaBaseFile
=> NumParts * sizeof( ImcEntry ) * ( Count + 1 ) + PreambleSize; => NumParts * sizeof( ImcEntry ) * ( Count + 1 ) + PreambleSize;
public int Count public int Count
=> *( ushort* )Data; => CountInternal( Data );
public ushort PartMask
=> *( ushort* )( Data + 2 );
public readonly int NumParts; public readonly int NumParts;
public readonly Utf8GamePath Path; public readonly Utf8GamePath Path;
public ImcEntry* DefaultPartPtr( int partIdx ) private static int CountInternal( byte* data )
=> *( ushort* )data;
private static ushort PartMask( byte* data )
=> *( ushort* )( data + 2 );
private static ImcEntry* DefaultPartPtr( byte* data, int partIdx )
{ {
var flag = 1 << partIdx; var flag = 1 << partIdx;
if( ( PartMask & flag ) == 0 ) if( ( PartMask( data ) & flag ) == 0 )
{ {
return null; return null;
} }
return ( ImcEntry* )( Data + PreambleSize ) + partIdx; return ( ImcEntry* )( data + PreambleSize ) + partIdx;
} }
public ImcEntry* VariantPtr( int partIdx, int variantIdx ) private static ImcEntry* VariantPtr( byte* data, int partIdx, int variantIdx )
{ {
if( variantIdx == 0 )
{
return DefaultPartPtr( data, partIdx );
}
--variantIdx;
var flag = 1 << partIdx; var flag = 1 << partIdx;
if( ( PartMask & flag ) == 0 || variantIdx >= Count )
if( ( PartMask( data ) & flag ) == 0 || variantIdx >= CountInternal( data ) )
{ {
return null; return null;
} }
var numParts = NumParts; var numParts = BitOperations.PopCount( PartMask( data ) );
var ptr = ( ImcEntry* )( Data + PreambleSize ); var ptr = ( ImcEntry* )( data + PreambleSize );
ptr += numParts; ptr += numParts;
ptr += variantIdx * numParts; ptr += variantIdx * numParts;
ptr += partIdx; ptr += partIdx;
return ptr; return ptr;
} }
public ImcEntry GetEntry( int partIdx, int variantIdx )
{
var ptr = VariantPtr( Data, partIdx, variantIdx );
return ptr == null ? new ImcEntry() : *ptr;
}
public static int PartIndex( EquipSlot slot ) public static int PartIndex( EquipSlot slot )
=> slot switch => slot switch
{ {
@ -122,7 +140,6 @@ public unsafe class ImcFile : MetaBaseFile
return true; return true;
} }
var numParts = NumParts;
if( ActualLength > Length ) if( ActualLength > Length )
{ {
PluginLog.Warning( "Adding too many variants to IMC, size exceeded." ); PluginLog.Warning( "Adding too many variants to IMC, size exceeded." );
@ -130,10 +147,10 @@ public unsafe class ImcFile : MetaBaseFile
} }
var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); var defaultPtr = ( ImcEntry* )( Data + PreambleSize );
var endPtr = defaultPtr + ( numVariants + 1 ) * numParts; var endPtr = defaultPtr + ( numVariants + 1 ) * NumParts;
for( var ptr = defaultPtr + numParts; ptr < endPtr; ptr += numParts ) for( var ptr = defaultPtr + NumParts; ptr < endPtr; ptr += NumParts )
{ {
Functions.MemCpyUnchecked( ptr, defaultPtr, numParts * sizeof( ImcEntry ) ); Functions.MemCpyUnchecked( ptr, defaultPtr, NumParts * sizeof( ImcEntry ) );
} }
PluginLog.Verbose( "Expanded imc from {Count} to {NewCount} variants.", Count, numVariants ); PluginLog.Verbose( "Expanded imc from {Count} to {NewCount} variants.", Count, numVariants );
@ -143,15 +160,14 @@ public unsafe class ImcFile : MetaBaseFile
public bool SetEntry( int partIdx, int variantIdx, ImcEntry entry ) public bool SetEntry( int partIdx, int variantIdx, ImcEntry entry )
{ {
var numParts = NumParts; if( partIdx >= NumParts )
if( partIdx >= numParts )
{ {
return false; return false;
} }
EnsureVariantCount( variantIdx + 1 ); EnsureVariantCount( variantIdx );
var variantPtr = VariantPtr( partIdx, variantIdx ); var variantPtr = VariantPtr( Data, partIdx, variantIdx );
if( variantPtr == null ) if( variantPtr == null )
{ {
PluginLog.Error( "Error during expansion of imc file." ); PluginLog.Error( "Error during expansion of imc file." );
@ -168,9 +184,20 @@ public unsafe class ImcFile : MetaBaseFile
} }
public override void Reset()
{
var file = Dalamud.GameData.GetFile( Path.ToString() );
fixed( byte* ptr = file!.Data )
{
Functions.MemCpyUnchecked( Data, ptr, file.Data.Length );
Functions.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length );
}
}
public ImcFile( Utf8GamePath path ) public ImcFile( Utf8GamePath path )
: base( 0 ) : base( 0 )
{ {
Path = path;
var file = Dalamud.GameData.GetFile( path.ToString() ); var file = Dalamud.GameData.GetFile( path.ToString() );
if( file == null ) if( file == null )
{ {
@ -182,6 +209,22 @@ public unsafe class ImcFile : MetaBaseFile
NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) );
AllocateData( file.Data.Length + sizeof( ImcEntry ) * 100 * NumParts ); AllocateData( file.Data.Length + sizeof( ImcEntry ) * 100 * NumParts );
Functions.MemCpyUnchecked( Data, ptr, file.Data.Length ); Functions.MemCpyUnchecked( Data, ptr, file.Data.Length );
Functions.MemSet( Data + file.Data.Length, 0, sizeof( ImcEntry ) * 100 * NumParts );
}
}
public static ImcEntry GetDefault( Utf8GamePath path, EquipSlot slot, int variantIdx )
{
var file = Dalamud.GameData.GetFile( path.ToString() );
if( file == null )
{
throw new Exception();
}
fixed( byte* ptr = file.Data )
{
var entry = VariantPtr( ptr, PartIndex( slot ), variantIdx );
return entry == null ? new ImcEntry() : *entry;
} }
} }

View file

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta.Manager;
public partial class MetaManager
{
public struct MetaManagerCmp : IDisposable
{
public CmpFile? File = null;
public readonly Dictionary< RspManipulation, Mod.Mod > Manipulations = new();
public MetaManagerCmp()
{ }
[Conditional( "USE_CMP" )]
public void Reset()
{
if( File == null )
{
return;
}
File.Reset( Manipulations.Keys.Select( m => ( m.SubRace, m.Attribute ) ) );
Manipulations.Clear();
}
[Conditional( "USE_CMP" )]
public void SetFiles()
=> SetFile( File, CharacterUtility.HumanCmpIdx );
public bool ApplyMod( RspManipulation m, Mod.Mod mod )
{
#if USE_CMP
if( !Manipulations.TryAdd( m, mod ) )
{
return false;
}
File ??= new CmpFile();
return m.Apply( File );
#else
return false;
#endif
}
public void Dispose()
{
File?.Dispose();
File = null;
Manipulations.Clear();
}
}
}

View file

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta.Manager;
public partial class MetaManager
{
public struct MetaManagerEqdp : IDisposable
{
public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles];
public readonly Dictionary< EqdpManipulation, Mod.Mod > Manipulations = new();
public MetaManagerEqdp()
{ }
[Conditional( "USE_EQDP" )]
public void SetFiles()
{
foreach( var idx in CharacterUtility.EqdpIndices )
{
SetFile( Files[ idx - CharacterUtility.EqdpStartIdx ], idx );
}
}
[Conditional( "USE_EQDP" )]
public void Reset()
{
foreach( var file in Files )
{
file?.Reset( Manipulations.Keys.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) );
}
Manipulations.Clear();
}
public bool ApplyMod( EqdpManipulation m, Mod.Mod mod )
{
#if USE_EQDP
if( !Manipulations.TryAdd( m, mod ) )
{
return false;
}
var file = Files[ m.FileIndex() - 2 ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() );
return m.Apply( file );
#else
return false;
#endif
}
public ExpandedEqdpFile? File( GenderRace race, bool accessory )
=> Files[ CharacterUtility.EqdpIdx( race, accessory ) - CharacterUtility.EqdpStartIdx ];
public void Dispose()
{
for( var i = 0; i < Files.Length; ++i )
{
Files[ i ]?.Dispose();
Files[ i ] = null;
}
Manipulations.Clear();
}
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta.Manager;
public partial class MetaManager
{
public struct MetaManagerEqp : IDisposable
{
public ExpandedEqpFile? File = null;
public readonly Dictionary< EqpManipulation, Mod.Mod > Manipulations = new();
public MetaManagerEqp()
{ }
[Conditional( "USE_EQP" )]
public void SetFiles()
=> SetFile( File, CharacterUtility.EqpIdx );
[Conditional( "USE_EQP" )]
public void Reset()
{
if( File == null )
{
return;
}
File.Reset( Manipulations.Keys.Select( m => ( int )m.SetId ) );
Manipulations.Clear();
}
public bool ApplyMod( EqpManipulation m, Mod.Mod mod )
{
#if USE_EQP
if( !Manipulations.TryAdd( m, mod ) )
{
return false;
}
File ??= new ExpandedEqpFile();
return m.Apply( File );
#else
return false;
#endif
}
public void Dispose()
{
File?.Dispose();
File = null;
Manipulations.Clear();
}
}
}

View file

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta.Manager;
public partial class MetaManager
{
public struct MetaManagerEst : IDisposable
{
public EstFile? FaceFile = null;
public EstFile? HairFile = null;
public EstFile? BodyFile = null;
public EstFile? HeadFile = null;
public readonly Dictionary< EstManipulation, Mod.Mod > Manipulations = new();
public MetaManagerEst()
{ }
[Conditional( "USE_EST" )]
public void SetFiles()
{
SetFile( FaceFile, CharacterUtility.FaceEstIdx );
SetFile( HairFile, CharacterUtility.HairEstIdx );
SetFile( BodyFile, CharacterUtility.BodyEstIdx );
SetFile( HeadFile, CharacterUtility.HeadEstIdx );
}
[Conditional( "USE_EST" )]
public void Reset()
{
FaceFile?.Reset();
HairFile?.Reset();
BodyFile?.Reset();
HeadFile?.Reset();
Manipulations.Clear();
}
public bool ApplyMod( EstManipulation m, Mod.Mod mod )
{
#if USE_EST
if( !Manipulations.TryAdd( m, mod ) )
{
return false;
}
var file = m.Slot switch
{
EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ),
EstManipulation.EstType.Face => FaceFile ??= new EstFile( EstManipulation.EstType.Face ),
EstManipulation.EstType.Body => BodyFile ??= new EstFile( EstManipulation.EstType.Body ),
EstManipulation.EstType.Head => HeadFile ??= new EstFile( EstManipulation.EstType.Head ),
_ => throw new ArgumentOutOfRangeException(),
};
return m.Apply( file );
#else
return false;
#endif
}
public void Dispose()
{
FaceFile?.Dispose();
HairFile?.Dispose();
BodyFile?.Dispose();
HeadFile?.Dispose();
FaceFile = null;
HairFile = null;
BodyFile = null;
HeadFile = null;
Manipulations.Clear();
}
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta.Manager;
public partial class MetaManager
{
public struct MetaManagerGmp : IDisposable
{
public ExpandedGmpFile? File = null;
public readonly Dictionary< GmpManipulation, Mod.Mod > Manipulations = new();
public MetaManagerGmp()
{ }
[Conditional( "USE_GMP" )]
public void Reset()
{
if( File != null )
{
File.Reset( Manipulations.Keys.Select( m => ( int )m.SetId ) );
Manipulations.Clear();
}
}
[Conditional( "USE_GMP" )]
public void SetFiles()
=> SetFile( File, CharacterUtility.GmpIdx );
public bool ApplyMod( GmpManipulation m, Mod.Mod mod )
{
#if USE_GMP
if( Manipulations.TryAdd( m, mod ) )
{
return false;
}
File ??= new ExpandedGmpFile();
return m.Apply( File );
#else
return false;
#endif
}
public void Dispose()
{
File?.Dispose();
File = null;
Manipulations.Clear();
}
}
}

View file

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.GameData.ByteString;
using Penumbra.Interop;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Meta.Manager;
public partial class MetaManager
{
public readonly struct MetaManagerImc : IDisposable
{
public readonly Dictionary< Utf8GamePath, ImcFile > Files = new();
public readonly Dictionary< ImcManipulation, Mod.Mod > Manipulations = new();
private readonly ModCollection _collection;
private readonly ResourceLoader.ResourceLoadCustomizationDelegate? _previousDelegate;
public MetaManagerImc( ModCollection collection )
{
_collection = collection;
_previousDelegate = Penumbra.ResourceLoader.ResourceLoadCustomization;
}
[Conditional( "USE_IMC" )]
public void SetFiles()
{
if( _collection.Cache == null )
{
return;
}
foreach( var path in Files.Keys )
{
_collection.Cache.ResolvedFiles[ path ] = CreateImcPath( path );
}
}
[Conditional( "USE_IMC" )]
public void Reset()
{
foreach( var (path, file) in Files )
{
_collection.Cache?.ResolvedFiles.Remove( path );
file.Reset();
}
Manipulations.Clear();
}
public unsafe bool ApplyMod( ImcManipulation m, Mod.Mod mod )
{
const uint imcExt = 0x00696D63;
#if USE_IMC
if( !Manipulations.TryAdd( m, mod ) )
{
return false;
}
var path = m.GamePath();
if( !Files.TryGetValue( path, out var file ) )
{
file = new ImcFile( path );
}
if( !m.Apply( file ) )
{
return false;
}
Files[ path ] = file;
var fullPath = CreateImcPath( path );
if( _collection.Cache != null )
{
_collection.Cache.ResolvedFiles[ path ] = fullPath;
}
var resource = ResourceLoader.FindResource( ResourceCategory.Chara, imcExt, ( uint )path.Path.Crc32 );
if( resource != null )
{
file.Replace( ( ResourceHandle* )resource );
}
return true;
#else
return false;
#endif
}
public void Dispose()
{
foreach( var file in Files.Values )
{
file.Dispose();
}
Files.Clear();
Manipulations.Clear();
RestoreDelegate();
}
[Conditional( "USE_IMC" )]
private unsafe void SetupDelegate()
{
Penumbra.ResourceLoader.ResourceLoadCustomization = ImcHandler;
}
[Conditional( "USE_IMC" )]
private unsafe void RestoreDelegate()
{
if( Penumbra.ResourceLoader.ResourceLoadCustomization == ImcHandler )
{
Penumbra.ResourceLoader.ResourceLoadCustomization = _previousDelegate;
}
}
private FullPath CreateImcPath( Utf8GamePath path )
=> new($"|{_collection.Name}|{path}");
private static unsafe byte ImcHandler( Utf8GamePath gamePath, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync )
{
var split = gamePath.Path.Split( ( byte )'|', 2, true );
fileDescriptor->ResourceHandle->FileNameData = split[ 1 ].Path;
fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length;
var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
if( Penumbra.ModManager.Collections.Collections.TryGetValue( split[ 0 ].ToString(), out var collection )
&& collection.Cache != null
&& collection.Cache.MetaManipulations.Imc.Files.TryGetValue(
Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) )
{
file.Replace( fileDescriptor->ResourceHandle );
}
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path;
fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length;
return ret;
}
}
}

View file

@ -0,0 +1,99 @@
using System;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Meta.Manager;
public partial class MetaManager : IDisposable
{
public MetaManagerEqp Eqp = new();
public MetaManagerEqdp Eqdp = new();
public MetaManagerGmp Gmp = new();
public MetaManagerEst Est = new();
public MetaManagerCmp Cmp = new();
public MetaManagerImc Imc;
private static unsafe void SetFile( MetaBaseFile? file, int index )
{
if( file == null )
{
Penumbra.CharacterUtility.ResetResource( index );
}
else
{
Penumbra.CharacterUtility.SetResource( index, ( IntPtr )file.Data, file.Length );
}
}
public bool TryGetValue( MetaManipulation manip, out Mod.Mod? mod )
{
mod = manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : null,
MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : null,
MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null,
MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : null,
MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : null,
MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : null,
_ => throw new ArgumentOutOfRangeException(),
};
return mod != null;
}
public int Count
=> Imc.Manipulations.Count
+ Eqdp.Manipulations.Count
+ Cmp.Manipulations.Count
+ Gmp.Manipulations.Count
+ Est.Manipulations.Count
+ Eqp.Manipulations.Count;
public MetaManager( ModCollection collection )
=> Imc = new MetaManagerImc( collection );
public void SetFiles()
{
Eqp.SetFiles();
Eqdp.SetFiles();
Gmp.SetFiles();
Est.SetFiles();
Cmp.SetFiles();
Imc.SetFiles();
}
public void Reset()
{
Eqp.Reset();
Eqdp.Reset();
Gmp.Reset();
Est.Reset();
Cmp.Reset();
Imc.Reset();
}
public void Dispose()
{
Eqp.Dispose();
Eqdp.Dispose();
Gmp.Dispose();
Est.Dispose();
Cmp.Dispose();
Imc.Dispose();
}
public bool ApplyMod( MetaManipulation m, Mod.Mod mod )
{
return m.ManipulationType switch
{
MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, mod ),
MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, mod ),
MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, mod ),
MetaManipulation.Type.Est => Est.ApplyMod( m.Est, mod ),
MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, mod ),
MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, mod ),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
}

View file

@ -61,6 +61,10 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation >
EquipSlot = equipSlot; EquipSlot = equipSlot;
} }
public ImcManipulation( ImcManipulation copy, ImcEntry entry )
: this( copy.ObjectType, copy.BodySlot, copy.PrimaryId, copy.SecondaryId, copy.Variant, copy.EquipSlot, entry )
{}
public override string ToString() public override string ToString()
=> ObjectType is ObjectType.Equipment or ObjectType.Accessory => ObjectType is ObjectType.Equipment or ObjectType.Accessory
? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}"

View file

@ -219,6 +219,12 @@ public class MetaCollection
if( collection != null ) if( collection != null )
{ {
if( collection.DefaultData.Concat( collection.GroupData.Values.SelectMany( kvp => kvp.Values.SelectMany( l => l ) ) )
.Any( m => m.ManipulationType == MetaManipulation.Type.Unknown || !Enum.IsDefined( m.ManipulationType ) ) )
{
throw new Exception( "Invalid collection" );
}
collection.Count = collection.DefaultData.Count collection.Count = collection.DefaultData.Count
+ collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count ); + collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count );
} }

View file

@ -1,367 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.Interop;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using CharacterUtility = Penumbra.Interop.Structs.CharacterUtility;
using ImcFile = Penumbra.Meta.Files.ImcFile;
namespace Penumbra.Meta;
public class MetaManager2 : IDisposable
{
public readonly List< MetaBaseFile > ChangedData = new(7 + CharacterUtility.NumEqdpFiles);
public ExpandedEqpFile? EqpFile;
public ExpandedGmpFile? GmpFile;
public ExpandedEqdpFile?[] EqdpFile = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles];
public EstFile? FaceEstFile;
public EstFile? HairEstFile;
public EstFile? BodyEstFile;
public EstFile? HeadEstFile;
public CmpFile? CmpFile;
public readonly Dictionary< EqpManipulation, Mod.Mod > EqpManipulations = new();
public readonly Dictionary< EstManipulation, Mod.Mod > EstManipulations = new();
public readonly Dictionary< GmpManipulation, Mod.Mod > GmpManipulations = new();
public readonly Dictionary< RspManipulation, Mod.Mod > RspManipulations = new();
public readonly Dictionary< EqdpManipulation, Mod.Mod > EqdpManipulations = new();
public readonly Dictionary< ImcManipulation, Mod.Mod > ImcManipulations = new();
public readonly Dictionary< Utf8GamePath, ImcFile > ImcFiles = new();
private readonly ModCollection _collection;
public unsafe void SetFiles()
{
foreach( var file in ChangedData )
{
Penumbra.CharacterUtility.SetResource( file.Index, ( IntPtr )file.Data, file.Length );
}
}
public bool TryGetValue( MetaManipulation manip, out Mod.Mod? mod )
{
mod = manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => EqpManipulations.TryGetValue( manip.Eqp, out var m ) ? m : null,
MetaManipulation.Type.Gmp => GmpManipulations.TryGetValue( manip.Gmp, out var m ) ? m : null,
MetaManipulation.Type.Eqdp => EqdpManipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null,
MetaManipulation.Type.Est => EstManipulations.TryGetValue( manip.Est, out var m ) ? m : null,
MetaManipulation.Type.Rsp => RspManipulations.TryGetValue( manip.Rsp, out var m ) ? m : null,
MetaManipulation.Type.Imc => ImcManipulations.TryGetValue( manip.Imc, out var m ) ? m : null,
_ => throw new ArgumentOutOfRangeException(),
};
return mod != null;
}
public int Count
=> ImcManipulations.Count
+ EqdpManipulations.Count
+ RspManipulations.Count
+ GmpManipulations.Count
+ EstManipulations.Count
+ EqpManipulations.Count;
public MetaManager2( ModCollection collection )
=> _collection = collection;
public void ApplyImcFiles( Dictionary< Utf8GamePath, FullPath > resolvedFiles )
{
foreach( var path in ImcFiles.Keys )
{
resolvedFiles[ path ] = CreateImcPath( path );
}
}
public void ResetEqp()
{
if( EqpFile != null )
{
EqpFile.Reset( EqpManipulations.Keys.Select( m => ( int )m.SetId ) );
EqpManipulations.Clear();
ChangedData.Remove( EqpFile );
}
}
public void ResetGmp()
{
if( GmpFile != null )
{
GmpFile.Reset( GmpManipulations.Keys.Select( m => ( int )m.SetId ) );
GmpManipulations.Clear();
ChangedData.Remove( GmpFile );
}
}
public void ResetCmp()
{
if( CmpFile != null )
{
CmpFile.Reset( RspManipulations.Keys.Select( m => ( m.SubRace, m.Attribute ) ) );
RspManipulations.Clear();
ChangedData.Remove( CmpFile );
}
}
public void ResetEst()
{
FaceEstFile?.Reset();
HairEstFile?.Reset();
BodyEstFile?.Reset();
HeadEstFile?.Reset();
RspManipulations.Clear();
ChangedData.RemoveAll( f => f is EstFile );
}
public void ResetEqdp()
{
foreach( var file in EqdpFile )
{
file?.Reset( EqdpManipulations.Keys.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) );
}
ChangedData.RemoveAll( f => f is ExpandedEqdpFile );
EqdpManipulations.Clear();
}
private FullPath CreateImcPath( Utf8GamePath path )
{
var d = new DirectoryInfo( $":{_collection.Name}/" );
return new FullPath( d, new Utf8RelPath( path ) );
}
public void ResetImc()
{
foreach( var (path, file) in ImcFiles )
{
_collection.Cache?.ResolvedFiles.Remove( path );
path.Dispose();
file.Dispose();
}
ImcFiles.Clear();
ImcManipulations.Clear();
}
public void Reset()
{
ChangedData.Clear();
ResetEqp();
ResetGmp();
ResetCmp();
ResetEst();
ResetEqdp();
ResetImc();
}
private static void Dispose< T >( ref T? file ) where T : class, IDisposable
{
if( file != null )
{
file.Dispose();
file = null;
}
}
public void Dispose()
{
ChangedData.Clear();
EqpManipulations.Clear();
EstManipulations.Clear();
GmpManipulations.Clear();
RspManipulations.Clear();
EqdpManipulations.Clear();
Dispose( ref EqpFile );
Dispose( ref GmpFile );
Dispose( ref FaceEstFile );
Dispose( ref HairEstFile );
Dispose( ref BodyEstFile );
Dispose( ref HeadEstFile );
Dispose( ref CmpFile );
for( var i = 0; i < CharacterUtility.NumEqdpFiles; ++i )
{
Dispose( ref EqdpFile[ i ] );
}
ResetImc();
}
private void AddFile( MetaBaseFile file )
{
if( !ChangedData.Contains( file ) )
{
ChangedData.Add( file );
}
}
public bool ApplyMod( EqpManipulation m, Mod.Mod mod )
{
if( !EqpManipulations.TryAdd( m, mod ) )
{
return false;
}
EqpFile ??= new ExpandedEqpFile();
if( !m.Apply( EqpFile ) )
{
return false;
}
AddFile( EqpFile );
return true;
}
public bool ApplyMod( GmpManipulation m, Mod.Mod mod )
{
if( !GmpManipulations.TryAdd( m, mod ) )
{
return false;
}
GmpFile ??= new ExpandedGmpFile();
if( !m.Apply( GmpFile ) )
{
return false;
}
AddFile( GmpFile );
return true;
}
public bool ApplyMod( EstManipulation m, Mod.Mod mod )
{
if( !EstManipulations.TryAdd( m, mod ) )
{
return false;
}
var file = m.Slot switch
{
EstManipulation.EstType.Hair => HairEstFile ??= new EstFile( EstManipulation.EstType.Hair ),
EstManipulation.EstType.Face => FaceEstFile ??= new EstFile( EstManipulation.EstType.Face ),
EstManipulation.EstType.Body => BodyEstFile ??= new EstFile( EstManipulation.EstType.Body ),
EstManipulation.EstType.Head => HeadEstFile ??= new EstFile( EstManipulation.EstType.Head ),
_ => throw new ArgumentOutOfRangeException(),
};
if( !m.Apply( file ) )
{
return false;
}
AddFile( file );
return true;
}
public bool ApplyMod( RspManipulation m, Mod.Mod mod )
{
if( !RspManipulations.TryAdd( m, mod ) )
{
return false;
}
CmpFile ??= new CmpFile();
if( !m.Apply( CmpFile ) )
{
return false;
}
AddFile( CmpFile );
return true;
}
public bool ApplyMod( EqdpManipulation m, Mod.Mod mod )
{
if( !EqdpManipulations.TryAdd( m, mod ) )
{
return false;
}
var file = EqdpFile[ m.FileIndex() - 2 ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() );
if( !m.Apply( file ) )
{
return false;
}
AddFile( file );
return true;
}
public unsafe bool ApplyMod( ImcManipulation m, Mod.Mod mod )
{
const uint imcExt = 0x00696D63;
if( !ImcManipulations.TryAdd( m, mod ) )
{
return false;
}
var path = m.GamePath();
if( !ImcFiles.TryGetValue( path, out var file ) )
{
file = new ImcFile( path );
}
if( !m.Apply( file ) )
{
return false;
}
ImcFiles[ path ] = file;
var fullPath = CreateImcPath( path );
if( _collection.Cache != null )
{
_collection.Cache.ResolvedFiles[ path ] = fullPath;
}
var resource = ResourceLoader.FindResource( ResourceCategory.Chara, imcExt, ( uint )path.Path.Crc32 );
if( resource != null )
{
file.Replace( ( ResourceHandle* )resource );
}
return true;
}
public static unsafe byte ImcHandler( Utf8GamePath gamePath, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync )
{
var split = gamePath.Path.Split( ( byte )'|', 2, true );
fileDescriptor->ResourceHandle->FileNameData = split[ 1 ].Path;
fileDescriptor->ResourceHandle->FileNameLength = split[ 1 ].Length;
var ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
if( Penumbra.ModManager.Collections.Collections.TryGetValue( split[ 0 ].ToString(), out var collection )
&& collection.Cache != null
&& collection.Cache.MetaManipulations.ImcFiles.TryGetValue(
Utf8GamePath.FromSpan( split[ 1 ].Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) )
{
file.Replace( fileDescriptor->ResourceHandle );
}
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path;
fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length;
return ret;
}
public bool ApplyMod( MetaManipulation m, Mod.Mod mod )
{
return m.ManipulationType switch
{
MetaManipulation.Type.Eqp => ApplyMod( m.Eqp, mod ),
MetaManipulation.Type.Gmp => ApplyMod( m.Gmp, mod ),
MetaManipulation.Type.Eqdp => ApplyMod( m.Eqdp, mod ),
MetaManipulation.Type.Est => ApplyMod( m.Est, mod ),
MetaManipulation.Type.Rsp => ApplyMod( m.Rsp, mod ),
MetaManipulation.Type.Imc => ApplyMod( m.Imc, mod ),
_ => throw new ArgumentOutOfRangeException(),
};
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.Interop.Structs;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Util; using Penumbra.Util;
@ -43,6 +44,14 @@ public class CollectionManager
{ {
ActiveCollection = newActive; ActiveCollection = newActive;
Penumbra.ResidentResources.Reload(); Penumbra.ResidentResources.Reload();
if( ActiveCollection.Cache == null )
{
Penumbra.CharacterUtility.ResetAll();
}
else
{
ActiveCollection.Cache.MetaManipulations.SetFiles();
}
} }
else else
{ {
@ -206,10 +215,9 @@ public class CollectionManager
public void SetDefaultCollection( ModCollection newCollection ) public void SetDefaultCollection( ModCollection newCollection )
=> SetCollection( newCollection, DefaultCollection, c => => SetCollection( newCollection, DefaultCollection, c =>
{ {
if( !CollectionChangedTo.Any() ) if( CollectionChangedTo.Length == 0 )
{ {
ActiveCollection = c; SetActiveCollection( c, string.Empty );
Penumbra.ResidentResources.Reload();
} }
DefaultCollection = c; DefaultCollection = c;
@ -228,8 +236,7 @@ public class CollectionManager
{ {
if( CollectionChangedTo == characterName && CharacterCollection.TryGetValue( characterName, out var collection ) ) if( CollectionChangedTo == characterName && CharacterCollection.TryGetValue( characterName, out var collection ) )
{ {
ActiveCollection = c; SetActiveCollection( c, CollectionChangedTo );
Penumbra.ResidentResources.Reload();
} }
CharacterCollection[ characterName ] = c; CharacterCollection[ characterName ] = c;

View file

@ -7,8 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.Meta.Manager;
using Penumbra.Meta;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Util; using Penumbra.Util;
@ -27,7 +26,7 @@ public class ModCollectionCache
private readonly SortedList< string, object? > _changedItems = new(); private readonly SortedList< string, object? > _changedItems = new();
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
public readonly HashSet< FullPath > MissingFiles = new(); public readonly HashSet< FullPath > MissingFiles = new();
public readonly MetaManager2 MetaManipulations; public readonly MetaManager MetaManipulations;
public IReadOnlyDictionary< string, object? > ChangedItems public IReadOnlyDictionary< string, object? > ChangedItems
{ {
@ -39,7 +38,7 @@ public class ModCollectionCache
} }
public ModCollectionCache( ModCollection collection ) public ModCollectionCache( ModCollection collection )
=> MetaManipulations = new MetaManager2( collection ); => MetaManipulations = new MetaManager( collection );
private static void ResetFileSeen( int size ) private static void ResetFileSeen( int size )
{ {
@ -258,7 +257,7 @@ public class ModCollectionCache
} }
private void AddMetaFiles() private void AddMetaFiles()
=> MetaManipulations.ApplyImcFiles( ResolvedFiles ); => MetaManipulations.Imc.SetFiles();
private void AddSwaps( Mod.Mod mod ) private void AddSwaps( Mod.Mod mod )
{ {

View file

@ -2,14 +2,16 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.Meta.Manipulations;
using Penumbra.Mod; using Penumbra.Mod;
namespace Penumbra.Mods; namespace Penumbra.Mods;
public partial class ModManagerNew public partial class ModManagerNew
{ {
private readonly List<ModData> _mods = new(); private readonly List< ModData > _mods = new();
public IReadOnlyList<ModData> Mods
public IReadOnlyList< ModData > Mods
=> _mods; => _mods;
public void DiscoverMods() public void DiscoverMods()
@ -35,7 +37,6 @@ public partial class ModManagerNew
//Collections.RecreateCaches(); //Collections.RecreateCaches();
} }
} }
public partial class ModManagerNew public partial class ModManagerNew
{ {
public DirectoryInfo BasePath { get; private set; } = null!; public DirectoryInfo BasePath { get; private set; } = null!;
@ -52,7 +53,7 @@ public partial class ModManagerNew
{ {
if( Valid ) if( Valid )
{ {
Valid = Directory.Exists(BasePath.FullName); Valid = Directory.Exists( BasePath.FullName );
} }
return Valid; return Valid;
@ -87,7 +88,6 @@ public partial class ModManagerNew
return; return;
} }
( BasePath, Valid ) = CreateDirectory( path ); ( BasePath, Valid ) = CreateDirectory( path );
if( Penumbra.Config.ModDirectory != BasePath.FullName ) if( Penumbra.Config.ModDirectory != BasePath.FullName )
@ -105,6 +105,6 @@ public partial class ModManagerNew
} }
InitBaseDirectory( path ); InitBaseDirectory( path );
BasePathChanged?.Invoke( BasePath ); BasePathChanged?.Invoke( BasePath );
} }
} }

View file

@ -238,7 +238,7 @@ public class Penumbra : IDalamudPlugin
//PathResolver.Dispose(); //PathResolver.Dispose();
ResourceLogger.Dispose(); ResourceLogger.Dispose();
ResourceLoader.Dispose(); ResourceLoader.Dispose();
CharacterUtility.Dispose();
ShutdownWebServer(); ShutdownWebServer();
} }

View file

@ -18,11 +18,12 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType> <DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType> <DebugType>pdbonly</DebugType>
<DefineConstants>$(DefineConstants)TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View file

@ -1,28 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing.Text;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.FFXIV.Client.System.String;
using ImGuiNET; using ImGuiNET;
using Penumbra.Api; using Penumbra.Api;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Interop; using Penumbra.Interop;
using Penumbra.Meta; using Penumbra.Meta.Files;
using Penumbra.Mods;
using Penumbra.UI.Custom; using Penumbra.UI.Custom;
using Penumbra.Util;
using CharacterUtility = Penumbra.Interop.Structs.CharacterUtility;
using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle; using ResourceHandle = Penumbra.Interop.Structs.ResourceHandle;
using Utf8String = Penumbra.GameData.ByteString.Utf8String;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -391,6 +382,31 @@ public partial class SettingsInterface
return; return;
} }
var eqp = 0;
ImGui.InputInt( "##EqpInput", ref eqp );
try
{
var def = ExpandedEqpFile.GetDefault( eqp );
var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Eqp.File?[ eqp ] ?? def;
ImGui.Text( Convert.ToString( ( long )def, 2 ).PadLeft( 64, '0' ) );
ImGui.Text( Convert.ToString( ( long )val, 2 ).PadLeft( 64, '0' ) );
}
catch
{ }
var eqdp = 0;
ImGui.InputInt( "##EqdpInput", ref eqdp );
try
{
var def = ExpandedEqdpFile.GetDefault(GenderRace.MidlanderMale, false, eqdp );
var val = Penumbra.ModManager.Collections.ActiveCollection.Cache?.MetaManipulations.Eqdp.File(GenderRace.MidlanderMale, false)?[eqdp] ?? def;
ImGui.Text( Convert.ToString( ( ushort )def, 2 ).PadLeft( 16, '0' ) );
ImGui.Text( Convert.ToString( ( ushort )val, 2 ).PadLeft( 16, '0' ) );
}
catch
{ }
if( !ImGui.BeginTable( "##CharacterUtilityDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) ) if( !ImGui.BeginTable( "##CharacterUtilityDebugList", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX ) )
{ {
return; return;
@ -398,9 +414,10 @@ public partial class SettingsInterface
using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable ); using var end = ImGuiRaii.DeferredEnd( ImGui.EndTable );
for( var i = 0; i < CharacterUtility.NumRelevantResources; ++i ) for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i )
{ {
var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ i ]; var idx = CharacterUtility.RelevantIndices[ i ];
var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resources[ idx ];
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text( $"0x{( ulong )resource:X}" ); ImGui.Text( $"0x{( ulong )resource:X}" );
ImGui.TableNextColumn(); ImGui.TableNextColumn();

View file

@ -1,11 +1,13 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Reflection.Metadata.Ecma335;
using Dalamud.Interface; using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.STD; using FFXIVClientStructs.STD;
using ImGuiNET; using ImGuiNET;
using Penumbra.GameData.ByteString;
using Penumbra.Interop; using Penumbra.Interop;
using Penumbra.UI.Custom; using Penumbra.UI.Custom;
@ -55,6 +57,11 @@ public partial class SettingsInterface
ResourceLoader.IterateResourceMap( map, ( hash, r ) => ResourceLoader.IterateResourceMap( map, ( hash, r ) =>
{ {
if( _filter.Length != 0 && !r->FileName.ToString().Contains( _filter, StringComparison.InvariantCultureIgnoreCase ) )
{
return;
}
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text( $"0x{hash:X8}" ); ImGui.Text( $"0x{hash:X8}" );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -148,6 +155,8 @@ public partial class SettingsInterface
} ); } );
} }
private string _filter = string.Empty;
private unsafe void DrawResourceManagerTab() private unsafe void DrawResourceManagerTab()
{ {
if( !ImGui.BeginTabItem( "Resource Manager Tab" ) ) if( !ImGui.BeginTabItem( "Resource Manager Tab" ) )
@ -164,6 +173,8 @@ public partial class SettingsInterface
return; return;
} }
ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _filter, Utf8GamePath.MaxGamePathLength );
raii.Push( ImGui.EndChild ); raii.Push( ImGui.EndChild );
if( !ImGui.BeginChild( "##ResourceManagerChild", -Vector2.One, true ) ) if( !ImGui.BeginChild( "##ResourceManagerChild", -Vector2.One, true ) )
{ {