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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
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.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 );
|
||||
|
|
|
|||
|
|
@ -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 ];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue