This commit is contained in:
Ottermandias 2022-03-11 14:09:45 +01:00
parent f5fccb0235
commit 46581780e0
37 changed files with 2343 additions and 2444 deletions

View file

@ -136,6 +136,7 @@ public sealed unsafe partial class Utf8String : IDisposable
if( isOwned )
{
GC.AddMemoryPressure( length + 1 );
_length |= OwnedFlag;
}

View file

@ -4,305 +4,307 @@ using System.Linq;
using System.ComponentModel;
using Penumbra.GameData.Enums;
namespace Penumbra.GameData.Structs
namespace Penumbra.GameData.Structs;
[Flags]
public enum EqpEntry : ulong
{
[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,
DisableBreastPhysics = 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,
LegsShowTail = 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
{
// cf. Client::Graphics::Scene::CharacterUtility.GetSlotEqpFlags
public const EqpEntry DefaultEntry = ( EqpEntry )0x3fe00070603f00;
public static (int, int) BytesAndOffset( EquipSlot slot )
{
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,
DisableBreastPhysics = 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,
LegsShowTail = 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
{
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 EquipSlot ToEquipSlot( this EqpEntry entry )
{
return entry switch
{
EqpEntry.BodyEnabled => EquipSlot.Body,
EqpEntry.BodyHideWaist => EquipSlot.Body,
EqpEntry._2 => EquipSlot.Body,
EqpEntry.BodyHideGlovesS => EquipSlot.Body,
EqpEntry._4 => EquipSlot.Body,
EqpEntry.BodyHideGlovesM => EquipSlot.Body,
EqpEntry.BodyHideGlovesL => EquipSlot.Body,
EqpEntry.BodyHideGorget => EquipSlot.Body,
EqpEntry.BodyShowLeg => EquipSlot.Body,
EqpEntry.BodyShowHand => EquipSlot.Body,
EqpEntry.BodyShowHead => EquipSlot.Body,
EqpEntry.BodyShowNecklace => EquipSlot.Body,
EqpEntry.BodyShowBracelet => EquipSlot.Body,
EqpEntry.BodyShowTail => EquipSlot.Body,
EqpEntry.DisableBreastPhysics => EquipSlot.Body,
EqpEntry._15 => EquipSlot.Body,
EqpEntry.LegsEnabled => EquipSlot.Legs,
EqpEntry.LegsHideKneePads => EquipSlot.Legs,
EqpEntry.LegsHideBootsS => EquipSlot.Legs,
EqpEntry.LegsHideBootsM => EquipSlot.Legs,
EqpEntry._20 => EquipSlot.Legs,
EqpEntry.LegsShowFoot => EquipSlot.Legs,
EqpEntry.LegsShowTail => EquipSlot.Legs,
EqpEntry._23 => EquipSlot.Legs,
EqpEntry.HandsEnabled => EquipSlot.Hands,
EqpEntry.HandsHideElbow => EquipSlot.Hands,
EqpEntry.HandsHideForearm => EquipSlot.Hands,
EqpEntry._27 => EquipSlot.Hands,
EqpEntry.HandShowBracelet => EquipSlot.Hands,
EqpEntry.HandShowRingL => EquipSlot.Hands,
EqpEntry.HandShowRingR => EquipSlot.Hands,
EqpEntry._31 => EquipSlot.Hands,
EqpEntry.FeetEnabled => EquipSlot.Feet,
EqpEntry.FeetHideKnee => EquipSlot.Feet,
EqpEntry.FeetHideCalf => EquipSlot.Feet,
EqpEntry.FeetHideAnkle => EquipSlot.Feet,
EqpEntry._36 => EquipSlot.Feet,
EqpEntry._37 => EquipSlot.Feet,
EqpEntry._38 => EquipSlot.Feet,
EqpEntry._39 => EquipSlot.Feet,
EqpEntry.HeadEnabled => EquipSlot.Head,
EqpEntry.HeadHideScalp => EquipSlot.Head,
EqpEntry.HeadHideHair => EquipSlot.Head,
EqpEntry.HeadShowHairOverride => EquipSlot.Head,
EqpEntry.HeadHideNeck => EquipSlot.Head,
EqpEntry.HeadShowNecklace => EquipSlot.Head,
EqpEntry._46 => EquipSlot.Head,
EqpEntry.HeadShowEarrings => EquipSlot.Head,
EqpEntry.HeadShowEarringsHuman => EquipSlot.Head,
EqpEntry.HeadShowEarringsAura => EquipSlot.Head,
EqpEntry.HeadShowEarHuman => EquipSlot.Head,
EqpEntry.HeadShowEarMiqote => EquipSlot.Head,
EqpEntry.HeadShowEarAuRa => EquipSlot.Head,
EqpEntry.HeadShowEarViera => EquipSlot.Head,
EqpEntry._54 => EquipSlot.Head,
EqpEntry._55 => EquipSlot.Head,
EqpEntry.HeadShowHrothgarHat => EquipSlot.Head,
EqpEntry.HeadShowVieraHat => EquipSlot.Head,
EqpEntry._58 => EquipSlot.Head,
EqpEntry._59 => EquipSlot.Head,
EqpEntry._60 => EquipSlot.Head,
EqpEntry._61 => EquipSlot.Head,
EqpEntry._62 => EquipSlot.Head,
EqpEntry._63 => EquipSlot.Head,
_ => EquipSlot.Unknown,
};
}
public static string ToLocalName( this EqpEntry entry )
{
return entry switch
{
EqpEntry.BodyEnabled => "Enabled",
EqpEntry.BodyHideWaist => "Hide Waist",
EqpEntry._2 => "Unknown 2",
EqpEntry.BodyHideGlovesS => "Hide Small Gloves",
EqpEntry._4 => "Unknown 4",
EqpEntry.BodyHideGlovesM => "Hide Medium Gloves",
EqpEntry.BodyHideGlovesL => "Hide Large Gloves",
EqpEntry.BodyHideGorget => "Hide Gorget",
EqpEntry.BodyShowLeg => "Show Legs",
EqpEntry.BodyShowHand => "Show Hands",
EqpEntry.BodyShowHead => "Show Head",
EqpEntry.BodyShowNecklace => "Show Necklace",
EqpEntry.BodyShowBracelet => "Show Bracelet",
EqpEntry.BodyShowTail => "Show Tail",
EqpEntry.DisableBreastPhysics => "Disable Breast Physics",
EqpEntry._15 => "Unknown 15",
EqpEntry.LegsEnabled => "Enabled",
EqpEntry.LegsHideKneePads => "Hide Knee Pads",
EqpEntry.LegsHideBootsS => "Hide Small Boots",
EqpEntry.LegsHideBootsM => "Hide Medium Boots",
EqpEntry._20 => "Unknown 20",
EqpEntry.LegsShowFoot => "Show Foot",
EqpEntry.LegsShowTail => "Show Tail",
EqpEntry._23 => "Unknown 23",
EqpEntry.HandsEnabled => "Enabled",
EqpEntry.HandsHideElbow => "Hide Elbow",
EqpEntry.HandsHideForearm => "Hide Forearm",
EqpEntry._27 => "Unknown 27",
EqpEntry.HandShowBracelet => "Show Bracelet",
EqpEntry.HandShowRingL => "Show Left Ring",
EqpEntry.HandShowRingR => "Show Right Ring",
EqpEntry._31 => "Unknown 31",
EqpEntry.FeetEnabled => "Enabled",
EqpEntry.FeetHideKnee => "Hide Knees",
EqpEntry.FeetHideCalf => "Hide Calves",
EqpEntry.FeetHideAnkle => "Hide Ankles",
EqpEntry._36 => "Unknown 36",
EqpEntry._37 => "Unknown 37",
EqpEntry._38 => "Unknown 38",
EqpEntry._39 => "Unknown 39",
EqpEntry.HeadEnabled => "Enabled",
EqpEntry.HeadHideScalp => "Hide Scalp",
EqpEntry.HeadHideHair => "Hide Hair",
EqpEntry.HeadShowHairOverride => "Show Hair Override",
EqpEntry.HeadHideNeck => "Hide Neck",
EqpEntry.HeadShowNecklace => "Show Necklace",
EqpEntry._46 => "Unknown 46",
EqpEntry.HeadShowEarrings => "Show Earrings",
EqpEntry.HeadShowEarringsHuman => "Show Earrings (Human)",
EqpEntry.HeadShowEarringsAura => "Show Earrings (Au Ra)",
EqpEntry.HeadShowEarHuman => "Show Ears (Human)",
EqpEntry.HeadShowEarMiqote => "Show Ears (Miqo'te)",
EqpEntry.HeadShowEarAuRa => "Show Ears (Au Ra)",
EqpEntry.HeadShowEarViera => "Show Ears (Viera)",
EqpEntry._54 => "Unknown 54",
EqpEntry._55 => "Unknown 55",
EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar",
EqpEntry.HeadShowVieraHat => "Show on Viera",
EqpEntry._58 => "Unknown 58",
EqpEntry._59 => "Unknown 59",
EqpEntry._60 => "Unknown 60",
EqpEntry._61 => "Unknown 61",
EqpEntry._62 => "Unknown 62",
EqpEntry._63 => "Unknown 63",
_ => throw new InvalidEnumArgumentException(),
};
}
private static EqpEntry[] GetEntriesForSlot( EquipSlot slot )
{
return ( ( EqpEntry[] )Enum.GetValues( typeof( EqpEntry ) ) )
.Where( e => e.ToEquipSlot() == slot )
.ToArray();
}
public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot( EquipSlot.Body );
public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot( EquipSlot.Legs );
public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot( EquipSlot.Hands );
public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot( EquipSlot.Feet );
public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot( EquipSlot.Head );
public static IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >()
{
[ EquipSlot.Body ] = EqpAttributesBody,
[ EquipSlot.Legs ] = EqpAttributesLegs,
[ EquipSlot.Hands ] = EqpAttributesHands,
[ EquipSlot.Feet ] = EqpAttributesFeet,
[ EquipSlot.Head ] = EqpAttributesHead,
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 EquipSlot ToEquipSlot( this EqpEntry entry )
{
return entry switch
{
EqpEntry.BodyEnabled => EquipSlot.Body,
EqpEntry.BodyHideWaist => EquipSlot.Body,
EqpEntry._2 => EquipSlot.Body,
EqpEntry.BodyHideGlovesS => EquipSlot.Body,
EqpEntry._4 => EquipSlot.Body,
EqpEntry.BodyHideGlovesM => EquipSlot.Body,
EqpEntry.BodyHideGlovesL => EquipSlot.Body,
EqpEntry.BodyHideGorget => EquipSlot.Body,
EqpEntry.BodyShowLeg => EquipSlot.Body,
EqpEntry.BodyShowHand => EquipSlot.Body,
EqpEntry.BodyShowHead => EquipSlot.Body,
EqpEntry.BodyShowNecklace => EquipSlot.Body,
EqpEntry.BodyShowBracelet => EquipSlot.Body,
EqpEntry.BodyShowTail => EquipSlot.Body,
EqpEntry.DisableBreastPhysics => EquipSlot.Body,
EqpEntry._15 => EquipSlot.Body,
EqpEntry.LegsEnabled => EquipSlot.Legs,
EqpEntry.LegsHideKneePads => EquipSlot.Legs,
EqpEntry.LegsHideBootsS => EquipSlot.Legs,
EqpEntry.LegsHideBootsM => EquipSlot.Legs,
EqpEntry._20 => EquipSlot.Legs,
EqpEntry.LegsShowFoot => EquipSlot.Legs,
EqpEntry.LegsShowTail => EquipSlot.Legs,
EqpEntry._23 => EquipSlot.Legs,
EqpEntry.HandsEnabled => EquipSlot.Hands,
EqpEntry.HandsHideElbow => EquipSlot.Hands,
EqpEntry.HandsHideForearm => EquipSlot.Hands,
EqpEntry._27 => EquipSlot.Hands,
EqpEntry.HandShowBracelet => EquipSlot.Hands,
EqpEntry.HandShowRingL => EquipSlot.Hands,
EqpEntry.HandShowRingR => EquipSlot.Hands,
EqpEntry._31 => EquipSlot.Hands,
EqpEntry.FeetEnabled => EquipSlot.Feet,
EqpEntry.FeetHideKnee => EquipSlot.Feet,
EqpEntry.FeetHideCalf => EquipSlot.Feet,
EqpEntry.FeetHideAnkle => EquipSlot.Feet,
EqpEntry._36 => EquipSlot.Feet,
EqpEntry._37 => EquipSlot.Feet,
EqpEntry._38 => EquipSlot.Feet,
EqpEntry._39 => EquipSlot.Feet,
EqpEntry.HeadEnabled => EquipSlot.Head,
EqpEntry.HeadHideScalp => EquipSlot.Head,
EqpEntry.HeadHideHair => EquipSlot.Head,
EqpEntry.HeadShowHairOverride => EquipSlot.Head,
EqpEntry.HeadHideNeck => EquipSlot.Head,
EqpEntry.HeadShowNecklace => EquipSlot.Head,
EqpEntry._46 => EquipSlot.Head,
EqpEntry.HeadShowEarrings => EquipSlot.Head,
EqpEntry.HeadShowEarringsHuman => EquipSlot.Head,
EqpEntry.HeadShowEarringsAura => EquipSlot.Head,
EqpEntry.HeadShowEarHuman => EquipSlot.Head,
EqpEntry.HeadShowEarMiqote => EquipSlot.Head,
EqpEntry.HeadShowEarAuRa => EquipSlot.Head,
EqpEntry.HeadShowEarViera => EquipSlot.Head,
EqpEntry._54 => EquipSlot.Head,
EqpEntry._55 => EquipSlot.Head,
EqpEntry.HeadShowHrothgarHat => EquipSlot.Head,
EqpEntry.HeadShowVieraHat => EquipSlot.Head,
EqpEntry._58 => EquipSlot.Head,
EqpEntry._59 => EquipSlot.Head,
EqpEntry._60 => EquipSlot.Head,
EqpEntry._61 => EquipSlot.Head,
EqpEntry._62 => EquipSlot.Head,
EqpEntry._63 => EquipSlot.Head,
_ => EquipSlot.Unknown,
};
}
public static string ToLocalName( this EqpEntry entry )
{
return entry switch
{
EqpEntry.BodyEnabled => "Enabled",
EqpEntry.BodyHideWaist => "Hide Waist",
EqpEntry._2 => "Unknown 2",
EqpEntry.BodyHideGlovesS => "Hide Small Gloves",
EqpEntry._4 => "Unknown 4",
EqpEntry.BodyHideGlovesM => "Hide Medium Gloves",
EqpEntry.BodyHideGlovesL => "Hide Large Gloves",
EqpEntry.BodyHideGorget => "Hide Gorget",
EqpEntry.BodyShowLeg => "Show Legs",
EqpEntry.BodyShowHand => "Show Hands",
EqpEntry.BodyShowHead => "Show Head",
EqpEntry.BodyShowNecklace => "Show Necklace",
EqpEntry.BodyShowBracelet => "Show Bracelet",
EqpEntry.BodyShowTail => "Show Tail",
EqpEntry.DisableBreastPhysics => "Disable Breast Physics",
EqpEntry._15 => "Unknown 15",
EqpEntry.LegsEnabled => "Enabled",
EqpEntry.LegsHideKneePads => "Hide Knee Pads",
EqpEntry.LegsHideBootsS => "Hide Small Boots",
EqpEntry.LegsHideBootsM => "Hide Medium Boots",
EqpEntry._20 => "Unknown 20",
EqpEntry.LegsShowFoot => "Show Foot",
EqpEntry.LegsShowTail => "Show Tail",
EqpEntry._23 => "Unknown 23",
EqpEntry.HandsEnabled => "Enabled",
EqpEntry.HandsHideElbow => "Hide Elbow",
EqpEntry.HandsHideForearm => "Hide Forearm",
EqpEntry._27 => "Unknown 27",
EqpEntry.HandShowBracelet => "Show Bracelet",
EqpEntry.HandShowRingL => "Show Left Ring",
EqpEntry.HandShowRingR => "Show Right Ring",
EqpEntry._31 => "Unknown 31",
EqpEntry.FeetEnabled => "Enabled",
EqpEntry.FeetHideKnee => "Hide Knees",
EqpEntry.FeetHideCalf => "Hide Calves",
EqpEntry.FeetHideAnkle => "Hide Ankles",
EqpEntry._36 => "Unknown 36",
EqpEntry._37 => "Unknown 37",
EqpEntry._38 => "Unknown 38",
EqpEntry._39 => "Unknown 39",
EqpEntry.HeadEnabled => "Enabled",
EqpEntry.HeadHideScalp => "Hide Scalp",
EqpEntry.HeadHideHair => "Hide Hair",
EqpEntry.HeadShowHairOverride => "Show Hair Override",
EqpEntry.HeadHideNeck => "Hide Neck",
EqpEntry.HeadShowNecklace => "Show Necklace",
EqpEntry._46 => "Unknown 46",
EqpEntry.HeadShowEarrings => "Show Earrings",
EqpEntry.HeadShowEarringsHuman => "Show Earrings (Human)",
EqpEntry.HeadShowEarringsAura => "Show Earrings (Au Ra)",
EqpEntry.HeadShowEarHuman => "Show Ears (Human)",
EqpEntry.HeadShowEarMiqote => "Show Ears (Miqo'te)",
EqpEntry.HeadShowEarAuRa => "Show Ears (Au Ra)",
EqpEntry.HeadShowEarViera => "Show Ears (Viera)",
EqpEntry._54 => "Unknown 54",
EqpEntry._55 => "Unknown 55",
EqpEntry.HeadShowHrothgarHat => "Show on Hrothgar",
EqpEntry.HeadShowVieraHat => "Show on Viera",
EqpEntry._58 => "Unknown 58",
EqpEntry._59 => "Unknown 59",
EqpEntry._60 => "Unknown 60",
EqpEntry._61 => "Unknown 61",
EqpEntry._62 => "Unknown 62",
EqpEntry._63 => "Unknown 63",
_ => throw new InvalidEnumArgumentException(),
};
}
private static EqpEntry[] GetEntriesForSlot( EquipSlot slot )
{
return ( ( EqpEntry[] )Enum.GetValues( typeof( EqpEntry ) ) )
.Where( e => e.ToEquipSlot() == slot )
.ToArray();
}
public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot( EquipSlot.Body );
public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot( EquipSlot.Legs );
public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot( EquipSlot.Hands );
public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot( EquipSlot.Feet );
public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot( EquipSlot.Head );
public static IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >()
{
[ EquipSlot.Body ] = EqpAttributesBody,
[ EquipSlot.Legs ] = EqpAttributesLegs,
[ EquipSlot.Hands ] = EqpAttributesHands,
[ EquipSlot.Feet ] = EqpAttributesFeet,
[ EquipSlot.Head ] = EqpAttributesHead,
};
}

View file

@ -4,6 +4,8 @@ namespace Penumbra.GameData.Structs
{
public struct GmpEntry
{
public static readonly GmpEntry Default = new ();
public bool Enabled
{
get => ( Value & 1 ) == 1;

View file

@ -137,15 +137,21 @@ public static class Functions
[DllImport( "msvcrt.dll", EntryPoint = "memcmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )]
private static extern unsafe int memcmp( byte* b1, byte* b2, int count );
private static extern unsafe int memcmp( void* b1, void* b2, int count );
public static unsafe int MemCmpUnchecked( byte* ptr1, byte* ptr2, int count )
public static unsafe int MemCmpUnchecked( void* ptr1, void* ptr2, int count )
=> memcmp( ptr1, ptr2, count );
[DllImport( "msvcrt.dll", EntryPoint = "_memicmp", CallingConvention = CallingConvention.Cdecl, SetLastError = false )]
private static extern unsafe int memicmp( byte* b1, byte* b2, int count );
private static extern unsafe int memicmp( void* b1, void* b2, int count );
public static unsafe int MemCmpCaseInsensitiveUnchecked( byte* ptr1, byte* ptr2, int count )
public static unsafe int MemCmpCaseInsensitiveUnchecked( void* ptr1, void* ptr2, int count )
=> memicmp( ptr1, ptr2, count );
[DllImport( "msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl, SetLastError = false )]
private static extern unsafe void* memset( void* dest, int c, int count );
public static unsafe void* MemSet( void* dest, byte value, int count )
=> memset( dest, value, count );
}

View file

@ -7,7 +7,6 @@ using Dalamud.Logging;
using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Importer.Models;
using Penumbra.Mod;
using Penumbra.Util;

View file

@ -7,376 +7,383 @@ using Lumina.Data.Files;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Util;
namespace Penumbra.Importer
namespace Penumbra.Importer;
// TexTools provices custom generated *.meta files for its modpacks, that contain changes to
// - imc files
// - eqp files
// - gmp files
// - est files
// - eqdp files
// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes.
// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json.
// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored.
// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file.
public class TexToolsMeta
{
// TexTools provices custom generated *.meta files for its modpacks, that contain changes to
// - imc files
// - eqp files
// - gmp files
// - est files
// - eqdp files
// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes.
// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json.
// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored.
public class TexToolsMeta
// The info class determines the files or table locations the changes need to apply to from the filename.
public class Info
{
// The info class determines the files or table locations the changes need to apply to from the filename.
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";
// These are the valid regexes for .meta files that we are able to support at the moment.
private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}", RegexOptions.Compiled);
private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}", RegexOptions.Compiled);
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 )
{
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";
// These are the valid regexes for .meta files that we are able to support at the moment.
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
{
return type switch
{
ObjectType.Accessory => true,
ObjectType.Character => true,
ObjectType.Equipment => true,
ObjectType.DemiHuman => true,
ObjectType.Housing => true,
ObjectType.Monster => true,
ObjectType.Weapon => true,
ObjectType.Icon => false,
ObjectType.Font => false,
ObjectType.Interface => false,
ObjectType.LoadingScreen => false,
ObjectType.Map => false,
ObjectType.Vfx => false,
ObjectType.Unknown => false,
ObjectType.World => false,
_ => false,
};
}
public Info( string fileName )
: this( new GamePath( fileName ) )
{ }
public Info( GamePath fileName )
{
PrimaryType = GameData.GameData.GetGamePathParser().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 )
{
switch( PrimaryType )
{
case ObjectType.Equipment:
case ObjectType.Accessory:
if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) )
{
EquipSlot = tmpSlot;
}
break;
case ObjectType.Character:
if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) )
{
CustomizationType = tmpCustom;
}
break;
}
}
if( match.Groups[ "SecondaryType" ].Success
&& Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) )
{
SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value );
}
}
ObjectType.Accessory => true,
ObjectType.Character => true,
ObjectType.Equipment => true,
ObjectType.DemiHuman => true,
ObjectType.Housing => true,
ObjectType.Monster => true,
ObjectType.Weapon => true,
ObjectType.Icon => false,
ObjectType.Font => false,
ObjectType.Interface => false,
ObjectType.LoadingScreen => false,
ObjectType.Map => false,
ObjectType.Vfx => false,
ObjectType.Unknown => false,
ObjectType.World => false,
_ => false,
};
}
public readonly uint Version;
public readonly string FilePath;
public readonly List< MetaManipulation > Manipulations = new();
public Info( string fileName )
: this( new GamePath( fileName ) )
{ }
private static string ReadNullTerminated( BinaryReader reader )
public Info( GamePath fileName )
{
var builder = new System.Text.StringBuilder();
for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() )
PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName );
PrimaryId = 0;
SecondaryType = BodySlot.Unknown;
SecondaryId = 0;
if( !ValidType( PrimaryType ) )
{
builder.Append( c );
PrimaryType = ObjectType.Unknown;
return;
}
return builder.ToString();
}
private void AddIfNotDefault( MetaManipulation manipulation )
{
try
if( PrimaryType == ObjectType.Housing )
{
if( Penumbra.MetaDefaults.CheckAgainstDefault( manipulation ) )
var housingMatch = HousingMeta.Match( fileName );
if( housingMatch.Success )
{
Manipulations.Add( manipulation );
PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value );
}
}
catch( Exception e )
{
PluginLog.Debug( "Skipped {Type}-manipulation:\n{e:l}", manipulation.Type, e );
}
}
private void DeserializeEqpEntry( Info info, byte[]? data )
{
if( data == null || !info.EquipSlot.IsEquipment() )
return;
}
var match = CharaMeta.Match( fileName );
if( !match.Success )
{
return;
}
try
PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value );
if( match.Groups[ "Slot" ].Success )
{
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() )
switch( PrimaryType )
{
continue;
}
case ObjectType.Equipment:
case ObjectType.Accessory:
if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) )
{
EquipSlot = tmpSlot;
}
var value = Eqdp.FromSlotAndBits( info.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 );
AddIfNotDefault( MetaManipulation.Eqdp( info.EquipSlot, gr, info.PrimaryId, value ) );
}
}
break;
case ObjectType.Character:
if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) )
{
CustomizationType = tmpCustom;
}
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 ) );
break;
}
}
}
public TexToolsMeta( byte[] data )
{
try
if( match.Groups[ "SecondaryType" ].Success
&& Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) )
{
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 ) );
SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value );
}
catch( Exception e )
{
FilePath = "";
PluginLog.Error( $"Error while parsing .meta file:\n{e}" );
}
}
private TexToolsMeta( string filePath, uint version )
{
FilePath = filePath;
Version = version;
}
public static TexToolsMeta Invalid = new(string.Empty, 0);
public static TexToolsMeta FromRgspFile( string filePath, byte[] data )
{
if( data.Length != 45 && data.Length != 42 )
{
PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." );
return Invalid;
}
using var s = new MemoryStream( data );
using var br = new BinaryReader( s );
var flag = br.ReadByte();
var version = flag != 255 ? ( uint )1 : br.ReadUInt16();
var ret = new TexToolsMeta( filePath, version );
var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 );
if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown )
{
PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." );
return Invalid;
}
var gender = br.ReadByte();
if( gender != 1 && gender != 0 )
{
PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." );
return Invalid;
}
if( gender == 1 )
{
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinSize, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxSize, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinTail, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxTail, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinX, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinY, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinZ, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxX, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxY, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxZ, br.ReadSingle() ) );
}
else
{
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinSize, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxSize, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinTail, br.ReadSingle() ) );
ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxTail, br.ReadSingle() ) );
}
return ret;
}
}
public readonly uint Version;
public readonly string FilePath;
public readonly List< EqpManipulation > EqpManipulations = new();
public readonly List< GmpManipulation > GmpManipulations = new();
public readonly List< EqdpManipulation > EqdpManipulations = new();
public readonly List< EstManipulation > EstManipulations = new();
public readonly List< RspManipulation > RspManipulations = new();
public readonly List< ImcManipulation > ImcManipulations = new();
private void DeserializeEqpEntry( Info info, byte[]? data )
{
if( data == null || !info.EquipSlot.IsEquipment() )
{
return;
}
var value = Eqp.FromSlotAndBytes( info.EquipSlot, data );
var def = new EqpManipulation( ExpandedEqpFile.GetDefault( info.PrimaryId ), info.EquipSlot, info.PrimaryId );
var manip = new EqpManipulation( value, info.EquipSlot, info.PrimaryId );
if( def.Entry != manip.Entry )
{
EqpManipulations.Add( manip );
}
}
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 );
var def = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, info.EquipSlot.IsAccessory(), info.PrimaryId ), info.EquipSlot,
gr.Split().Item1, gr.Split().Item2, info.PrimaryId );
var manip = new EqdpManipulation( value, info.EquipSlot, gr.Split().Item1, gr.Split().Item2, info.PrimaryId );
if( def.Entry != manip.Entry )
{
EqdpManipulations.Add( manip );
}
}
}
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();
var def = ExpandedGmpFile.GetDefault( info.PrimaryId );
if( value != def )
{
GmpManipulations.Add( new GmpManipulation( value, info.PrimaryId ) );
}
}
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();
var type = ( info.SecondaryType, info.EquipSlot ) switch
{
(BodySlot.Face, _) => EstManipulation.EstType.Face,
(BodySlot.Hair, _) => EstManipulation.EstType.Hair,
(_, EquipSlot.Head) => EstManipulation.EstType.Head,
(_, EquipSlot.Body) => EstManipulation.EstType.Body,
_ => ( EstManipulation.EstType )0,
};
if( !gr.IsValid() || type == 0 )
{
continue;
}
var def = EstFile.GetDefault( type, gr, id );
if( def != value )
{
EstManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, 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 ) );
var values = reader.ReadStructures< ImcEntry >( num );
ushort i = 0;
if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory )
{
foreach( var value in values )
{
ImcEntry def;
if( !value.Equals( def ) )
ImcManipulations.Add(new ImcManipulation(info.EquipSlot, i, info.PrimaryId, value) );
++i;
}
}
else
{
foreach( var value in values )
{
ImcEntry def;
if( !value.Equals( def ) )
ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i, value ) );
++i;
}
}
}
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( MetaManipulation.Type.Eqp ) );
//DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) );
//DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) );
//DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) );
//DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) );
}
catch( Exception e )
{
FilePath = "";
PluginLog.Error( $"Error while parsing .meta file:\n{e}" );
}
}
private TexToolsMeta( string filePath, uint version )
{
FilePath = filePath;
Version = version;
}
public static TexToolsMeta Invalid = new(string.Empty, 0);
public static TexToolsMeta FromRgspFile( string filePath, byte[] data )
{
if( data.Length != 45 && data.Length != 42 )
{
PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." );
return Invalid;
}
using var s = new MemoryStream( data );
using var br = new BinaryReader( s );
var flag = br.ReadByte();
var version = flag != 255 ? ( uint )1 : br.ReadUInt16();
var ret = new TexToolsMeta( filePath, version );
var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 );
if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown )
{
PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." );
return Invalid;
}
var gender = br.ReadByte();
if( gender != 1 && gender != 0 )
{
PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." );
return Invalid;
}
//if( gender == 1 )
//{
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinSize, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxSize, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinTail, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxTail, br.ReadSingle() ) );
//
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinX, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinY, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinZ, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxX, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxY, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxZ, br.ReadSingle() ) );
//}
//else
//{
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinSize, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxSize, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinTail, br.ReadSingle() ) );
// ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxTail, br.ReadSingle() ) );
//}
//
return ret;
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
namespace Penumbra.Interop.Structs;
@ -14,10 +15,11 @@ public unsafe struct CharacterUtility
public const int HairEstIdx = 65;
public const int BodyEstIdx = 66;
public const int HeadEstIdx = 67;
public const int NumEqdpFiles = 2 * 28;
public static int EqdpIdx( ushort raceCode, bool accessory )
public static int EqdpIdx( GenderRace raceCode, bool accessory )
=> ( accessory ? 28 : 0 )
+ raceCode switch
+ ( int )raceCode switch
{
0101 => 2,
0201 => 3,
@ -65,7 +67,7 @@ public unsafe struct CharacterUtility
public ResourceHandle* Resource( int idx )
=> ( ResourceHandle* )Resources[ idx ];
public ResourceHandle* EqdpResource( ushort raceCode, bool accessory )
public ResourceHandle* EqdpResource( GenderRace raceCode, bool accessory )
=> Resource( EqdpIdx( raceCode, accessory ) );
[FieldOffset( 8 + HumanCmpIdx * 8 )]

View file

@ -9,6 +9,9 @@ public unsafe struct ResourceHandle
[StructLayout( LayoutKind.Explicit )]
public struct DataIndirection
{
[FieldOffset( 0x00 )]
public void** VTable;
[FieldOffset( 0x10 )]
public byte* DataPtr;

View file

@ -1,60 +0,0 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Penumbra.Meta
{
public static class EqdpEntryExtensions
{
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 );
}
public static class EqpEntryExtensions
{
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 );
}
public static class GmpEntryExtension
{
public static GmpEntry Apply( this ref GmpEntry entry, MetaManipulation manipulation )
{
if( manipulation.Type != MetaType.Gmp )
{
return entry;
}
entry.Value = manipulation.GmpValue.Value;
return entry;
}
}
}

View file

@ -1,73 +1,42 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Interop.Structs;
using System.Collections.Generic;
namespace Penumbra.Meta.Files
namespace Penumbra.Meta.Files;
public sealed unsafe class CmpFile : MetaBaseFile
{
public class CmpFile
private const int RacialScalingStart = 0x2A800;
public float this[ SubRace subRace, RspAttribute attribute ]
{
private const int RacialScalingStart = 0x2A800;
get => *( float* )( Data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 );
set => *( float* )( Data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 ) = value;
}
private readonly byte[] _byteData = new byte[RacialScalingStart];
private readonly RspEntry[] _rspEntries;
public override void Reset()
=> Functions.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length );
public CmpFile( byte[] bytes )
public void Reset( IEnumerable< (SubRace, RspAttribute) > entries )
{
foreach( var (r, a) in entries )
{
if( bytes.Length < RacialScalingStart )
{
throw new ArgumentOutOfRangeException();
}
Array.Copy( bytes, _byteData, RacialScalingStart );
var rspEntryNum = ( bytes.Length - RacialScalingStart ) / RspEntry.ByteSize;
var tmp = new List< RspEntry >( rspEntryNum );
for( var i = 0; i < rspEntryNum; ++i )
{
tmp.Add( new RspEntry( bytes, RacialScalingStart + i * RspEntry.ByteSize ) );
}
_rspEntries = tmp.ToArray();
this[ r, a ] = GetDefault( r, a );
}
}
public RspEntry this[ SubRace subRace ]
=> _rspEntries[ subRace.ToRspIndex() ];
public CmpFile()
: base( CharacterUtility.HumanCmpIdx )
{
AllocateData( DefaultData.Length );
Reset();
}
public bool Set( SubRace subRace, RspAttribute attribute, float value )
{
var entry = _rspEntries[ subRace.ToRspIndex() ];
var oldValue = entry[ attribute ];
if( oldValue == value )
{
return false;
}
entry[ attribute ] = value;
return true;
}
public byte[] WriteBytes()
{
using var s = new MemoryStream( RacialScalingStart + _rspEntries.Length * RspEntry.ByteSize );
s.Write( _byteData, 0, _byteData.Length );
foreach( var entry in _rspEntries )
{
var bytes = entry.ToBytes();
s.Write( bytes, 0, bytes.Length );
}
return s.ToArray();
}
private CmpFile( byte[] data, RspEntry[] entries )
{
_byteData = data.ToArray();
_rspEntries = entries.Select( e => new RspEntry( e ) ).ToArray();
}
public CmpFile Clone()
=> new( _byteData, _rspEntries );
public static float GetDefault( SubRace subRace, RspAttribute attribute )
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ CharacterUtility.HumanCmpIdx ].Address;
return *( float* )( data + RacialScalingStart + subRace.ToRspIndex() * RspEntry.ByteSize + ( int )attribute * 4 );
}
}

View file

@ -1,214 +1,137 @@
using System;
using System.IO;
using System.Linq;
using Lumina.Data;
using System.Collections.Generic;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Files
namespace Penumbra.Meta.Files;
// EQDP file structure:
// [Identifier][BlockSize:ushort][BlockCount:ushort]
// BlockCount x [BlockHeader:ushort]
// Containing offsets for blocks, ushort.Max means collapsed.
// Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2.
// ExpandedBlockCount x [Entry]
// Expanded Eqdp File just expands all blocks for easy read and write access to single entries and to keep the same memory for it.
public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
{
// 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;
public readonly int DataOffset;
public ushort Identifier
=> *( ushort* )Data;
public ushort BlockSize
=> *( ushort* )( Data + 2 );
public ushort BlockCount
=> *( ushort* )( Data + 4 );
public int Count
=> ( Length - DataOffset ) / EqdpEntrySize;
public EqdpEntry this[ int idx ]
{
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 )
get
{
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( idx >= Count || idx < 0 )
{
if( clone.Blocks[ i ] != null )
{
Blocks[ i ] = ( EqdpEntry[] )clone.Blocks[ i ]!.Clone();
}
throw new IndexOutOfRangeException();
}
return *( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx );
}
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 )
set
{
if( idx < TotalBlockCount && Blocks[ idx ] == null )
if( idx >= Count || idx < 0 )
{
Blocks[ idx ] = new EqdpEntry[BlockSize];
++ExpandedBlockCount;
return true;
throw new IndexOutOfRangeException();
}
return false;
*( EqdpEntry* )( Data + DataOffset + EqdpEntrySize * idx ) = value;
}
}
private bool CollapseBlock( int idx )
public override void Reset()
{
var def = ( byte* )DefaultData.Data;
Functions.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize );
var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize );
var dataBasePtr = ( byte* )( controlPtr + BlockCount );
var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount );
for( var i = 0; i < BlockCount; ++i )
{
if( idx >= TotalBlockCount || Blocks[ idx ] == null )
if( controlPtr[ i ] == CollapsedBlock )
{
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;
}
Functions.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize );
}
else
{
var array = Blocks[ block ];
if( array != null )
{
array[ SubIdx( idx ) ] = entry;
if( array.All( e => e == 0 ) )
{
CollapseBlock( block );
}
return true;
}
Functions.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize );
}
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();
}
}
myDataPtr += BlockSize;
}
}
public void Reset( IEnumerable< int > entries )
{
foreach( var entry in entries )
{
this[ entry ] = GetDefault( entry );
}
}
public ExpandedEqdpFile( GenderRace raceCode, bool accessory )
: base( CharacterUtility.EqdpIdx( raceCode, accessory ) )
{
var def = ( byte* )DefaultData.Data;
var blockSize = *( ushort* )( def + IdentifierSize );
var totalBlockCount = *( ushort* )( def + IdentifierSize + 2 );
var totalBlockSize = blockSize * EqdpEntrySize;
DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize;
var fullLength = DataOffset + totalBlockCount * totalBlockSize;
fullLength += ( FileAlignment - ( Length & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 );
AllocateData( fullLength );
Reset();
}
public EqdpEntry GetDefault( int setIdx )
=> GetDefault( Index, setIdx );
public static EqdpEntry GetDefault( int fileIdx, int setIdx )
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address;
var blockSize = *( ushort* )( data + IdentifierSize );
var totalBlockCount = *( ushort* )( data + IdentifierSize + 2 );
var blockIdx = setIdx / blockSize;
if( blockIdx >= totalBlockCount )
{
return 0;
}
var block = ( ( ushort* )( data + IdentifierSize + PreambleSize ) )[ blockIdx ];
if( block == CollapsedBlock )
{
return 0;
}
var blockData = ( EqdpEntry* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block );
return *( blockData + blockIdx % blockSize );
}
public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx )
=> GetDefault( CharacterUtility.EqdpIdx( raceCode, accessory ), setIdx );
}

View file

@ -1,216 +0,0 @@
using System;
using System.IO;
using System.Linq;
using Lumina.Data;
using Penumbra.GameData.Structs;
namespace Penumbra.Meta.Files
{
// EQP Structure:
// 64 x [Block collapsed or not bit]
// 159 x [EquipmentParameter:ulong]
// (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong]
// Item 0 does not exist and is sent to Item 1 instead.
public sealed class EqpFile : EqpGmpBase
{
private readonly EqpEntry[]?[] _entries = new EqpEntry[TotalBlockCount][];
protected override ulong ControlBlock
{
get => ( ulong )_entries[ 0 ]![ 0 ];
set => _entries[ 0 ]![ 0 ] = ( EqpEntry )value;
}
private EqpFile( EqpFile clone )
{
ExpandedBlockCount = clone.ExpandedBlockCount;
_entries = clone.Clone( clone._entries );
}
public byte[] WriteBytes()
=> WriteBytes( _entries, e => ( ulong )e );
public EqpFile Clone()
=> new( this );
public EqpFile( FileResource file )
=> ReadFile( _entries, file, I => ( EqpEntry )I );
public EqpEntry GetEntry( ushort setId )
=> GetEntry( _entries, setId, ( EqpEntry )0 );
public bool SetEntry( ushort setId, EqpEntry entry )
=> SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 );
public ref EqpEntry this[ ushort setId ]
=> ref GetTrueEntry( _entries, setId );
}
public class EqpGmpBase
{
protected const ushort ParameterSize = 8;
protected const ushort BlockSize = 160;
protected const ushort TotalBlockCount = 64;
protected int ExpandedBlockCount { get; set; }
private static int BlockIdx( ushort idx )
=> idx / BlockSize;
private static int SubIdx( ushort idx )
=> idx % BlockSize;
protected virtual ulong ControlBlock { get; set; }
protected T[]?[] Clone< T >( T[]?[] clone )
{
var ret = new T[TotalBlockCount][];
for( var i = 0; i < TotalBlockCount; ++i )
{
if( clone[ i ] != null )
{
ret[ i ] = ( T[] )clone[ i ]!.Clone();
}
}
return ret;
}
protected EqpGmpBase()
{ }
protected bool ExpandBlock< T >( T[]?[] blocks, int idx )
{
if( idx >= TotalBlockCount || blocks[ idx ] != null )
{
return false;
}
blocks[ idx ] = new T[BlockSize];
++ExpandedBlockCount;
ControlBlock |= 1ul << idx;
return true;
}
protected bool CollapseBlock< T >( T[]?[] blocks, int idx )
{
if( idx >= TotalBlockCount || blocks[ idx ] == null )
{
return false;
}
blocks[ idx ] = null;
--ExpandedBlockCount;
ControlBlock &= ~( 1ul << idx );
return true;
}
protected T GetEntry< T >( T[]?[] blocks, ushort idx, T defaultEntry )
{
// Skip the zeroth item.
idx = idx == 0 ? ( ushort )1 : idx;
var block = BlockIdx( idx );
var array = block < blocks.Length ? blocks[ block ] : null;
if( array == null )
{
return defaultEntry;
}
return array[ SubIdx( idx ) ];
}
protected ref T GetTrueEntry< T >( T[]?[] blocks, ushort idx )
{
// Skip the zeroth item.
idx = idx == 0 ? ( ushort )1 : idx;
var block = BlockIdx( idx );
if( block >= TotalBlockCount )
{
throw new ArgumentOutOfRangeException();
}
ExpandBlock( blocks, block );
var array = blocks[ block ]!;
return ref array[ SubIdx( idx ) ];
}
protected byte[] WriteBytes< T >( T[]?[] blocks, Func< T, ulong > transform )
{
var dataSize = ExpandedBlockCount * BlockSize * ParameterSize;
using var mem = new MemoryStream( dataSize );
using var bw = new BinaryWriter( mem );
foreach( var parameter in blocks.Where( array => array != null )
.SelectMany( array => array! ) )
{
bw.Write( transform( parameter ) );
}
return mem.ToArray();
}
protected void ReadFile< T >( T[]?[] blocks, FileResource file, Func< ulong, T > convert )
{
file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin );
var blockBits = file.Reader.ReadUInt64();
// reset to 0 and just put the bitmask in the first block
// item 0 is not accessible and it simplifies printing.
file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin );
ExpandedBlockCount = 0;
for( var i = 0; i < TotalBlockCount; ++i )
{
var flag = 1ul << i;
if( ( blockBits & flag ) != flag )
{
continue;
}
++ExpandedBlockCount;
var tmp = new T[BlockSize];
for( var j = 0; j < BlockSize; ++j )
{
tmp[ j ] = convert( file.Reader.ReadUInt64() );
}
blocks[ i ] = tmp;
}
}
protected bool SetEntry< T >( T[]?[] blocks, ushort idx, T entry, Func< T, bool > isDefault, Func< T, T, bool > isEqual )
{
var block = BlockIdx( idx );
if( block >= TotalBlockCount )
{
return false;
}
if( !isDefault( entry ) )
{
ExpandBlock( blocks, block );
if( !isEqual( entry, blocks[ block ]![ SubIdx( idx ) ] ) )
{
blocks[ block ]![ SubIdx( idx ) ] = entry;
return true;
}
}
else
{
var array = blocks[ block ];
if( array != null )
{
array[ SubIdx( idx ) ] = entry;
if( array.All( e => e!.Equals( 0ul ) ) )
{
CollapseBlock( blocks, block );
}
return true;
}
}
return false;
}
}
}

View file

@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Files;
// EQP/GMP 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 unsafe class ExpandedEqpGmpBase : MetaBaseFile
{
protected const int BlockSize = 160;
protected const int NumBlocks = 64;
protected const int EntrySize = 8;
protected const int MaxSize = BlockSize * NumBlocks * EntrySize;
public const int Count = BlockSize * NumBlocks;
public ulong ControlBlock
=> *( ulong* )Data;
protected T Get< T >( int idx ) where T : unmanaged
{
return idx switch
{
>= Count => throw new IndexOutOfRangeException(),
<= 1 => *( ( T* )Data + 1 ),
_ => *( ( T* )Data + idx ),
};
}
protected void Set< T >( int idx, T value ) where T : unmanaged
{
idx = idx switch
{
>= Count => throw new IndexOutOfRangeException(),
<= 0 => 1,
_ => idx,
};
*( ( T* )Data + idx ) = value;
}
protected virtual void SetEmptyBlock( int idx )
{
Functions.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize );
}
public sealed override void Reset()
{
var ptr = ( byte* )DefaultData.Data;
var controlBlock = *( ulong* )ptr;
*( ulong* )ptr = ulong.MaxValue;
for( var i = 0; i < 64; ++i )
{
var collapsed = ( ( controlBlock >> i ) & 1 ) == 0;
if( !collapsed )
{
Functions.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + i * BlockSize * EntrySize, BlockSize * EntrySize );
}
else
{
SetEmptyBlock( i );
}
}
}
public ExpandedEqpGmpBase( bool gmp )
: base( gmp ? CharacterUtility.GmpIdx : CharacterUtility.EqpIdx )
{
AllocateData( MaxSize );
Reset();
}
protected static T GetDefault< T >( int fileIdx, int setIdx, T def ) where T : unmanaged
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ fileIdx ].Address;
if( setIdx == 0 )
{
setIdx = 1;
}
var blockIdx = setIdx / BlockSize;
if( blockIdx >= NumBlocks )
{
return def;
}
var control = *( ulong* )data;
var blockBit = 1ul << blockIdx;
if( ( control & blockBit ) == 0 )
{
return def;
}
var count = BitOperations.PopCount( control & ( blockBit - 1 ) );
var idx = setIdx % BlockSize;
var ptr = ( T* )data + BlockSize * count + idx;
return *ptr;
}
}
public sealed class ExpandedEqpFile : ExpandedEqpGmpBase
{
public ExpandedEqpFile()
: base( false )
{ }
public EqpEntry this[ int idx ]
{
get => Get< EqpEntry >( idx );
set => Set( idx, value );
}
public static EqpEntry GetDefault( int setIdx )
=> GetDefault( CharacterUtility.EqpIdx, setIdx, Eqp.DefaultEntry );
protected override unsafe void SetEmptyBlock( int idx )
{
var blockPtr = ( ulong* )( Data + idx * BlockSize * EntrySize );
var endPtr = blockPtr + BlockSize;
for( var ptr = blockPtr; ptr < endPtr; ++ptr )
{
*ptr = ( ulong )Eqp.DefaultEntry;
}
}
public void Reset( IEnumerable< int > entries )
{
foreach( var entry in entries )
{
this[ entry ] = GetDefault( entry );
}
}
}
public sealed class ExpandedGmpFile : ExpandedEqpGmpBase
{
public ExpandedGmpFile()
: base( true )
{ }
public GmpEntry this[ int idx ]
{
get => Get< GmpEntry >( idx );
set => Set( idx, value );
}
public static GmpEntry GetDefault( int setIdx )
=> GetDefault( CharacterUtility.GmpIdx, setIdx, GmpEntry.Default );
public void Reset( IEnumerable< int > entries )
{
foreach( var entry in entries )
{
this[ entry ] = GetDefault( entry );
}
}
}

View file

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Lumina.Data;
using System;
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Util;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta.Files;
@ -11,152 +11,192 @@ namespace Penumbra.Meta.Files;
// Apparently entries need to be sorted.
// #NumEntries x [SetId : UInt16] [RaceId : UInt16]
// #NumEntries x [SkeletonId : UInt16]
public class EstFile
public sealed unsafe class EstFile : MetaBaseFile
{
private const ushort EntryDescSize = 4;
private const ushort EntrySize = 2;
private const int IncreaseSize = 100;
private readonly SortedList< GenderRace, SortedList< ushort, ushort > > _entries = new();
private uint NumEntries { get; set; }
public int Count
=> *( int* )Data;
private EstFile( EstFile clone )
private int Size
=> 4 + Count * ( EntryDescSize + EntrySize );
public enum EstEntryChange
{
NumEntries = clone.NumEntries;
_entries = new SortedList< GenderRace, SortedList< ushort, ushort > >( clone._entries.Count );
foreach( var (genderRace, data) in clone._entries )
Unchanged,
Changed,
Added,
Removed,
}
public ushort this[ GenderRace genderRace, ushort setId ]
{
get
{
var dict = new SortedList< ushort, ushort >( data.Count );
foreach( var (setId, value) in data )
var (idx, exists) = FindEntry( genderRace, setId );
if( !exists )
{
dict.Add( setId, value );
return 0;
}
_entries.Add( genderRace, dict );
return *( ushort* )( Data + EntryDescSize * ( Count + 1 ) + EntrySize * idx );
}
set => SetEntry( genderRace, setId, value );
}
private void InsertEntry( int idx, GenderRace genderRace, ushort setId, ushort skeletonId )
{
if( Length < Size + EntryDescSize + EntrySize )
{
var data = Data;
var length = Length;
AllocateData( length + IncreaseSize * ( EntryDescSize + EntrySize ) );
Functions.MemCpyUnchecked( Data, data, length );
Functions.MemSet( Data + length, 0, IncreaseSize * ( EntryDescSize + EntrySize ) );
GC.RemoveMemoryPressure( length );
Marshal.FreeHGlobal( ( IntPtr )data );
}
var control = ( uint* )( Data + 4 );
var entries = ( ushort* )( Data + 4 * ( Count + 1 ) );
for( var i = Count; i > idx; --i )
{
*( entries + i + 2 ) = entries[ i - 1 ];
}
for( var i = idx - 1; i >= 0; --i )
{
*( entries + i + 2 ) = entries[ i ];
}
for( var i = Count; i > idx; --i )
{
*( control + i ) = control[ i - 1 ];
}
*( int* )Data = Count + 1;
*( ushort* )control = setId;
*( ( ushort* )control + 1 ) = ( ushort )genderRace;
control[ idx ] = skeletonId;
}
private void RemoveEntry( int idx )
{
var entries = ( ushort* )( Data + 4 * Count );
var control = ( uint* )( Data + 4 );
*( int* )Data = Count - 1;
var count = Count;
for( var i = idx; i < count; ++i )
{
control[ i ] = control[ i + 1 ];
}
for( var i = 0; i < count; ++i )
{
entries[ i ] = entries[ i + 1 ];
}
entries[ count ] = 0;
entries[ count + 1 ] = 0;
entries[ count + 2 ] = 0;
}
[StructLayout( LayoutKind.Sequential, Size = 4 )]
private struct Info : IComparable< Info >
{
public readonly ushort SetId;
public readonly GenderRace GenderRace;
public Info( GenderRace gr, ushort setId )
{
GenderRace = gr;
SetId = setId;
}
public int CompareTo( Info other )
{
var genderRaceComparison = GenderRace.CompareTo( other.GenderRace );
return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo( other.SetId );
}
}
public EstFile Clone()
=> new(this);
private bool DeleteEntry( GenderRace gr, ushort setId )
private static (int, bool) FindEntry( ReadOnlySpan< Info > data, GenderRace genderRace, 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;
var idx = data.BinarySearch( new Info( genderRace, setId ) );
return idx < 0 ? ( ~idx, false ) : ( idx, true );
}
private (bool, bool) AddEntry( GenderRace gr, ushort setId, ushort entry )
private (int, bool) FindEntry( GenderRace genderRace, ushort setId )
{
if( !_entries.TryGetValue( gr, out var setDict ) )
{
_entries[ gr ] = new SortedList< ushort, ushort >();
setDict = _entries[ gr ];
}
var span = new ReadOnlySpan< Info >( Data + 4, Count );
return FindEntry( span, genderRace, setId );
}
if( setDict.TryGetValue( setId, out var oldEntry ) )
public EstEntryChange SetEntry( GenderRace genderRace, ushort setId, ushort skeletonId )
{
var (idx, exists) = FindEntry( genderRace, setId );
if( exists )
{
if( oldEntry == entry )
var value = *( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx );
if( value == skeletonId )
{
return ( false, false );
return EstEntryChange.Unchanged;
}
setDict[ setId ] = entry;
return ( false, true );
if( skeletonId == 0 )
{
RemoveEntry( idx );
return EstEntryChange.Removed;
}
*( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx ) = skeletonId;
return EstEntryChange.Changed;
}
setDict[ setId ] = entry;
return ( true, true );
if( skeletonId == 0 )
{
return EstEntryChange.Unchanged;
}
InsertEntry( idx, genderRace, setId, skeletonId );
return EstEntryChange.Added;
}
public bool SetEntry( GenderRace gr, ushort setId, ushort entry )
public override void Reset()
{
if( entry == 0 )
{
return DeleteEntry( gr, setId );
}
var (addedNew, changed) = AddEntry( gr, setId, entry );
if( !addedNew )
{
return changed;
}
++NumEntries;
return true;
var (d, length) = DefaultData;
var data = ( byte* )d;
Functions.MemCpyUnchecked( Data, data, length );
Functions.MemSet( Data + length, 0, Length - length );
}
public ushort GetEntry( GenderRace gr, ushort setId )
public EstFile( EstManipulation.EstType estType )
: base( ( int )estType )
{
if( !_entries.TryGetValue( gr, out var setDict ) )
var length = DefaultData.Length;
AllocateData( length + IncreaseSize * ( EntryDescSize + EntrySize ) );
Reset();
}
public ushort GetDefault( GenderRace genderRace, ushort setId )
=> GetDefault( ( EstManipulation.EstType )Index, genderRace, setId );
public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId )
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResources[ ( int )estType ].Address;
var count = *( int* )data;
var span = new ReadOnlySpan< Info >( data + 4, count );
var (idx, found) = FindEntry( span, genderRace, setId );
if( !found )
{
return 0;
}
return !setDict.TryGetValue( setId, out var entry ) ? ( ushort )0 : entry;
}
public byte[] WriteBytes()
{
using MemoryStream mem = new(( int )( 4 + ( EntryDescSize + EntrySize ) * NumEntries ));
using BinaryWriter bw = new(mem);
bw.Write( NumEntries );
foreach( var kvp1 in _entries )
{
foreach( var kvp2 in kvp1.Value )
{
bw.Write( kvp2.Key );
bw.Write( ( ushort )kvp1.Key );
}
}
foreach( var kvp2 in _entries.SelectMany( kvp1 => kvp1.Value ) )
{
bw.Write( kvp2.Value );
}
return mem.ToArray();
}
public EstFile( FileResource file )
{
file.Reader.BaseStream.Seek( 0, SeekOrigin.Begin );
NumEntries = file.Reader.ReadUInt32();
var currentEntryDescOffset = 4;
var currentEntryOffset = 4 + EntryDescSize * NumEntries;
for( var i = 0; i < NumEntries; ++i )
{
file.Reader.BaseStream.Seek( currentEntryDescOffset, SeekOrigin.Begin );
currentEntryDescOffset += EntryDescSize;
var setId = file.Reader.ReadUInt16();
var raceId = ( GenderRace )file.Reader.ReadUInt16();
if( !raceId.IsValid() )
{
continue;
}
file.Reader.BaseStream.Seek( currentEntryOffset, SeekOrigin.Begin );
currentEntryOffset += EntrySize;
var entry = file.Reader.ReadUInt16();
AddEntry( raceId, setId, entry );
}
return *( ushort* )( data + 4 + count * EntryDescSize + idx * EntrySize );
}
}

View file

@ -1,42 +0,0 @@
using Lumina.Data;
using Penumbra.GameData.Structs;
namespace Penumbra.Meta.Files
{
// GmpFiles use the same structure as Eqp Files.
// Entries are also one ulong.
public sealed class GmpFile : EqpGmpBase
{
private readonly GmpEntry[]?[] _entries = new GmpEntry[TotalBlockCount][];
protected override ulong ControlBlock
{
get => _entries[ 0 ]![ 0 ];
set => _entries[ 0 ]![ 0 ] = ( GmpEntry )value;
}
private GmpFile( GmpFile clone )
{
ExpandedBlockCount = clone.ExpandedBlockCount;
_entries = clone.Clone( clone._entries );
}
public byte[] WriteBytes()
=> WriteBytes( _entries, e => ( ulong )e );
public GmpFile Clone()
=> new( this );
public GmpFile( FileResource file )
=> ReadFile( _entries, file, i => ( GmpEntry )i );
public GmpEntry GetEntry( ushort setId )
=> GetEntry( _entries, setId, ( GmpEntry )0 );
public bool SetEntry( ushort setId, GmpEntry entry )
=> SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 );
public ref GmpEntry this[ ushort setId ]
=> ref GetTrueEntry( _entries, setId );
}
}

View file

@ -1,151 +0,0 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using Lumina.Data.Files;
using Penumbra.GameData.Enums;
namespace Penumbra.Meta.Files
{
public class InvalidImcVariantException : ArgumentOutOfRangeException
{
public InvalidImcVariantException()
: base( "Trying to manipulate invalid variant." )
{ }
}
// Imc files are already supported in Lumina, but changing the provided data is not supported.
// We use reflection and extension methods to support changing the data of a given Imc file.
public static class ImcExtensions
{
public static ulong ToInteger( this ImcFile.ImageChangeData imc )
{
ulong ret = imc.MaterialId;
ret |= ( ulong )imc.DecalId << 8;
ret |= ( ulong )imc.AttributeMask << 16;
ret |= ( ulong )imc.SoundId << 16;
ret |= ( ulong )imc.VfxId << 32;
ret |= ( ulong )imc.ActualMaterialAnimationId() << 40;
return ret;
}
public static byte ActualMaterialAnimationId( this ImcFile.ImageChangeData imc )
{
var tmp = imc.GetType().GetField( "_MaterialAnimationIdMask",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
return ( byte )( tmp?.GetValue( imc ) ?? 0 );
}
public static ImcFile.ImageChangeData FromValues( byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId,
byte materialAnimationId )
{
var ret = new ImcFile.ImageChangeData()
{
DecalId = decalId,
MaterialId = materialId,
VfxId = vfxId,
};
ret.GetType().GetField( "_AttributeAndSound",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance )!
.SetValue( ret, ( ushort )( ( attributeMask & 0x3FF ) | ( soundId << 10 ) ) );
ret.GetType().GetField( "_AttributeAndSound",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance )!.SetValue( ret, materialAnimationId );
return ret;
}
public static bool Equal( this ImcFile.ImageChangeData lhs, ImcFile.ImageChangeData rhs )
=> lhs.MaterialId == rhs.MaterialId
&& lhs.DecalId == rhs.DecalId
&& lhs.AttributeMask == rhs.AttributeMask
&& lhs.SoundId == rhs.SoundId
&& lhs.VfxId == rhs.VfxId
&& lhs.MaterialAnimationId == rhs.MaterialAnimationId;
private static void WriteBytes( this ImcFile.ImageChangeData variant, BinaryWriter bw )
{
bw.Write( variant.MaterialId );
bw.Write( variant.DecalId );
bw.Write( ( ushort )( variant.AttributeMask | variant.SoundId ) );
bw.Write( variant.VfxId );
bw.Write( variant.ActualMaterialAnimationId() );
}
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.RFinger => 3,
EquipSlot.Feet => 4,
EquipSlot.LFinger => 4,
_ => throw new InvalidEnumArgumentException(),
};
}
if( imc.Variant == 0 )
{
return ref parts[ idx ].DefaultVariant;
}
if( imc.Variant > parts[ idx ].Variants.Length )
{
throw new InvalidImcVariantException();
}
return ref parts[ idx ].Variants[ imc.Variant - 1 ];
}
public static ImcFile Clone( this ImcFile file )
{
var ret = new ImcFile
{
Count = file.Count,
PartMask = file.PartMask,
};
var parts = file.GetParts().Select( p => new ImcFile.ImageChangeParts()
{
DefaultVariant = p.DefaultVariant,
Variants = ( ImcFile.ImageChangeData[] )p.Variants.Clone(),
} ).ToArray();
var prop = ret.GetType().GetField( "Parts", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
prop!.SetValue( ret, parts );
return ret;
}
}
}

View file

@ -0,0 +1,166 @@
using System;
using System.Numerics;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Util;
namespace Penumbra.Meta.Files;
public struct ImcEntry : IEquatable< ImcEntry >
{
public byte MaterialId;
public byte DecalId;
private ushort _attributeAndSound;
public byte VfxId;
public byte MaterialAnimationId;
public ushort AttributeMask
=> ( ushort )( _attributeAndSound & 0x3FF );
public byte SoundId
=> ( byte )( _attributeAndSound >> 10 );
public bool Equals( ImcEntry other )
=> MaterialId == other.MaterialId
&& DecalId == other.DecalId
&& _attributeAndSound == other._attributeAndSound
&& VfxId == other.VfxId
&& MaterialAnimationId == other.MaterialAnimationId;
public override bool Equals( object? obj )
=> obj is ImcEntry other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( MaterialId, DecalId, _attributeAndSound, VfxId, MaterialAnimationId );
}
public unsafe class ImcFile : MetaBaseFile
{
private const int PreambleSize = 4;
public int ActualLength
=> NumParts * sizeof( ImcEntry ) * ( Count + 1 ) + PreambleSize;
public int Count
=> *( ushort* )Data;
public ushort PartMask
=> *( ushort* )( Data + 2 );
public readonly int NumParts;
public readonly Utf8GamePath Path;
public ImcEntry* DefaultPartPtr( int partIdx )
{
var flag = 1 << partIdx;
if( ( PartMask & flag ) == 0 )
{
return null;
}
return ( ImcEntry* )( Data + PreambleSize ) + partIdx;
}
public ImcEntry* VariantPtr( int partIdx, int variantIdx )
{
var flag = 1 << partIdx;
if( ( PartMask & flag ) == 0 || variantIdx >= Count )
{
return null;
}
var numParts = NumParts;
var ptr = ( ImcEntry* )( Data + PreambleSize );
ptr += numParts;
ptr += variantIdx * numParts;
ptr += partIdx;
return ptr;
}
public static int PartIndex( EquipSlot slot )
=> slot switch
{
EquipSlot.Head => 0,
EquipSlot.Ears => 0,
EquipSlot.Body => 1,
EquipSlot.Neck => 1,
EquipSlot.Hands => 2,
EquipSlot.Wrists => 2,
EquipSlot.Legs => 3,
EquipSlot.RFinger => 3,
EquipSlot.Feet => 4,
EquipSlot.LFinger => 4,
_ => 0,
};
public bool EnsureVariantCount( int numVariants )
{
if( numVariants <= Count )
{
return true;
}
var numParts = NumParts;
if( ActualLength > Length )
{
PluginLog.Warning( "Adding too many variants to IMC, size exceeded." );
return false;
}
var defaultPtr = ( ImcEntry* )( Data + PreambleSize );
var endPtr = defaultPtr + ( numVariants + 1 ) * numParts;
for( var ptr = defaultPtr + numParts; ptr < endPtr; ptr += numParts )
{
Functions.MemCpyUnchecked( ptr, defaultPtr, numParts * sizeof( ImcEntry ) );
}
PluginLog.Verbose( "Expanded imc from {Count} to {NewCount} variants.", Count, numVariants );
*( ushort* )Count = ( ushort )numVariants;
return true;
}
public bool SetEntry( int partIdx, int variantIdx, ImcEntry entry )
{
var numParts = NumParts;
if( partIdx >= numParts )
{
return false;
}
EnsureVariantCount( variantIdx + 1 );
var variantPtr = VariantPtr( partIdx, variantIdx );
if( variantPtr == null )
{
PluginLog.Error( "Error during expansion of imc file." );
return false;
}
if( variantPtr->Equals( entry ) )
{
return false;
}
*variantPtr = entry;
return true;
}
public ImcFile( Utf8GamePath path )
: base( 0 )
{
var file = Dalamud.GameData.GetFile( path.ToString() );
if( file == null )
{
throw new Exception();
}
fixed( byte* ptr = file.Data )
{
NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) );
AllocateData( file.Data.Length + sizeof( ImcEntry ) * 100 * NumParts );
Functions.MemCpyUnchecked( Data, ptr, file.Data.Length );
}
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Runtime.InteropServices;
namespace Penumbra.Meta.Files;
public unsafe class MetaBaseFile : IDisposable
{
public byte* Data { get; private set; }
public int Length { get; private set; }
public int Index { get; }
public MetaBaseFile( int idx )
=> Index = idx;
protected (IntPtr Data, int Length) DefaultData
=> Penumbra.CharacterUtility.DefaultResources[ Index ];
// Reset to default values.
public virtual void Reset()
{}
// Obtain memory.
protected void AllocateData( int length )
{
Length = length;
Data = ( byte* )Marshal.AllocHGlobal( length );
GC.AddMemoryPressure( length );
}
// Free memory.
protected void ReleaseUnmanagedResources()
{
Marshal.FreeHGlobal( ( IntPtr )Data );
GC.RemoveMemoryPressure( Length );
Length = 0;
Data = null;
}
// Manually free memory.
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize( this );
}
~MetaBaseFile()
{
ReleaseUnmanagedResources();
}
}

View file

@ -1,196 +0,0 @@
using System;
using System.Collections.Generic;
using Dalamud.Logging;
using Dalamud.Plugin;
using Lumina.Data;
using Lumina.Data.Files;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Util;
namespace Penumbra.Meta.Files
{
// This class manages the default meta files obtained via lumina from the game files themselves.
// On first call, the default version of any supported file will be cached and can be returned without reparsing.
public class MetaDefaults
{
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 );
}
if( path.EndsWith( ".cmp" ) )
{
return new CmpFile( rawFile.Data );
}
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" );
private CmpFile? GetDefaultCmpFile()
=> GetDefaultFile< CmpFile >( MetaFileNames.Cmp(), "Could not obtain Cmp file:\n" );
public EqdpFile? GetNewEqdpFile( EquipSlot slot, GenderRace gr )
=> GetDefaultEqdpFile( slot, gr )?.Clone();
public GmpFile? GetNewGmpFile()
=> GetDefaultGmpFile()?.Clone();
public EqpFile? GetNewEqpFile()
=> GetDefaultEqpFile()?.Clone();
public EstFile? GetNewEstFile( ObjectType type, EquipSlot equip, BodySlot body )
=> GetDefaultEstFile( type, equip, body )?.Clone();
public ImcFile? GetNewImcFile( ObjectType type, ushort primarySetId, ushort secondarySetId = 0 )
=> GetDefaultImcFile( type, primarySetId, secondarySetId )?.Clone();
public CmpFile? GetNewCmpFile()
=> GetDefaultCmpFile()?.Clone();
private static ImcFile GetImcFile( string path )
=> Dalamud.GameData.GetFile< ImcFile >( path )!;
private static FileResource FetchFile( string name )
=> Dalamud.GameData.GetFile( name )!;
// Check that a given meta manipulation is an actual change to the default value. We don't need to keep changes to default.
public bool CheckAgainstDefault( MetaManipulation m )
{
try
{
return m.Type switch
{
MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId )
?.GetValue( m ).Equal( m.ImcValue )
?? true,
MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId )
== m.GmpValue,
MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId )
.Reduce( m.EqpIdentifier.Slot )
== m.EqpValue,
MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )
?.GetEntry( m.EqdpIdentifier.SetId )
.Reduce( m.EqdpIdentifier.Slot )
== m.EqdpValue,
MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot )
?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId )
== m.EstValue,
MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ]
== m.RspValue,
_ => false,
};
}
catch( Exception e )
{
PluginLog.Error( $"Could not obtain default value for {m.CorrespondingFilename()} - {m.IdentifierString()}:\n{e}" );
}
return false;
}
public object? GetDefaultValue( MetaManipulation m )
{
try
{
return m.Type switch
{
MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId )
?.GetValue( m ),
MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ),
MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId )
.Reduce( m.EqpIdentifier.Slot ),
MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )
?.GetEntry( m.EqdpIdentifier.SetId )
.Reduce( m.EqdpIdentifier.Slot ),
MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot )
?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ),
MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ],
_ => false,
};
}
catch( Exception e )
{
PluginLog.Error( $"Could not obtain default value for {m.CorrespondingFilename()} - {m.IdentifierString()}:\n{e}" );
}
return false;
}
// Create a deep copy of a default file as a new file.
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 ),
MetaType.Rsp => GetNewCmpFile(),
_ => throw new NotImplementedException(),
};
}
}
}

View file

@ -1,172 +0,0 @@
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
// A struct for each type of meta change that contains all relevant information,
// to uniquely identify the corresponding file and location for the change.
// The first byte is guaranteed to be the MetaType enum for each case.
namespace Penumbra.Meta
{
public enum MetaType : byte
{
Unknown = 0,
Imc = 1,
Eqdp = 2,
Eqp = 3,
Est = 4,
Gmp = 5,
Rsp = 6,
};
[StructLayout( LayoutKind.Explicit )]
public struct EqpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public EquipSlot Slot;
[FieldOffset( 2 )]
public ushort SetId;
public override string ToString()
=> $"Eqp - {SetId} - {Slot}";
}
[StructLayout( LayoutKind.Explicit )]
public struct EqdpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public EquipSlot Slot;
[FieldOffset( 2 )]
public GenderRace GenderRace;
[FieldOffset( 4 )]
public ushort SetId;
public override string ToString()
=> $"Eqdp - {SetId} - {Slot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}";
}
[StructLayout( LayoutKind.Explicit )]
public struct GmpIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ushort SetId;
public override string ToString()
=> $"Gmp - {SetId}";
}
[StructLayout( LayoutKind.Explicit )]
public struct EstIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public ObjectType ObjectType;
[FieldOffset( 2 )]
public EquipSlot EquipSlot;
[FieldOffset( 3 )]
public BodySlot BodySlot;
[FieldOffset( 4 )]
public GenderRace GenderRace;
[FieldOffset( 6 )]
public ushort PrimaryId;
public override string ToString()
=> ObjectType == ObjectType.Equipment
? $"Est - {PrimaryId} - {EquipSlot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}"
: $"Est - {PrimaryId} - {BodySlot} - {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()}";
}
[StructLayout( LayoutKind.Explicit )]
public struct ImcIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public byte _objectAndBody;
public ObjectType ObjectType
{
get => ( ObjectType )( _objectAndBody & 0b00011111 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b11100000 ) | ( byte )value );
}
public BodySlot BodySlot
{
get => ( BodySlot )( _objectAndBody >> 5 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( ( byte )value << 5 ) );
}
[FieldOffset( 2 )]
public ushort PrimaryId;
[FieldOffset( 4 )]
public ushort Variant;
[FieldOffset( 6 )]
public ushort SecondaryId;
[FieldOffset( 6 )]
public EquipSlot EquipSlot;
public override string ToString()
{
return ObjectType switch
{
ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}",
ObjectType.Equipment => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}",
_ => $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}",
};
}
}
[StructLayout( LayoutKind.Explicit )]
public struct RspIdentifier
{
[FieldOffset( 0 )]
public ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 1 )]
public SubRace SubRace;
[FieldOffset( 2 )]
public RspAttribute Attribute;
public override string ToString()
=> $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}";
}
}

View file

@ -0,0 +1,56 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct EqdpManipulation : IEquatable< EqdpManipulation >
{
public readonly EqdpEntry Entry;
public readonly Gender Gender;
public readonly ModelRace Race;
public readonly ushort SetId;
public readonly EquipSlot Slot;
public EqdpManipulation( EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId )
{
Entry = Eqdp.Mask( slot ) & entry;
Gender = gender;
Race = race;
SetId = setId;
Slot = slot;
}
public override string ToString()
=> $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}";
public bool Equals( EqdpManipulation other )
=> Gender == other.Gender
&& Race == other.Race
&& SetId == other.SetId
&& Slot == other.Slot;
public override bool Equals( object? obj )
=> obj is EqdpManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Slot );
public int FileIndex()
=> CharacterUtility.EqdpIdx( Names.CombinedRace( Gender, Race ), Slot.IsAccessory() );
public bool Apply( ExpandedEqdpFile file )
{
var entry = file[ SetId ];
var mask = Eqdp.Mask( Slot );
if( ( entry & mask ) == Entry )
{
return false;
}
file[ SetId ] = ( entry & ~mask ) | Entry;
return true;
}
}

View file

@ -0,0 +1,50 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct EqpManipulation : IEquatable< EqpManipulation >
{
public readonly EqpEntry Entry;
public readonly ushort SetId;
public readonly EquipSlot Slot;
public EqpManipulation( EqpEntry entry, EquipSlot slot, ushort setId )
{
Slot = slot;
SetId = setId;
Entry = Eqp.Mask( slot ) & entry;
}
public override string ToString()
=> $"Eqp - {SetId} - {Slot}";
public bool Equals( EqpManipulation other )
=> Slot == other.Slot
&& SetId == other.SetId;
public override bool Equals( object? obj )
=> obj is EqpManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( ( int )Slot, SetId );
public int FileIndex()
=> CharacterUtility.EqpIdx;
public bool Apply( ExpandedEqpFile file )
{
var entry = file[ SetId ];
var mask = Eqp.Mask( Slot );
if( ( entry & mask ) == Entry )
{
return false;
}
file[ SetId ] = ( entry & ~mask ) | Entry;
return true;
}
}

View file

@ -0,0 +1,63 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct EstManipulation : IEquatable< EstManipulation >
{
public enum EstType : byte
{
Hair = CharacterUtility.HairEstIdx,
Face = CharacterUtility.FaceEstIdx,
Body = CharacterUtility.BodyEstIdx,
Head = CharacterUtility.HeadEstIdx,
}
public readonly ushort SkeletonIdx;
public readonly Gender Gender;
public readonly ModelRace Race;
public readonly ushort SetId;
public readonly EstType Type;
public EstManipulation( Gender gender, ModelRace race, EstType estType, ushort setId, ushort skeletonIdx )
{
SkeletonIdx = skeletonIdx;
Gender = gender;
Race = race;
SetId = setId;
Type = estType;
}
public override string ToString()
=> $"Est - {SetId} - {Type} - {Race.ToName()} {Gender.ToName()}";
public bool Equals( EstManipulation other )
=> Gender == other.Gender
&& Race == other.Race
&& SetId == other.SetId
&& Type == other.Type;
public override bool Equals( object? obj )
=> obj is EstManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( ( int )Gender, ( int )Race, SetId, ( int )Type );
public int FileIndex()
=> ( int )Type;
public bool Apply( EstFile file )
{
return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId, SkeletonIdx ) switch
{
EstFile.EstEntryChange.Unchanged => false,
EstFile.EstEntryChange.Changed => true,
EstFile.EstEntryChange.Added => true,
EstFile.EstEntryChange.Removed => true,
_ => throw new ArgumentOutOfRangeException(),
};
}
}

View file

@ -0,0 +1,45 @@
using System;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct GmpManipulation : IEquatable< GmpManipulation >
{
public readonly GmpEntry Entry;
public readonly ushort SetId;
public GmpManipulation( GmpEntry entry, ushort setId )
{
Entry = entry;
SetId = setId;
}
public override string ToString()
=> $"Gmp - {SetId}";
public bool Equals( GmpManipulation other )
=> SetId == other.SetId;
public override bool Equals( object? obj )
=> obj is GmpManipulation other && Equals( other );
public override int GetHashCode()
=> SetId.GetHashCode();
public int FileIndex()
=> CharacterUtility.GmpIdx;
public bool Apply( ExpandedGmpFile file )
{
var entry = file[ SetId ];
if( entry == Entry )
{
return false;
}
file[ SetId ] = Entry;
return true;
}
}

View file

@ -0,0 +1,61 @@
using System;
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
using Penumbra.Meta.Files;
using ImcFile = Lumina.Data.Files.ImcFile;
namespace Penumbra.Meta.Manipulations;
[StructLayout( LayoutKind.Sequential )]
public readonly struct ImcManipulation : IEquatable< ImcManipulation >
{
public readonly ImcEntry Entry;
public readonly ushort PrimaryId;
public readonly ushort Variant;
public readonly ushort SecondaryId;
public readonly ObjectType ObjectType;
public readonly EquipSlot EquipSlot;
public readonly BodySlot BodySlot;
public ImcManipulation( EquipSlot equipSlot, ushort variant, ushort primaryId, ImcEntry entry )
{
Entry = entry;
PrimaryId = primaryId;
Variant = variant;
SecondaryId = 0;
ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment;
EquipSlot = equipSlot;
BodySlot = BodySlot.Unknown;
}
public ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant,
ImcEntry entry )
{
Entry = entry;
ObjectType = objectType;
BodySlot = bodySlot;
SecondaryId = secondaryId;
PrimaryId = primaryId;
Variant = variant;
EquipSlot = EquipSlot.Unknown;
}
public override string ToString()
=> ObjectType is ObjectType.Equipment or ObjectType.Accessory
? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}"
: $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}";
public bool Equals( ImcManipulation other )
=> PrimaryId == other.PrimaryId
&& Variant == other.Variant
&& SecondaryId == other.SecondaryId
&& ObjectType == other.ObjectType
&& EquipSlot == other.EquipSlot
&& BodySlot == other.BodySlot;
public override bool Equals( object? obj )
=> obj is ImcManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( PrimaryId, Variant, SecondaryId, ( int )ObjectType, ( int )EquipSlot, ( int )BodySlot );
}

View file

@ -0,0 +1,109 @@
using System;
using System.Runtime.InteropServices;
namespace Penumbra.Meta.Manipulations;
[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )]
public readonly struct MetaManipulation : IEquatable< MetaManipulation >
{
public enum Type : byte
{
Eqp,
Gmp,
Eqdp,
Est,
Rsp,
Imc,
}
[FieldOffset( 0 )]
public readonly EqpManipulation Eqp = default;
[FieldOffset( 0 )]
public readonly GmpManipulation Gmp = default;
[FieldOffset( 0 )]
public readonly EqdpManipulation Eqdp = default;
[FieldOffset( 0 )]
public readonly EstManipulation Est = default;
[FieldOffset( 0 )]
public readonly RspManipulation Rsp = default;
[FieldOffset( 0 )]
public readonly ImcManipulation Imc = default;
[FieldOffset( 15 )]
public readonly Type ManipulationType;
public MetaManipulation( EqpManipulation eqp )
=> ( ManipulationType, Eqp ) = ( Type.Eqp, eqp );
public MetaManipulation( GmpManipulation gmp )
=> ( ManipulationType, Gmp ) = ( Type.Gmp, gmp );
public MetaManipulation( EqdpManipulation eqdp )
=> ( ManipulationType, Eqdp ) = ( Type.Eqdp, eqdp );
public MetaManipulation( EstManipulation est )
=> ( ManipulationType, Est ) = ( Type.Est, est );
public MetaManipulation( RspManipulation rsp )
=> ( ManipulationType, Rsp ) = ( Type.Rsp, rsp );
public MetaManipulation( ImcManipulation imc )
=> ( ManipulationType, Imc ) = ( Type.Imc, imc );
public static implicit operator MetaManipulation( EqpManipulation eqp )
=> new(eqp);
public static implicit operator MetaManipulation( GmpManipulation gmp )
=> new(gmp);
public static implicit operator MetaManipulation( EqdpManipulation eqdp )
=> new(eqdp);
public static implicit operator MetaManipulation( EstManipulation est )
=> new(est);
public static implicit operator MetaManipulation( RspManipulation rsp )
=> new(rsp);
public static implicit operator MetaManipulation( ImcManipulation imc )
=> new(imc);
public bool Equals( MetaManipulation other )
{
if( ManipulationType != other.ManipulationType )
{
return false;
}
return ManipulationType switch
{
Type.Eqp => Eqp.Equals( other.Eqp ),
Type.Gmp => Gmp.Equals( other.Gmp ),
Type.Eqdp => Eqdp.Equals( other.Eqdp ),
Type.Est => Est.Equals( other.Est ),
Type.Rsp => Rsp.Equals( other.Rsp ),
Type.Imc => Imc.Equals( other.Imc ),
_ => throw new ArgumentOutOfRangeException(),
};
}
public override bool Equals( object? obj )
=> obj is MetaManipulation other && Equals( other );
public override int GetHashCode()
=> ManipulationType switch
{
Type.Eqp => Eqp.GetHashCode(),
Type.Gmp => Gmp.GetHashCode(),
Type.Eqdp => Eqdp.GetHashCode(),
Type.Est => Est.GetHashCode(),
Type.Rsp => Rsp.GetHashCode(),
Type.Imc => Imc.GetHashCode(),
_ => throw new ArgumentOutOfRangeException(),
};
}

View file

@ -0,0 +1,48 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly struct RspManipulation : IEquatable< RspManipulation >
{
public readonly float Entry;
public readonly SubRace SubRace;
public readonly RspAttribute Attribute;
public RspManipulation( SubRace subRace, RspAttribute attribute, float entry )
{
Entry = entry;
SubRace = subRace;
Attribute = attribute;
}
public override string ToString()
=> $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}";
public bool Equals( RspManipulation other )
=> SubRace == other.SubRace
&& Attribute == other.Attribute;
public override bool Equals( object? obj )
=> obj is RspManipulation other && Equals( other );
public override int GetHashCode()
=> HashCode.Combine( ( int )SubRace, ( int )Attribute );
public int FileIndex()
=> CharacterUtility.HumanCmpIdx;
public bool Apply( CmpFile file )
{
var value = file[ SubRace, Attribute ];
if( value == Entry )
{
return false;
}
file[ SubRace, Attribute ] = Entry;
return true;
}
}

View file

@ -6,9 +6,8 @@ using Dalamud.Logging;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Importer;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mod;
using Penumbra.Util;
namespace Penumbra.Meta;

View file

@ -5,12 +5,273 @@ using System.Linq;
using Dalamud.Logging;
using Lumina.Data.Files;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Util;
namespace Penumbra.Meta;
public struct TemporaryImcFile : IDisposable
{
public void Dispose()
{
}
}
public class MetaManager2 : IDisposable
{
public readonly List< MetaBaseFile > ChangedData = new(7 + CharacterUtility.NumEqdpFiles);
public ExpandedEqpFile? EqpFile;
public ExpandedGmpFile? GmpFile;
public ExpandedEqdpFile?[] EqdpFile = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles];
public EstFile? FaceEstFile;
public EstFile? HairEstFile;
public EstFile? BodyEstFile;
public EstFile? HeadEstFile;
public CmpFile? CmpFile;
public readonly Dictionary< EqpManipulation, Mod.Mod > EqpManipulations = new();
public readonly Dictionary< EstManipulation, Mod.Mod > EstManipulations = new();
public readonly Dictionary< GmpManipulation, Mod.Mod > GmpManipulations = new();
public readonly Dictionary< RspManipulation, Mod.Mod > RspManipulations = new();
public readonly Dictionary< EqdpManipulation, Mod.Mod > EqdpManipulations = new();
public readonly Dictionary< ImcManipulation, Mod.Mod > ImcManipulations = new();
public readonly List< TemporaryImcFile > ImcFiles = new();
public void ResetEqp()
{
if( EqpFile != null )
{
EqpFile.Reset( EqpManipulations.Keys.Select( m => ( int )m.SetId ) );
EqpManipulations.Clear();
ChangedData.Remove( EqpFile );
}
}
public void ResetGmp()
{
if( GmpFile != null )
{
GmpFile.Reset( GmpManipulations.Keys.Select( m => ( int )m.SetId ) );
GmpManipulations.Clear();
ChangedData.Remove( GmpFile );
}
}
public void ResetCmp()
{
if( CmpFile != null )
{
CmpFile.Reset( RspManipulations.Keys.Select( m => ( m.SubRace, m.Attribute ) ) );
RspManipulations.Clear();
ChangedData.Remove( CmpFile );
}
}
public void ResetEst()
{
FaceEstFile?.Reset();
HairEstFile?.Reset();
BodyEstFile?.Reset();
HeadEstFile?.Reset();
RspManipulations.Clear();
ChangedData.RemoveAll( f => f is EstFile );
}
public void ResetEqdp()
{
foreach( var file in EqdpFile )
{
file?.Reset( EqdpManipulations.Keys.Where( m => m.FileIndex() == file.Index ).Select( m => ( int )m.SetId ) );
}
ChangedData.RemoveAll( f => f is ExpandedEqdpFile );
EqdpManipulations.Clear();
}
public void ResetImc()
{
foreach( var file in ImcFiles )
file.Dispose();
ImcFiles.Clear();
ImcManipulations.Clear();
}
public void Reset()
{
ChangedData.Clear();
ResetEqp();
ResetGmp();
ResetCmp();
ResetEst();
ResetEqdp();
ResetImc();
}
private static void Dispose< T >( ref T? file ) where T : class, IDisposable
{
if( file != null )
{
file.Dispose();
file = null;
}
}
public void Dispose()
{
ChangedData.Clear();
EqpManipulations.Clear();
EstManipulations.Clear();
GmpManipulations.Clear();
RspManipulations.Clear();
EqdpManipulations.Clear();
Dispose( ref EqpFile );
Dispose( ref GmpFile );
Dispose( ref FaceEstFile );
Dispose( ref HairEstFile );
Dispose( ref BodyEstFile );
Dispose( ref HeadEstFile );
Dispose( ref CmpFile );
for( var i = 0; i < CharacterUtility.NumEqdpFiles; ++i )
{
Dispose( ref EqdpFile[ i ] );
}
ResetImc();
}
private void AddFile( MetaBaseFile file )
{
if( !ChangedData.Contains( file ) )
{
ChangedData.Add( file );
}
}
public bool ApplyMod( EqpManipulation m, Mod.Mod mod )
{
if( !EqpManipulations.TryAdd( m, mod ) )
{
return false;
}
EqpFile ??= new ExpandedEqpFile();
if( !m.Apply( EqpFile ) )
{
return false;
}
AddFile( EqpFile );
return true;
}
public bool ApplyMod( GmpManipulation m, Mod.Mod mod )
{
if( !GmpManipulations.TryAdd( m, mod ) )
{
return false;
}
GmpFile ??= new ExpandedGmpFile();
if( !m.Apply( GmpFile ) )
{
return false;
}
AddFile( GmpFile );
return true;
}
public bool ApplyMod( EstManipulation m, Mod.Mod mod )
{
if( !EstManipulations.TryAdd( m, mod ) )
{
return false;
}
var file = m.Type switch
{
EstManipulation.EstType.Hair => HairEstFile ??= new EstFile( EstManipulation.EstType.Hair ),
EstManipulation.EstType.Face => FaceEstFile ??= new EstFile( EstManipulation.EstType.Face ),
EstManipulation.EstType.Body => BodyEstFile ??= new EstFile( EstManipulation.EstType.Body ),
EstManipulation.EstType.Head => HeadEstFile ??= new EstFile( EstManipulation.EstType.Head ),
_ => throw new ArgumentOutOfRangeException(),
};
if( !m.Apply( file ) )
{
return false;
}
AddFile( file );
return true;
}
public bool ApplyMod( RspManipulation m, Mod.Mod mod )
{
if( !RspManipulations.TryAdd( m, mod ) )
{
return false;
}
CmpFile ??= new CmpFile();
if( !m.Apply( CmpFile ) )
{
return false;
}
AddFile( CmpFile );
return true;
}
public bool ApplyMod( EqdpManipulation m, Mod.Mod mod )
{
if( !EqdpManipulations.TryAdd( m, mod ) )
{
return false;
}
var file = EqdpFile[ m.FileIndex() - 2 ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() );
if( !m.Apply( file ) )
{
return false;
}
AddFile( file );
return true;
}
public bool ApplyMod( ImcManipulation m, Mod.Mod mod )
{
if( !ImcManipulations.TryAdd( m, mod ) )
{
return false;
}
return true;
}
public bool ApplyMod( MetaManipulation m, Mod.Mod mod )
{
return m.ManipulationType switch
{
MetaManipulation.Type.Eqp => ApplyMod( m.Eqp, mod ),
MetaManipulation.Type.Gmp => ApplyMod( m.Gmp, mod ),
MetaManipulation.Type.Eqdp => ApplyMod( m.Eqdp, mod ),
MetaManipulation.Type.Est => ApplyMod( m.Est, mod ),
MetaManipulation.Type.Rsp => ApplyMod( m.Rsp, mod ),
MetaManipulation.Type.Imc => ApplyMod( m.Imc, mod ),
_ => throw new ArgumentOutOfRangeException()
};
}
}
public class MetaManager : IDisposable
{
internal class FileInformation
@ -27,12 +288,7 @@ public class MetaManager : IDisposable
{
ByteData = Data switch
{
EqdpFile eqdp => eqdp.WriteBytes(),
EqpFile eqp => eqp.WriteBytes(),
GmpFile gmp => gmp.WriteBytes(),
EstFile est => est.WriteBytes(),
ImcFile imc => imc.WriteBytes(),
CmpFile cmp => cmp.WriteBytes(),
_ => throw new NotImplementedException(),
};
DisposeFile( CurrentFile );
@ -138,50 +394,6 @@ public class MetaManager : IDisposable
{
value.Write( _dir, key );
_resolvedFiles[ key ] = value.CurrentFile!.Value;
if( value.Data is EqpFile )
{
EqpData = value.ByteData;
}
}
}
public bool ApplyMod( MetaManipulation m, Mod.Mod mod )
{
if( _currentManipulations.ContainsKey( m ) )
{
return false;
}
_currentManipulations.Add( m, mod );
var gamePath = Utf8GamePath.FromString(m.CorrespondingFilename(), out var p, false) ? p : Utf8GamePath.Empty; // TODO
try
{
if( !_currentFiles.TryGetValue( gamePath, out var file ) )
{
file = new FileInformation( Penumbra.MetaDefaults.CreateNewFile( m ) ?? throw new IOException() )
{
Changed = true,
CurrentFile = null,
};
_currentFiles[ gamePath ] = file;
}
file.Changed |= m.Type switch
{
MetaType.Eqp => m.Apply( ( EqpFile )file.Data ),
MetaType.Eqdp => m.Apply( ( EqdpFile )file.Data ),
MetaType.Gmp => m.Apply( ( GmpFile )file.Data ),
MetaType.Est => m.Apply( ( EstFile )file.Data ),
MetaType.Imc => m.Apply( ( ImcFile )file.Data ),
MetaType.Rsp => m.Apply( ( CmpFile )file.Data ),
_ => throw new NotImplementedException(),
};
return true;
}
catch( Exception e )
{
PluginLog.Error( $"Could not obtain default file for manipulation {m.CorrespondingFilename()}:\n{e}" );
return false;
}
}
}

View file

@ -1,259 +0,0 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Penumbra.Meta.Files;
using Swan;
using ImcFile = Lumina.Data.Files.ImcFile;
namespace Penumbra.Meta
{
// Write a single meta manipulation as a Base64string of the 16 bytes defining it.
public class MetaManipulationConverter : JsonConverter< MetaManipulation >
{
public override void WriteJson( JsonWriter writer, MetaManipulation manip, JsonSerializer serializer )
{
var s = Convert.ToBase64String( manip.ToBytes() );
writer.WriteValue( s );
}
public override MetaManipulation ReadJson( JsonReader reader, Type objectType, MetaManipulation existingValue, bool hasExistingValue,
JsonSerializer serializer )
{
if( reader.TokenType != JsonToken.String )
{
throw new JsonReaderException();
}
var bytes = Convert.FromBase64String( ( string )reader.Value! );
using MemoryStream m = new( bytes );
using BinaryReader br = new( m );
var i = br.ReadUInt64();
var v = br.ReadUInt64();
return new MetaManipulation( i, v );
}
}
// A MetaManipulation is a union of a type of Identifier (first 8 bytes, cf. Identifier.cs)
// and the appropriate Value to change the meta entry to (the other 8 bytes).
// Its comparison for sorting and hashes depends only on the identifier.
// The first byte is guaranteed to be a MetaType enum value in any case, so Type can always be read.
[StructLayout( LayoutKind.Explicit )]
[JsonConverter( typeof( MetaManipulationConverter ) )]
public struct MetaManipulation : IComparable
{
public static MetaManipulation Eqp( EquipSlot equipSlot, ushort setId, EqpEntry value )
=> new()
{
EqpIdentifier = new EqpIdentifier()
{
Type = MetaType.Eqp,
Slot = equipSlot,
SetId = setId,
},
EqpValue = value,
};
public static MetaManipulation Eqdp( EquipSlot equipSlot, GenderRace gr, ushort setId, EqdpEntry value )
=> new()
{
EqdpIdentifier = new EqdpIdentifier()
{
Type = MetaType.Eqdp,
Slot = equipSlot,
GenderRace = gr,
SetId = setId,
},
EqdpValue = value,
};
public static MetaManipulation Gmp( ushort setId, GmpEntry value )
=> new()
{
GmpIdentifier = new GmpIdentifier()
{
Type = MetaType.Gmp,
SetId = setId,
},
GmpValue = value,
};
public static MetaManipulation Est( ObjectType type, EquipSlot equipSlot, GenderRace gr, BodySlot bodySlot, ushort setId,
ushort value )
=> new()
{
EstIdentifier = new EstIdentifier()
{
Type = MetaType.Est,
ObjectType = type,
GenderRace = gr,
EquipSlot = equipSlot,
BodySlot = bodySlot,
PrimaryId = setId,
},
EstValue = value,
};
public static MetaManipulation Imc( ObjectType type, BodySlot secondaryType, ushort primaryId, ushort secondaryId
, ushort idx, ImcFile.ImageChangeData value )
=> new()
{
ImcIdentifier = new ImcIdentifier()
{
Type = MetaType.Imc,
ObjectType = type,
BodySlot = secondaryType,
PrimaryId = primaryId,
SecondaryId = secondaryId,
Variant = idx,
},
ImcValue = value,
};
public static MetaManipulation Imc( EquipSlot slot, ushort primaryId, ushort idx, ImcFile.ImageChangeData value )
=> new()
{
ImcIdentifier = new ImcIdentifier()
{
Type = MetaType.Imc,
ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment,
EquipSlot = slot,
PrimaryId = primaryId,
Variant = idx,
},
ImcValue = value,
};
public static MetaManipulation Rsp( SubRace subRace, RspAttribute attribute, float value )
=> new()
{
RspIdentifier = new RspIdentifier()
{
Type = MetaType.Rsp,
SubRace = subRace,
Attribute = attribute,
},
RspValue = value,
};
internal MetaManipulation( ulong identifier, ulong value )
: this()
{
Identifier = identifier;
Value = value;
}
[FieldOffset( 0 )]
public readonly ulong Identifier;
[FieldOffset( 8 )]
public readonly ulong Value;
[FieldOffset( 0 )]
public MetaType Type;
[FieldOffset( 0 )]
public EqpIdentifier EqpIdentifier;
[FieldOffset( 0 )]
public GmpIdentifier GmpIdentifier;
[FieldOffset( 0 )]
public EqdpIdentifier EqdpIdentifier;
[FieldOffset( 0 )]
public EstIdentifier EstIdentifier;
[FieldOffset( 0 )]
public ImcIdentifier ImcIdentifier;
[FieldOffset( 0 )]
public RspIdentifier RspIdentifier;
[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.
[FieldOffset( 8 )]
public float RspValue;
public override int GetHashCode()
=> Identifier.GetHashCode();
public int CompareTo( object? rhs )
=> Identifier.CompareTo( rhs is MetaManipulation m ? m.Identifier : null );
public GamePath CorrespondingFilename()
{
return Type switch
{
MetaType.Eqp => MetaFileNames.Eqp(),
MetaType.Eqdp => MetaFileNames.Eqdp( EqdpIdentifier.Slot, EqdpIdentifier.GenderRace ),
MetaType.Est => MetaFileNames.Est( EstIdentifier.ObjectType, EstIdentifier.EquipSlot, EstIdentifier.BodySlot ),
MetaType.Gmp => MetaFileNames.Gmp(),
MetaType.Imc => MetaFileNames.Imc( ImcIdentifier.ObjectType, ImcIdentifier.PrimaryId, ImcIdentifier.SecondaryId ),
MetaType.Rsp => MetaFileNames.Cmp(),
_ => throw new InvalidEnumArgumentException(),
};
}
// No error checking.
public bool Apply( EqpFile file )
=> file[ EqpIdentifier.SetId ].Apply( this );
public bool Apply( EqdpFile file )
=> file[ EqdpIdentifier.SetId ].Apply( this );
public bool Apply( GmpFile file )
=> file.SetEntry( GmpIdentifier.SetId, GmpValue );
public bool Apply( EstFile file )
=> file.SetEntry( EstIdentifier.GenderRace, EstIdentifier.PrimaryId, EstValue );
public bool Apply( ImcFile file )
{
ref var value = ref file.GetValue( this );
if( ImcValue.Equal( value ) )
{
return false;
}
value = ImcValue;
return true;
}
public bool Apply( CmpFile file )
=> file.Set( RspIdentifier.SubRace, RspIdentifier.Attribute, RspValue );
public string IdentifierString()
{
return Type switch
{
MetaType.Eqp => EqpIdentifier.ToString(),
MetaType.Eqdp => EqdpIdentifier.ToString(),
MetaType.Est => EstIdentifier.ToString(),
MetaType.Gmp => GmpIdentifier.ToString(),
MetaType.Imc => ImcIdentifier.ToString(),
MetaType.Rsp => RspIdentifier.ToString(),
_ => throw new InvalidEnumArgumentException(),
};
}
}
}

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Mod;

View file

@ -304,7 +304,7 @@ public class ModCollectionCache
{
if( !MetaManipulations.TryGetValue( manip, out var oldMod ) )
{
MetaManipulations.ApplyMod( manip, mod );
//MetaManipulations.ApplyMod( manip, mod );
}
else
{

View file

@ -15,9 +15,13 @@ using Penumbra.PlayerWatch;
using Penumbra.UI;
using Penumbra.Util;
using System.Linq;
using Penumbra.Meta.Manipulations;
namespace Penumbra;
public class MetaDefaults
{
}
public class Penumbra : IDalamudPlugin
{
public string Name
@ -33,6 +37,7 @@ public class Penumbra : IDalamudPlugin
public static ResidentResourceManager ResidentResources { get; private set; } = null!;
public static CharacterUtility CharacterUtility { get; private set; } = null!;
public static MetaDefaults MetaDefaults { get; private set; } = null!;
public static ModManager ModManager { get; private set; } = null!;
@ -114,6 +119,17 @@ public class Penumbra : IDalamudPlugin
ResourceLoader.EnableDebug();
if (Config.EnableFullResourceLogging)
ResourceLoader.EnableFullLogging();
unsafe
{
PluginLog.Information( $"MetaManipulation: {sizeof( MetaManipulation )}" );
PluginLog.Information( $"EqpManipulation: {sizeof( EqpManipulation )}" );
PluginLog.Information( $"GmpManipulation: {sizeof( GmpManipulation )}" );
PluginLog.Information( $"EqdpManipulation: {sizeof( EqdpManipulation )}" );
PluginLog.Information( $"EstManipulation: {sizeof( EstManipulation )}" );
PluginLog.Information( $"RspManipulation: {sizeof( RspManipulation )}" );
PluginLog.Information( $"ImcManipulation: {sizeof( ImcManipulation )}" );
}
}
public bool Enable()

View file

@ -108,12 +108,12 @@ public partial class SettingsInterface
DrawLine( gp, fp );
}
foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations
.Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) )
.Where( CheckFilters ) )
{
DrawLine( mp, mod );
}
//foreach( var (mp, mod, _) in cache.MetaManipulations.Manipulations
// .Select( p => ( p.Item1.IdentifierString(), p.Item2.Data.Meta.Name, p.Item2.Data.Meta.LowerName ) )
// .Where( CheckFilters ) )
//{
// DrawLine( mp, mod );
//}
}
if( active != null )
@ -193,7 +193,7 @@ public partial class SettingsInterface
else if( ( row -= activeResolved ) < activeMeta )
{
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
DrawLine( manip.ToString(), mod.Data.Meta.Name );
}
else if( ( row -= activeMeta ) < forcedResolved )
{
@ -204,7 +204,7 @@ public partial class SettingsInterface
{
row -= forcedResolved;
var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row );
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
DrawLine( manip.ToString(), mod.Data.Meta.Name );
}
}
}

View file

@ -225,7 +225,7 @@ public partial class SettingsInterface
foreach( var manip in manipulations )
{
ImGui.Text( manip.IdentifierString() );
//ImGui.Text( manip.IdentifierString() );
}
indent.Pop( 15f );

View file

@ -10,6 +10,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.UI.Custom;
using Penumbra.Util;
using ObjectType = Penumbra.GameData.Enums.ObjectType;
@ -223,32 +224,32 @@ namespace Penumbra.UI
private bool DrawEqpRow( int manipIdx, IList< MetaManipulation > list )
{
var ret = false;
var id = list[ manipIdx ].EqpIdentifier;
var val = list[ manipIdx ].EqpValue;
if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
{
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
var defaults = ( EqpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!;
var attributes = Eqp.EqpAttributes[ id.Slot ];
foreach( var flag in attributes )
{
var name = flag.ToLocalName();
var tmp = val.HasFlag( flag );
if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) )
{
list[ manipIdx ] = MetaManipulation.Eqp( id.Slot, id.SetId, tmp ? val | flag : val & ~flag );
ret = true;
}
}
}
ImGui.Text( ObjectType.Equipment.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.SetId.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.Slot.ToString() );
//var id = list[ manipIdx ].EqpIdentifier;
//var val = list[ manipIdx ].EqpValue;
//
//if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
//{
// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
// var defaults = ( EqpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!;
// var attributes = Eqp.EqpAttributes[ id.Slot ];
//
// foreach( var flag in attributes )
// {
// var name = flag.ToLocalName();
// var tmp = val.HasFlag( flag );
// if( PrintCheckBox( $"{name}##manip", ref tmp, defaults.HasFlag( flag ) ) && _editMode && tmp != val.HasFlag( flag ) )
// {
// list[ manipIdx ] = MetaManipulation.Eqp( id.Slot, id.SetId, tmp ? val | flag : val & ~flag );
// ret = true;
// }
// }
//}
//
//ImGui.Text( ObjectType.Equipment.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.SetId.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.Slot.ToString() );
return ret;
}
@ -256,42 +257,42 @@ namespace Penumbra.UI
{
var defaults = ( GmpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!;
var ret = false;
var id = list[ manipIdx ].GmpIdentifier;
var val = list[ manipIdx ].GmpValue;
if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
{
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
var enabled = val.Enabled;
var animated = val.Animated;
var rotationA = val.RotationA;
var rotationB = val.RotationB;
var rotationC = val.RotationC;
ushort unk = val.UnknownTotal;
ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled;
ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated );
ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF );
ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF );
ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF );
ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF );
if( ret && _editMode )
{
list[ manipIdx ] = MetaManipulation.Gmp( id.SetId,
new GmpEntry
{
Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk,
RotationA = rotationA, RotationB = rotationB, RotationC = rotationC,
} );
}
}
ImGui.Text( ObjectType.Equipment.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.SetId.ToString() );
ImGui.TableNextColumn();
ImGui.Text( EquipSlot.Head.ToString() );
//var id = list[ manipIdx ].GmpIdentifier;
//var val = list[ manipIdx ].GmpValue;
//
//if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
//{
// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
// var enabled = val.Enabled;
// var animated = val.Animated;
// var rotationA = val.RotationA;
// var rotationB = val.RotationB;
// var rotationC = val.RotationC;
// ushort unk = val.UnknownTotal;
//
// ret |= PrintCheckBox( "Visor Enabled##manip", ref enabled, defaults.Enabled ) && enabled != val.Enabled;
// ret |= PrintCheckBox( "Visor Animated##manip", ref animated, defaults.Animated );
// ret |= DrawInputWithDefault( "Rotation A##manip", ref rotationA, defaults.RotationA, 0x3FF );
// ret |= DrawInputWithDefault( "Rotation B##manip", ref rotationB, defaults.RotationB, 0x3FF );
// ret |= DrawInputWithDefault( "Rotation C##manip", ref rotationC, defaults.RotationC, 0x3FF );
// ret |= DrawInputWithDefault( "Unknown Byte##manip", ref unk, defaults.UnknownTotal, 0xFF );
//
// if( ret && _editMode )
// {
// list[ manipIdx ] = MetaManipulation.Gmp( id.SetId,
// new GmpEntry
// {
// Animated = animated, Enabled = enabled, UnknownTotal = ( byte )unk,
// RotationA = rotationA, RotationB = rotationB, RotationC = rotationC,
// } );
// }
//}
//
//ImGui.Text( ObjectType.Equipment.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.SetId.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( EquipSlot.Head.ToString() );
return ret;
}
@ -366,36 +367,36 @@ namespace Penumbra.UI
{
var defaults = ( EqdpEntry )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!;
var ret = false;
var id = list[ manipIdx ].EqdpIdentifier;
var val = list[ manipIdx ].EqdpValue;
if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
{
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
var (bit1, bit2) = GetEqdpBits( id.Slot, val );
var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults );
ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 );
ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 );
if( ret && _editMode )
{
list[ manipIdx ] = MetaManipulation.Eqdp( id.Slot, id.GenderRace, id.SetId, SetEqdpBits( id.Slot, val, bit1, bit2 ) );
}
}
ImGui.Text( id.Slot.IsAccessory()
? ObjectType.Accessory.ToString()
: ObjectType.Equipment.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.SetId.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.Slot.ToString() );
ImGui.TableNextColumn();
var (gender, race) = id.GenderRace.Split();
ImGui.Text( race.ToString() );
ImGui.TableNextColumn();
ImGui.Text( gender.ToString() );
//var id = list[ manipIdx ].EqdpIdentifier;
//var val = list[ manipIdx ].EqdpValue;
//
//if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
//{
// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
// var (bit1, bit2) = GetEqdpBits( id.Slot, val );
// var (defBit1, defBit2) = GetEqdpBits( id.Slot, defaults );
//
// ret |= PrintCheckBox( "Bit 1##manip", ref bit1, defBit1 );
// ret |= PrintCheckBox( "Bit 2##manip", ref bit2, defBit2 );
//
// if( ret && _editMode )
// {
// list[ manipIdx ] = MetaManipulation.Eqdp( id.Slot, id.GenderRace, id.SetId, SetEqdpBits( id.Slot, val, bit1, bit2 ) );
// }
//}
//
//ImGui.Text( id.Slot.IsAccessory()
// ? ObjectType.Accessory.ToString()
// : ObjectType.Equipment.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.SetId.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.Slot.ToString() );
//ImGui.TableNextColumn();
//var (gender, race) = id.GenderRace.Split();
//ImGui.Text( race.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( gender.ToString() );
return ret;
}
@ -403,30 +404,30 @@ namespace Penumbra.UI
{
var defaults = ( ushort )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!;
var ret = false;
var id = list[ manipIdx ].EstIdentifier;
var val = list[ manipIdx ].EstValue;
if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
{
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode )
{
list[ manipIdx ] = new MetaManipulation( id.Value, val );
ret = true;
}
}
ImGui.Text( id.ObjectType.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.PrimaryId.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.ObjectType == ObjectType.Equipment
? id.EquipSlot.ToString()
: id.BodySlot.ToString() );
ImGui.TableNextColumn();
var (gender, race) = id.GenderRace.Split();
ImGui.Text( race.ToString() );
ImGui.TableNextColumn();
ImGui.Text( gender.ToString() );
//var id = list[ manipIdx ].EstIdentifier;
//var val = list[ manipIdx ].EstValue;
//if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
//{
// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
// if( DrawInputWithDefault( "No Idea what this does!##manip", ref val, defaults, ushort.MaxValue ) && _editMode )
// {
// list[ manipIdx ] = new MetaManipulation( id.Value, val );
// ret = true;
// }
//}
//
//ImGui.Text( id.ObjectType.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.PrimaryId.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.ObjectType == ObjectType.Equipment
// ? id.EquipSlot.ToString()
// : id.BodySlot.ToString() );
//ImGui.TableNextColumn();
//var (gender, race) = id.GenderRace.Split();
//ImGui.Text( race.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( gender.ToString() );
return ret;
}
@ -435,58 +436,58 @@ namespace Penumbra.UI
{
var defaults = ( ImcFile.ImageChangeData )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!;
var ret = false;
var id = list[ manipIdx ].ImcIdentifier;
var val = list[ manipIdx ].ImcValue;
if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
{
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
ushort materialId = val.MaterialId;
ushort vfxId = val.VfxId;
ushort decalId = val.DecalId;
var soundId = ( ushort )( val.SoundId >> 10 );
var attributeMask = val.AttributeMask;
var materialAnimationId = ( ushort )( val.MaterialAnimationId >> 12 );
ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue );
ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue );
ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue );
ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F );
ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF );
ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId,
byte.MaxValue );
if( ret && _editMode )
{
var value = ImcExtensions.FromValues( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId,
( byte )vfxId, ( byte )materialAnimationId );
list[ manipIdx ] = new MetaManipulation( id.Value, value.ToInteger() );
}
}
ImGui.Text( id.ObjectType.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.PrimaryId.ToString() );
ImGui.TableNextColumn();
if( id.ObjectType == ObjectType.Accessory
|| id.ObjectType == ObjectType.Equipment )
{
ImGui.Text( id.ObjectType == ObjectType.Equipment
|| id.ObjectType == ObjectType.Accessory
? id.EquipSlot.ToString()
: id.BodySlot.ToString() );
}
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
if( id.ObjectType != ObjectType.Equipment
&& id.ObjectType != ObjectType.Accessory )
{
ImGui.Text( id.SecondaryId.ToString() );
}
ImGui.TableNextColumn();
ImGui.Text( id.Variant.ToString() );
//var id = list[ manipIdx ].ImcIdentifier;
//var val = list[ manipIdx ].ImcValue;
//
//if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
//{
// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
// ushort materialId = val.MaterialId;
// ushort vfxId = val.VfxId;
// ushort decalId = val.DecalId;
// var soundId = ( ushort )( val.SoundId >> 10 );
// var attributeMask = val.AttributeMask;
// var materialAnimationId = ( ushort )( val.MaterialAnimationId >> 12 );
// ret |= DrawInputWithDefault( "Material Id", ref materialId, defaults.MaterialId, byte.MaxValue );
// ret |= DrawInputWithDefault( "Vfx Id", ref vfxId, defaults.VfxId, byte.MaxValue );
// ret |= DrawInputWithDefault( "Decal Id", ref decalId, defaults.DecalId, byte.MaxValue );
// ret |= DrawInputWithDefault( "Sound Id", ref soundId, defaults.SoundId, 0x3F );
// ret |= DrawInputWithDefault( "Attribute Mask", ref attributeMask, defaults.AttributeMask, 0x3FF );
// ret |= DrawInputWithDefault( "Material Animation Id", ref materialAnimationId, defaults.MaterialAnimationId,
// byte.MaxValue );
//
// if( ret && _editMode )
// {
// var value = ImcExtensions.FromValues( ( byte )materialId, ( byte )decalId, attributeMask, ( byte )soundId,
// ( byte )vfxId, ( byte )materialAnimationId );
// list[ manipIdx ] = new MetaManipulation( id.Value, value.ToInteger() );
// }
//}
//
//ImGui.Text( id.ObjectType.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.PrimaryId.ToString() );
//ImGui.TableNextColumn();
//if( id.ObjectType == ObjectType.Accessory
// || id.ObjectType == ObjectType.Equipment )
//{
// ImGui.Text( id.ObjectType == ObjectType.Equipment
// || id.ObjectType == ObjectType.Accessory
// ? id.EquipSlot.ToString()
// : id.BodySlot.ToString() );
//}
//
//ImGui.TableNextColumn();
//ImGui.TableNextColumn();
//ImGui.TableNextColumn();
//if( id.ObjectType != ObjectType.Equipment
// && id.ObjectType != ObjectType.Accessory )
//{
// ImGui.Text( id.SecondaryId.ToString() );
//}
//
//ImGui.TableNextColumn();
//ImGui.Text( id.Variant.ToString() );
return ret;
}
@ -494,45 +495,45 @@ namespace Penumbra.UI
{
var defaults = ( float )Penumbra.MetaDefaults.GetDefaultValue( list[ manipIdx ] )!;
var ret = false;
var id = list[ manipIdx ].RspIdentifier;
var val = list[ manipIdx ].RspValue;
if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
{
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
if( DefaultButton(
$"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults )
&& _editMode )
{
list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, defaults );
ret = true;
}
ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f",
_editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly )
&& val >= 0
&& val <= 5
&& _editMode )
{
list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, val );
ret = true;
}
}
ImGui.Text( id.Attribute.ToUngenderedString() );
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.Text( id.SubRace.ToString() );
ImGui.TableNextColumn();
ImGui.Text( id.Attribute.ToGender().ToString() );
//var id = list[ manipIdx ].RspIdentifier;
//var val = list[ manipIdx ].RspValue;
//
//if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) )
//{
// using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
// if( DefaultButton(
// $"{( _editMode ? "Set to " : "" )}Default: {defaults:F3}##scaleManip", ref val, defaults )
// && _editMode )
// {
// list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, defaults );
// ret = true;
// }
//
// ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
// if( ImGui.InputFloat( "Scale###manip", ref val, 0, 0, "%.3f",
// _editMode ? ImGuiInputTextFlags.EnterReturnsTrue : ImGuiInputTextFlags.ReadOnly )
// && val >= 0
// && val <= 5
// && _editMode )
// {
// list[ manipIdx ] = MetaManipulation.Rsp( id.SubRace, id.Attribute, val );
// ret = true;
// }
//}
//
//ImGui.Text( id.Attribute.ToUngenderedString() );
//ImGui.TableNextColumn();
//ImGui.TableNextColumn();
//ImGui.TableNextColumn();
//ImGui.Text( id.SubRace.ToString() );
//ImGui.TableNextColumn();
//ImGui.Text( id.Attribute.ToGender().ToString() );
return ret;
}
private bool DrawManipulationRow( ref int manipIdx, IList< MetaManipulation > list, ref int count )
{
var type = list[ manipIdx ].Type;
var type = list[ manipIdx ].ManipulationType;
if( _editMode )
{
@ -553,40 +554,40 @@ namespace Penumbra.UI
ImGui.TableNextColumn();
var changes = false;
switch( type )
{
case MetaType.Eqp:
changes = DrawEqpRow( manipIdx, list );
break;
case MetaType.Gmp:
changes = DrawGmpRow( manipIdx, list );
break;
case MetaType.Eqdp:
changes = DrawEqdpRow( manipIdx, list );
break;
case MetaType.Est:
changes = DrawEstRow( manipIdx, list );
break;
case MetaType.Imc:
changes = DrawImcRow( manipIdx, list );
break;
case MetaType.Rsp:
changes = DrawRspRow( manipIdx, list );
break;
}
ImGui.TableSetColumnIndex( 9 );
if( ImGui.Selectable( $"{list[ manipIdx ].Value:X}##{manipIdx}" ) )
{
ImGui.OpenPopup( $"##MetaPopup{manipIdx}" );
}
ImGui.TableNextRow();
//switch( type )
//{
// case MetaType.Eqp:
// changes = DrawEqpRow( manipIdx, list );
// break;
// case MetaType.Gmp:
// changes = DrawGmpRow( manipIdx, list );
// break;
// case MetaType.Eqdp:
// changes = DrawEqdpRow( manipIdx, list );
// break;
// case MetaType.Est:
// changes = DrawEstRow( manipIdx, list );
// break;
// case MetaType.Imc:
// changes = DrawImcRow( manipIdx, list );
// break;
// case MetaType.Rsp:
// changes = DrawRspRow( manipIdx, list );
// break;
//}
//
//ImGui.TableSetColumnIndex( 9 );
//if( ImGui.Selectable( $"{list[ manipIdx ].Value:X}##{manipIdx}" ) )
//{
// ImGui.OpenPopup( $"##MetaPopup{manipIdx}" );
//}
//
//ImGui.TableNextRow();
return changes;
}
private MetaType DrawNewTypeSelection()
private MetaManipulation.Type DrawNewTypeSelection()
{
ImGui.RadioButton( "IMC##newManipType", ref _newManipTypeIdx, 1 );
ImGui.SameLine();
@ -599,7 +600,7 @@ namespace Penumbra.UI
ImGui.RadioButton( "GMP##newManipType", ref _newManipTypeIdx, 5 );
ImGui.SameLine();
ImGui.RadioButton( "RSP##newManipType", ref _newManipTypeIdx, 6 );
return ( MetaType )_newManipTypeIdx;
return ( MetaManipulation.Type )_newManipTypeIdx;
}
private bool DrawNewManipulationPopup( string popupName, IList< MetaManipulation > list, ref int count )
@ -615,7 +616,7 @@ namespace Penumbra.UI
MetaManipulation? newManip = null;
switch( manipType )
{
case MetaType.Imc:
case MetaManipulation.Type.Imc:
{
RestrictedInputInt( "Set Id##newManipImc", ref _newManipSetId, 0, ushort.MaxValue );
RestrictedInputInt( "Variant##newManipImc", ref _newManipVariant, 0, byte.MaxValue );
@ -625,39 +626,39 @@ namespace Penumbra.UI
{
case ObjectType.Equipment:
CustomCombo( "Equipment Slot", EqdpEquipSlots, out equipSlot, ref _newManipEquipSlot );
newManip = MetaManipulation.Imc( equipSlot, _newManipSetId, _newManipVariant,
new ImcFile.ImageChangeData() );
//newManip = MetaManipulation.Imc( equipSlot, _newManipSetId, _newManipVariant,
// new ImcFile.ImageChangeData() );
break;
case ObjectType.DemiHuman:
case ObjectType.Weapon:
case ObjectType.Monster:
RestrictedInputInt( "Secondary Id##newManipImc", ref _newManipSecondaryId, 0, ushort.MaxValue );
CustomCombo( "Body Slot", ImcBodySlots, out var bodySlot, ref _newManipBodySlot );
newManip = MetaManipulation.Imc( objectType, bodySlot, _newManipSetId, _newManipSecondaryId,
_newManipVariant, new ImcFile.ImageChangeData() );
//newManip = MetaManipulation.Imc( objectType, bodySlot, _newManipSetId, _newManipSecondaryId,
// _newManipVariant, new ImcFile.ImageChangeData() );
break;
}
break;
}
case MetaType.Eqdp:
case MetaManipulation.Type.Eqdp:
{
RestrictedInputInt( "Set Id##newManipEqdp", ref _newManipSetId, 0, ushort.MaxValue );
CustomCombo( "Equipment Slot", EqdpEquipSlots, out var equipSlot, ref _newManipEquipSlot );
CustomCombo( "Race", Races, out var race, ref _newManipRace );
CustomCombo( "Gender", Genders, out var gender, ref _newManipGender );
newManip = MetaManipulation.Eqdp( equipSlot, Names.CombinedRace( gender, race ), ( ushort )_newManipSetId,
new EqdpEntry() );
//newManip = MetaManipulation.Eqdp( equipSlot, Names.CombinedRace( gender, race ), ( ushort )_newManipSetId,
// new EqdpEntry() );
break;
}
case MetaType.Eqp:
case MetaManipulation.Type.Eqp:
{
RestrictedInputInt( "Set Id##newManipEqp", ref _newManipSetId, 0, ushort.MaxValue );
CustomCombo( "Equipment Slot", EqpEquipSlots, out var equipSlot, ref _newManipEquipSlot );
newManip = MetaManipulation.Eqp( equipSlot, ( ushort )_newManipSetId, 0 );
//newManip = MetaManipulation.Eqp( equipSlot, ( ushort )_newManipSetId, 0 );
break;
}
case MetaType.Est:
case MetaManipulation.Type.Est:
{
RestrictedInputInt( "Set Id##newManipEst", ref _newManipSetId, 0, ushort.MaxValue );
CustomCombo( "Object Type", ObjectTypes, out var objectType, ref _newManipObjectType );
@ -675,47 +676,47 @@ namespace Penumbra.UI
CustomCombo( "Race", Races, out var race, ref _newManipRace );
CustomCombo( "Gender", Genders, out var gender, ref _newManipGender );
newManip = MetaManipulation.Est( objectType, equipSlot, Names.CombinedRace( gender, race ), bodySlot,
( ushort )_newManipSetId, 0 );
//newManip = MetaManipulation.Est( objectType, equipSlot, Names.CombinedRace( gender, race ), bodySlot,
// ( ushort )_newManipSetId, 0 );
break;
}
case MetaType.Gmp:
case MetaManipulation.Type.Gmp:
RestrictedInputInt( "Set Id##newManipGmp", ref _newManipSetId, 0, ushort.MaxValue );
newManip = MetaManipulation.Gmp( ( ushort )_newManipSetId, new GmpEntry() );
//newManip = MetaManipulation.Gmp( ( ushort )_newManipSetId, new GmpEntry() );
break;
case MetaType.Rsp:
case MetaManipulation.Type.Rsp:
CustomCombo( "Subrace", Subraces, out var subRace, ref _newManipSubrace );
CustomCombo( "Attribute", RspAttributes, out var rspAttribute, ref _newManipAttribute );
newManip = MetaManipulation.Rsp( subRace, rspAttribute, 1f );
//newManip = MetaManipulation.Rsp( subRace, rspAttribute, 1f );
break;
}
if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 )
&& newManip != null
&& list.All( m => m.Identifier != newManip.Value.Identifier ) )
{
var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value );
if( def != null )
{
var manip = newManip.Value.Type switch
{
MetaType.Est => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ),
MetaType.Eqp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ),
MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, (ushort) def ),
MetaType.Gmp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ),
MetaType.Imc => new MetaManipulation( newManip.Value.Identifier,
( ( ImcFile.ImageChangeData )def ).ToInteger() ),
MetaType.Rsp => MetaManipulation.Rsp( newManip.Value.RspIdentifier.SubRace,
newManip.Value.RspIdentifier.Attribute, ( float )def ),
_ => throw new InvalidEnumArgumentException(),
};
list.Add( manip );
change = true;
++count;
}
ImGui.CloseCurrentPopup();
}
//if( ImGui.Button( "Create Manipulation##newManip", Vector2.UnitX * -1 )
// && newManip != null
// && list.All( m => m.Identifier != newManip.Value.Identifier ) )
//{
// var def = Penumbra.MetaDefaults.GetDefaultValue( newManip.Value );
// if( def != null )
// {
// var manip = newManip.Value.Type switch
// {
// MetaType.Est => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ),
// MetaType.Eqp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ),
// MetaType.Eqdp => new MetaManipulation( newManip.Value.Identifier, (ushort) def ),
// MetaType.Gmp => new MetaManipulation( newManip.Value.Identifier, ( ulong )def ),
// MetaType.Imc => new MetaManipulation( newManip.Value.Identifier,
// ( ( ImcFile.ImageChangeData )def ).ToInteger() ),
// MetaType.Rsp => MetaManipulation.Rsp( newManip.Value.RspIdentifier.SubRace,
// newManip.Value.RspIdentifier.Attribute, ( float )def ),
// _ => throw new InvalidEnumArgumentException(),
// };
// list.Add( manip );
// change = true;
// ++count;
// }
//
// ImGui.CloseCurrentPopup();
//}
return change;
}

View file

@ -82,7 +82,7 @@ public partial class SettingsInterface
if( ImGui.IsItemClicked() )
{
var data = ( ( Interop.Structs.ResourceHandle* )r )->GetData();
ImGui.SetClipboardText( string.Join( " ",
ImGui.SetClipboardText( ((IntPtr)( ( Interop.Structs.ResourceHandle* )r )->Data->VTable).ToString("X") + string.Join( " ",
new ReadOnlySpan< byte >( ( byte* )data.Data, data.Length ).ToArray().Select( b => b.ToString( "X2" ) ) ) );
//ImGuiNative.igSetClipboardText( ( byte* )Structs.ResourceHandle.GetData( ( IntPtr )r ) );
}