diff --git a/Penumbra.GameData/ByteString/Utf8String.Construction.cs b/Penumbra.GameData/ByteString/Utf8String.Construction.cs index 96ec7313..487b1a16 100644 --- a/Penumbra.GameData/ByteString/Utf8String.Construction.cs +++ b/Penumbra.GameData/ByteString/Utf8String.Construction.cs @@ -136,6 +136,7 @@ public sealed unsafe partial class Utf8String : IDisposable if( isOwned ) { + GC.AddMemoryPressure( length + 1 ); _length |= OwnedFlag; } diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index 5b54baa8..18b3075a 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -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, + }; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/GmpEntry.cs b/Penumbra.GameData/Structs/GmpEntry.cs index be9a7b58..c6af3fba 100644 --- a/Penumbra.GameData/Structs/GmpEntry.cs +++ b/Penumbra.GameData/Structs/GmpEntry.cs @@ -4,6 +4,8 @@ namespace Penumbra.GameData.Structs { public struct GmpEntry { + public static readonly GmpEntry Default = new (); + public bool Enabled { get => ( Value & 1 ) == 1; diff --git a/Penumbra.GameData/Util/Functions.cs b/Penumbra.GameData/Util/Functions.cs index 5d9ab10e..81e3dc28 100644 --- a/Penumbra.GameData/Util/Functions.cs +++ b/Penumbra.GameData/Util/Functions.cs @@ -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 ); } \ No newline at end of file diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index aac92447..46e3bcd5 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -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; diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index 343c128c..4dc10b26 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -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; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 35f274c4..9164f6e8 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -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 )] diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index d0f05f07..6ed2cc8e 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -9,6 +9,9 @@ public unsafe struct ResourceHandle [StructLayout( LayoutKind.Explicit )] public struct DataIndirection { + [FieldOffset( 0x00 )] + public void** VTable; + [FieldOffset( 0x10 )] public byte* DataPtr; diff --git a/Penumbra/Meta/EntryExtensions.cs b/Penumbra/Meta/EntryExtensions.cs deleted file mode 100644 index 293d135e..00000000 --- a/Penumbra/Meta/EntryExtensions.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index d02a3486..e54cc3e4 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -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 ); } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index fa373a4f..ae64ad1c 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -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 ); } \ No newline at end of file diff --git a/Penumbra/Meta/Files/EqpFile.cs b/Penumbra/Meta/Files/EqpFile.cs deleted file mode 100644 index 7de89fdb..00000000 --- a/Penumbra/Meta/Files/EqpFile.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs new file mode 100644 index 00000000..62aff162 --- /dev/null +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -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 ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index 18a53062..4eb53be3 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -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 ); } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/GmpFile.cs b/Penumbra/Meta/Files/GmpFile.cs deleted file mode 100644 index 35603500..00000000 --- a/Penumbra/Meta/Files/GmpFile.cs +++ /dev/null @@ -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 ); - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcExtensions.cs b/Penumbra/Meta/Files/ImcExtensions.cs deleted file mode 100644 index 3b97f751..00000000 --- a/Penumbra/Meta/Files/ImcExtensions.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs new file mode 100644 index 00000000..36576d00 --- /dev/null +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -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 ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs new file mode 100644 index 00000000..1d82e224 --- /dev/null +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -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(); + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaDefaults.cs b/Penumbra/Meta/Files/MetaDefaults.cs deleted file mode 100644 index 34600aa8..00000000 --- a/Penumbra/Meta/Files/MetaDefaults.cs +++ /dev/null @@ -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(), - }; - } - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Identifier.cs b/Penumbra/Meta/Identifier.cs deleted file mode 100644 index 896a5a8b..00000000 --- a/Penumbra/Meta/Identifier.cs +++ /dev/null @@ -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()}"; - } -} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs new file mode 100644 index 00000000..c7fdd553 --- /dev/null +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -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; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs new file mode 100644 index 00000000..eab5b864 --- /dev/null +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -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; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs new file mode 100644 index 00000000..985f9a65 --- /dev/null +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -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(), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs new file mode 100644 index 00000000..2af52dfb --- /dev/null +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -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; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs new file mode 100644 index 00000000..287708a0 --- /dev/null +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -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 ); +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs new file mode 100644 index 00000000..f33e7a97 --- /dev/null +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -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(), + }; +} \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs new file mode 100644 index 00000000..0ad017cd --- /dev/null +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -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; + } +} \ No newline at end of file diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 059999af..a0ec0027 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -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; diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 0fac0dc3..1c56b35b 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -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; } } } \ No newline at end of file diff --git a/Penumbra/Meta/MetaManipulation.cs b/Penumbra/Meta/MetaManipulation.cs deleted file mode 100644 index 326b8519..00000000 --- a/Penumbra/Meta/MetaManipulation.cs +++ /dev/null @@ -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(), - }; - } - } -} \ No newline at end of file diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Mod/ModCache.cs index 7764828f..3c413428 100644 --- a/Penumbra/Mod/ModCache.cs +++ b/Penumbra/Mod/ModCache.cs @@ -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; diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 2dee1a90..d9f4b144 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -304,7 +304,7 @@ public class ModCollectionCache { if( !MetaManipulations.TryGetValue( manip, out var oldMod ) ) { - MetaManipulations.ApplyMod( manip, mod ); + //MetaManipulations.ApplyMod( manip, mod ); } else { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index cfbb5f02..d37a1531 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -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() diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 83c157d3..0ff8a78c 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -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 ); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index e2d0a710..7da647b1 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -225,7 +225,7 @@ public partial class SettingsInterface foreach( var manip in manipulations ) { - ImGui.Text( manip.IdentifierString() ); + //ImGui.Text( manip.IdentifierString() ); } indent.Pop( 15f ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index 4d3148ae..a48a1732 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -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; } diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index f46ad018..501653d4 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -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 ) ); }