From 739627b7c2f24ab6fd93266ba88eca9b58f4075a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 4 Mar 2021 17:50:50 +0100 Subject: [PATCH] Support for parsing TexTools .Meta Files and the corresponding game data. --- Penumbra/Game/EqdpEntry.cs | 127 ++++++++++++ Penumbra/Game/EqpEntry.cs | 150 ++++++++++++++ Penumbra/Game/GamePathParser.cs | 1 - Penumbra/Game/GmpEntry.cs | 104 ++++++++++ Penumbra/Importer/TexToolsMeta.cs | 303 +++++++++++++++++++++++++++++ Penumbra/MetaData/EqdpFile.cs | 210 ++++++++++++++++++++ Penumbra/MetaData/EqpFile.cs | 216 ++++++++++++++++++++ Penumbra/MetaData/EstFile.cs | 157 +++++++++++++++ Penumbra/MetaData/GmpFile.cs | 42 ++++ Penumbra/MetaData/ImcExtensions.cs | 81 ++++++++ Penumbra/MetaData/MetaDefaults.cs | 140 +++++++++++++ Penumbra/MetaData/MetaFilenames.cs | 76 ++++++++ Penumbra/Mods/MetaManipulation.cs | 300 ++++++++++++++++++++++++++++ Penumbra/Plugin.cs | 2 + Penumbra/Util/ArrayExtensions.cs | 7 + 15 files changed, 1915 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Game/EqdpEntry.cs create mode 100644 Penumbra/Game/EqpEntry.cs create mode 100644 Penumbra/Game/GmpEntry.cs create mode 100644 Penumbra/Importer/TexToolsMeta.cs create mode 100644 Penumbra/MetaData/EqdpFile.cs create mode 100644 Penumbra/MetaData/EqpFile.cs create mode 100644 Penumbra/MetaData/EstFile.cs create mode 100644 Penumbra/MetaData/GmpFile.cs create mode 100644 Penumbra/MetaData/ImcExtensions.cs create mode 100644 Penumbra/MetaData/MetaDefaults.cs create mode 100644 Penumbra/MetaData/MetaFilenames.cs create mode 100644 Penumbra/Mods/MetaManipulation.cs diff --git a/Penumbra/Game/EqdpEntry.cs b/Penumbra/Game/EqdpEntry.cs new file mode 100644 index 00000000..18cc4c90 --- /dev/null +++ b/Penumbra/Game/EqdpEntry.cs @@ -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 ); + } +} \ No newline at end of file diff --git a/Penumbra/Game/EqpEntry.cs b/Penumbra/Game/EqpEntry.cs new file mode 100644 index 00000000..f32e992c --- /dev/null +++ b/Penumbra/Game/EqpEntry.cs @@ -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 ); + } +} \ No newline at end of file diff --git a/Penumbra/Game/GamePathParser.cs b/Penumbra/Game/GamePathParser.cs index 321584d5..1935fd4a 100644 --- a/Penumbra/Game/GamePathParser.cs +++ b/Penumbra/Game/GamePathParser.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Dalamud.Plugin; diff --git a/Penumbra/Game/GmpEntry.cs b/Penumbra/Game/GmpEntry.cs new file mode 100644 index 00000000..5a78cd6b --- /dev/null +++ b/Penumbra/Game/GmpEntry.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs new file mode 100644 index 00000000..feda070b --- /dev/null +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -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}" ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/MetaData/EqdpFile.cs b/Penumbra/MetaData/EqdpFile.cs new file mode 100644 index 00000000..2cb37f1a --- /dev/null +++ b/Penumbra/MetaData/EqdpFile.cs @@ -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(); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/MetaData/EqpFile.cs b/Penumbra/MetaData/EqpFile.cs new file mode 100644 index 00000000..53c31cf1 --- /dev/null +++ b/Penumbra/MetaData/EqpFile.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Penumbra/MetaData/EstFile.cs b/Penumbra/MetaData/EstFile.cs new file mode 100644 index 00000000..b06d342c --- /dev/null +++ b/Penumbra/MetaData/EstFile.cs @@ -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 ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/MetaData/GmpFile.cs b/Penumbra/MetaData/GmpFile.cs new file mode 100644 index 00000000..67112940 --- /dev/null +++ b/Penumbra/MetaData/GmpFile.cs @@ -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 ); + } +} \ No newline at end of file diff --git a/Penumbra/MetaData/ImcExtensions.cs b/Penumbra/MetaData/ImcExtensions.cs new file mode 100644 index 00000000..dc8b3825 --- /dev/null +++ b/Penumbra/MetaData/ImcExtensions.cs @@ -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 ]; + } + } +} \ No newline at end of file diff --git a/Penumbra/MetaData/MetaDefaults.cs b/Penumbra/MetaData/MetaDefaults.cs new file mode 100644 index 00000000..e258c455 --- /dev/null +++ b/Penumbra/MetaData/MetaDefaults.cs @@ -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() + }; + } + } +} \ No newline at end of file diff --git a/Penumbra/MetaData/MetaFilenames.cs b/Penumbra/MetaData/MetaFilenames.cs new file mode 100644 index 00000000..0ce4f526 --- /dev/null +++ b/Penumbra/MetaData/MetaFilenames.cs @@ -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() + }; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/MetaManipulation.cs b/Penumbra/Mods/MetaManipulation.cs new file mode 100644 index 00000000..681d85ff --- /dev/null +++ b/Penumbra/Mods/MetaManipulation.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Penumbra/Plugin.cs b/Penumbra/Plugin.cs index ae23a73d..ce3ed811 100644 --- a/Penumbra/Plugin.cs +++ b/Penumbra/Plugin.cs @@ -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 ); diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index 41c04478..f7dd5324 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -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 ];