mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 21:24:18 +01:00
Complete refactoring of most code, indiscriminate application of .editorconfig and general cleanup.
This commit is contained in:
parent
5332119a63
commit
a19ec226c5
84 changed files with 3168 additions and 1709 deletions
214
Penumbra/Meta/Files/EqdpFile.cs
Normal file
214
Penumbra/Meta/Files/EqdpFile.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
216
Penumbra/Meta/Files/EqpFile.cs
Normal file
216
Penumbra/Meta/Files/EqpFile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
157
Penumbra/Meta/Files/EstFile.cs
Normal file
157
Penumbra/Meta/Files/EstFile.cs
Normal 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Penumbra/Meta/Files/GmpFile.cs
Normal file
42
Penumbra/Meta/Files/GmpFile.cs
Normal 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 );
|
||||
}
|
||||
}
|
||||
127
Penumbra/Meta/Files/ImcExtensions.cs
Normal file
127
Penumbra/Meta/Files/ImcExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
145
Penumbra/Meta/Files/MetaDefaults.cs
Normal file
145
Penumbra/Meta/Files/MetaDefaults.cs
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Penumbra/Meta/Files/MetaFilenames.cs
Normal file
79
Penumbra/Meta/Files/MetaFilenames.cs
Normal 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
149
Penumbra/Meta/Identifier.cs
Normal 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}",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
221
Penumbra/Meta/MetaCollection.cs
Normal file
221
Penumbra/Meta/MetaCollection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
178
Penumbra/Meta/MetaManager.cs
Normal file
178
Penumbra/Meta/MetaManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
231
Penumbra/Meta/MetaManipulation.cs
Normal file
231
Penumbra/Meta/MetaManipulation.cs
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue