Complete refactoring of most code, indiscriminate application of .editorconfig and general cleanup.

This commit is contained in:
Ottermandias 2021-06-19 11:53:54 +02:00
parent 5332119a63
commit a19ec226c5
84 changed files with 3168 additions and 1709 deletions

View file

@ -0,0 +1,214 @@
using System;
using System.IO;
using System.Linq;
using Lumina.Data;
using Penumbra.Game;
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]
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;
private EqdpFile( EqdpFile clone )
{
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( clone.Blocks[ i ] != null )
{
Blocks[ i ] = ( EqdpEntry[] )clone.Blocks[ i ]!.Clone();
}
}
}
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 )
{
if( idx < TotalBlockCount && Blocks[ idx ] == null )
{
Blocks[ idx ] = new EqdpEntry[BlockSize];
++ExpandedBlockCount;
return true;
}
return false;
}
private bool CollapseBlock( int idx )
{
if( idx >= TotalBlockCount || Blocks[ idx ] == null )
{
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;
}
}
else
{
var array = Blocks[ block ];
if( array != null )
{
array[ SubIdx( idx ) ] = entry;
if( array.All( e => e == 0 ) )
{
CollapseBlock( block );
}
return true;
}
}
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();
}
}
}
}
}

View file

@ -0,0 +1,216 @@
using System;
using System.IO;
using System.Linq;
using Lumina.Data;
using Penumbra.Game;
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,157 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Lumina.Data;
using Penumbra.Game.Enums;
namespace Penumbra.Meta.Files
{
// EST Structure:
// 1x [NumEntries : UInt32]
// #NumEntries x [SetId : UInt16] [RaceId : UInt16]
// #NumEntries x [SkeletonId : UInt16]
public class EstFile
{
private const ushort EntryDescSize = 4;
private const ushort EntrySize = 2;
private readonly Dictionary< GenderRace, Dictionary< ushort, ushort > > _entries = new();
private uint NumEntries { get; set; }
private EstFile( EstFile clone )
{
NumEntries = clone.NumEntries;
_entries = new Dictionary< GenderRace, Dictionary< ushort, ushort > >( clone._entries.Count );
foreach( var kvp in clone._entries )
{
var dict = kvp.Value.ToDictionary( k => k.Key, k => k.Value );
_entries.Add( kvp.Key, dict );
}
}
public EstFile Clone()
=> new( this );
private bool DeleteEntry( GenderRace gr, 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;
}
private (bool, bool) AddEntry( GenderRace gr, ushort setId, ushort entry )
{
if( !_entries.TryGetValue( gr, out var setDict ) )
{
_entries[ gr ] = new Dictionary< ushort, ushort >();
setDict = _entries[ gr ];
}
if( setDict.TryGetValue( setId, out var oldEntry ) )
{
if( oldEntry == entry )
{
return ( false, false );
}
setDict[ setId ] = entry;
return ( false, true );
}
setDict[ setId ] = entry;
return ( true, true );
}
public bool SetEntry( GenderRace gr, ushort setId, ushort entry )
{
if( entry == 0 )
{
return DeleteEntry( gr, setId );
}
var (addedNew, changed) = AddEntry( gr, setId, entry );
if( !addedNew )
{
return changed;
}
++NumEntries;
return true;
}
public ushort GetEntry( GenderRace gr, ushort setId )
{
if( !_entries.TryGetValue( gr, out var setDict ) )
{
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 );
}
}
}
}

View file

@ -0,0 +1,42 @@
using Lumina.Data;
using Penumbra.Game;
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

@ -0,0 +1,127 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using Lumina.Data.Files;
using Penumbra.Game.Enums;
namespace Penumbra.Meta.Files
{
public class InvalidImcVariantException : ArgumentOutOfRangeException
{
public InvalidImcVariantException()
: base( "Trying to manipulate invalid variant." )
{ }
}
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;
var tmp = imc.GetType().GetField( "_MaterialAnimationIdMask",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
ret |= ( ulong )( byte )tmp!.GetValue( imc ) << 40;
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.MaterialAnimationId );
}
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.RingR => 3,
EquipSlot.Feet => 4,
EquipSlot.RingL => 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,145 @@
using System;
using System.Collections.Generic;
using Dalamud.Plugin;
using Lumina.Data;
using Lumina.Data.Files;
using Penumbra.Game;
using Penumbra.Game.Enums;
using Penumbra.Util;
namespace Penumbra.Meta.Files
{
public class MetaDefaults
{
private readonly DalamudPluginInterface _pi;
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 );
}
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" );
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 MetaDefaults( DalamudPluginInterface pi )
=> _pi = pi;
private ImcFile GetImcFile( string path )
=> _pi.Data.GetFile< ImcFile >( path );
private FileResource FetchFile( string name )
=> _pi.Data.GetFile( name );
public bool CheckAgainstDefault( MetaManipulation m )
{
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,
_ => throw new NotImplementedException(),
};
}
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 ),
_ => throw new NotImplementedException(),
};
}
}
}

View file

@ -0,0 +1,79 @@
using System;
using Penumbra.Game.Enums;
using Penumbra.Util;
namespace Penumbra.Meta.Files
{
public static class MetaFileNames
{
public static GamePath Eqp()
=> GamePath.GenerateUnchecked( "chara/xls/equipmentparameter/equipmentparameter.eqp" );
public static GamePath Gmp()
=> GamePath.GenerateUnchecked( "chara/xls/equipmentparameter/gimmickparameter.gmp" );
public static GamePath Est( ObjectType type, EquipSlot equip, BodySlot slot )
{
return type switch
{
ObjectType.Equipment => equip switch
{
EquipSlot.Body => GamePath.GenerateUnchecked( "chara/xls/charadb/extra_top.est" ),
EquipSlot.Head => GamePath.GenerateUnchecked( "chara/xls/charadb/extra_met.est" ),
_ => throw new NotImplementedException(),
},
ObjectType.Character => slot switch
{
BodySlot.Hair => GamePath.GenerateUnchecked( "chara/xls/charadb/hairskeletontemplate.est" ),
BodySlot.Face => GamePath.GenerateUnchecked( "chara/xls/charadb/faceskeletontemplate.est" ),
_ => throw new NotImplementedException(),
},
_ => throw new NotImplementedException(),
};
}
public static GamePath Imc( ObjectType type, ushort primaryId, ushort secondaryId )
{
return type switch
{
ObjectType.Accessory => GamePath.GenerateUnchecked( $"chara/accessory/a{primaryId:D4}/a{primaryId:D4}.imc" ),
ObjectType.Equipment => GamePath.GenerateUnchecked( $"chara/equipment/e{primaryId:D4}/e{primaryId:D4}.imc" ),
ObjectType.DemiHuman => GamePath.GenerateUnchecked(
$"chara/demihuman/d{primaryId:D4}/obj/equipment/e{secondaryId:D4}/e{secondaryId:D4}.imc" ),
ObjectType.Monster => GamePath.GenerateUnchecked(
$"chara/monster/m{primaryId:D4}/obj/body/b{secondaryId:D4}/b{secondaryId:D4}.imc" ),
ObjectType.Weapon => GamePath.GenerateUnchecked(
$"chara/weapon/w{primaryId:D4}/obj/body/b{secondaryId:D4}/b{secondaryId:D4}.imc" ),
_ => throw new NotImplementedException(),
};
}
public static GamePath Eqdp( ObjectType type, GenderRace gr )
{
return type switch
{
ObjectType.Accessory => GamePath.GenerateUnchecked( $"chara/xls/charadb/accessorydeformerparameter/c{gr.ToRaceCode()}.eqdp" ),
ObjectType.Equipment => GamePath.GenerateUnchecked( $"chara/xls/charadb/equipmentdeformerparameter/c{gr.ToRaceCode()}.eqdp" ),
_ => throw new NotImplementedException(),
};
}
public static GamePath Eqdp( EquipSlot slot, GenderRace gr )
{
return slot switch
{
EquipSlot.Head => Eqdp( ObjectType.Equipment, gr ),
EquipSlot.Body => Eqdp( ObjectType.Equipment, gr ),
EquipSlot.Feet => Eqdp( ObjectType.Equipment, gr ),
EquipSlot.Hands => Eqdp( ObjectType.Equipment, gr ),
EquipSlot.Legs => Eqdp( ObjectType.Equipment, gr ),
EquipSlot.Neck => Eqdp( ObjectType.Accessory, gr ),
EquipSlot.Ears => Eqdp( ObjectType.Accessory, gr ),
EquipSlot.Wrists => Eqdp( ObjectType.Accessory, gr ),
EquipSlot.RingL => Eqdp( ObjectType.Accessory, gr ),
EquipSlot.RingR => Eqdp( ObjectType.Accessory, gr ),
_ => throw new NotImplementedException(),
};
}
}
}

149
Penumbra/Meta/Identifier.cs Normal file
View file

@ -0,0 +1,149 @@
using System.Runtime.InteropServices;
using Penumbra.Game.Enums;
namespace Penumbra.Meta
{
public enum MetaType : byte
{
Unknown = 0,
Imc = 1,
Eqdp = 2,
Eqp = 3,
Est = 4,
Gmp = 5,
};
[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} {GenderRace.Split().Item1}";
}
[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} {GenderRace.Split().Item1}"
: $"Est - {PrimaryId} - {BodySlot} - {GenderRace.Split().Item2} {GenderRace.Split().Item1}";
}
[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}",
};
}
}
}

View file

@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json;
using Penumbra.Importer;
using Penumbra.Meta.Files;
using Penumbra.Mod;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Meta
{
// Corresponds meta manipulations of any kind with the settings for a mod.
// DefaultData contains all manipulations that are active regardless of option groups.
// GroupData contains a mapping of Group -> { Options -> {Manipulations} }.
public class MetaCollection
{
public List< MetaManipulation > DefaultData = new();
public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new();
// Store total number of manipulations for some ease of access.
[JsonProperty]
public int Count { get; private set; } = 0;
// Return an enumeration of all active meta manipulations for a given mod with given settings.
public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta )
{
if( Count == DefaultData.Count )
{
return DefaultData;
}
IEnumerable< MetaManipulation > ret = DefaultData;
foreach( var group in modMeta.Groups )
{
if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) )
{
continue;
}
if( group.Value.SelectionType == SelectType.Single )
{
var settingName = group.Value.Options[ setting ].OptionName;
if( metas.TryGetValue( settingName, out var meta ) )
{
ret = ret.Concat( meta );
}
}
else
{
for( var i = 0; i < group.Value.Options.Count; ++i )
{
var flag = 1 << i;
if( ( setting & flag ) == 0 )
{
continue;
}
var settingName = group.Value.Options[ i ].OptionName;
if( metas.TryGetValue( settingName, out var meta ) )
{
ret = ret.Concat( meta );
}
}
}
}
return ret;
}
// Check that the collection is still basically valid,
// i.e. keep it sorted, and verify that the options stored by name are all still part of the mod,
// and that the contained manipulations are still valid and non-default manipulations.
public bool Validate( ModMeta modMeta )
{
var defaultFiles = Service< MetaDefaults >.Get();
SortLists();
foreach( var group in GroupData )
{
if( !modMeta.Groups.TryGetValue( group.Key, out var options ) )
{
return false;
}
foreach( var option in group.Value )
{
if( options.Options.All( o => o.OptionName != option.Key ) )
{
return false;
}
if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) )
{
return false;
}
}
}
return DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) );
}
// Re-sort all manipulations.
private void SortLists()
{
DefaultData.Sort();
foreach( var list in GroupData.Values.SelectMany( g => g.Values ) )
{
list.Sort();
}
}
// Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default.
// Creates the option group and the option if necessary.
private void AddMeta( string group, string option, TexToolsMeta meta )
{
if( meta.Manipulations.Count == 0 )
{
return;
}
if( group.Length == 0 )
{
DefaultData.AddRange( meta.Manipulations );
}
else if( option.Length == 0 )
{ }
else if( !GroupData.TryGetValue( group, out var options ) )
{
GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, meta.Manipulations.ToList() } } );
}
else if( !options.TryGetValue( option, out var list ) )
{
options.Add( option, meta.Manipulations.ToList() );
}
else
{
list.AddRange( meta.Manipulations );
}
Count += meta.Manipulations.Count;
}
// Update the whole meta collection by reading all TexTools .meta files in a mod directory anew,
// combining them with the given ModMeta.
public void Update( IEnumerable< FileInfo > files, DirectoryInfo basePath, ModMeta modMeta )
{
DefaultData.Clear();
GroupData.Clear();
foreach( var file in files.Where( f => f.Extension == ".meta" ) )
{
var metaData = new TexToolsMeta( File.ReadAllBytes( file.FullName ) );
if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 )
{
continue;
}
var path = new RelPath( file, basePath );
var foundAny = false;
foreach( var group in modMeta.Groups )
{
foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) )
{
foundAny = true;
AddMeta( group.Key, option.OptionName, metaData );
}
}
if( !foundAny )
{
AddMeta( string.Empty, string.Empty, metaData );
}
}
SortLists();
}
public static FileInfo FileName( DirectoryInfo basePath )
=> new( Path.Combine( basePath.FullName, "metadata_manipulations.json" ) );
public void SaveToFile( FileInfo file )
{
try
{
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
File.WriteAllText( file.FullName, text );
}
catch( Exception e )
{
PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" );
}
}
public static MetaCollection? LoadFromFile( FileInfo file )
{
if( !file.Exists )
{
return null;
}
try
{
var text = File.ReadAllText( file.FullName );
var collection = JsonConvert.DeserializeObject< MetaCollection >( text,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
return collection;
}
catch( Exception e )
{
PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" );
return null;
}
}
}
}

View file

@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Lumina.Data.Files;
using Penumbra.Hooks;
using Penumbra.Meta.Files;
using Penumbra.Util;
namespace Penumbra.Meta
{
public class MetaManager : IDisposable
{
private class FileInformation
{
public readonly object Data;
public bool Changed;
public FileInfo? CurrentFile;
public FileInformation( object data )
=> Data = data;
public void Write( DirectoryInfo dir )
{
byte[] data = Data switch
{
EqdpFile eqdp => eqdp.WriteBytes(),
EqpFile eqp => eqp.WriteBytes(),
GmpFile gmp => gmp.WriteBytes(),
EstFile est => est.WriteBytes(),
ImcFile imc => imc.WriteBytes(),
_ => throw new NotImplementedException(),
};
DisposeFile( CurrentFile );
CurrentFile = TempFile.WriteNew( dir, data );
Changed = false;
}
}
public const string TmpDirectory = "penumbrametatmp";
private readonly MetaDefaults _default;
private readonly DirectoryInfo _dir;
private readonly GameResourceManagement _resourceManagement;
private readonly Dictionary< GamePath, FileInfo > _resolvedFiles;
private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new();
private readonly Dictionary< GamePath, FileInformation > _currentFiles = new();
public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations
=> _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) );
public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod )
=> _currentManipulations.TryGetValue( manip, out mod );
private static void DisposeFile( FileInfo? file )
{
if( !( file?.Exists ?? false ) )
{
return;
}
try
{
file.Delete();
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete temporary file \"{file.FullName}\":\n{e}" );
}
}
private void Reset( bool reload )
{
foreach( var file in _currentFiles )
{
_resolvedFiles.Remove( file.Key );
DisposeFile( file.Value.CurrentFile );
}
_currentManipulations.Clear();
_currentFiles.Clear();
ClearDirectory();
if( reload )
{
_resourceManagement.ReloadPlayerResources();
}
}
public void Reset()
=> Reset( true );
public void Dispose()
=> Reset();
~MetaManager()
{
Reset( false );
}
private void ClearDirectory()
{
_dir.Refresh();
if( _dir.Exists )
{
try
{
Directory.Delete( _dir.FullName, true );
}
catch( Exception e )
{
PluginLog.Error( $"Could not clear temporary metafile directory \"{_dir.FullName}\":\n{e}" );
}
}
}
public MetaManager( string name, Dictionary< GamePath, FileInfo > resolvedFiles, DirectoryInfo modDir )
{
_resolvedFiles = resolvedFiles;
_default = Service< MetaDefaults >.Get();
_resourceManagement = Service< GameResourceManagement >.Get();
_dir = new DirectoryInfo( Path.Combine( modDir.FullName, TmpDirectory, name.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols() ) );
ClearDirectory();
}
public void WriteNewFiles()
{
Directory.CreateDirectory( _dir.FullName );
foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) )
{
kvp.Value.Write( _dir );
_resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!;
}
_resourceManagement.ReloadPlayerResources();
}
public bool ApplyMod( MetaManipulation m, Mod.Mod mod )
{
if( _currentManipulations.ContainsKey( m ) )
{
return false;
}
_currentManipulations.Add( m, mod );
var gamePath = m.CorrespondingFilename();
try
{
if( !_currentFiles.TryGetValue( gamePath, out var file ) )
{
file = new FileInformation( _default.CreateNewFile( m ) ?? throw new IOException() )
{
Changed = true,
CurrentFile = null,
};
_currentFiles[ gamePath ] = file;
}
file.Changed |= m.Type switch
{
MetaType.Eqp => m.Apply( ( EqpFile )file.Data ),
MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ),
MetaType.Gmp => m.Apply( ( GmpFile )file.Data ),
MetaType.Est => m.Apply( ( EstFile )file.Data ),
MetaType.Imc => m.Apply( ( ImcFile )file.Data ),
_ => 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

@ -0,0 +1,231 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Penumbra.Game;
using Penumbra.Game.Enums;
using Penumbra.Meta.Files;
using Penumbra.Util;
using Swan;
using ImcFile = Lumina.Data.Files.ImcFile;
namespace Penumbra.Meta
{
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 );
}
}
[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,
};
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( 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.
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 ),
_ => 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 string IdentifierString()
{
return Type switch
{
MetaType.Eqp => $"EQP - {EqpIdentifier}",
MetaType.Eqdp => $"EQDP - {EqdpIdentifier}",
MetaType.Est => $"EST - {EstIdentifier}",
MetaType.Gmp => $"GMP - {GmpIdentifier}",
MetaType.Imc => $"IMC - {ImcIdentifier}",
_ => throw new InvalidEnumArgumentException(),
};
}
}
}