Support for parsing TexTools .Meta Files and the corresponding game data.

This commit is contained in:
Ottermandias 2021-03-04 17:50:50 +01:00
parent 88ba14e595
commit 739627b7c2
15 changed files with 1915 additions and 1 deletions

127
Penumbra/Game/EqdpEntry.cs Normal file
View file

@ -0,0 +1,127 @@
using System;
using System.ComponentModel;
using Penumbra.Mods;
namespace Penumbra.Game
{
[Flags]
public enum EqdpEntry : ushort
{
Invalid = 0,
Head1 = 0b0000000001,
Head2 = 0b0000000010,
HeadMask = 0b0000000011,
Body1 = 0b0000000100,
Body2 = 0b0000001000,
BodyMask = 0b0000001100,
Hands1 = 0b0000010000,
Hands2 = 0b0000100000,
HandsMask = 0b0000110000,
Legs1 = 0b0001000000,
Legs2 = 0b0010000000,
LegsMask = 0b0011000000,
Feet1 = 0b0100000000,
Feet2 = 0b1000000000,
FeetMask = 0b1100000000,
Ears1 = 0b0000000001,
Ears2 = 0b0000000010,
EarsMask = 0b0000000011,
Neck1 = 0b0000000100,
Neck2 = 0b0000001000,
NeckMask = 0b0000001100,
Wrists1 = 0b0000010000,
Wrists2 = 0b0000100000,
WristsMask = 0b0000110000,
RingR1 = 0b0001000000,
RingR2 = 0b0010000000,
RingRMask = 0b0011000000,
RingL1 = 0b0100000000,
RingL2 = 0b1000000000,
RingLMask = 0b1100000000
}
public static class Eqdp
{
public static int Offset( EquipSlot slot )
{
return slot switch
{
EquipSlot.Head => 0,
EquipSlot.Body => 2,
EquipSlot.Hands => 4,
EquipSlot.Legs => 6,
EquipSlot.Feet => 8,
EquipSlot.Ears => 0,
EquipSlot.Neck => 2,
EquipSlot.Wrists => 4,
EquipSlot.RingR => 6,
EquipSlot.RingL => 8,
_ => throw new InvalidEnumArgumentException()
};
}
public static EqdpEntry FromSlotAndBits( EquipSlot slot, bool bit1, bool bit2 )
{
EqdpEntry ret = 0;
var offset = Offset( slot );
if( bit1 )
{
ret |= ( EqdpEntry )( 1 << offset );
}
if( bit2 )
{
ret |= ( EqdpEntry )( 1 << ( offset + 1 ) );
}
return ret;
}
public static EqdpEntry Mask( EquipSlot slot )
{
return slot switch
{
EquipSlot.Head => EqdpEntry.HeadMask,
EquipSlot.Body => EqdpEntry.BodyMask,
EquipSlot.Hands => EqdpEntry.HandsMask,
EquipSlot.Legs => EqdpEntry.LegsMask,
EquipSlot.Feet => EqdpEntry.FeetMask,
EquipSlot.Ears => EqdpEntry.EarsMask,
EquipSlot.Neck => EqdpEntry.NeckMask,
EquipSlot.Wrists => EqdpEntry.WristsMask,
EquipSlot.RingR => EqdpEntry.RingRMask,
EquipSlot.RingL => EqdpEntry.RingLMask,
_ => 0
};
}
}
public static class EqdpEntryExtension
{
public static bool Apply( this ref EqdpEntry entry, MetaManipulation manipulation )
{
if( manipulation.Type != MetaType.Eqdp )
{
return false;
}
var mask = Eqdp.Mask( manipulation.EqdpIdentifier.Slot );
var result = ( entry & ~mask ) | manipulation.EqdpValue;
var ret = result == entry;
entry = result;
return ret;
}
public static EqdpEntry Reduce( this EqdpEntry entry, EquipSlot slot )
=> entry & Eqdp.Mask( slot );
}
}

150
Penumbra/Game/EqpEntry.cs Normal file
View file

@ -0,0 +1,150 @@
using System;
using System.ComponentModel;
using Penumbra.Mods;
namespace Penumbra.Game
{
[Flags]
public enum EqpEntry : ulong
{
BodyEnabled = 0x00_01ul,
BodyHideWaist = 0x00_02ul,
_2 = 0x00_04ul,
BodyHideGlovesS = 0x00_08ul,
_4 = 0x00_10ul,
BodyHideGlovesM = 0x00_20ul,
BodyHideGlovesL = 0x00_40ul,
BodyHideGorget = 0x00_80ul,
BodyShowLeg = 0x01_00ul,
BodyShowHand = 0x02_00ul,
BodyShowHead = 0x04_00ul,
BodyShowNecklace = 0x08_00ul,
BodyShowBracelet = 0x10_00ul,
BodyShowTail = 0x20_00ul,
_14 = 0x40_00ul,
_15 = 0x80_00ul,
BodyMask = 0xFF_FFul,
LegsEnabled = 0x01ul << 16,
LegsHideKneePads = 0x02ul << 16,
LegsHideBootsS = 0x04ul << 16,
LegsHideBootsM = 0x08ul << 16,
_20 = 0x10ul << 16,
LegsShowFoot = 0x20ul << 16,
_22 = 0x40ul << 16,
_23 = 0x80ul << 16,
LegsMask = 0xFFul << 16,
HandsEnabled = 0x01ul << 24,
HandsHideElbow = 0x02ul << 24,
HandsHideForearm = 0x04ul << 24,
_27 = 0x08ul << 24,
HandShowBracelet = 0x10ul << 24,
HandShowRingL = 0x20ul << 24,
HandShowRingR = 0x40ul << 24,
_31 = 0x80ul << 24,
HandsMask = 0xFFul << 24,
FeetEnabled = 0x01ul << 32,
FeetHideKnee = 0x02ul << 32,
FeetHideCalf = 0x04ul << 32,
FeetHideAnkle = 0x08ul << 32,
_36 = 0x10ul << 32,
_37 = 0x20ul << 32,
_38 = 0x40ul << 32,
_39 = 0x80ul << 32,
FeetMask = 0xFFul << 32,
HeadEnabled = 0x00_00_01ul << 40,
HeadHideScalp = 0x00_00_02ul << 40,
HeadHideHair = 0x00_00_04ul << 40,
HeadShowHairOverride = 0x00_00_08ul << 40,
HeadHideNeck = 0x00_00_10ul << 40,
HeadShowNecklace = 0x00_00_20ul << 40,
_46 = 0x00_00_40ul << 40,
HeadShowEarrings = 0x00_00_80ul << 40,
HeadShowEarringsHuman = 0x00_01_00ul << 40,
HeadShowEarringsAura = 0x00_02_00ul << 40,
HeadShowEarHuman = 0x00_04_00ul << 40,
HeadShowEarMiqote = 0x00_08_00ul << 40,
HeadShowEarAuRa = 0x00_10_00ul << 40,
HeadShowEarViera = 0x00_20_00ul << 40,
_54 = 0x00_40_00ul << 40,
_55 = 0x00_80_00ul << 40,
HeadShowHrothgarHat = 0x01_00_00ul << 40,
HeadShowVieraHat = 0x02_00_00ul << 40,
_58 = 0x04_00_00ul << 40,
_59 = 0x08_00_00ul << 40,
_60 = 0x10_00_00ul << 40,
_61 = 0x20_00_00ul << 40,
_62 = 0x40_00_00ul << 40,
_63 = 0x80_00_00ul << 40,
HeadMask = 0xFF_FF_FFul << 40
}
public static class Eqp
{
public static (int, int) BytesAndOffset( EquipSlot slot )
{
return slot switch
{
EquipSlot.Body => ( 2, 0 ),
EquipSlot.Legs => ( 1, 2 ),
EquipSlot.Hands => ( 1, 3 ),
EquipSlot.Feet => ( 1, 4 ),
EquipSlot.Head => ( 3, 5 ),
_ => throw new InvalidEnumArgumentException()
};
}
public static EqpEntry FromSlotAndBytes( EquipSlot slot, byte[] value )
{
EqpEntry ret = 0;
var (bytes, offset) = BytesAndOffset( slot );
if( bytes != value.Length )
{
throw new ArgumentException();
}
for( var i = 0; i < bytes; ++i )
{
ret |= ( EqpEntry )( ( ulong )value[ i ] << ( ( offset + i ) * 8 ) );
}
return ret;
}
public static EqpEntry Mask( EquipSlot slot )
{
return slot switch
{
EquipSlot.Body => EqpEntry.BodyMask,
EquipSlot.Head => EqpEntry.HeadMask,
EquipSlot.Legs => EqpEntry.LegsMask,
EquipSlot.Feet => EqpEntry.FeetMask,
EquipSlot.Hands => EqpEntry.HandsMask,
_ => 0
};
}
}
public static class EqpEntryExtension
{
public static bool Apply( this ref EqpEntry entry, MetaManipulation manipulation )
{
if( manipulation.Type != MetaType.Eqp )
{
return false;
}
var mask = Eqp.Mask( manipulation.EqpIdentifier.Slot );
var result = ( entry & ~mask ) | manipulation.EqpValue;
var ret = result != entry;
entry = result;
return ret;
}
public static EqpEntry Reduce( this EqpEntry entry, EquipSlot slot )
=> entry & Eqp.Mask( slot );
}
}

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Plugin;

104
Penumbra/Game/GmpEntry.cs Normal file
View file

@ -0,0 +1,104 @@
using System.IO;
using Penumbra.Mods;
namespace Penumbra.Game
{
public struct GmpEntry
{
public bool Enabled
{
get => ( Value & 1 ) == 1;
set
{
if( value )
{
Value |= 1ul;
}
else
{
Value &= ~1ul;
}
}
}
public bool Animated
{
get => ( Value & 2 ) == 2;
set
{
if( value )
{
Value |= 2ul;
}
else
{
Value &= ~2ul;
}
}
}
public ushort RotationA
{
get => ( ushort )( ( Value >> 2 ) & 0x3FF );
set => Value = ( Value & ~0xFFCul ) | ( ( value & 0x3FFul ) << 2 );
}
public ushort RotationB
{
get => ( ushort )( ( Value >> 12 ) & 0x3FF );
set => Value = ( Value & ~0x3FF000ul ) | ( ( value & 0x3FFul ) << 12 );
}
public ushort RotationC
{
get => ( ushort )( ( Value >> 22 ) & 0x3FF );
set => Value = ( Value & ~0xFFC00000ul ) | ( ( value & 0x3FFul ) << 22 );
}
public byte UnknownA
{
get => ( byte )( ( Value >> 32 ) & 0x0F );
set => Value = ( Value & ~0x0F00000000ul ) | ( ( value & 0x0Ful ) << 32 );
}
public byte UnknownB
{
get => ( byte )( ( Value >> 36 ) & 0x0F );
set => Value = ( Value & ~0xF000000000ul ) | ( ( value & 0x0Ful ) << 36 );
}
public byte UnknownTotal
{
get => ( byte )( ( Value >> 32 ) & 0xFF );
set => Value = ( Value & ~0xFF00000000ul ) | ( ( value & 0xFFul ) << 32 );
}
public ulong Value { get; set; }
public static GmpEntry FromTexToolsMeta( byte[] data )
{
GmpEntry ret = new();
using var reader = new BinaryReader( new MemoryStream( data ) );
ret.Value = reader.ReadUInt32();
ret.UnknownTotal = data[ 4 ];
return ret;
}
public static implicit operator ulong( GmpEntry entry )
=> entry.Value;
public static explicit operator GmpEntry( ulong entry )
=> new() { Value = entry };
public GmpEntry Apply( MetaManipulation manipulation )
{
if( manipulation.Type != MetaType.Gmp )
{
return this;
}
Value = manipulation.GmpValue.Value;
return this;
}
}
}

View file

@ -0,0 +1,303 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Dalamud.Plugin;
using Lumina.Data.Files;
using Penumbra.Game;
using Penumbra.MetaData;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Importer
{
public class TexToolsMeta
{
public class Info
{
private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex
private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex
private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex
private const string Pir = @"\k'PrimaryId'"; // language=regex
private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex
private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex
private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex
private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex
private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex
private const string Ext = @"\.meta";
private static readonly Regex HousingMeta = new( $"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}" );
private static readonly Regex CharaMeta = new( $"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}" );
public readonly ObjectType PrimaryType;
public readonly BodySlot SecondaryType;
public readonly ushort PrimaryId;
public readonly ushort SecondaryId;
public readonly EquipSlot EquipSlot = EquipSlot.Unknown;
public readonly CustomizationType CustomizationType = CustomizationType.Unknown;
private static bool ValidType( ObjectType type )
{
return type switch
{
ObjectType.Accessory => true,
ObjectType.Character => true,
ObjectType.Equipment => true,
ObjectType.DemiHuman => true,
ObjectType.Housing => true,
ObjectType.Monster => true,
ObjectType.Icon => false,
ObjectType.Font => false,
ObjectType.Interface => false,
ObjectType.LoadingScreen => false,
ObjectType.Map => false,
ObjectType.Vfx => false,
ObjectType.Unknown => false,
ObjectType.Weapon => false,
ObjectType.World => false,
_ => false
};
}
public Info( string fileName )
: this( new GamePath( fileName ) )
{ }
public Info( GamePath fileName )
{
PrimaryType = GamePathParser.PathToObjectType( fileName );
PrimaryId = 0;
SecondaryType = BodySlot.Unknown;
SecondaryId = 0;
if( !ValidType( PrimaryType ) )
{
PrimaryType = ObjectType.Unknown;
return;
}
if( PrimaryType == ObjectType.Housing )
{
var housingMatch = HousingMeta.Match( fileName );
if( housingMatch.Success )
{
PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value );
}
return;
}
var match = CharaMeta.Match( fileName );
if( !match.Success )
{
return;
}
PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value );
if( !match.Groups[ "Slot" ].Success )
{
return;
}
switch( PrimaryType )
{
case ObjectType.Equipment:
case ObjectType.Accessory:
if( GameData.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) )
{
EquipSlot = tmpSlot;
}
break;
case ObjectType.Character:
if( GameData.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) )
{
CustomizationType = tmpCustom;
}
break;
}
if( match.Groups[ "SecondaryType" ].Success
&& GameData.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) )
{
SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value );
}
}
}
public readonly uint Version;
public readonly string FilePath;
public readonly List< MetaManipulation > Manipulations = new();
private static string ReadNullTerminated( BinaryReader reader )
{
var builder = new System.Text.StringBuilder();
for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() )
{
builder.Append( c );
}
return builder.ToString();
}
private void AddIfNotDefault( MetaManipulation manipulation )
{
if( !Service< MetaDefaults >.Get().CheckAgainstDefault( manipulation ) )
{
Service< MetaDefaults >.Get().CheckAgainstDefault( manipulation );
Manipulations.Add( manipulation );
}
}
private void DeserializeEqpEntry( Info info, byte[]? data )
{
if( data == null || !info.EquipSlot.IsEquipment() )
{
return;
}
try
{
var value = Eqp.FromSlotAndBytes( info.EquipSlot, data );
AddIfNotDefault( MetaManipulation.Eqp( info.EquipSlot, info.PrimaryId, value ) );
}
catch( ArgumentException )
{ }
}
private void DeserializeEqdpEntries( Info info, byte[]? data )
{
if( data == null )
{
return;
}
var num = data.Length / 5;
using var reader = new BinaryReader( new MemoryStream( data ) );
for( var i = 0; i < num; ++i )
{
var gr = ( GenderRace )reader.ReadUInt32();
var byteValue = reader.ReadByte();
if( !gr.IsValid() || !info.EquipSlot.IsEquipment() && !info.EquipSlot.IsAccessory() )
{
continue;
}
var value = Eqdp.FromSlotAndBits( info.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 );
AddIfNotDefault( MetaManipulation.Eqdp( info.EquipSlot, gr, info.PrimaryId, value ) );
}
}
private void DeserializeGmpEntry( Info info, byte[]? data )
{
if( data == null )
{
return;
}
using var reader = new BinaryReader( new MemoryStream( data ) );
var value = ( GmpEntry )reader.ReadUInt32();
value.UnknownTotal = reader.ReadByte();
AddIfNotDefault( MetaManipulation.Gmp( info.PrimaryId, value ) );
}
private void DeserializeEstEntries( Info info, byte[]? data )
{
if( data == null )
{
return;
}
var num = data.Length / 6;
using var reader = new BinaryReader( new MemoryStream( data ) );
for( var i = 0; i < num; ++i )
{
var gr = ( GenderRace )reader.ReadUInt16();
var id = reader.ReadUInt16();
var value = reader.ReadUInt16();
if( !gr.IsValid()
|| info.PrimaryType == ObjectType.Character && info.SecondaryType != BodySlot.Face && info.SecondaryType != BodySlot.Hair
|| info.PrimaryType == ObjectType.Equipment && info.EquipSlot != EquipSlot.Head && info.EquipSlot != EquipSlot.Body )
{
continue;
}
AddIfNotDefault( MetaManipulation.Est( info.PrimaryType, info.EquipSlot, gr, info.SecondaryType, id, value ) );
}
}
private void DeserializeImcEntries( Info info, byte[]? data )
{
if( data == null )
{
return;
}
var num = data.Length / 6;
using var reader = new BinaryReader( new MemoryStream( data ) );
for( var i = 0; i < num; ++i )
{
var value = ImcFile.ImageChangeData.Read( reader );
if( info.PrimaryType == ObjectType.Equipment || info.PrimaryType == ObjectType.Accessory )
{
AddIfNotDefault( MetaManipulation.Imc( info.EquipSlot, info.PrimaryId, ( ushort )i, value ) );
}
else
{
AddIfNotDefault( MetaManipulation.Imc( info.PrimaryType, info.SecondaryType, info.PrimaryId
, info.SecondaryId, ( ushort )i, value ) );
}
}
}
public TexToolsMeta( byte[] data )
{
try
{
using var reader = new BinaryReader( new MemoryStream( data ) );
Version = reader.ReadUInt32();
FilePath = ReadNullTerminated( reader );
var metaInfo = new Info( FilePath );
var numHeaders = reader.ReadUInt32();
var headerSize = reader.ReadUInt32();
var headerStart = reader.ReadUInt32();
reader.BaseStream.Seek( headerStart, SeekOrigin.Begin );
List< (MetaType type, uint offset, int size) > entries = new();
for( var i = 0; i < numHeaders; ++i )
{
var currentOffset = reader.BaseStream.Position;
var type = ( MetaType )reader.ReadUInt32();
var offset = reader.ReadUInt32();
var size = reader.ReadInt32();
entries.Add( ( type, offset, size ) );
reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin );
}
byte[]? ReadEntry( MetaType type )
{
var idx = entries.FindIndex( t => t.type == type );
if( idx < 0 )
{
return null;
}
reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin );
return reader.ReadBytes( entries[ idx ].size );
}
DeserializeEqpEntry( metaInfo, ReadEntry( MetaType.Eqp ) );
DeserializeGmpEntry( metaInfo, ReadEntry( MetaType.Gmp ) );
DeserializeEqdpEntries( metaInfo, ReadEntry( MetaType.Eqdp ) );
DeserializeEstEntries( metaInfo, ReadEntry( MetaType.Est ) );
DeserializeImcEntries( metaInfo, ReadEntry( MetaType.Imc ) );
}
catch( Exception e )
{
FilePath = "";
PluginLog.Error( $"Error while parsing .meta file:\n{e}" );
}
}
}
}

View file

@ -0,0 +1,210 @@
using System;
using System.IO;
using System.Linq;
using Lumina.Data;
using Penumbra.Game;
namespace Penumbra.MetaData
{
// 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.MetaData
{
// 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 ? 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 ? 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;
namespace Penumbra.MetaData
{
// 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 ) ? 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.MetaData
{
// 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,81 @@
using System.ComponentModel;
using System.IO;
using Lumina.Data.Files;
using Penumbra.Game;
using Penumbra.Mods;
namespace Penumbra.MetaData
{
public static class ImcExtensions
{
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;
}
return ref parts[ idx ].Variants[ imc.Variant - 1 ];
}
}
}

View file

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using Dalamud.Plugin;
using Lumina.Data;
using Lumina.Data.Files;
using Penumbra.Game;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.MetaData
{
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 ); // todo ?.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 ) ?? false,
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,76 @@
using System;
using Penumbra.Game;
using Penumbra.Util;
namespace Penumbra.MetaData
{
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()
};
}
}
}

View file

@ -0,0 +1,300 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using Penumbra.Game;
using Penumbra.MetaData;
using Penumbra.Util;
using ImcFile = Lumina.Data.Files.ImcFile;
namespace Penumbra.Mods
{
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;
}
[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;
}
[StructLayout( LayoutKind.Explicit )]
public struct GmpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ushort 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;
}
[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 & 0b11100000 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( byte )value );
}
[FieldOffset( 2 )]
public ushort PrimaryId;
[FieldOffset( 4 )]
public ushort Variant;
[FieldOffset( 6 )]
public ushort SecondaryId;
[FieldOffset( 6 )]
public EquipSlot EquipSlot;
}
[StructLayout( LayoutKind.Explicit )]
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
};
[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 );
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;
}
}
}

View file

@ -6,6 +6,7 @@ using EmbedIO.WebApi;
using Penumbra.API;
using Penumbra.Game;
using Penumbra.Hooks;
using Penumbra.MetaData;
using Penumbra.Mods;
using Penumbra.UI;
@ -44,6 +45,7 @@ namespace Penumbra
var gameUtils = Service< GameResourceManagement >.Set( PluginInterface );
var modManager = Service< ModManager >.Set( this );
Service< MetaDefaults >.Set( PluginInterface );
modManager.DiscoverMods( Configuration.CurrentCollection );
ResourceLoader = new ResourceLoader( this );

View file

@ -5,6 +5,13 @@ namespace Penumbra
{
public static class ArrayExtensions
{
public static T[] Slice< T >( this T[] source, int index, int length )
{
var slice = new T[length];
Array.Copy( source, index * length, slice, 0, length );
return slice;
}
public static void Swap< T >( this T[] array, int idx1, int idx2 )
{
var tmp = array[ idx1 ];