mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Support for parsing TexTools .Meta Files and the corresponding game data.
This commit is contained in:
parent
88ba14e595
commit
739627b7c2
15 changed files with 1915 additions and 1 deletions
127
Penumbra/Game/EqdpEntry.cs
Normal file
127
Penumbra/Game/EqdpEntry.cs
Normal 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
150
Penumbra/Game/EqpEntry.cs
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
|
|
||||||
104
Penumbra/Game/GmpEntry.cs
Normal file
104
Penumbra/Game/GmpEntry.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
303
Penumbra/Importer/TexToolsMeta.cs
Normal file
303
Penumbra/Importer/TexToolsMeta.cs
Normal 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}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
210
Penumbra/MetaData/EqdpFile.cs
Normal file
210
Penumbra/MetaData/EqdpFile.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
216
Penumbra/MetaData/EqpFile.cs
Normal file
216
Penumbra/MetaData/EqpFile.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
Penumbra/MetaData/EstFile.cs
Normal file
157
Penumbra/MetaData/EstFile.cs
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Penumbra/MetaData/GmpFile.cs
Normal file
42
Penumbra/MetaData/GmpFile.cs
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Penumbra/MetaData/ImcExtensions.cs
Normal file
81
Penumbra/MetaData/ImcExtensions.cs
Normal 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 ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
140
Penumbra/MetaData/MetaDefaults.cs
Normal file
140
Penumbra/MetaData/MetaDefaults.cs
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Penumbra/MetaData/MetaFilenames.cs
Normal file
76
Penumbra/MetaData/MetaFilenames.cs
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
300
Penumbra/Mods/MetaManipulation.cs
Normal file
300
Penumbra/Mods/MetaManipulation.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ using EmbedIO.WebApi;
|
||||||
using Penumbra.API;
|
using Penumbra.API;
|
||||||
using Penumbra.Game;
|
using Penumbra.Game;
|
||||||
using Penumbra.Hooks;
|
using Penumbra.Hooks;
|
||||||
|
using Penumbra.MetaData;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.UI;
|
using Penumbra.UI;
|
||||||
|
|
||||||
|
|
@ -44,6 +45,7 @@ namespace Penumbra
|
||||||
|
|
||||||
var gameUtils = Service< GameResourceManagement >.Set( PluginInterface );
|
var gameUtils = Service< GameResourceManagement >.Set( PluginInterface );
|
||||||
var modManager = Service< ModManager >.Set( this );
|
var modManager = Service< ModManager >.Set( this );
|
||||||
|
Service< MetaDefaults >.Set( PluginInterface );
|
||||||
modManager.DiscoverMods( Configuration.CurrentCollection );
|
modManager.DiscoverMods( Configuration.CurrentCollection );
|
||||||
|
|
||||||
ResourceLoader = new ResourceLoader( this );
|
ResourceLoader = new ResourceLoader( this );
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@ namespace Penumbra
|
||||||
{
|
{
|
||||||
public static class ArrayExtensions
|
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 )
|
public static void Swap< T >( this T[] array, int idx1, int idx2 )
|
||||||
{
|
{
|
||||||
var tmp = array[ idx1 ];
|
var tmp = array[ idx1 ];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue