From 0c3c7ea3632ff6eb1691c3458470a94acfaac26b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 18 May 2022 17:40:41 +0200 Subject: [PATCH] Stop using windows forms, add extensive meta manipulation editing, fix a concurrency crash and a dumb crash. --- OtterGui | 2 +- Penumbra.GameData/Enums/BodySlot.cs | 65 +- Penumbra.GameData/Enums/EquipSlot.cs | 38 +- Penumbra.GameData/Enums/ObjectType.cs | 70 +- Penumbra.GameData/Structs/EqdpEntry.cs | 199 ++--- Penumbra.GameData/Structs/EqpEntry.cs | 16 +- .../Interop/Resolver/PathResolver.Data.cs | 2 +- Penumbra/Interop/Structs/CharacterUtility.cs | 2 +- Penumbra/Meta/Files/EstFile.cs | 1 - Penumbra/Meta/Files/ImcFile.cs | 10 +- .../Meta/Manipulations/EqdpManipulation.cs | 14 +- .../Meta/Manipulations/EqpManipulation.cs | 6 +- .../Meta/Manipulations/EstManipulation.cs | 10 +- .../Meta/Manipulations/GmpManipulation.cs | 4 +- .../Meta/Manipulations/ImcManipulation.cs | 41 +- .../Meta/Manipulations/RspManipulation.cs | 6 +- Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 15 +- Penumbra/Mods/Editor/Mod.Editor.Meta.cs | 166 ++++ Penumbra/Mods/Manager/Mod.Manager.Options.cs | 2 +- Penumbra/Penumbra.csproj | 1 - Penumbra/UI/Classes/Colors.cs | 28 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 773 ++++++++++++++++++ Penumbra/UI/Classes/ModEditWindow.cs | 83 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 13 +- Penumbra/Util/DictionaryExtensions.cs | 1 - 25 files changed, 1266 insertions(+), 302 deletions(-) create mode 100644 Penumbra/Mods/Editor/Mod.Editor.Meta.cs create mode 100644 Penumbra/UI/Classes/ModEditWindow.Meta.cs diff --git a/OtterGui b/OtterGui index cbc29200..732c9a3b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit cbc29200e8b80d264c8a326cdc62e841e12d1c53 +Subproject commit 732c9a3bd7c967ca427e24f4b8df65f722fe72d2 diff --git a/Penumbra.GameData/Enums/BodySlot.cs b/Penumbra.GameData/Enums/BodySlot.cs index 0e1220ce..31e77417 100644 --- a/Penumbra.GameData/Enums/BodySlot.cs +++ b/Penumbra.GameData/Enums/BodySlot.cs @@ -1,43 +1,42 @@ using System.Collections.Generic; using System.ComponentModel; -namespace Penumbra.GameData.Enums +namespace Penumbra.GameData.Enums; + +public enum BodySlot : byte { - public enum BodySlot : byte - { - Unknown, - Hair, - Face, - Tail, - Body, - Zear, - } + Unknown, + Hair, + Face, + Tail, + Body, + Zear, +} - public static class BodySlotEnumExtension +public static class BodySlotEnumExtension +{ + public static string ToSuffix( this BodySlot value ) { - public static string ToSuffix( this BodySlot value ) + return value switch { - return value switch - { - BodySlot.Zear => "zear", - BodySlot.Face => "face", - BodySlot.Hair => "hair", - BodySlot.Body => "body", - BodySlot.Tail => "tail", - _ => throw new InvalidEnumArgumentException(), - }; - } - } - - public static partial class Names - { - public static readonly Dictionary< string, BodySlot > StringToBodySlot = new() - { - { BodySlot.Zear.ToSuffix(), BodySlot.Zear }, - { BodySlot.Face.ToSuffix(), BodySlot.Face }, - { BodySlot.Hair.ToSuffix(), BodySlot.Hair }, - { BodySlot.Body.ToSuffix(), BodySlot.Body }, - { BodySlot.Tail.ToSuffix(), BodySlot.Tail }, + BodySlot.Zear => "zear", + BodySlot.Face => "face", + BodySlot.Hair => "hair", + BodySlot.Body => "body", + BodySlot.Tail => "tail", + _ => throw new InvalidEnumArgumentException(), }; } +} + +public static partial class Names +{ + public static readonly Dictionary< string, BodySlot > StringToBodySlot = new() + { + { BodySlot.Zear.ToSuffix(), BodySlot.Zear }, + { BodySlot.Face.ToSuffix(), BodySlot.Face }, + { BodySlot.Hair.ToSuffix(), BodySlot.Hair }, + { BodySlot.Body.ToSuffix(), BodySlot.Body }, + { BodySlot.Tail.ToSuffix(), BodySlot.Tail }, + }; } \ No newline at end of file diff --git a/Penumbra.GameData/Enums/EquipSlot.cs b/Penumbra.GameData/Enums/EquipSlot.cs index 061787e6..628acad7 100644 --- a/Penumbra.GameData/Enums/EquipSlot.cs +++ b/Penumbra.GameData/Enums/EquipSlot.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; namespace Penumbra.GameData.Enums { @@ -30,7 +32,7 @@ namespace Penumbra.GameData.Enums All = 22, // Not officially existing } - public static class EquipSlotEnumExtension + public static class EquipSlotExtensions { public static string ToSuffix( this EquipSlot value ) { @@ -79,6 +81,36 @@ namespace Penumbra.GameData.Enums }; } + public static string ToName( this EquipSlot value ) + { + return value switch + { + EquipSlot.Head => "Head", + EquipSlot.Hands => "Hands", + EquipSlot.Legs => "Legs", + EquipSlot.Feet => "Feet", + EquipSlot.Body => "Body", + EquipSlot.Ears => "Earrings", + EquipSlot.Neck => "Necklace", + EquipSlot.RFinger => "Right Ring", + EquipSlot.LFinger => "Left Ring", + EquipSlot.Wrists => "Bracelets", + EquipSlot.MainHand => "Primary Weapon", + EquipSlot.OffHand => "Secondary Weapon", + EquipSlot.Belt => "Belt", + EquipSlot.BothHand => "Primary Weapon", + EquipSlot.HeadBody => "Head and Body", + EquipSlot.BodyHandsLegsFeet => "Costume", + EquipSlot.SoulCrystal => "Soul Crystal", + EquipSlot.LegsFeet => "Bottom", + EquipSlot.FullBody => "Costume", + EquipSlot.BodyHands => "Top", + EquipSlot.BodyLegsFeet => "Costume", + EquipSlot.All => "Costume", + _ => "Unknown", + }; + } + public static bool IsEquipment( this EquipSlot value ) { return value switch @@ -104,6 +136,10 @@ namespace Penumbra.GameData.Enums _ => false, }; } + + public static readonly EquipSlot[] EquipmentSlots = Enum.GetValues< EquipSlot >().Where( e => e.IsEquipment() ).ToArray(); + public static readonly EquipSlot[] AccessorySlots = Enum.GetValues< EquipSlot >().Where( e => e.IsAccessory() ).ToArray(); + public static readonly EquipSlot[] EqdpSlots = EquipmentSlots.Concat( AccessorySlots ).ToArray(); } public static partial class Names diff --git a/Penumbra.GameData/Enums/ObjectType.cs b/Penumbra.GameData/Enums/ObjectType.cs index 8b62e42b..414497b1 100644 --- a/Penumbra.GameData/Enums/ObjectType.cs +++ b/Penumbra.GameData/Enums/ObjectType.cs @@ -1,21 +1,55 @@ -namespace Penumbra.GameData.Enums +using System; + +namespace Penumbra.GameData.Enums; + +public enum ObjectType : byte { - public enum ObjectType : byte + Unknown, + Vfx, + DemiHuman, + Accessory, + World, + Housing, + Monster, + Icon, + LoadingScreen, + Map, + Interface, + Equipment, + Character, + Weapon, + Font, +} + +public static class ObjectTypeExtensions +{ + public static string ToName( this ObjectType type ) + => type switch + { + ObjectType.Vfx => "Visual Effect", + ObjectType.DemiHuman => "Demi Human", + ObjectType.Accessory => "Accessory", + ObjectType.World => "Doodad", + ObjectType.Housing => "Housing Object", + ObjectType.Monster => "Monster", + ObjectType.Icon => "Icon", + ObjectType.LoadingScreen => "Loading Screen", + ObjectType.Map => "Map", + ObjectType.Interface => "UI Element", + ObjectType.Equipment => "Equipment", + ObjectType.Character => "Character", + ObjectType.Weapon => "Weapon", + ObjectType.Font => "Font", + _ => "Unknown", + }; + + + public static readonly ObjectType[] ValidImcTypes = { - Unknown, - Vfx, - DemiHuman, - Accessory, - World, - Housing, - Monster, - Icon, - LoadingScreen, - Map, - Interface, - Equipment, - Character, - Weapon, - Font, - } + ObjectType.Equipment, + ObjectType.Accessory, + ObjectType.DemiHuman, + ObjectType.Monster, + ObjectType.Weapon, + }; } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/EqdpEntry.cs b/Penumbra.GameData/Structs/EqdpEntry.cs index 763809ae..00d05bc5 100644 --- a/Penumbra.GameData/Structs/EqdpEntry.cs +++ b/Penumbra.GameData/Structs/EqdpEntry.cs @@ -2,106 +2,119 @@ using System; using System.ComponentModel; using Penumbra.GameData.Enums; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +[Flags] +public enum EqdpEntry : ushort { - [Flags] - public enum EqdpEntry : ushort + Invalid = 0, + Head1 = 0b0000000001, + Head2 = 0b0000000010, + HeadMask = 0b0000000011, + + Body1 = 0b0000000100, + Body2 = 0b0000001000, + BodyMask = 0b0000001100, + + Hands1 = 0b0000010000, + Hands2 = 0b0000100000, + HandsMask = 0b0000110000, + + Legs1 = 0b0001000000, + Legs2 = 0b0010000000, + LegsMask = 0b0011000000, + + Feet1 = 0b0100000000, + Feet2 = 0b1000000000, + FeetMask = 0b1100000000, + + Ears1 = 0b0000000001, + Ears2 = 0b0000000010, + EarsMask = 0b0000000011, + + Neck1 = 0b0000000100, + Neck2 = 0b0000001000, + NeckMask = 0b0000001100, + + Wrists1 = 0b0000010000, + Wrists2 = 0b0000100000, + WristsMask = 0b0000110000, + + RingR1 = 0b0001000000, + RingR2 = 0b0010000000, + RingRMask = 0b0011000000, + + RingL1 = 0b0100000000, + RingL2 = 0b1000000000, + RingLMask = 0b1100000000, +} + +public static class Eqdp +{ + public static int Offset( EquipSlot slot ) + => slot switch + { + EquipSlot.Head => 0, + EquipSlot.Body => 2, + EquipSlot.Hands => 4, + EquipSlot.Legs => 6, + EquipSlot.Feet => 8, + EquipSlot.Ears => 0, + EquipSlot.Neck => 2, + EquipSlot.Wrists => 4, + EquipSlot.RFinger => 6, + EquipSlot.LFinger => 8, + _ => throw new InvalidEnumArgumentException(), + }; + + public static (bool, bool) ToBits( this EqdpEntry entry, EquipSlot slot ) + => slot switch + { + EquipSlot.Head => ( entry.HasFlag( EqdpEntry.Head1 ), entry.HasFlag( EqdpEntry.Head2 ) ), + EquipSlot.Body => ( entry.HasFlag( EqdpEntry.Body1 ), entry.HasFlag( EqdpEntry.Body2 ) ), + EquipSlot.Hands => ( entry.HasFlag( EqdpEntry.Hands1 ), entry.HasFlag( EqdpEntry.Hands2 ) ), + EquipSlot.Legs => ( entry.HasFlag( EqdpEntry.Legs1 ), entry.HasFlag( EqdpEntry.Legs2 ) ), + EquipSlot.Feet => ( entry.HasFlag( EqdpEntry.Feet1 ), entry.HasFlag( EqdpEntry.Feet2 ) ), + EquipSlot.Ears => ( entry.HasFlag( EqdpEntry.Ears1 ), entry.HasFlag( EqdpEntry.Ears2 ) ), + EquipSlot.Neck => ( entry.HasFlag( EqdpEntry.Neck1 ), entry.HasFlag( EqdpEntry.Neck2 ) ), + EquipSlot.Wrists => ( entry.HasFlag( EqdpEntry.Wrists1 ), entry.HasFlag( EqdpEntry.Wrists2 ) ), + EquipSlot.RFinger => ( entry.HasFlag( EqdpEntry.RingR1 ), entry.HasFlag( EqdpEntry.RingR2 ) ), + EquipSlot.LFinger => ( entry.HasFlag( EqdpEntry.RingL1 ), entry.HasFlag( EqdpEntry.RingL2 ) ), + _ => throw new InvalidEnumArgumentException(), + }; + + public static EqdpEntry FromSlotAndBits( EquipSlot slot, bool bit1, bool bit2 ) { - Invalid = 0, - Head1 = 0b0000000001, - Head2 = 0b0000000010, - HeadMask = 0b0000000011, + EqdpEntry ret = 0; + var offset = Offset( slot ); + if( bit1 ) + { + ret |= ( EqdpEntry )( 1 << offset ); + } - Body1 = 0b0000000100, - Body2 = 0b0000001000, - BodyMask = 0b0000001100, + if( bit2 ) + { + ret |= ( EqdpEntry )( 1 << ( offset + 1 ) ); + } - Hands1 = 0b0000010000, - Hands2 = 0b0000100000, - HandsMask = 0b0000110000, - - Legs1 = 0b0001000000, - Legs2 = 0b0010000000, - LegsMask = 0b0011000000, - - Feet1 = 0b0100000000, - Feet2 = 0b1000000000, - FeetMask = 0b1100000000, - - Ears1 = 0b0000000001, - Ears2 = 0b0000000010, - EarsMask = 0b0000000011, - - Neck1 = 0b0000000100, - Neck2 = 0b0000001000, - NeckMask = 0b0000001100, - - Wrists1 = 0b0000010000, - Wrists2 = 0b0000100000, - WristsMask = 0b0000110000, - - RingR1 = 0b0001000000, - RingR2 = 0b0010000000, - RingRMask = 0b0011000000, - - RingL1 = 0b0100000000, - RingL2 = 0b1000000000, - RingLMask = 0b1100000000, + return ret; } - public static class Eqdp + public static EqdpEntry Mask( EquipSlot slot ) { - public static int Offset( EquipSlot slot ) + return slot switch { - return slot switch - { - EquipSlot.Head => 0, - EquipSlot.Body => 2, - EquipSlot.Hands => 4, - EquipSlot.Legs => 6, - EquipSlot.Feet => 8, - EquipSlot.Ears => 0, - EquipSlot.Neck => 2, - EquipSlot.Wrists => 4, - EquipSlot.RFinger => 6, - EquipSlot.LFinger => 8, - _ => throw new InvalidEnumArgumentException(), - }; - } - - public static EqdpEntry FromSlotAndBits( EquipSlot slot, bool bit1, bool bit2 ) - { - EqdpEntry ret = 0; - var offset = Offset( slot ); - if( bit1 ) - { - ret |= ( EqdpEntry )( 1 << offset ); - } - - if( bit2 ) - { - ret |= ( EqdpEntry )( 1 << ( offset + 1 ) ); - } - - return ret; - } - - public static EqdpEntry Mask( EquipSlot slot ) - { - return slot switch - { - EquipSlot.Head => EqdpEntry.HeadMask, - EquipSlot.Body => EqdpEntry.BodyMask, - EquipSlot.Hands => EqdpEntry.HandsMask, - EquipSlot.Legs => EqdpEntry.LegsMask, - EquipSlot.Feet => EqdpEntry.FeetMask, - EquipSlot.Ears => EqdpEntry.EarsMask, - EquipSlot.Neck => EqdpEntry.NeckMask, - EquipSlot.Wrists => EqdpEntry.WristsMask, - EquipSlot.RFinger => EqdpEntry.RingRMask, - EquipSlot.LFinger => EqdpEntry.RingLMask, - _ => 0, - }; - } + EquipSlot.Head => EqdpEntry.HeadMask, + EquipSlot.Body => EqdpEntry.BodyMask, + EquipSlot.Hands => EqdpEntry.HandsMask, + EquipSlot.Legs => EqdpEntry.LegsMask, + EquipSlot.Feet => EqdpEntry.FeetMask, + EquipSlot.Ears => EqdpEntry.EarsMask, + EquipSlot.Neck => EqdpEntry.NeckMask, + EquipSlot.Wrists => EqdpEntry.WristsMask, + EquipSlot.RFinger => EqdpEntry.RingRMask, + EquipSlot.LFinger => EqdpEntry.RingLMask, + _ => 0, + }; } } \ No newline at end of file diff --git a/Penumbra.GameData/Structs/EqpEntry.cs b/Penumbra.GameData/Structs/EqpEntry.cs index 18b3075a..56033d02 100644 --- a/Penumbra.GameData/Structs/EqpEntry.cs +++ b/Penumbra.GameData/Structs/EqpEntry.cs @@ -198,12 +198,14 @@ public static class Eqp 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, + + // Currently unused. + EqpEntry._58 => EquipSlot.Unknown, + EqpEntry._59 => EquipSlot.Unknown, + EqpEntry._60 => EquipSlot.Unknown, + EqpEntry._61 => EquipSlot.Unknown, + EqpEntry._62 => EquipSlot.Unknown, + EqpEntry._63 => EquipSlot.Unknown, _ => EquipSlot.Unknown, }; @@ -299,7 +301,7 @@ public static class Eqp 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[] >() + public static readonly IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >() { [ EquipSlot.Body ] = EqpAttributesBody, [ EquipSlot.Legs ] = EqpAttributesLegs, diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 9701cd2a..df6e9933 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -21,7 +21,7 @@ public unsafe partial class PathResolver // and use the last game object that called EnableDraw to link them. public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d ); - [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40" )] + [Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = "CharacterBaseCreateDetour" )] public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook; private ModCollection? _lastCreatedCollection; diff --git a/Penumbra/Interop/Structs/CharacterUtility.cs b/Penumbra/Interop/Structs/CharacterUtility.cs index 08fd12e3..32770fe6 100644 --- a/Penumbra/Interop/Structs/CharacterUtility.cs +++ b/Penumbra/Interop/Structs/CharacterUtility.cs @@ -55,7 +55,7 @@ public unsafe struct CharacterUtility 1404 => EqdpStartIdx + 25, 9104 => EqdpStartIdx + 26, 9204 => EqdpStartIdx + 27, - _ => throw new ArgumentException(), + _ => -1, }; [FieldOffset( 0 )] diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index aacfbf00..cd67a6ba 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using System.Windows.Forms; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 8a092052..e0a5c7c9 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,23 +1,21 @@ using System; using System.Numerics; using Dalamud.Logging; -using Dalamud.Memory; using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; -using Penumbra.Interop; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Files; public readonly struct ImcEntry : IEquatable< ImcEntry > { - public readonly byte MaterialId; - public readonly byte DecalId; + public byte MaterialId { get; init; } + public byte DecalId { get; init; } private readonly ushort _attributeAndSound; - public readonly byte VfxId; - public readonly byte MaterialAnimationId; + public byte VfxId { get; init; } + public byte MaterialAnimationId { get; init; } public ushort AttributeMask { diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 9abcd47b..287a5367 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -12,14 +12,18 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct EqdpManipulation : IMetaManipulation< EqdpManipulation > { - public readonly EqdpEntry Entry; + public EqdpEntry Entry { get; init; } + [JsonConverter( typeof( StringEnumConverter ) )] - public readonly Gender Gender; + public Gender Gender { get; init; } + [JsonConverter( typeof( StringEnumConverter ) )] - public readonly ModelRace Race; - public readonly ushort SetId; + public ModelRace Race { get; init; } + + public ushort SetId { get; init; } + [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EquipSlot Slot; + public EquipSlot Slot { get; init; } public EqdpManipulation( EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, ushort setId ) { diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index b87d0d7a..c80696ac 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -14,12 +14,12 @@ namespace Penumbra.Meta.Manipulations; public readonly struct EqpManipulation : IMetaManipulation< EqpManipulation > { [JsonConverter( typeof( ForceNumericFlagEnumConverter ) )] - public readonly EqpEntry Entry; + public EqpEntry Entry { get; init; } - public readonly ushort SetId; + public ushort SetId { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EquipSlot Slot; + public EquipSlot Slot { get; init; } public EqpManipulation( EqpEntry entry, EquipSlot slot, ushort setId ) { diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index b607e5e5..314972b3 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -19,18 +19,18 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Head = CharacterUtility.HeadEstIdx, } - public readonly ushort Entry; // SkeletonIdx. + public ushort Entry { get; init; } // SkeletonIdx. [JsonConverter( typeof( StringEnumConverter ) )] - public readonly Gender Gender; + public Gender Gender { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly ModelRace Race; + public ModelRace Race { get; init; } - public readonly ushort SetId; + public ushort SetId { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EstType Slot; + public EstType Slot { get; init; } [JsonConstructor] public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort entry ) diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index edae0b72..ad31d6b2 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -9,8 +9,8 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct GmpManipulation : IMetaManipulation< GmpManipulation > { - public readonly GmpEntry Entry; - public readonly ushort SetId; + public GmpEntry Entry { get; init; } + public ushort SetId { get; init; } public GmpManipulation( GmpEntry entry, ushort setId ) { diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 5ac63e99..b3acf680 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -11,19 +11,19 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > { - public readonly ImcEntry Entry; - public readonly ushort PrimaryId; - public readonly ushort Variant; - public readonly ushort SecondaryId; + public ImcEntry Entry { get; init; } + public ushort PrimaryId { get; init; } + public ushort Variant { get; init; } + public ushort SecondaryId { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly ObjectType ObjectType; + public ObjectType ObjectType { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly EquipSlot EquipSlot; + public EquipSlot EquipSlot { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly BodySlot BodySlot; + public BodySlot BodySlot { get; init; } public ImcManipulation( EquipSlot equipSlot, ushort variant, ushort primaryId, ImcEntry entry ) { @@ -52,19 +52,24 @@ public readonly struct ImcManipulation : IMetaManipulation< ImcManipulation > internal ImcManipulation( ObjectType objectType, BodySlot bodySlot, ushort primaryId, ushort secondaryId, ushort variant, EquipSlot equipSlot, ImcEntry entry ) { - Entry = entry; - ObjectType = objectType; - BodySlot = bodySlot; - PrimaryId = primaryId; - SecondaryId = secondaryId; - Variant = variant; - EquipSlot = equipSlot; + Entry = entry; + ObjectType = objectType; + PrimaryId = primaryId; + Variant = variant; + if( objectType is ObjectType.Accessory or ObjectType.Equipment ) + { + BodySlot = BodySlot.Unknown; + SecondaryId = 0; + EquipSlot = equipSlot; + } + else + { + BodySlot = bodySlot; + SecondaryId = secondaryId; + EquipSlot = EquipSlot.Unknown; + } } - public ImcManipulation( ImcManipulation copy, ImcEntry entry ) - : this( copy.ObjectType, copy.BodySlot, copy.PrimaryId, copy.SecondaryId, copy.Variant, copy.EquipSlot, entry ) - {} - public override string ToString() => ObjectType is ObjectType.Equipment or ObjectType.Accessory ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index d25b695f..1f10cf7b 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -11,13 +11,13 @@ namespace Penumbra.Meta.Manipulations; [StructLayout( LayoutKind.Sequential, Pack = 1 )] public readonly struct RspManipulation : IMetaManipulation< RspManipulation > { - public readonly float Entry; + public float Entry { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly SubRace SubRace; + public SubRace SubRace { get; init; } [JsonConverter( typeof( StringEnumConverter ) )] - public readonly RspAttribute Attribute; + public RspAttribute Attribute { get; init; } public RspManipulation( SubRace subRace, RspAttribute attribute, float entry ) { diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index e0378d26..35d41003 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; using Penumbra.Util; namespace Penumbra.Mods; @@ -11,7 +10,7 @@ public partial class Mod public partial class Editor { public int GroupIdx { get; private set; } = -1; - public int OptionIdx { get; private set; } = 0; + public int OptionIdx { get; private set; } private IModGroup? _modGroup; private SubMod _subMod; @@ -21,7 +20,6 @@ public partial class Mod public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new(); public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); - public readonly HashSet< MetaManipulation > CurrentManipulations = new(); public void SetSubMod( int groupIdx, int optionIdx ) { @@ -62,16 +60,5 @@ public partial class Mod { CurrentSwaps.SetTo( _subMod.FileSwaps ); } - - public void ApplyManipulations() - { - Penumbra.ModManager.OptionSetManipulations( _mod, GroupIdx, OptionIdx, CurrentManipulations.ToHashSet() ); - } - - public void RevertManipulations() - { - CurrentManipulations.Clear(); - CurrentManipulations.UnionWith( _subMod.Manipulations ); - } } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs new file mode 100644 index 00000000..19a48f95 --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using System.Linq; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + public struct Manipulations + { + private readonly HashSet< ImcManipulation > _imc = new(); + private readonly HashSet< EqpManipulation > _eqp = new(); + private readonly HashSet< EqdpManipulation > _eqdp = new(); + private readonly HashSet< GmpManipulation > _gmp = new(); + private readonly HashSet< EstManipulation > _est = new(); + private readonly HashSet< RspManipulation > _rsp = new(); + + public bool Changes { get; private set; } = false; + + public IReadOnlySet< ImcManipulation > Imc + => _imc; + + public IReadOnlySet< EqpManipulation > Eqp + => _eqp; + + public IReadOnlySet< EqdpManipulation > Eqdp + => _eqdp; + + public IReadOnlySet< GmpManipulation > Gmp + => _gmp; + + public IReadOnlySet< EstManipulation > Est + => _est; + + public IReadOnlySet< RspManipulation > Rsp + => _rsp; + + public Manipulations() + { } + + public bool CanAdd( MetaManipulation m ) + { + return m.ManipulationType switch + { + MetaManipulation.Type.Imc => !_imc.Contains( m.Imc ), + MetaManipulation.Type.Eqdp => !_eqdp.Contains( m.Eqdp ), + MetaManipulation.Type.Eqp => !_eqp.Contains( m.Eqp ), + MetaManipulation.Type.Est => !_est.Contains( m.Est ), + MetaManipulation.Type.Gmp => !_gmp.Contains( m.Gmp ), + MetaManipulation.Type.Rsp => !_rsp.Contains( m.Rsp ), + _ => false, + }; + } + + public bool Add( MetaManipulation m ) + { + var added = m.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.Add( m.Imc ), + MetaManipulation.Type.Eqdp => _eqdp.Add( m.Eqdp ), + MetaManipulation.Type.Eqp => _eqp.Add( m.Eqp ), + MetaManipulation.Type.Est => _est.Add( m.Est ), + MetaManipulation.Type.Gmp => _gmp.Add( m.Gmp ), + MetaManipulation.Type.Rsp => _rsp.Add( m.Rsp ), + _ => false, + }; + Changes |= added; + return added; + } + + public bool Delete( MetaManipulation m ) + { + var deleted = m.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.Remove( m.Imc ), + MetaManipulation.Type.Eqdp => _eqdp.Remove( m.Eqdp ), + MetaManipulation.Type.Eqp => _eqp.Remove( m.Eqp ), + MetaManipulation.Type.Est => _est.Remove( m.Est ), + MetaManipulation.Type.Gmp => _gmp.Remove( m.Gmp ), + MetaManipulation.Type.Rsp => _rsp.Remove( m.Rsp ), + _ => false, + }; + Changes |= deleted; + return deleted; + } + + public bool Change( MetaManipulation m ) + => Delete( m ) && Add( m ); + + public bool Set( MetaManipulation m ) + => Delete( m ) | Add( m ); + + public void Clear() + { + _imc.Clear(); + _eqp.Clear(); + _eqdp.Clear(); + _gmp.Clear(); + _est.Clear(); + _rsp.Clear(); + Changes = true; + } + + public void Split( IEnumerable< MetaManipulation > manips ) + { + Clear(); + foreach( var manip in manips ) + { + switch( manip.ManipulationType ) + { + case MetaManipulation.Type.Imc: + _imc.Add( manip.Imc ); + break; + case MetaManipulation.Type.Eqdp: + _eqdp.Add( manip.Eqdp ); + break; + case MetaManipulation.Type.Eqp: + _eqp.Add( manip.Eqp ); + break; + case MetaManipulation.Type.Est: + _est.Add( manip.Est ); + break; + case MetaManipulation.Type.Gmp: + _gmp.Add( manip.Gmp ); + break; + case MetaManipulation.Type.Rsp: + _rsp.Add( manip.Rsp ); + break; + } + } + + Changes = false; + } + + private HashSet< MetaManipulation > Recombine() + => _imc.Select( m => ( MetaManipulation )m ) + .Concat( _eqdp.Select( m => ( MetaManipulation )m ) ) + .Concat( _eqp.Select( m => ( MetaManipulation )m ) ) + .Concat( _est.Select( m => ( MetaManipulation )m ) ) + .Concat( _gmp.Select( m => ( MetaManipulation )m ) ) + .Concat( _rsp.Select( m => ( MetaManipulation )m ) ) + .ToHashSet(); + + public void Apply( Mod mod, int groupIdx, int optionIdx ) + { + if( Changes ) + { + Penumbra.ModManager.OptionSetManipulations( mod, groupIdx, optionIdx, Recombine() ); + Changes = false; + } + } + } + + public Manipulations Meta = new(); + + public void RevertManipulations() + => Meta.Split( _subMod.Manipulations ); + + public void ApplyManipulations() + { + Meta.Apply( _mod, GroupIdx, OptionIdx ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index ea8c838e..71cd0b02 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -278,7 +278,7 @@ public sealed partial class Mod public void OptionSetManipulations( Mod mod, int groupIdx, int optionIdx, HashSet< MetaManipulation > manipulations ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.Manipulations.SetEquals( manipulations ) ) + if( subMod.Manipulations.All( m => manipulations.TryGetValue( m, out var old ) && old.EntryEquals( m ) ) ) { return; } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2a14edb7..0626797f 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -12,7 +12,6 @@ bin\$(Configuration)\ true enable - true true diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index b3ec1ea4..5f3844ec 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -16,6 +16,8 @@ public enum ColorId FolderCollapsed, FolderLine, ItemId, + IncreasedMetaValue, + DecreasedMetaValue, } public static class Colors @@ -30,18 +32,20 @@ public static class Colors => color switch { // @formatter:off - ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), - ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), - ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), - ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), - ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), - ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), - ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), - ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), - ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), - ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), - ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), - ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), + ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), + ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), + ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), + ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), + ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), + ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), + ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), + ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), + ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), + ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), + ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), + ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), + ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."), + ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs new file mode 100644 index 00000000..60ce5b0f --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private void DrawMetaTab() + { + using var tab = ImRaii.TabItem( "Meta Manipulations" ); + if( !tab ) + { + return; + } + + DrawOptionSelectHeader(); + + var setsEqual = !_editor!.Meta.Changes; + var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + ImGui.NewLine(); + if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.ApplyManipulations(); + } + + ImGui.SameLine(); + tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; + if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.RevertManipulations(); + } + + using var child = ImRaii.Child( "##meta", -Vector2.One, true ); + if( !child ) + { + return; + } + + DrawEditHeader( _editor.Meta.Eqp, "Equipment Parameter Edits (EQP)###EQP", 4, EqpRow.Draw, EqpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Eqdp, "Racial Model Edits (EQDP)###EQDP", 6, EqdpRow.Draw, EqdpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 8, ImcRow.Draw, ImcRow.DrawNew ); + DrawEditHeader( _editor.Meta.Est, "Extra Skeleton Parameters (EST)###EST", 6, EstRow.Draw, EstRow.DrawNew ); + DrawEditHeader( _editor.Meta.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 6, GmpRow.Draw, GmpRow.DrawNew ); + DrawEditHeader( _editor.Meta.Rsp, "Racial Scaling Edits (RSP)###RSP", 4, RspRow.Draw, RspRow.DrawNew ); + } + + + // The headers for the different meta changes all have basically the same structure for different types. + private void DrawEditHeader< T >( IReadOnlyCollection< T > items, string label, int numColumns, Action< T, Mod.Editor, Vector2 > draw, + Action< Mod.Editor, Vector2 > drawNew ) + { + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + if( !ImGui.CollapsingHeader( $"{items.Count} {label}" ) ) + { + return; + } + + using( var table = ImRaii.Table( label, numColumns, flags ) ) + { + if( table ) + { + drawNew( _editor!, _iconSize ); + foreach( var (item, index) in items.ToArray().WithIndex() ) + { + using var id = ImRaii.PushId( index ); + draw( item, _editor!, _iconSize ); + } + } + } + + ImGui.NewLine(); + } + + private static class EqpRow + { + private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); + + private static float IdWidth + => 100 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var canAdd = editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = ExpandedEqpFile.GetDefault( _new.SetId ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + { + _new = _new with { SetId = setId }; + } + + ImGui.TableNextColumn(); + if( EqpEquipSlotCombo( "##eqpSlot", _new.Slot, out var slot ) ) + { + _new = _new with { Slot = slot }; + } + + // Values + ImGui.TableNextColumn(); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + foreach( var flag in Eqp.EqpAttributes[ _new.Slot ] ) + { + using var id = ImRaii.PushId( ( int )flag ); + var value = defaultEntry.HasFlag( flag ); + Checkmark( string.Empty, flag.ToLocalName(), value, value, out _ ); + ImGui.SameLine(); + } + } + + public static void Draw( EqpManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SetId.ToString() ); + var defaultEntry = ExpandedEqpFile.GetDefault( meta.SetId ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Slot.ToName() ); + + // Values + ImGui.TableNextColumn(); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + foreach( var flag in Eqp.EqpAttributes[ meta.Slot ] ) + { + using var id = ImRaii.PushId( ( int )flag ); + var defaultValue = defaultEntry.HasFlag( flag ); + var currentValue = meta.Entry.HasFlag( flag ); + if( Checkmark( string.Empty, flag.ToLocalName(), currentValue, defaultValue, out var value ) ) + { + editor.Meta.Change( meta with { Entry = value ? meta.Entry | flag : meta.Entry & ~flag } ); + } + + ImGui.SameLine(); + } + } + } + + + private static class EqdpRow + { + private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); + + private static float IdWidth + => 100 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var raceCode = Names.CombinedRace( _new.Gender, _new.Race ); + var validRaceCode = CharacterUtility.EqdpIdx( raceCode, false ) >= 0; + var canAdd = validRaceCode && editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : + validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; + var defaultEntry = validRaceCode + ? ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId ) + : 0; + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + { + _new = _new with { SetId = setId }; + } + + ImGui.TableNextColumn(); + if( RaceCombo( "##eqdpRace", _new.Race, out var race ) ) + { + _new = _new with { Race = race }; + } + + ImGui.TableNextColumn(); + if( GenderCombo( "##eqdpGender", _new.Gender, out var gender ) ) + { + _new = _new with { Gender = gender }; + } + + ImGui.TableNextColumn(); + if( EqdpEquipSlotCombo( "##eqdpSlot", _new.Slot, out var slot ) ) + { + _new = _new with { Slot = slot }; + } + + // Values + ImGui.TableNextColumn(); + var (bit1, bit2) = defaultEntry.ToBits( _new.Slot ); + Checkmark( "##eqdpCheck1", string.Empty, bit1, bit1, out _ ); + ImGui.SameLine(); + Checkmark( "##eqdpCheck2", string.Empty, bit2, bit2, out _ ); + } + + public static void Draw( EqdpManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Race.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Gender.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Slot.ToName() ); + + // Values + var defaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( meta.Gender, meta.Race ), meta.Slot.IsAccessory(), meta.SetId ); + var (defaultBit1, defaultBit2) = defaultEntry.ToBits( meta.Slot ); + var (bit1, bit2) = meta.Entry.ToBits( meta.Slot ); + ImGui.TableNextColumn(); + if( Checkmark( "##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1 ) ) + { + editor.Meta.Change( meta with { Entry = Eqdp.FromSlotAndBits( meta.Slot, newBit1, bit2 ) } ); + } + + ImGui.SameLine(); + if( Checkmark( "##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2 ) ) + { + editor.Meta.Change( meta with { Entry = Eqdp.FromSlotAndBits( meta.Slot, bit1, newBit2 ) } ); + } + } + } + + private static class ImcRow + { + private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); + + private static float IdWidth + => 80 * ImGuiHelpers.GlobalScale; + + private static float SmallIdWidth + => 45 * ImGuiHelpers.GlobalScale; + + // Convert throwing to null-return if the file does not exist. + private static ImcEntry? GetDefault( ImcManipulation imc ) + { + try + { + return ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant ); + } + catch + { + return null; + } + } + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var defaultEntry = GetDefault( _new ); + var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; + defaultEntry ??= new ImcEntry(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry.Value } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( ImcTypeCombo( "##imcType", _new.ObjectType, out var type ) ) + { + _new = new ImcManipulation( type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? ( ushort )1 : _new.SecondaryId, + _new.Variant, _new.EquipSlot == EquipSlot.Unknown ? EquipSlot.Head : _new.EquipSlot, _new.Entry ); + } + + ImGui.TableNextColumn(); + if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, ushort.MaxValue ) ) + { + _new = _new with { PrimaryId = setId }; + } + + ImGui.TableNextColumn(); + // Equipment and accessories are slightly different imcs than other types. + if( _new.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) + { + if( EqdpEquipSlotCombo( "##imcSlot", _new.EquipSlot, out var slot ) ) + { + _new = _new with { EquipSlot = slot }; + } + } + else + { + if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, ushort.MaxValue ) ) + { + _new = _new with { SecondaryId = setId2 }; + } + } + + ImGui.TableNextColumn(); + if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, byte.MaxValue ) ) + { + _new = _new with { Variant = variant }; + } + + // Values + ImGui.TableNextColumn(); + IntDragInput( "##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _, + 1, byte.MaxValue, 0f ); + ImGui.SameLine(); + IntDragInput( "##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId, + defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f ); + ImGui.TableNextColumn(); + IntDragInput( "##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0, + byte.MaxValue, 0f ); + ImGui.SameLine(); + IntDragInput( "##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue, + 0f ); + ImGui.SameLine(); + IntDragInput( "##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111, + 0f ); + ImGui.TableNextColumn(); + IntDragInput( "##imcAttributes", "Attributes", IdWidth, defaultEntry.Value.AttributeMask, defaultEntry.Value.AttributeMask, out _, + 0, 0b1111111111, 0f ); + } + + public static void Draw( ImcManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.ObjectType.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.PrimaryId.ToString() ); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + if( meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory ) + { + ImGui.TextUnformatted( meta.EquipSlot.ToName() ); + } + else + { + ImGui.TextUnformatted( meta.SecondaryId.ToString() ); + } + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Variant.ToString() ); + + // Values + ImGui.TableNextColumn(); + var defaultEntry = GetDefault( meta ) ?? new ImcEntry(); + if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, + defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { MaterialId = ( byte )materialId } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth, + meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { MaterialAnimationId = ( byte )materialAnimId } } ); + } + + ImGui.TableNextColumn(); + if( IntDragInput( "##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId, + defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { DecalId = ( byte )decalId } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId, + out var vfxId, 0, byte.MaxValue, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { VfxId = ( byte )vfxId } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId, + defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { SoundId = ( byte )soundId } } ); + } + + ImGui.TableNextColumn(); + if( IntDragInput( "##imcAttributes", $"Attributes\nDefault Value: {defaultEntry.AttributeMask}", IdWidth, + meta.Entry.AttributeMask, defaultEntry.AttributeMask, out var attributeMask, 0, 0b1111111111, 0.1f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { AttributeMask = ( ushort )attributeMask } } ); + } + } + } + + private static class EstRow + { + private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); + + private static float IdWidth + => 100 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var canAdd = editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + { + _new = _new with { SetId = setId }; + } + + ImGui.TableNextColumn(); + if( RaceCombo( "##estRace", _new.Race, out var race ) ) + { + _new = _new with { Race = race }; + } + + ImGui.TableNextColumn(); + if( GenderCombo( "##estGender", _new.Gender, out var gender ) ) + { + _new = _new with { Gender = gender }; + } + + ImGui.TableNextColumn(); + if( EstSlotCombo( "##estSlot", _new.Slot, out var slot ) ) + { + _new = _new with { Slot = slot }; + } + + // Values + ImGui.TableNextColumn(); + IntDragInput( "##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f ); + } + + public static void Draw( EstManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Race.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Gender.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Slot.ToString() ); + + // Values + var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId ); + ImGui.TableNextColumn(); + if( IntDragInput( "##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, + out var entry, 0, ushort.MaxValue, 0.05f ) ) + { + editor.Meta.Change( meta with { Entry = ( ushort )entry } ); + } + } + } + + private static class GmpRow + { + private static GmpManipulation _new = new(GmpEntry.Default, 1); + + private static float RotationWidth + => 75 * ImGuiHelpers.GlobalScale; + + private static float UnkWidth + => 50 * ImGuiHelpers.GlobalScale; + + private static float IdWidth + => 100 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var canAdd = editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = ExpandedGmpFile.GetDefault( _new.SetId ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) ) + { + _new = _new with { SetId = setId }; + } + + // Values + ImGui.TableNextColumn(); + Checkmark( "##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _ ); + ImGui.TableNextColumn(); + Checkmark( "##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _ ); + ImGui.TableNextColumn(); + IntDragInput( "##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0, + 360, 0f ); + ImGui.SameLine(); + IntDragInput( "##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0, + 360, 0f ); + ImGui.SameLine(); + IntDragInput( "##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0, + 360, 0f ); + ImGui.TableNextColumn(); + IntDragInput( "##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f ); + ImGui.SameLine(); + IntDragInput( "##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f ); + } + + public static void Draw( GmpManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SetId.ToString() ); + + // Values + var defaultEntry = ExpandedGmpFile.GetDefault( meta.SetId ); + ImGui.TableNextColumn(); + if( Checkmark( "##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { Enabled = enabled } } ); + } + + ImGui.TableNextColumn(); + if( Checkmark( "##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { Animated = animated } } ); + } + + ImGui.TableNextColumn(); + if( IntDragInput( "##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth, + meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { RotationA = ( ushort )rotationA } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth, + meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { RotationB = ( ushort )rotationB } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth, + meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { RotationC = ( ushort )rotationC } } ); + } + + ImGui.TableNextColumn(); + if( IntDragInput( "##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA, + defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { UnknownA = ( byte )unkA } } ); + } + + ImGui.SameLine(); + if( IntDragInput( "##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, + defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f ) ) + { + editor.Meta.Change( meta with { Entry = meta.Entry with { UnknownA = ( byte )unkB } } ); + } + } + } + + private static class RspRow + { + private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); + + private static float FloatWidth + => 150 * ImGuiHelpers.GlobalScale; + + public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + var canAdd = editor.Meta.CanAdd( _new ); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var defaultEntry = CmpFile.GetDefault( _new.SubRace, _new.Attribute ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) ) + { + editor.Meta.Add( _new with { Entry = defaultEntry } ); + } + + // Identifier + ImGui.TableNextColumn(); + if( SubRaceCombo( "##rspSubRace", _new.SubRace, out var subRace ) ) + { + _new = _new with { SubRace = subRace }; + } + + ImGui.TableNextColumn(); + if( RspAttributeCombo( "##rspAttribute", _new.Attribute, out var attribute ) ) + { + _new = _new with { Attribute = attribute }; + } + + // Values + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( FloatWidth ); + ImGui.DragFloat( "##rspValue", ref defaultEntry, 0f ); + } + + public static void Draw( RspManipulation meta, Mod.Editor editor, Vector2 iconSize ) + { + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta edit.", false, true ) ) + { + editor.Meta.Delete( meta ); + } + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.SubRace.ToName() ); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X ); + ImGui.TextUnformatted( meta.Attribute.ToFullString() ); + ImGui.TableNextColumn(); + + // Values + var def = CmpFile.GetDefault( meta.SubRace, meta.Attribute ); + var value = meta.Entry; + ImGui.SetNextItemWidth( FloatWidth ); + using var color = ImRaii.PushColor( ImGuiCol.FrameBg, + def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), + def != value ); + if( ImGui.DragFloat( "##rspValue", ref value, 0.001f, 0.01f, 8f ) && value is >= 0.01f and <= 8f ) + { + editor.Meta.Change( meta with { Entry = value } ); + } + + ImGuiUtil.HoverTooltip( $"Default Value: {def:0.###}" ); + } + } + + // Different combos to use with enums. + private static bool RaceCombo( string label, ModelRace current, out ModelRace race ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1 ); + + private static bool GenderCombo( string label, Gender current, out Gender gender ) + => ImGuiUtil.GenericEnumCombo( label, 120 * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 ); + + private static bool EqdpEquipSlotCombo( string label, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, + EquipSlotExtensions.ToName ); + + private static bool EqpEquipSlotCombo( string label, EquipSlot current, out EquipSlot slot ) + => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, + EquipSlotExtensions.ToName ); + + private static bool SubRaceCombo( string label, SubRace current, out SubRace subRace ) + => ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 ); + + private static bool RspAttributeCombo( string label, RspAttribute current, out RspAttribute attribute ) + => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute, + RspAttributeExtensions.ToFullString, 0, 1 ); + + private static bool EstSlotCombo( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute ) + => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute ); + + private static bool ImcTypeCombo( string label, ObjectType current, out ObjectType type ) + => ImGuiUtil.GenericEnumCombo( label, 110 * ImGuiHelpers.GlobalScale, current, out type, ObjectTypeExtensions.ValidImcTypes, + ObjectTypeExtensions.ToName ); + + // A number input for ids with a optional max id of given width. + // Returns true if newId changed against currentId. + private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int maxId ) + { + int tmp = currentId; + ImGui.SetNextItemWidth( width ); + if( ImGui.InputInt( label, ref tmp, 0 ) ) + { + tmp = Math.Clamp( tmp, 1, maxId ); + } + + newId = ( ushort )tmp; + return newId != currentId; + } + + // A checkmark that compares against a default value and shows a tooltip. + // Returns true if newValue is changed against currentValue. + private static bool Checkmark( string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue ) + { + using var color = ImRaii.PushColor( ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), defaultValue != currentValue ); + newValue = currentValue; + ImGui.Checkbox( label, ref newValue ); + ImGuiUtil.HoverTooltip( tooltip ); + return newValue != currentValue; + } + + // A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + // Returns true if newValue changed against currentValue. + private static bool IntDragInput( string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue, + int minValue, int maxValue, float speed ) + { + newValue = currentValue; + using var color = ImRaii.PushColor( ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue ); + ImGui.SetNextItemWidth( width ); + if( ImGui.DragInt( label, ref newValue, speed, minValue, maxValue ) ) + { + newValue = Math.Clamp( newValue, minValue, maxValue ); + } + + ImGuiUtil.HoverTooltip( tooltip ); + + return newValue != currentValue; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 630c07bf..4d919444 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -10,17 +9,18 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Meta.Manipulations; +using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.UI.Classes; -public class ModEditWindow : Window, IDisposable +public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; private Mod.Editor? _editor; private Mod? _mod; + private Vector2 _iconSize = Vector2.Zero; public void ChangeMod( Mod mod ) { @@ -54,6 +54,7 @@ public class ModEditWindow : Window, IDisposable return; } + _iconSize = new Vector2( ImGui.GetFrameHeight() ); DrawFileTab(); DrawMetaTab(); DrawSwapTab(); @@ -117,8 +118,9 @@ public class ModEditWindow : Window, IDisposable : disabled ? "The suffix is invalid." : _materialSuffixFrom.Length == 0 - ? _raceCode == GenderRace.Unknown ? "Convert all skin material suffices to the target." - : "Convert all skin material suffices for the given race code to the target." + ? _raceCode == GenderRace.Unknown + ? "Convert all skin material suffices to the target." + : "Convert all skin material suffices for the given race code to the target." : _raceCode == GenderRace.Unknown ? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'." : $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; @@ -374,6 +376,7 @@ public class ModEditWindow : Window, IDisposable isDefaultOption ) ) { _editor.SetSubMod( -1, 0 ); + isDefaultOption = true; } ImGui.SameLine(); @@ -395,8 +398,7 @@ public class ModEditWindow : Window, IDisposable return $"{group.Name}: {group[ _editor.OptionIdx ].Name}"; } - var groupLabel = GetLabel(); - using var combo = ImRaii.Combo( "##optionSelector", groupLabel, ImGuiComboFlags.NoArrowButton ); + using var combo = ImRaii.Combo( "##optionSelector", GetLabel(), ImGuiComboFlags.NoArrowButton ); if( !combo ) { return; @@ -497,74 +499,9 @@ public class ModEditWindow : Window, IDisposable } } - private void DrawMetaTab() - { - using var tab = ImRaii.TabItem( "Meta Manipulations" ); - if( !tab ) - { - return; - } - - DrawOptionSelectHeader(); - - var setsEqual = _editor!.CurrentManipulations.SetEquals( _editor.CurrentOption.Manipulations ); - var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; - ImGui.NewLine(); - if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) - { - _editor.ApplyManipulations(); - } - - ImGui.SameLine(); - tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; - if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) - { - _editor.RevertManipulations(); - } - - using var child = ImRaii.Child( "##meta", -Vector2.One, true ); - if( !child ) - { - return; - } - - using var list = ImRaii.Table( "##table", 3 ); - if( !list ) - { - return; - } - - foreach( var manip in _editor!.CurrentManipulations ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType switch - { - MetaManipulation.Type.Imc => manip.Imc.ToString(), - MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(), - MetaManipulation.Type.Eqp => manip.Eqp.ToString(), - MetaManipulation.Type.Est => manip.Est.ToString(), - MetaManipulation.Type.Gmp => manip.Gmp.ToString(), - MetaManipulation.Type.Rsp => manip.Rsp.ToString(), - _ => string.Empty, - } ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( manip.ManipulationType switch - { - MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(), - MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(), - MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(), - MetaManipulation.Type.Est => manip.Est.Entry.ToString(), - MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(), - MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(), - _ => string.Empty, - } ); - } - } - private string _newSwapKey = string.Empty; private string _newSwapValue = string.Empty; + private void DrawSwapTab() { using var tab = ImRaii.TabItem( "File Swaps" ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 7c4c5280..ab39f539 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Numerics; @@ -96,6 +97,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod PluginLog.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" ); } } + + while( _modsToAdd.TryDequeue( out var dir ) ) + { + Penumbra.ModManager.AddMod( dir ); + } } protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) @@ -212,9 +218,12 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } + // Mods need to be added thread-safely outside of iteration. + private readonly ConcurrentQueue< DirectoryInfo > _modsToAdd = new(); + // Clean up invalid directory if necessary. // Add successfully extracted mods. - private static void AddNewMod( FileInfo file, DirectoryInfo? dir, Exception? error ) + private void AddNewMod( FileInfo file, DirectoryInfo? dir, Exception? error ) { if( error != null ) { @@ -237,7 +246,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } else if( dir != null ) { - Penumbra.ModManager.AddMod( dir ); + _modsToAdd.Enqueue( dir ); } } diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index 2aeb44e5..ad832457 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Windows.Forms; namespace Penumbra.Util;