This commit is contained in:
Ottermandias 2022-03-11 14:09:45 +01:00
parent f5fccb0235
commit 46581780e0
37 changed files with 2343 additions and 2444 deletions

View file

@ -1,60 +0,0 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Penumbra.Meta
{
public static class EqdpEntryExtensions
{
public static bool Apply( this ref EqdpEntry entry, MetaManipulation manipulation )
{
if( manipulation.Type != MetaType.Eqdp )
{
return false;
}
var mask = Eqdp.Mask( manipulation.EqdpIdentifier.Slot );
var result = ( entry & ~mask ) | manipulation.EqdpValue;
var ret = result == entry;
entry = result;
return ret;
}
public static EqdpEntry Reduce( this EqdpEntry entry, EquipSlot slot )
=> entry & Eqdp.Mask( slot );
}
public static class EqpEntryExtensions
{
public static bool Apply( this ref EqpEntry entry, MetaManipulation manipulation )
{
if( manipulation.Type != MetaType.Eqp )
{
return false;
}
var mask = Eqp.Mask( manipulation.EqpIdentifier.Slot );
var result = ( entry & ~mask ) | manipulation.EqpValue;
var ret = result != entry;
entry = result;
return ret;
}
public static EqpEntry Reduce( this EqpEntry entry, EquipSlot slot )
=> entry & Eqp.Mask( slot );
}
public static class GmpEntryExtension
{
public static GmpEntry Apply( this ref GmpEntry entry, MetaManipulation manipulation )
{
if( manipulation.Type != MetaType.Gmp )
{
return entry;
}
entry.Value = manipulation.GmpValue.Value;
return entry;
}
}
}

View file

@ -1,73 +1,42 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Interop.Structs;
using System.Collections.Generic;
namespace Penumbra.Meta.Files
namespace Penumbra.Meta.Files;
public sealed unsafe class CmpFile : MetaBaseFile
{
public class CmpFile
private const int RacialScalingStart = 0x2A800;
public float this[ SubRace subRace, RspAttribute attribute ]
{
private const int RacialScalingStart = 0x2A800;
get => *( float* )( Data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 );
set => *( float* )( Data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 ) = value;
}
private readonly byte[] _byteData = new byte[RacialScalingStart];
private readonly RspEntry[] _rspEntries;
public override void Reset()
=> Functions.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length );
public CmpFile( byte[] bytes )
public void Reset( IEnumerable< (SubRace, RspAttribute) > entries )
{
foreach( var (r, a) in entries )
{
if( bytes.Length < RacialScalingStart )
{
throw new ArgumentOutOfRangeException();
}
Array.Copy( bytes, _byteData, RacialScalingStart );
var rspEntryNum = ( bytes.Length - RacialScalingStart ) / RspEntry.ByteSize;
var tmp = new List< RspEntry >( rspEntryNum );
for( var i = 0; i < rspEntryNum; ++i )
{
tmp.Add( new RspEntry( bytes, RacialScalingStart + i * RspEntry.ByteSize ) );
}
_rspEntries = tmp.ToArray();
this[ r, a ] = GetDefault( r, a );
}
}
public RspEntry this[ SubRace subRace ]
=> _rspEntries[ subRace.ToRspIndex() ];
public CmpFile()
: base( CharacterUtility.HumanCmpIdx )
{
AllocateData( DefaultData.Length );
Reset();
}
public bool Set( SubRace subRace, RspAttribute attribute, float value )
{
var entry = _rspEntries[ subRace.ToRspIndex() ];
var oldValue = entry[ attribute ];
if( oldValue == value )
{
return false;
}
entry[ attribute ] = value;
return true;
}
public byte[] WriteBytes()
{
using var s = new MemoryStream( RacialScalingStart + _rspEntries.Length * RspEntry.ByteSize );
s.Write( _byteData, 0, _byteData.Length );
foreach( var entry in _rspEntries )
{
var bytes = entry.ToBytes();
s.Write( bytes, 0, bytes.Length );
}
return s.ToArray();
}
private CmpFile( byte[] data, RspEntry[] entries )
{
_byteData = data.ToArray();
_rspEntries = entries.Select( e => new RspEntry( e ) ).ToArray();
}
public CmpFile Clone()
=> new( _byteData, _rspEntries );
public static float GetDefault( SubRace subRace, RspAttribute attribute )
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ CharacterUtility.HumanCmpIdx ].Address;
return *( float* )( data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 );
}
}

View file

@ -1,214 +1,137 @@
using System;
using System.IO;
using System.Linq;
using Lumina.Data;
using System.Collections.Generic;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Files
namespace Penumbra.Meta.Files;
// EQDP file structure:
// [Identifier][BlockSize:ushort][BlockCount:ushort]
// BlockCount x [BlockHeader:ushort]
// Containing offsets for blocks, ushort.Max means collapsed.
// Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2.
// ExpandedBlockCount x [Entry]
// Expanded Eqdp File just expands all blocks for easy read and write access to single entries and to keep the same memory for it.
public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
{
// EQDP file structure:
// [Identifier][BlockSize:ushort][BlockCount:ushort]
// BlockCount x [BlockHeader:ushort]
// Containing offsets for blocks, ushort.Max means collapsed.
// Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2.
// ExpandedBlockCount x [Entry]
public class EqdpFile
private const ushort BlockHeaderSize = 2;
private const ushort PreambleSize = 4;
private const ushort CollapsedBlock = ushort.MaxValue;
private const ushort IdentifierSize = 2;
private const ushort EqdpEntrySize = 2;
private const int FileAlignment = 1 << 9;
public readonly int DataOffset;
public ushort Identifier
=> *( ushort* )Data;
public ushort BlockSize
=> *( ushort* )( Data + 2 );
public ushort BlockCount
=> *( ushort* )( Data + 4 );
public int Count
=> ( Length - DataOffset ) / EqdpEntrySize;
public EqdpEntry this[ int idx ]
{
private const ushort BlockHeaderSize = 2;
private const ushort PreambleSize = 4;
private const ushort CollapsedBlock = ushort.MaxValue;
private const ushort IdentifierSize = 2;
private const ushort EqdpEntrySize = 2;
private const int FileAlignment = 1 << 9;
private EqdpFile( EqdpFile clone )
get
{
Identifier = clone.Identifier;
BlockSize = clone.BlockSize;
TotalBlockCount = clone.TotalBlockCount;
ExpandedBlockCount = clone.ExpandedBlockCount;
Blocks = new EqdpEntry[clone.TotalBlockCount][];
for( var i = 0; i < TotalBlockCount; ++i )
if( idx >= Count || idx < 0 )
{
if( clone.Blocks[ i ] != null )
{
Blocks[ i ] = ( EqdpEntry[] )clone.Blocks[ i ]!.Clone();
}
throw new IndexOutOfRangeException();
}
return *( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx );
}
public ref EqdpEntry this[ ushort setId ]
=> ref GetTrueEntry( setId );
public EqdpFile Clone()
=> new( this );
private ushort Identifier { get; }
private ushort BlockSize { get; }
private ushort TotalBlockCount { get; }
private ushort ExpandedBlockCount { get; set; }
private EqdpEntry[]?[] Blocks { get; }
private int BlockIdx( ushort id )
=> ( ushort )( id / BlockSize );
private int SubIdx( ushort id )
=> ( ushort )( id % BlockSize );
private bool ExpandBlock( int idx )
set
{
if( idx < TotalBlockCount && Blocks[ idx ] == null )
if( idx >= Count || idx < 0 )
{
Blocks[ idx ] = new EqdpEntry[BlockSize];
++ExpandedBlockCount;
return true;
throw new IndexOutOfRangeException();
}
return false;
*( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx ) = value;
}
}
private bool CollapseBlock( int idx )
public override void Reset()
{
var def = ( byte* )DefaultData.Data;
Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize );
var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize );
var dataBasePtr = ( byte* )( controlPtr + BlockCount );
var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount );
for( var i = 0; i < BlockCount; ++i )
{
if( idx >= TotalBlockCount || Blocks[ idx ] == null )
if( controlPtr[ i ] == CollapsedBlock )
{
return false;
}
Blocks[ idx ] = null;
--ExpandedBlockCount;
return true;
}
public bool SetEntry( ushort idx, EqdpEntry entry )
{
var block = BlockIdx( idx );
if( block >= TotalBlockCount )
{
return false;
}
if( entry != 0 )
{
ExpandBlock( block );
if( Blocks[ block ]![ SubIdx( idx ) ] != entry )
{
Blocks[ block ]![ SubIdx( idx ) ] = entry;
return true;
}
Functions.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize );
}
else
{
var array = Blocks[ block ];
if( array != null )
{
array[ SubIdx( idx ) ] = entry;
if( array.All( e => e == 0 ) )
{
CollapseBlock( block );
}
return true;
}
Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize );
}
return false;
}
public EqdpEntry GetEntry( ushort idx )
{
var block = BlockIdx( idx );
var array = block < Blocks.Length ? Blocks[ block ] : null;
return array?[ SubIdx( idx ) ] ?? 0;
}
private ref EqdpEntry GetTrueEntry( ushort idx )
{
var block = BlockIdx( idx );
if( block >= TotalBlockCount )
{
throw new ArgumentOutOfRangeException();
}
ExpandBlock( block );
var array = Blocks[ block ]!;
return ref array[ SubIdx( idx ) ];
}
private void WriteHeaders( BinaryWriter bw )
{
ushort offset = 0;
foreach( var block in Blocks )
{
if( block == null )
{
bw.Write( CollapsedBlock );
continue;
}
bw.Write( offset );
offset += BlockSize;
}
}
private static void WritePadding( BinaryWriter bw, int paddingSize )
{
var buffer = new byte[paddingSize];
bw.Write( buffer, 0, paddingSize );
}
private void WriteBlocks( BinaryWriter bw )
{
foreach( var entry in Blocks.Where( block => block != null )
.SelectMany( block => block! ) )
{
bw.Write( ( ushort )entry );
}
}
public byte[] WriteBytes()
{
var dataSize = PreambleSize + IdentifierSize + BlockHeaderSize * TotalBlockCount + ExpandedBlockCount * BlockSize * EqdpEntrySize;
var paddingSize = FileAlignment - ( dataSize & ( FileAlignment - 1 ) );
using var mem =
new MemoryStream( dataSize + paddingSize );
using var bw = new BinaryWriter( mem );
bw.Write( Identifier );
bw.Write( BlockSize );
bw.Write( TotalBlockCount );
WriteHeaders( bw );
WriteBlocks( bw );
WritePadding( bw, paddingSize );
return mem.ToArray();
}
public EqdpFile( FileResource file )
{
file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin );
Identifier = file.Reader.ReadUInt16();
BlockSize = file.Reader.ReadUInt16();
TotalBlockCount = file.Reader.ReadUInt16();
Blocks = new EqdpEntry[TotalBlockCount][];
ExpandedBlockCount = 0;
for( var i = 0; i < TotalBlockCount; ++i )
{
var offset = file.Reader.ReadUInt16();
if( offset != CollapsedBlock )
{
ExpandBlock( ( ushort )i );
}
}
foreach( var array in Blocks.Where( array => array != null ) )
{
for( var i = 0; i < BlockSize; ++i )
{
array![ i ] = ( EqdpEntry )file.Reader.ReadUInt16();
}
}
myDataPtr += BlockSize;
}
}
public void Reset( IEnumerable< int > entries )
{
foreach( var entry in entries )
{
this[ entry ] = GetDefault( entry );
}
}
public ExpandedEqdpFile( GenderRace raceCode, bool accessory )
: base( CharacterUtility.EqdpIdx( raceCode, accessory ) )
{
var def = ( byte* )DefaultData.Data;
var blockSize = *( ushort* )( def + IdentifierSize );
var totalBlockCount = *( ushort* )( def + IdentifierSize + 2 );
var totalBlockSize = blockSize * EqdpEntrySize;
DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize;
var fullLength = DataOffset + totalBlockCount * totalBlockSize;
fullLength += ( FileAlignment - ( Length & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 );
AllocateData( fullLength );
Reset();
}
public EqdpEntry GetDefault( int setIdx )
=> GetDefault( Index, setIdx );
public static EqdpEntry GetDefault( int fileIdx, int setIdx )
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address;
var blockSize = *( ushort* )( data + IdentifierSize );
var totalBlockCount = *( ushort* )( data + IdentifierSize + 2 );
var blockIdx = setIdx / blockSize;
if( blockIdx >= totalBlockCount )
{
return 0;
}
var block = ( ( ushort* )( data + IdentifierSize + PreambleSize ) )[ blockIdx ];
if( block == CollapsedBlock )
{
return 0;
}
var blockData = ( EqdpEntry* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block );
return *( blockData + blockIdx % blockSize );
}
public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx )
=> GetDefault( CharacterUtility.EqdpIdx( raceCode, accessory ), setIdx );
}

View file

@ -1,216 +0,0 @@
using System;
using System.IO;
using System.Linq;
using Lumina.Data;
using Penumbra.GameData.Structs;
namespace Penumbra.Meta.Files
{
// EQP Structure:
// 64 x [Block collapsed or not bit]
// 159 x [EquipmentParameter:ulong]
// (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong]
// Item 0 does not exist and is sent to Item 1 instead.
public sealed class EqpFile : EqpGmpBase
{
private readonly EqpEntry[]?[] _entries = new EqpEntry[TotalBlockCount][];
protected override ulong ControlBlock
{
get => ( ulong )_entries[ 0 ]![ 0 ];
set => _entries[ 0 ]![ 0 ] = ( EqpEntry )value;
}
private EqpFile( EqpFile clone )
{
ExpandedBlockCount = clone.ExpandedBlockCount;
_entries = clone.Clone( clone._entries );
}
public byte[] WriteBytes()
=> WriteBytes( _entries, e => ( ulong )e );
public EqpFile Clone()
=> new( this );
public EqpFile( FileResource file )
=> ReadFile( _entries, file, I => ( EqpEntry )I );
public EqpEntry GetEntry( ushort setId )
=> GetEntry( _entries, setId, ( EqpEntry )0 );
public bool SetEntry( ushort setId, EqpEntry entry )
=> SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 );
public ref EqpEntry this[ ushort setId ]
=> ref GetTrueEntry( _entries, setId );
}
public class EqpGmpBase
{
protected const ushort ParameterSize = 8;
protected const ushort BlockSize = 160;
protected const ushort TotalBlockCount = 64;
protected int ExpandedBlockCount { get; set; }
private static int BlockIdx( ushort idx )
=> idx / BlockSize;
private static int SubIdx( ushort idx )
=> idx % BlockSize;
protected virtual ulong ControlBlock { get; set; }
protected T[]?[] Clone< T >( T[]?[] clone )
{
var ret = new T[TotalBlockCount][];
for( var i = 0; i < TotalBlockCount; ++i )
{
if( clone[ i ] != null )
{
ret[ i ] = ( T[] )clone[ i ]!.Clone();
}
}
return ret;
}
protected EqpGmpBase()
{ }
protected bool ExpandBlock< T >( T[]?[] blocks, int idx )
{
if( idx >= TotalBlockCount || blocks[ idx ] != null )
{
return false;
}
blocks[ idx ] = new T[BlockSize];
++ExpandedBlockCount;
ControlBlock |= 1ul << idx;
return true;
}
protected bool CollapseBlock< T >( T[]?[] blocks, int idx )
{
if( idx >= TotalBlockCount || blocks[ idx ] == null )
{
return false;
}
blocks[ idx ] = null;
--ExpandedBlockCount;
ControlBlock &= ~( 1ul << idx );
return true;
}
protected T GetEntry< T >( T[]?[] blocks, ushort idx, T defaultEntry )
{
// Skip the zeroth item.
idx = idx == 0 ? ( ushort )1 : idx;
var block = BlockIdx( idx );
var array = block < blocks.Length ? blocks[ block ] : null;
if( array == null )
{
return defaultEntry;
}
return array[ SubIdx( idx ) ];
}
protected ref T GetTrueEntry< T >( T[]?[] blocks, ushort idx )
{
// Skip the zeroth item.
idx = idx == 0 ? ( ushort )1 : idx;
var block = BlockIdx( idx );
if( block >= TotalBlockCount )
{
throw new ArgumentOutOfRangeException();
}
ExpandBlock( blocks, block );
var array = blocks[ block ]!;
return ref array[ SubIdx( idx ) ];
}
protected byte[] WriteBytes< T >( T[]?[] blocks, Func< T, ulong > transform )
{
var dataSize = ExpandedBlockCount * BlockSize * ParameterSize;
using var mem = new MemoryStream( dataSize );
using var bw = new BinaryWriter( mem );
foreach( var parameter in blocks.Where( array => array != null )
.SelectMany( array => array! ) )
{
bw.Write( transform( parameter ) );
}
return mem.ToArray();
}
protected void ReadFile< T >( T[]?[] blocks, FileResource file, Func< ulong, T > convert )
{
file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin );
var blockBits = file.Reader.ReadUInt64();
// reset to 0 and just put the bitmask in the first block
// item 0 is not accessible and it simplifies printing.
file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin );
ExpandedBlockCount = 0;
for( var i = 0; i < TotalBlockCount; ++i )
{
var flag = 1ul << i;
if( ( blockBits & flag ) != flag )
{
continue;
}
++ExpandedBlockCount;
var tmp = new T[BlockSize];
for( var j = 0; j < BlockSize; ++j )
{
tmp[ j ] = convert( file.Reader.ReadUInt64() );
}
blocks[ i ] = tmp;
}
}
protected bool SetEntry< T >( T[]?[] blocks, ushort idx, T entry, Func< T, bool > isDefault, Func< T, T, bool > isEqual )
{
var block = BlockIdx( idx );
if( block >= TotalBlockCount )
{
return false;
}
if( !isDefault( entry ) )
{
ExpandBlock( blocks, block );
if( !isEqual( entry, blocks[ block ]![ SubIdx( idx ) ] ) )
{
blocks[ block ]![ SubIdx( idx ) ] = entry;
return true;
}
}
else
{
var array = blocks[ block ];
if( array != null )
{
array[ SubIdx( idx ) ] = entry;
if( array.All( e => e!.Equals( 0ul ) ) )
{
CollapseBlock( blocks, block );
}
return true;
}
}
return false;
}
}
}

View file

@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Files;
// EQP/GMP Structure:
// 64 x [Block collapsed or not bit]
// 159 x [EquipmentParameter:ulong]
// (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong]
// Item 0 does not exist and is sent to Item 1 instead.
public unsafe class ExpandedEqpGmpBase : MetaBaseFile
{
protected const int BlockSize = 160;
protected const int NumBlocks = 64;
protected const int EntrySize = 8;
protected const int MaxSize = BlockSize * NumBlocks * EntrySize;
public const int Count = BlockSize * NumBlocks;
public ulong ControlBlock
=> *( ulong* )Data;
protected T Get< T >( int idx ) where T : unmanaged
{
return idx switch
{
>= Count => throw new IndexOutOfRangeException(),
<= 1 => *( ( T* )Data + 1 ),
_ => *( ( T* )Data + idx ),
};
}
protected void Set< T >( int idx, T value ) where T : unmanaged
{
idx = idx switch
{
>= Count => throw new IndexOutOfRangeException(),
<= 0 => 1,
_ => idx,
};
*( ( T* )Data + idx ) = value;
}
protected virtual void SetEmptyBlock( int idx )
{
Functions.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize );
}
public sealed override void Reset()
{
var ptr = ( byte* )DefaultData.Data;
var controlBlock = *( ulong* )ptr;
*( ulong* )ptr = ulong.MaxValue;
for( var i = 0; i < 64; ++i )
{
var collapsed = ( ( controlBlock >> i ) & 1 ) == 0;
if( !collapsed )
{
Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + i * BlockSize * EntrySize, BlockSize * EntrySize );
}
else
{
SetEmptyBlock( i );
}
}
}
public ExpandedEqpGmpBase( bool gmp )
: base( gmp ? CharacterUtility.GmpIdx : CharacterUtility.EqpIdx )
{
AllocateData( MaxSize );
Reset();
}
protected static T GetDefault< T >( int fileIdx, int setIdx, T def ) where T : unmanaged
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address;
if( setIdx == 0 )
{
setIdx = 1;
}
var blockIdx = setIdx / BlockSize;
if( blockIdx >= NumBlocks )
{
return def;
}
var control = *( ulong* )data;
var blockBit = 1ul << blockIdx;
if( ( control & blockBit ) == 0 )
{
return def;
}
var count = BitOperations.PopCount( control & ( blockBit - 1 ) );
var idx = setIdx % BlockSize;
var ptr = ( T* )data + BlockSize * count + idx;
return *ptr;
}
}
public sealed class ExpandedEqpFile : ExpandedEqpGmpBase
{
public ExpandedEqpFile()
: base( false )
{ }
public EqpEntry this[ int idx ]
{
get => Get< EqpEntry >( idx );
set => Set( idx, value );
}
public static EqpEntry GetDefault( int setIdx )
=> GetDefault( CharacterUtility.EqpIdx, setIdx, Eqp.DefaultEntry );
protected override unsafe void SetEmptyBlock( int idx )
{
var blockPtr = ( ulong* )( Data + idx * BlockSize * EntrySize );
var endPtr = blockPtr + BlockSize;
for( var ptr = blockPtr; ptr < endPtr; ++ptr )
{
*ptr = ( ulong )Eqp.DefaultEntry;
}
}
public void Reset( IEnumerable< int > entries )
{
foreach( var entry in entries )
{
this[ entry ] = GetDefault( entry );
}
}
}
public sealed class ExpandedGmpFile : ExpandedEqpGmpBase
{
public ExpandedGmpFile()
: base( true )
{ }
public GmpEntry this[ int idx ]
{
get => Get< GmpEntry >( idx );
set => Set( idx, value );
}
public static GmpEntry GetDefault( int setIdx )
=> GetDefault( CharacterUtility.GmpIdx, setIdx, GmpEntry.Default );
public void Reset( IEnumerable< int > entries )
{
foreach( var entry in entries )
{
this[ entry ] = GetDefault( entry );
}
}
}

View file

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Lumina.Data;
using System;
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Util;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta.Files;
@ -11,152 +11,192 @@ namespace Penumbra.Meta.Files;
// Apparently entries need to be sorted.
// #NumEntries x [SetId : UInt16] [RaceId : UInt16]
// #NumEntries x [SkeletonId : UInt16]
public class EstFile
public sealed unsafe class EstFile : MetaBaseFile
{
private const ushort EntryDescSize = 4;
private const ushort EntrySize = 2;
private const int IncreaseSize = 100;
private readonly SortedList< GenderRace, SortedList< ushort, ushort > > _entries = new();
private uint NumEntries { get; set; }
public int Count
=> *( int* )Data;
private EstFile( EstFile clone )
private int Size
=> 4 + Count * ( EntryDescSize + EntrySize );
public enum EstEntryChange
{
NumEntries = clone.NumEntries;
_entries = new SortedList< GenderRace, SortedList< ushort, ushort > >( clone._entries.Count );
foreach( var (genderRace, data) in clone._entries )
Unchanged,
Changed,
Added,
Removed,
}
public ushort this[ GenderRace genderRace, ushort setId ]
{
get
{
var dict = new SortedList< ushort, ushort >( data.Count );
foreach( var (setId, value) in data )
var (idx, exists) = FindEntry( genderRace, setId );
if( !exists )
{
dict.Add( setId, value );
return 0;
}
_entries.Add( genderRace, dict );
return *( ushort* )( Data + EntryDescSize * ( Count + 1 ) + EntrySize * idx );
}
set => SetEntry( genderRace, setId, value );
}
private void InsertEntry( int idx, GenderRace genderRace, ushort setId, ushort skeletonId )
{
if( Length < Size + EntryDescSize + EntrySize )
{
var data = Data;
var length = Length;
AllocateData( length + IncreaseSize * ( EntryDescSize + EntrySize ) );
Functions.MemCpyUnchecked( Data, data, length );
Functions.MemSet( Data + length, 0, IncreaseSize * ( EntryDescSize + EntrySize ) );
GC.RemoveMemoryPressure( length );
Marshal.FreeHGlobal( ( IntPtr )data );
}
var control = ( uint* )( Data + 4 );
var entries = ( ushort* )( Data + 4 * ( Count + 1 ) );
for( var i = Count; i > idx; --i )
{
*( entries + i + 2 ) = entries[ i - 1 ];
}
for( var i = idx - 1; i >= 0; --i )
{
*( entries + i + 2 ) = entries[ i ];
}
for( var i = Count; i > idx; --i )
{
*( control + i ) = control[ i - 1 ];
}
*( int* )Data = Count + 1;
*( ushort* )control = setId;
*( ( ushort* )control + 1 ) = ( ushort )genderRace;
control[ idx ] = skeletonId;
}
private void RemoveEntry( int idx )
{
var entries = ( ushort* )( Data + 4 * Count );
var control = ( uint* )( Data + 4 );
*( int* )Data = Count - 1;
var count = Count;
for( var i = idx; i < count; ++i )
{
control[ i ] = control[ i + 1 ];
}
for( var i = 0; i < count; ++i )
{
entries[ i ] = entries[ i + 1 ];
}
entries[ count ] = 0;
entries[ count + 1 ] = 0;
entries[ count + 2 ] = 0;
}
[StructLayout( LayoutKind.Sequential, Size = 4 )]
private struct Info : IComparable< Info >
{
public readonly ushort SetId;
public readonly GenderRace GenderRace;
public Info( GenderRace gr, ushort setId )
{
GenderRace = gr;
SetId = setId;
}
public int CompareTo( Info other )
{
var genderRaceComparison = GenderRace.CompareTo( other.GenderRace );
return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo( other.SetId );
}
}
public EstFile Clone()
=> new(this);
private bool DeleteEntry( GenderRace gr, ushort setId )
private static (int, bool) FindEntry( ReadOnlySpan< Info > data, GenderRace genderRace, ushort setId )
{
if( !_entries.TryGetValue( gr, out var setDict ) )
{
return false;
}
if( !setDict.ContainsKey( setId ) )
{
return false;
}
setDict.Remove( setId );
if( setDict.Count == 0 )
{
_entries.Remove( gr );
}
--NumEntries;
return true;
var idx = data.BinarySearch( new Info( genderRace, setId ) );
return idx < 0 ? ( ~idx, false ) : ( idx, true );
}
private (bool, bool) AddEntry( GenderRace gr, ushort setId, ushort entry )
private (int, bool) FindEntry( GenderRace genderRace, ushort setId )
{
if( !_entries.TryGetValue( gr, out var setDict ) )
{
_entries[ gr ] = new SortedList< ushort, ushort >();
setDict = _entries[ gr ];
}
var span = new ReadOnlySpan< Info >( Data + 4, Count );
return FindEntry( span, genderRace, setId );
}
if( setDict.TryGetValue( setId, out var oldEntry ) )
public EstEntryChange SetEntry( GenderRace genderRace, ushort setId, ushort skeletonId )
{
var (idx, exists) = FindEntry( genderRace, setId );
if( exists )
{
if( oldEntry == entry )
var value = *( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx );
if( value == skeletonId )
{
return ( false, false );
return EstEntryChange.Unchanged;
}
setDict[ setId ] = entry;
return ( false, true );
if( skeletonId == 0 )
{
RemoveEntry( idx );
return EstEntryChange.Removed;
}
*( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx ) = skeletonId;
return EstEntryChange.Changed;
}
setDict[ setId ] = entry;
return ( true, true );
if( skeletonId == 0 )
{
return EstEntryChange.Unchanged;
}
InsertEntry( idx, genderRace, setId, skeletonId );
return EstEntryChange.Added;
}
public bool SetEntry( GenderRace gr, ushort setId, ushort entry )
public override void Reset()
{
if( entry == 0 )
{
return DeleteEntry( gr, setId );
}
var (addedNew, changed) = AddEntry( gr, setId, entry );
if( !addedNew )
{
return changed;
}
++NumEntries;
return true;
var (d, length) = DefaultData;
var data = ( byte* )d;
Functions.MemCpyUnchecked( Data, data, length );
Functions.MemSet( Data + length, 0, Length - length );
}
public ushort GetEntry( GenderRace gr, ushort setId )
public EstFile( EstManipulation.EstType estType )
: base( ( int )estType )
{
if( !_entries.TryGetValue( gr, out var setDict ) )
var length = DefaultData.Length;
AllocateData( length + IncreaseSize * ( EntryDescSize + EntrySize ) );
Reset();
}
public ushort GetDefault( GenderRace genderRace, ushort setId )
=> GetDefault( ( EstManipulation.EstType )Index, genderRace, setId );
public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId )
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ ( int )estType ].Address;
var count = *( int* )data;
var span = new ReadOnlySpan< Info >( data + 4, count );
var (idx, found) = FindEntry( span, genderRace, setId );
if( !found )
{
return 0;
}
return !setDict.TryGetValue( setId, out var entry ) ? ( ushort )0 : entry;
}
public byte[] WriteBytes()
{
using MemoryStream mem = new(( int )( 4 + ( EntryDescSize + EntrySize ) * NumEntries ));
using BinaryWriter bw = new(mem);
bw.Write( NumEntries );
foreach( var kvp1 in _entries )
{
foreach( var kvp2 in kvp1.Value )
{
bw.Write( kvp2.Key );
bw.Write( ( ushort )kvp1.Key );
}
}
foreach( var kvp2 in _entries.SelectMany( kvp1 => kvp1.Value ) )
{
bw.Write( kvp2.Value );
}
return mem.ToArray();
}
public EstFile( FileResource file )
{
file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin );
NumEntries = file.Reader.ReadUInt32();
var currentEntryDescOffset = 4;
var currentEntryOffset = 4 + EntryDescSize * NumEntries;
for( var i = 0; i < NumEntries; ++i )
{
file.Reader.BaseStream.Seek( currentEntryDescOffset, SeekOrigin.Begin );
currentEntryDescOffset += EntryDescSize;
var setId = file.Reader.ReadUInt16();
var raceId = ( GenderRace )file.Reader.ReadUInt16();
if( !raceId.IsValid() )
{
continue;
}
file.Reader.BaseStream.Seek( currentEntryOffset, SeekOrigin.Begin );
currentEntryOffset += EntrySize;
var entry = file.Reader.ReadUInt16();
AddEntry( raceId, setId, entry );
}
return *( ushort* )( data + 4 + count * EntryDescSize + idx * EntrySize );
}
}

View file

@ -1,42 +0,0 @@
using Lumina.Data;
using Penumbra.GameData.Structs;
namespace Penumbra.Meta.Files
{
// GmpFiles use the same structure as Eqp Files.
// Entries are also one ulong.
public sealed class GmpFile : EqpGmpBase
{
private readonly GmpEntry[]?[] _entries = new GmpEntry[TotalBlockCount][];
protected override ulong ControlBlock
{
get => _entries[ 0 ]![ 0 ];
set => _entries[ 0 ]![ 0 ] = ( GmpEntry )value;
}
private GmpFile( GmpFile clone )
{
ExpandedBlockCount = clone.ExpandedBlockCount;
_entries = clone.Clone( clone._entries );
}
public byte[] WriteBytes()
=> WriteBytes( _entries, e => ( ulong )e );
public GmpFile Clone()
=> new( this );
public GmpFile( FileResource file )
=> ReadFile( _entries, file, i => ( GmpEntry )i );
public GmpEntry GetEntry( ushort setId )
=> GetEntry( _entries, setId, ( GmpEntry )0 );
public bool SetEntry( ushort setId, GmpEntry entry )
=> SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 );
public ref GmpEntry this[ ushort setId ]
=> ref GetTrueEntry( _entries, setId );
}
}

View file

@ -1,151 +0,0 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using Lumina.Data.Files;
using Penumbra.GameData.Enums;
namespace Penumbra.Meta.Files
{
public class InvalidImcVariantException : ArgumentOutOfRangeException
{
public InvalidImcVariantException()
: base( "Trying to manipulate invalid variant." )
{ }
}
// Imc files are already supported in Lumina, but changing the provided data is not supported.
// We use reflection and extension methods to support changing the data of a given Imc file.
public static class ImcExtensions
{
public static ulong ToInteger( this ImcFile.ImageChangeData imc )
{
ulong ret = imc.MaterialId;
ret |= ( ulong )imc.DecalId << 8;
ret |= ( ulong )imc.AttributeMask << 16;
ret |= ( ulong )imc.SoundId << 16;
ret |= ( ulong )imc.VfxId << 32;
ret |= ( ulong )imc.ActualMaterialAnimationId() << 40;
return ret;
}
public static byte ActualMaterialAnimationId( this ImcFile.ImageChangeData imc )
{
var tmp = imc.GetType().GetField( "_MaterialAnimationIdMask",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
return ( byte )( tmp?.GetValue( imc ) ?? 0 );
}
public static ImcFile.ImageChangeData FromValues( byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId,
byte materialAnimationId )
{
var ret = new ImcFile.ImageChangeData()
{
DecalId = decalId,
MaterialId = materialId,
VfxId = vfxId,
};
ret.GetType().GetField( "_AttributeAndSound",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance )!
.SetValue( ret, ( ushort )( ( attributeMask & 0x3FF ) | ( soundId << 10 ) ) );
ret.GetType().GetField( "_AttributeAndSound",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance )!.SetValue( ret, materialAnimationId );
return ret;
}
public static bool Equal( this ImcFile.ImageChangeData lhs, ImcFile.ImageChangeData rhs )
=> lhs.MaterialId == rhs.MaterialId
&& lhs.DecalId == rhs.DecalId
&& lhs.AttributeMask == rhs.AttributeMask
&& lhs.SoundId == rhs.SoundId
&& lhs.VfxId == rhs.VfxId
&& lhs.MaterialAnimationId == rhs.MaterialAnimationId;
private static void WriteBytes( this ImcFile.ImageChangeData variant, BinaryWriter bw )
{
bw.Write( variant.MaterialId );
bw.Write( variant.DecalId );
bw.Write( ( ushort )( variant.AttributeMask | variant.SoundId ) );
bw.Write( variant.VfxId );
bw.Write( variant.ActualMaterialAnimationId() );
}
public static byte[] WriteBytes( this ImcFile file )
{
var parts = file.PartMask == 31 ? 5 : 1;
var dataSize = 4 + 6 * parts * ( 1 + file.Count );
using var mem = new MemoryStream( dataSize );
using var bw = new BinaryWriter( mem );
bw.Write( file.Count );
bw.Write( file.PartMask );
for( var i = 0; i < parts; ++i )
{
file.GetDefaultVariant( i ).WriteBytes( bw );
}
for( var i = 0; i < file.Count; ++i )
{
for( var j = 0; j < parts; ++j )
{
file.GetVariant( j, i ).WriteBytes( bw );
}
}
return mem.ToArray();
}
public static ref ImcFile.ImageChangeData GetValue( this ImcFile file, MetaManipulation manipulation )
{
var parts = file.GetParts();
var imc = manipulation.ImcIdentifier;
var idx = 0;
if( imc.ObjectType == ObjectType.Equipment || imc.ObjectType == ObjectType.Accessory )
{
idx = imc.EquipSlot switch
{
EquipSlot.Head => 0,
EquipSlot.Ears => 0,
EquipSlot.Body => 1,
EquipSlot.Neck => 1,
EquipSlot.Hands => 2,
EquipSlot.Wrists => 2,
EquipSlot.Legs => 3,
EquipSlot.RFinger => 3,
EquipSlot.Feet => 4,
EquipSlot.LFinger => 4,
_ => throw new InvalidEnumArgumentException(),
};
}
if( imc.Variant == 0 )
{
return ref parts[ idx ].DefaultVariant;
}
if( imc.Variant > parts[ idx ].Variants.Length )
{
throw new InvalidImcVariantException();
}
return ref parts[ idx ].Variants[ imc.Variant - 1 ];
}
public static ImcFile Clone( this ImcFile file )
{
var ret = new ImcFile
{
Count = file.Count,
PartMask = file.PartMask,
};
var parts = file.GetParts().Select( p => new ImcFile.ImageChangeParts()
{
DefaultVariant = p.DefaultVariant,
Variants = ( ImcFile.ImageChangeData[] )p.Variants.Clone(),
} ).ToArray();
var prop = ret.GetType().GetField( "Parts", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
prop!.SetValue( ret, parts );
return ret;
}
}
}

View file

@ -0,0 +1,166 @@
using System;
using System.Numerics;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Util;
namespace Penumbra.Meta.Files;
public struct ImcEntry : IEquatable< ImcEntry >
{
public byte MaterialId;
public byte DecalId;
private ushort _attributeAndSound;
public byte VfxId;
public byte MaterialAnimationId;
public ushort AttributeMask
=> ( ushort )( _attributeAndSound & 0x3FF );
public byte SoundId
=> ( byte )( _attributeAndSound >> 10 );
public bool Equals( ImcEntry other )
=> MaterialId == other.MaterialId
&& DecalId == other.DecalId
&& _attributeAndSound == other._attributeAndSound
&& VfxId == other.VfxId
&& MaterialAnimationId == other.MaterialAnimationId;
public override bool Equals( object? obj )
=> obj is ImcEntry other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( MaterialId, DecalId, _attributeAndSound, VfxId, MaterialAnimationId );
}
public unsafe class ImcFile : MetaBaseFile
{
private const int PreambleSize = 4;
public int ActualLength
=> NumParts * sizeof( ImcEntry ) * ( Count + 1 ) + PreambleSize;
public int Count
=> *( ushort* )Data;
public ushort PartMask
=> *( ushort* )( Data + 2 );
public readonly int NumParts;
public readonly Utf8GamePath Path;
public ImcEntry* DefaultPartPtr( int partIdx )
{
var flag = 1 << partIdx;
if( ( PartMask & flag ) == 0 )
{
return null;
}
return ( ImcEntry* )( Data + PreambleSize ) + partIdx;
}
public ImcEntry* VariantPtr( int partIdx, int variantIdx )
{
var flag = 1 << partIdx;
if( ( PartMask & flag ) == 0 || variantIdx >= Count )
{
return null;
}
var numParts = NumParts;
var ptr = ( ImcEntry* )( Data + PreambleSize );
ptr += numParts;
ptr += variantIdx * numParts;
ptr += partIdx;
return ptr;
}
public static int PartIndex( EquipSlot slot )
=> slot switch
{
EquipSlot.Head => 0,
EquipSlot.Ears => 0,
EquipSlot.Body => 1,
EquipSlot.Neck => 1,
EquipSlot.Hands => 2,
EquipSlot.Wrists => 2,
EquipSlot.Legs => 3,
EquipSlot.RFinger => 3,
EquipSlot.Feet => 4,
EquipSlot.LFinger => 4,
_ => 0,
};
public bool EnsureVariantCount( int numVariants )
{
if( numVariants <= Count )
{
return true;
}
var numParts = NumParts;
if( ActualLength > Length )
{
PluginLog.Warning( "Adding too many variants to IMC, size exceeded." );
return false;
}
var defaultPtr = ( ImcEntry* )( Data + PreambleSize );
var endPtr = defaultPtr + ( numVariants + 1 ) * numParts;
for( var ptr = defaultPtr + numParts; ptr < endPtr; ptr += numParts )
{
Functions.MemCpyUnchecked( ptr, defaultPtr, numParts * sizeof( ImcEntry ) );
}
PluginLog.Verbose( "Expanded imc from {Count} to {NewCount} variants.", Count, numVariants );
*( ushort* )Count = ( ushort )numVariants;
return true;
}
public bool SetEntry( int partIdx, int variantIdx, ImcEntry entry )
{
var numParts = NumParts;
if( partIdx >= numParts )
{
return false;
}
EnsureVariantCount( variantIdx + 1 );
var variantPtr = VariantPtr( partIdx, variantIdx );
if( variantPtr == null )
{
PluginLog.Error( "Error during expansion of imc file." );
return false;
}
if( variantPtr->Equals( entry ) )
{
return false;
}
*variantPtr = entry;
return true;
}
public ImcFile( Utf8GamePath path )
: base( 0 )
{
var file = Dalamud.GameData.GetFile( path.ToString() );
if( file == null )
{
throw new Exception();
}
fixed( byte* ptr = file.Data )
{
NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) );
AllocateData( file.Data.Length + sizeof( ImcEntry ) * 100 * NumParts );
Functions.MemCpyUnchecked( Data, ptr, file.Data.Length );
}
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Runtime.InteropServices;
namespace Penumbra.Meta.Files;
public unsafe class MetaBaseFile : IDisposable
{
public byte* Data { get; private set; }
public int Length { get; private set; }
public int Index { get; }
public MetaBaseFile( int idx )
=> Index = idx;
protected (IntPtr Data, int Length) DefaultData
=> Penumbra.CharacterUtility.DefaultResources[ Index ];
// Reset to default values.
public virtual void Reset()
{}
// Obtain memory.
protected void AllocateData( int length )
{
Length = length;
Data = ( byte* )Marshal.AllocHGlobal( length );
GC.AddMemoryPressure( length );
}
// Free memory.
protected void ReleaseUnmanagedResources()
{
Marshal.FreeHGlobal( ( IntPtr )Data );
GC.RemoveMemoryPressure( Length );
Length = 0;
Data = null;
}
// Manually free memory.
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize( this );
}
~MetaBaseFile()
{
ReleaseUnmanagedResources();
}
}

View file

@ -1,196 +0,0 @@
using System;
using System.Collections.Generic;
using Dalamud.Logging;
using Dalamud.Plugin;
using Lumina.Data;
using Lumina.Data.Files;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Util;
namespace Penumbra.Meta.Files
{
// This class manages the default meta files obtained via lumina from the game files themselves.
// On first call, the default version of any supported file will be cached and can be returned without reparsing.
public class MetaDefaults
{
private readonly Dictionary< GamePath, object > _defaultFiles = new();
private object CreateNewFile( string path )
{
if( path.EndsWith( ".imc" ) )
{
return GetImcFile( path );
}
var rawFile = FetchFile( path );
if( path.EndsWith( ".eqp" ) )
{
return new EqpFile( rawFile );
}
if( path.EndsWith( ".gmp" ) )
{
return new GmpFile( rawFile );
}
if( path.EndsWith( ".eqdp" ) )
{
return new EqdpFile( rawFile );
}
if( path.EndsWith( ".est" ) )
{
return new EstFile( rawFile );
}
if( path.EndsWith( ".cmp" ) )
{
return new CmpFile( rawFile.Data );
}
throw new NotImplementedException();
}
private T? GetDefaultFile< T >( GamePath path, string error = "" ) where T : class
{
try
{
if( _defaultFiles.TryGetValue( path, out var file ) )
{
return ( T )file;
}
var newFile = CreateNewFile( path );
_defaultFiles.Add( path, newFile );
return ( T )_defaultFiles[ path ];
}
catch( Exception e )
{
PluginLog.Error( $"{error}{e}" );
return null;
}
}
private EqdpFile? GetDefaultEqdpFile( EquipSlot slot, GenderRace gr )
=> GetDefaultFile< EqdpFile >( MetaFileNames.Eqdp( slot, gr ),
$"Could not obtain Eqdp file for {slot} {gr}:\n" );
private GmpFile? GetDefaultGmpFile()
=> GetDefaultFile< GmpFile >( MetaFileNames.Gmp(), "Could not obtain Gmp file:\n" );
private EqpFile? GetDefaultEqpFile()
=> GetDefaultFile< EqpFile >( MetaFileNames.Eqp(), "Could not obtain Eqp file:\n" );
private EstFile? GetDefaultEstFile( ObjectType type, EquipSlot equip, BodySlot body )
=> GetDefaultFile< EstFile >( MetaFileNames.Est( type, equip, body ), $"Could not obtain Est file for {type} {equip} {body}:\n" );
private ImcFile? GetDefaultImcFile( ObjectType type, ushort primarySetId, ushort secondarySetId = 0 )
=> GetDefaultFile< ImcFile >( MetaFileNames.Imc( type, primarySetId, secondarySetId ),
$"Could not obtain Imc file for {type}, {primarySetId} {secondarySetId}:\n" );
private CmpFile? GetDefaultCmpFile()
=> GetDefaultFile< CmpFile >( MetaFileNames.Cmp(), "Could not obtain Cmp file:\n" );
public EqdpFile? GetNewEqdpFile( EquipSlot slot, GenderRace gr )
=> GetDefaultEqdpFile( slot, gr )?.Clone();
public GmpFile? GetNewGmpFile()
=> GetDefaultGmpFile()?.Clone();
public EqpFile? GetNewEqpFile()
=> GetDefaultEqpFile()?.Clone();
public EstFile? GetNewEstFile( ObjectType type, EquipSlot equip, BodySlot body )
=> GetDefaultEstFile( type, equip, body )?.Clone();
public ImcFile? GetNewImcFile( ObjectType type, ushort primarySetId, ushort secondarySetId = 0 )
=> GetDefaultImcFile( type, primarySetId, secondarySetId )?.Clone();
public CmpFile? GetNewCmpFile()
=> GetDefaultCmpFile()?.Clone();
private static ImcFile GetImcFile( string path )
=> Dalamud.GameData.GetFile< ImcFile >( path )!;
private static FileResource FetchFile( string name )
=> Dalamud.GameData.GetFile( name )!;
// Check that a given meta manipulation is an actual change to the default value. We don't need to keep changes to default.
public bool CheckAgainstDefault( MetaManipulation m )
{
try
{
return m.Type switch
{
MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId )
?.GetValue( m ).Equal( m.ImcValue )
?? true,
MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId )
== m.GmpValue,
MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId )
.Reduce( m.EqpIdentifier.Slot )
== m.EqpValue,
MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )
?.GetEntry( m.EqdpIdentifier.SetId )
.Reduce( m.EqdpIdentifier.Slot )
== m.EqdpValue,
MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot )
?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId )
== m.EstValue,
MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ]
== m.RspValue,
_ => false,
};
}
catch( Exception e )
{
PluginLog.Error( $"Could not obtain default value for {m.CorrespondingFilename()} - {m.IdentifierString()}:\n{e}" );
}
return false;
}
public object? GetDefaultValue( MetaManipulation m )
{
try
{
return m.Type switch
{
MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId )
?.GetValue( m ),
MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ),
MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId )
.Reduce( m.EqpIdentifier.Slot ),
MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )
?.GetEntry( m.EqdpIdentifier.SetId )
.Reduce( m.EqdpIdentifier.Slot ),
MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot )
?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ),
MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ],
_ => false,
};
}
catch( Exception e )
{
PluginLog.Error( $"Could not obtain default value for {m.CorrespondingFilename()} - {m.IdentifierString()}:\n{e}" );
}
return false;
}
// Create a deep copy of a default file as a new file.
public object? CreateNewFile( MetaManipulation m )
{
return m.Type switch
{
MetaType.Imc => GetNewImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ),
MetaType.Gmp => GetNewGmpFile(),
MetaType.Eqp => GetNewEqpFile(),
MetaType.Eqdp => GetNewEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ),
MetaType.Est => GetNewEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ),
MetaType.Rsp => GetNewCmpFile(),
_ => throw new NotImplementedException(),
};
}
}
}

View file

@ -1,172 +0,0 @@
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
// A struct for each type of meta change that contains all relevant information,
// to uniquely identify the corresponding file and location for the change.
// The first byte is guaranteed to be the MetaType enum for each case.
namespace Penumbra.Meta
{
public enum MetaType : byte
{
Unknown = 0,
Imc = 1,
Eqdp = 2,
Eqp = 3,
Est = 4,
Gmp = 5,
Rsp = 6,
};
[StructLayout( LayoutKind.Explicit )]
public struct EqpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public EquipSlot Slot;
[FieldOffset( 2 )]
public ushort SetId;
public override string ToString()
=> $"Eqp - {SetId} - {Slot}";
}
[StructLayout( LayoutKind.Explicit )]
public struct EqdpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public EquipSlot Slot;
[FieldOffset( 2 )]
public GenderRace GenderRace;
[FieldOffset( 4 )]
public ushort SetId;
public override string ToString()
=> $"Eqdp - {SetId} - {Slot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}";
}
[StructLayout( LayoutKind.Explicit )]
public struct GmpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ushort SetId;
public override string ToString()
=> $"Gmp - {SetId}";
}
[StructLayout( LayoutKind.Explicit )]
public struct EstIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ObjectType ObjectType;
[FieldOffset( 2 )]
public EquipSlot EquipSlot;
[FieldOffset( 3 )]
public BodySlot BodySlot;
[FieldOffset( 4 )]
public GenderRace GenderRace;
[FieldOffset( 6 )]
public ushort PrimaryId;
public override string ToString()
=> ObjectType == ObjectType.Equipment
? $"Est - {PrimaryId} - {EquipSlot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}"
: $"Est - {PrimaryId} - {BodySlot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}";
}
[StructLayout( LayoutKind.Explicit )]
public struct ImcIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public byte _objectAndBody;
public ObjectType ObjectType
{
get => ( ObjectType )( _objectAndBody & 0b00011111 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b11100000 ) | ( byte )value );
}
public BodySlot BodySlot
{
get => ( BodySlot )( _objectAndBody >> 5 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( ( byte )value << 5 ) );
}
[FieldOffset( 2 )]
public ushort PrimaryId;
[FieldOffset( 4 )]
public ushort Variant;
[FieldOffset( 6 )]
public ushort SecondaryId;
[FieldOffset( 6 )]
public EquipSlot EquipSlot;
public override string ToString()
{
return ObjectType switch
{
ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}",
ObjectType.Equipment => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}",
_ => $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}",
};
}
}
[StructLayout( LayoutKind.Explicit )]
public struct RspIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public SubRace SubRace;
[FieldOffset( 2 )]
public RspAttribute Attribute;
public override string ToString()
=> $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}";
}
}

View file

@ -0,0 +1,56 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct EqdpManipulation : IEquatable< EqdpManipulation >
{
public readonly EqdpEntry Entry;
public readonly Gender Gender;
public readonly ModelRace Race;
public readonly ushort SetId;
public readonly EquipSlot Slot;
public EqdpManipulation( EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId )
{
Entry = Eqdp.Mask( slot ) & entry;
Gender = gender;
Race = race;
SetId = setId;
Slot = slot;
}
public override string ToString()
=> $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}";
public bool Equals( EqdpManipulation other )
=> Gender == other.Gender
&& Race == other.Race
&& SetId == other.SetId
&& Slot == other.Slot;
public override bool Equals( object? obj )
=> obj is EqdpManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Slot );
public int FileIndex()
=> CharacterUtility.EqdpIdx( Names.CombinedRace( Gender, Race ), Slot.IsAccessory() );
public bool Apply( ExpandedEqdpFile file )
{
var entry = file[ SetId ];
var mask = Eqdp.Mask( Slot );
if( ( entry & mask ) == Entry )
{
return false;
}
file[ SetId ] = ( entry & ~mask ) | Entry;
return true;
}
}

View file

@ -0,0 +1,50 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct EqpManipulation : IEquatable< EqpManipulation >
{
public readonly EqpEntry Entry;
public readonly ushort SetId;
public readonly EquipSlot Slot;
public EqpManipulation( EqpEntry entry, EquipSlot slot, ushort setId )
{
Slot = slot;
SetId = setId;
Entry = Eqp.Mask( slot ) & entry;
}
public override string ToString()
=> $"Eqp - {SetId} - {Slot}";
public bool Equals( EqpManipulation other )
=> Slot == other.Slot
&& SetId == other.SetId;
public override bool Equals( object? obj )
=> obj is EqpManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( ( int )Slot, SetId );
public int FileIndex()
=> CharacterUtility.EqpIdx;
public bool Apply( ExpandedEqpFile file )
{
var entry = file[ SetId ];
var mask = Eqp.Mask( Slot );
if( ( entry & mask ) == Entry )
{
return false;
}
file[ SetId ] = ( entry & ~mask ) | Entry;
return true;
}
}

View file

@ -0,0 +1,63 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct EstManipulation : IEquatable< EstManipulation >
{
public enum EstType : byte
{
Hair = CharacterUtility.HairEstIdx,
Face = CharacterUtility.FaceEstIdx,
Body = CharacterUtility.BodyEstIdx,
Head = CharacterUtility.HeadEstIdx,
}
public readonly ushort SkeletonIdx;
public readonly Gender Gender;
public readonly ModelRace Race;
public readonly ushort SetId;
public readonly EstType Type;
public EstManipulation( Gender gender, ModelRace race, EstType estType, ushort setId, ushort skeletonIdx )
{
SkeletonIdx = skeletonIdx;
Gender = gender;
Race = race;
SetId = setId;
Type = estType;
}
public override string ToString()
=> $"Est - {SetId} - {Type} - {Race.ToName()} {Gender.ToName()}";
public bool Equals( EstManipulation other )
=> Gender == other.Gender
&& Race == other.Race
&& SetId == other.SetId
&& Type == other.Type;
public override bool Equals( object? obj )
=> obj is EstManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Type );
public int FileIndex()
=> ( int )Type;
public bool Apply( EstFile file )
{
return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId, SkeletonIdx ) switch
{
EstFile.EstEntryChange.Unchanged => false,
EstFile.EstEntryChange.Changed => true,
EstFile.EstEntryChange.Added => true,
EstFile.EstEntryChange.Removed => true,
_ => throw new ArgumentOutOfRangeException(),
};
}
}

View file

@ -0,0 +1,45 @@
using System;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct GmpManipulation : IEquatable< GmpManipulation >
{
public readonly GmpEntry Entry;
public readonly ushort SetId;
public GmpManipulation( GmpEntry entry, ushort setId )
{
Entry = entry;
SetId = setId;
}
public override string ToString()
=> $"Gmp - {SetId}";
public bool Equals( GmpManipulation other )
=> SetId == other.SetId;
public override bool Equals( object? obj )
=> obj is GmpManipulation other && Equals( other );
public override int GetHashCode()
=> SetId.GetHashCode();
public int FileIndex()
=> CharacterUtility.GmpIdx;
public bool Apply( ExpandedGmpFile file )
{
var entry = file[ SetId ];
if( entry == Entry )
{
return false;
}
file[ SetId ] = Entry;
return true;
}
}

View file

@ -0,0 +1,61 @@
using System;
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
using Penumbra.Meta.Files;
using ImcFile = Lumina.Data.Files.ImcFile;
namespace Penumbra.Meta.Manipulations;
[StructLayout( LayoutKind.Sequential )]
public readonly struct ImcManipulation : IEquatable< ImcManipulation >
{
public readonly ImcEntry Entry;
public readonly ushort PrimaryId;
public readonly ushort Variant;
public readonly ushort SecondaryId;
public readonly ObjectType ObjectType;
public readonly EquipSlot EquipSlot;
public readonly BodySlot BodySlot;
public ImcManipulation( EquipSlot equipSlot, ushort variant, ushort primaryId, ImcEntry entry )
{
Entry = entry;
PrimaryId = primaryId;
Variant = variant;
SecondaryId = 0;
ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment;
EquipSlot = equipSlot;
BodySlot = BodySlot.Unknown;
}
public ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant,
ImcEntry entry )
{
Entry = entry;
ObjectType = objectType;
BodySlot = bodySlot;
SecondaryId = secondaryId;
PrimaryId = primaryId;
Variant = variant;
EquipSlot = EquipSlot.Unknown;
}
public override string ToString()
=> ObjectType is ObjectType.Equipment or ObjectType.Accessory
? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}"
: $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}";
public bool Equals( ImcManipulation other )
=> PrimaryId == other.PrimaryId
&& Variant == other.Variant
&& SecondaryId == other.SecondaryId
&& ObjectType == other.ObjectType
&& EquipSlot == other.EquipSlot
&& BodySlot == other.BodySlot;
public override bool Equals( object? obj )
=> obj is ImcManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( PrimaryId, Variant, SecondaryId, ( int )ObjectType, ( int )EquipSlot, ( int )BodySlot );
}

View file

@ -0,0 +1,109 @@
using System;
using System.Runtime.InteropServices;
namespace Penumbra.Meta.Manipulations;
[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )]
public readonly struct MetaManipulation : IEquatable< MetaManipulation >
{
public enum Type : byte
{
Eqp,
Gmp,
Eqdp,
Est,
Rsp,
Imc,
}
[FieldOffset( 0 )]
public readonly EqpManipulation Eqp = default;
[FieldOffset( 0 )]
public readonly GmpManipulation Gmp = default;
[FieldOffset( 0 )]
public readonly EqdpManipulation Eqdp = default;
[FieldOffset( 0 )]
public readonly EstManipulation Est = default;
[FieldOffset( 0 )]
public readonly RspManipulation Rsp = default;
[FieldOffset( 0 )]
public readonly ImcManipulation Imc = default;
[FieldOffset( 15 )]
public readonly Type ManipulationType;
public MetaManipulation( EqpManipulation eqp )
=> ( ManipulationType, Eqp ) = ( Type.Eqp, eqp );
public MetaManipulation( GmpManipulation gmp )
=> ( ManipulationType, Gmp ) = ( Type.Gmp, gmp );
public MetaManipulation( EqdpManipulation eqdp )
=> ( ManipulationType, Eqdp ) = ( Type.Eqdp, eqdp );
public MetaManipulation( EstManipulation est )
=> ( ManipulationType, Est ) = ( Type.Est, est );
public MetaManipulation( RspManipulation rsp )
=> ( ManipulationType, Rsp ) = ( Type.Rsp, rsp );
public MetaManipulation( ImcManipulation imc )
=> ( ManipulationType, Imc ) = ( Type.Imc, imc );
public static implicit operator MetaManipulation( EqpManipulation eqp )
=> new(eqp);
public static implicit operator MetaManipulation( GmpManipulation gmp )
=> new(gmp);
public static implicit operator MetaManipulation( EqdpManipulation eqdp )
=> new(eqdp);
public static implicit operator MetaManipulation( EstManipulation est )
=> new(est);
public static implicit operator MetaManipulation( RspManipulation rsp )
=> new(rsp);
public static implicit operator MetaManipulation( ImcManipulation imc )
=> new(imc);
public bool Equals( MetaManipulation other )
{
if( ManipulationType != other.ManipulationType )
{
return false;
}
return ManipulationType switch
{
Type.Eqp => Eqp.Equals( other.Eqp ),
Type.Gmp => Gmp.Equals( other.Gmp ),
Type.Eqdp => Eqdp.Equals( other.Eqdp ),
Type.Est => Est.Equals( other.Est ),
Type.Rsp => Rsp.Equals( other.Rsp ),
Type.Imc => Imc.Equals( other.Imc ),
_ => throw new ArgumentOutOfRangeException(),
};
}
public override bool Equals( object? obj )
=> obj is MetaManipulation other && Equals( other );
public override int GetHashCode()
=> ManipulationType switch
{
Type.Eqp => Eqp.GetHashCode(),
Type.Gmp => Gmp.GetHashCode(),
Type.Eqdp => Eqdp.GetHashCode(),
Type.Est => Est.GetHashCode(),
Type.Rsp => Rsp.GetHashCode(),
Type.Imc => Imc.GetHashCode(),
_ => throw new ArgumentOutOfRangeException(),
};
}

View file

@ -0,0 +1,48 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct RspManipulation : IEquatable< RspManipulation >
{
public readonly float Entry;
public readonly SubRace SubRace;
public readonly RspAttribute Attribute;
public RspManipulation( SubRace subRace, RspAttribute attribute, float entry )
{
Entry = entry;
SubRace = subRace;
Attribute = attribute;
}
public override string ToString()
=> $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}";
public bool Equals( RspManipulation other )
=> SubRace == other.SubRace
&& Attribute == other.Attribute;
public override bool Equals( object? obj )
=> obj is RspManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( ( int )SubRace, ( int )Attribute );
public int FileIndex()
=> CharacterUtility.HumanCmpIdx;
public bool Apply( CmpFile file )
{
var value = file[ SubRace, Attribute ];
if( value == Entry )
{
return false;
}
file[ SubRace, Attribute ] = Entry;
return true;
}
}

View file

@ -6,9 +6,8 @@ using Dalamud.Logging;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Importer;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mod;
using Penumbra.Util;
namespace Penumbra.Meta;

View file

@ -5,12 +5,273 @@ using System.Linq;
using Dalamud.Logging;
using Lumina.Data.Files;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Util;
namespace Penumbra.Meta;
public struct TemporaryImcFile : IDisposable
{
public void Dispose()
{
}
}
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 List< TemporaryImcFile > ImcFiles = new();
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();
}
public void ResetImc()
{
foreach( var file in ImcFiles )
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.Type 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 bool ApplyMod( ImcManipulation m, Mod.Mod mod )
{
if( !ImcManipulations.TryAdd( m, mod ) )
{
return false;
}
return true;
}
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()
};
}
}
public class MetaManager : IDisposable
{
internal class FileInformation
@ -27,12 +288,7 @@ public class MetaManager : IDisposable
{
ByteData = 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 );
@ -138,50 +394,6 @@ public class MetaManager : IDisposable
{
value.Write( _dir, key );
_resolvedFiles[ key ] = value.CurrentFile!.Value;
if( value.Data is EqpFile )
{
EqpData = value.ByteData;
}
}
}
public bool ApplyMod( MetaManipulation m, Mod.Mod mod )
{
if( _currentManipulations.ContainsKey( m ) )
{
return false;
}
_currentManipulations.Add( m, mod );
var gamePath = Utf8GamePath.FromString(m.CorrespondingFilename(), out var p, false) ? p : Utf8GamePath.Empty; // TODO
try
{
if( !_currentFiles.TryGetValue( gamePath, out var file ) )
{
file = new FileInformation( Penumbra.MetaDefaults.CreateNewFile( m ) ?? throw new IOException() )
{
Changed = true,
CurrentFile = null,
};
_currentFiles[ gamePath ] = file;
}
file.Changed |= m.Type switch
{
MetaType.Eqp => m.Apply( ( EqpFile )file.Data ),
MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ),
MetaType.Gmp => m.Apply( ( GmpFile )file.Data ),
MetaType.Est => m.Apply( ( EstFile )file.Data ),
MetaType.Imc => m.Apply( ( ImcFile )file.Data ),
MetaType.Rsp => m.Apply( ( CmpFile )file.Data ),
_ => throw new NotImplementedException(),
};
return true;
}
catch( Exception e )
{
PluginLog.Error( $"Could not obtain default file for manipulation {m.CorrespondingFilename()}:\n{e}" );
return false;
}
}
}

View file

@ -1,259 +0,0 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Meta.Files;
using Swan;
using ImcFile = Lumina.Data.Files.ImcFile;
namespace Penumbra.Meta
{
// Write a single meta manipulation as a Base64string of the 16 bytes defining it.
public class MetaManipulationConverter : JsonConverter< MetaManipulation >
{
public override void WriteJson( JsonWriter writer, MetaManipulation manip, JsonSerializer serializer )
{
var s = Convert.ToBase64String( manip.ToBytes() );
writer.WriteValue( s );
}
public override MetaManipulation ReadJson( JsonReader reader, Type objectType, MetaManipulation existingValue, bool hasExistingValue,
JsonSerializer serializer )
{
if( reader.TokenType != JsonToken.String )
{
throw new JsonReaderException();
}
var bytes = Convert.FromBase64String( ( string )reader.Value! );
using MemoryStream m = new( bytes );
using BinaryReader br = new( m );
var i = br.ReadUInt64();
var v = br.ReadUInt64();
return new MetaManipulation( i, v );
}
}
// A MetaManipulation is a union of a type of Identifier (first 8 bytes, cf. Identifier.cs)
// and the appropriate Value to change the meta entry to (the other 8 bytes).
// Its comparison for sorting and hashes depends only on the identifier.
// The first byte is guaranteed to be a MetaType enum value in any case, so Type can always be read.
[StructLayout( LayoutKind.Explicit )]
[JsonConverter( typeof( MetaManipulationConverter ) )]
public struct MetaManipulation : IComparable
{
public static MetaManipulation Eqp( EquipSlot equipSlot, ushort setId, EqpEntry value )
=> new()
{
EqpIdentifier = new EqpIdentifier()
{
Type = MetaType.Eqp,
Slot = equipSlot,
SetId = setId,
},
EqpValue = value,
};
public static MetaManipulation Eqdp( EquipSlot equipSlot, GenderRace gr, ushort setId, EqdpEntry value )
=> new()
{
EqdpIdentifier = new EqdpIdentifier()
{
Type = MetaType.Eqdp,
Slot = equipSlot,
GenderRace = gr,
SetId = setId,
},
EqdpValue = value,
};
public static MetaManipulation Gmp( ushort setId, GmpEntry value )
=> new()
{
GmpIdentifier = new GmpIdentifier()
{
Type = MetaType.Gmp,
SetId = setId,
},
GmpValue = value,
};
public static MetaManipulation Est( ObjectType type, EquipSlot equipSlot, GenderRace gr, BodySlot bodySlot, ushort setId,
ushort value )
=> new()
{
EstIdentifier = new EstIdentifier()
{
Type = MetaType.Est,
ObjectType = type,
GenderRace = gr,
EquipSlot = equipSlot,
BodySlot = bodySlot,
PrimaryId = setId,
},
EstValue = value,
};
public static MetaManipulation Imc( ObjectType type, BodySlot secondaryType, ushort primaryId, ushort secondaryId
, ushort idx, ImcFile.ImageChangeData value )
=> new()
{
ImcIdentifier = new ImcIdentifier()
{
Type = MetaType.Imc,
ObjectType = type,
BodySlot = secondaryType,
PrimaryId = primaryId,
SecondaryId = secondaryId,
Variant = idx,
},
ImcValue = value,
};
public static MetaManipulation Imc( EquipSlot slot, ushort primaryId, ushort idx, ImcFile.ImageChangeData value )
=> new()
{
ImcIdentifier = new ImcIdentifier()
{
Type = MetaType.Imc,
ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment,
EquipSlot = slot,
PrimaryId = primaryId,
Variant = idx,
},
ImcValue = value,
};
public static MetaManipulation Rsp( SubRace subRace, RspAttribute attribute, float value )
=> new()
{
RspIdentifier = new RspIdentifier()
{
Type = MetaType.Rsp,
SubRace = subRace,
Attribute = attribute,
},
RspValue = value,
};
internal MetaManipulation( ulong identifier, ulong value )
: this()
{
Identifier = identifier;
Value = value;
}
[FieldOffset( 0 )]
public readonly ulong Identifier;
[FieldOffset( 8 )]
public readonly ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 0 )]
public EqpIdentifier EqpIdentifier;
[FieldOffset( 0 )]
public GmpIdentifier GmpIdentifier;
[FieldOffset( 0 )]
public EqdpIdentifier EqdpIdentifier;
[FieldOffset( 0 )]
public EstIdentifier EstIdentifier;
[FieldOffset( 0 )]
public ImcIdentifier ImcIdentifier;
[FieldOffset( 0 )]
public RspIdentifier RspIdentifier;
[FieldOffset( 8 )]
public EqpEntry EqpValue;
[FieldOffset( 8 )]
public GmpEntry GmpValue;
[FieldOffset( 8 )]
public EqdpEntry EqdpValue;
[FieldOffset( 8 )]
public ushort EstValue;
[FieldOffset( 8 )]
public ImcFile.ImageChangeData ImcValue; // 6 bytes.
[FieldOffset( 8 )]
public float RspValue;
public override int GetHashCode()
=> Identifier.GetHashCode();
public int CompareTo( object? rhs )
=> Identifier.CompareTo( rhs is MetaManipulation m ? m.Identifier : null );
public GamePath CorrespondingFilename()
{
return Type switch
{
MetaType.Eqp => MetaFileNames.Eqp(),
MetaType.Eqdp => MetaFileNames.Eqdp( EqdpIdentifier.Slot, EqdpIdentifier.GenderRace ),
MetaType.Est => MetaFileNames.Est( EstIdentifier.ObjectType, EstIdentifier.EquipSlot, EstIdentifier.BodySlot ),
MetaType.Gmp => MetaFileNames.Gmp(),
MetaType.Imc => MetaFileNames.Imc( ImcIdentifier.ObjectType, ImcIdentifier.PrimaryId, ImcIdentifier.SecondaryId ),
MetaType.Rsp => MetaFileNames.Cmp(),
_ => throw new InvalidEnumArgumentException(),
};
}
// No error checking.
public bool Apply( EqpFile file )
=> file[ EqpIdentifier.SetId ].Apply( this );
public bool Apply( EqdpFile file )
=> file[ EqdpIdentifier.SetId ].Apply( this );
public bool Apply( GmpFile file )
=> file.SetEntry( GmpIdentifier.SetId, GmpValue );
public bool Apply( EstFile file )
=> file.SetEntry( EstIdentifier.GenderRace, EstIdentifier.PrimaryId, EstValue );
public bool Apply( ImcFile file )
{
ref var value = ref file.GetValue( this );
if( ImcValue.Equal( value ) )
{
return false;
}
value = ImcValue;
return true;
}
public bool Apply( CmpFile file )
=> file.Set( RspIdentifier.SubRace, RspIdentifier.Attribute, RspValue );
public string IdentifierString()
{
return Type switch
{
MetaType.Eqp => EqpIdentifier.ToString(),
MetaType.Eqdp => EqdpIdentifier.ToString(),
MetaType.Est => EstIdentifier.ToString(),
MetaType.Gmp => GmpIdentifier.ToString(),
MetaType.Imc => ImcIdentifier.ToString(),
MetaType.Rsp => RspIdentifier.ToString(),
_ => throw new InvalidEnumArgumentException(),
};
}
}
}