diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs new file mode 100644 index 00000000..96e305ad --- /dev/null +++ b/Penumbra.GameData/Data/ItemData.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Enums; + +namespace Penumbra.GameData.Data; + +public sealed class ItemData : DataSharer, IReadOnlyDictionary> +{ + private readonly IReadOnlyList> _items; + + private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) + { + var tmp = Enum.GetValues().Select(t => new List(1024)).ToArray(); + + var itemSheet = dataManager.GetExcelSheet(language)!; + foreach (var item in itemSheet) + { + var type = item.ToEquipType(); + if (type != FullEquipType.Unknown && item.Name.RawData.Length > 1) + tmp[(int)type].Add(item); + } + + var ret = new IReadOnlyList[tmp.Length]; + ret[0] = Array.Empty(); + for (var i = 1; i < tmp.Length; ++i) + ret[i] = tmp[i].OrderBy(item => item.Name.ToDalamudString().TextValue).ToArray(); + + return ret; + } + + public ItemData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + : base(pluginInterface, language, 1) + { + _items = TryCatchData("ItemList", () => CreateItems(dataManager, language)); + } + + protected override void DisposeInternal() + => DisposeTag("ItemList"); + + public IEnumerator>> GetEnumerator() + { + for (var i = 1; i < _items.Count; ++i) + yield return new KeyValuePair>((FullEquipType)i, _items[i]); + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _items.Count - 1; + + public bool ContainsKey(FullEquipType key) + => (int)key < _items.Count && key != FullEquipType.Unknown; + + public bool TryGetValue(FullEquipType key, out IReadOnlyList value) + { + if (ContainsKey(key)) + { + value = _items[(int)key]; + return true; + } + + value = _items[0]; + return false; + } + + public IReadOnlyList this[FullEquipType key] + => TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException(); + + public IEnumerable Keys + => Enum.GetValues().Skip(1); + + public IEnumerable> Values + => _items.Skip(1); +} diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs new file mode 100644 index 00000000..27c046ea --- /dev/null +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Lumina.Excel.GeneratedSheets; + +namespace Penumbra.GameData.Enums; + +public enum FullEquipType : byte +{ + Unknown, + + Head, + Body, + Hands, + Legs, + Feet, + + Ears, + Neck, + Wrists, + Finger, + + Fists, // PGL, MNK + Sword, // GLA, PLD Main + Axe, // MRD, WAR + Bow, // ARC, BRD + Lance, // LNC, DRG, + Staff, // THM, BLM, CNJ, WHM + Wand, // THM, BLM, CNJ, WHM Main + Book, // ACN, SMN, SCH + Daggers, // ROG, NIN + Broadsword, // DRK, + Gun, // MCH, + Orrery, // AST, + Katana, // SAM + Rapier, // RDM + Cane, // BLU + Gunblade, // GNB, + Glaives, // DNC, + Scythe, // RPR, + Nouliths, // SGE + Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off + + Saw, // CRP + CrossPeinHammer, // BSM + RaisingHammer, // ARM + LapidaryHammer, // GSM + Knife, // LTW + Needle, // WVR + Alembic, // ALC + Frypan, // CUL + Pickaxe, // MIN + Hatchet, // BTN + FishingRod, // FSH + + ClawHammer, // CRP Off + File, // BSM Off + Pliers, // ARM Off + GrindingWheel, // GSM Off + Awl, // LTW Off + SpinningWheel, // WVR Off + Mortar, // ALC Off + CulinaryKnife, // CUL Off + Sledgehammer, // MIN Off + GardenScythe, // BTN Off + Gig, // FSH Off +} + +public static class FullEquipTypeExtensions +{ + public static FullEquipType ToEquipType(this Item item) + { + var slot = (EquipSlot)item.EquipSlotCategory.Row; + var weapon = (WeaponCategory)item.ItemUICategory.Row; + return slot.ToEquipType(weapon); + } + + public static bool IsWeapon(this FullEquipType type) + => type switch + { + FullEquipType.Fists => true, + FullEquipType.Sword => true, + FullEquipType.Axe => true, + FullEquipType.Bow => true, + FullEquipType.Lance => true, + FullEquipType.Staff => true, + FullEquipType.Wand => true, + FullEquipType.Book => true, + FullEquipType.Daggers => true, + FullEquipType.Broadsword => true, + FullEquipType.Gun => true, + FullEquipType.Orrery => true, + FullEquipType.Katana => true, + FullEquipType.Rapier => true, + FullEquipType.Cane => true, + FullEquipType.Gunblade => true, + FullEquipType.Glaives => true, + FullEquipType.Scythe => true, + FullEquipType.Nouliths => true, + FullEquipType.Shield => true, + _ => false, + }; + + public static bool IsTool(this FullEquipType type) + => type switch + { + FullEquipType.Saw => true, + FullEquipType.CrossPeinHammer => true, + FullEquipType.RaisingHammer => true, + FullEquipType.LapidaryHammer => true, + FullEquipType.Knife => true, + FullEquipType.Needle => true, + FullEquipType.Alembic => true, + FullEquipType.Frypan => true, + FullEquipType.Pickaxe => true, + FullEquipType.Hatchet => true, + FullEquipType.FishingRod => true, + _ => false, + }; + + public static bool IsEquipment(this FullEquipType type) + => type switch + { + FullEquipType.Head => true, + FullEquipType.Body => true, + FullEquipType.Hands => true, + FullEquipType.Legs => true, + FullEquipType.Feet => true, + _ => false, + }; + + public static bool IsAccessory(this FullEquipType type) + => type switch + { + FullEquipType.Ears => true, + FullEquipType.Neck => true, + FullEquipType.Wrists => true, + FullEquipType.Finger => true, + _ => false, + }; + + public static string ToName(this FullEquipType type) + => type switch + { + FullEquipType.Head => EquipSlot.Head.ToName(), + FullEquipType.Body => EquipSlot.Body.ToName(), + FullEquipType.Hands => EquipSlot.Hands.ToName(), + FullEquipType.Legs => EquipSlot.Legs.ToName(), + FullEquipType.Feet => EquipSlot.Feet.ToName(), + FullEquipType.Ears => EquipSlot.Ears.ToName(), + FullEquipType.Neck => EquipSlot.Neck.ToName(), + FullEquipType.Wrists => EquipSlot.Wrists.ToName(), + FullEquipType.Finger => "Ring", + FullEquipType.Fists => "Fist Weapon", + FullEquipType.Sword => "Sword", + FullEquipType.Axe => "Axe", + FullEquipType.Bow => "Bow", + FullEquipType.Lance => "Lance", + FullEquipType.Staff => "Staff", + FullEquipType.Wand => "Mace", + FullEquipType.Book => "Book", + FullEquipType.Daggers => "Dagger", + FullEquipType.Broadsword => "Broadsword", + FullEquipType.Gun => "Gun", + FullEquipType.Orrery => "Orrery", + FullEquipType.Katana => "Katana", + FullEquipType.Rapier => "Rapier", + FullEquipType.Cane => "Cane", + FullEquipType.Gunblade => "Gunblade", + FullEquipType.Glaives => "Glaive", + FullEquipType.Scythe => "Scythe", + FullEquipType.Nouliths => "Nouliths", + FullEquipType.Shield => "Shield", + FullEquipType.Saw => "Saw (Carpenter)", + FullEquipType.CrossPeinHammer => "Hammer (Blacksmith)", + FullEquipType.RaisingHammer => "Hammer (Armorsmith)", + FullEquipType.LapidaryHammer => "Hammer (Goldsmith)", + FullEquipType.Knife => "Knife (Leatherworker)", + FullEquipType.Needle => "Needle (Weaver)", + FullEquipType.Alembic => "Alembic (Alchemist)", + FullEquipType.Frypan => "Frypan (Culinarian)", + FullEquipType.Pickaxe => "Pickaxe (Miner)", + FullEquipType.Hatchet => "Hatchet (Botanist)", + FullEquipType.FishingRod => "Fishing Rod", + FullEquipType.ClawHammer => "Clawhammer (Carpenter)", + FullEquipType.File => "File (Blacksmith)", + FullEquipType.Pliers => "Pliers (Armorsmith)", + FullEquipType.GrindingWheel => "Grinding Wheel (Goldsmith)", + FullEquipType.Awl => "Awl (Leatherworker)", + FullEquipType.SpinningWheel => "Spinning Wheel (Weaver)", + FullEquipType.Mortar => "Mortar (Alchemist)", + FullEquipType.CulinaryKnife => "Knife (Culinarian)", + FullEquipType.Sledgehammer => "Sledgehammer (Miner)", + FullEquipType.GardenScythe => "Garden Scythe (Botanist)", + FullEquipType.Gig => "Gig (Fisher)", + _ => "Unknown", + }; + + public static EquipSlot ToSlot(this FullEquipType type) + => type switch + { + FullEquipType.Head => EquipSlot.Head, + FullEquipType.Body => EquipSlot.Body, + FullEquipType.Hands => EquipSlot.Hands, + FullEquipType.Legs => EquipSlot.Legs, + FullEquipType.Feet => EquipSlot.Feet, + FullEquipType.Ears => EquipSlot.Ears, + FullEquipType.Neck => EquipSlot.Neck, + FullEquipType.Wrists => EquipSlot.Wrists, + FullEquipType.Finger => EquipSlot.RFinger, + FullEquipType.Fists => EquipSlot.MainHand, + FullEquipType.Sword => EquipSlot.MainHand, + FullEquipType.Axe => EquipSlot.MainHand, + FullEquipType.Bow => EquipSlot.MainHand, + FullEquipType.Lance => EquipSlot.MainHand, + FullEquipType.Staff => EquipSlot.MainHand, + FullEquipType.Wand => EquipSlot.MainHand, + FullEquipType.Book => EquipSlot.MainHand, + FullEquipType.Daggers => EquipSlot.MainHand, + FullEquipType.Broadsword => EquipSlot.MainHand, + FullEquipType.Gun => EquipSlot.MainHand, + FullEquipType.Orrery => EquipSlot.MainHand, + FullEquipType.Katana => EquipSlot.MainHand, + FullEquipType.Rapier => EquipSlot.MainHand, + FullEquipType.Cane => EquipSlot.MainHand, + FullEquipType.Gunblade => EquipSlot.MainHand, + FullEquipType.Glaives => EquipSlot.MainHand, + FullEquipType.Scythe => EquipSlot.MainHand, + FullEquipType.Nouliths => EquipSlot.MainHand, + FullEquipType.Shield => EquipSlot.OffHand, + FullEquipType.Saw => EquipSlot.MainHand, + FullEquipType.CrossPeinHammer => EquipSlot.MainHand, + FullEquipType.RaisingHammer => EquipSlot.MainHand, + FullEquipType.LapidaryHammer => EquipSlot.MainHand, + FullEquipType.Knife => EquipSlot.MainHand, + FullEquipType.Needle => EquipSlot.MainHand, + FullEquipType.Alembic => EquipSlot.MainHand, + FullEquipType.Frypan => EquipSlot.MainHand, + FullEquipType.Pickaxe => EquipSlot.MainHand, + FullEquipType.Hatchet => EquipSlot.MainHand, + FullEquipType.FishingRod => EquipSlot.MainHand, + FullEquipType.ClawHammer => EquipSlot.OffHand, + FullEquipType.File => EquipSlot.OffHand, + FullEquipType.Pliers => EquipSlot.OffHand, + FullEquipType.GrindingWheel => EquipSlot.OffHand, + FullEquipType.Awl => EquipSlot.OffHand, + FullEquipType.SpinningWheel => EquipSlot.OffHand, + FullEquipType.Mortar => EquipSlot.OffHand, + FullEquipType.CulinaryKnife => EquipSlot.OffHand, + FullEquipType.Sledgehammer => EquipSlot.OffHand, + FullEquipType.GardenScythe => EquipSlot.OffHand, + FullEquipType.Gig => EquipSlot.OffHand, + _ => EquipSlot.Unknown, + }; + + public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown) + => slot switch + { + EquipSlot.Head => FullEquipType.Head, + EquipSlot.Body => FullEquipType.Body, + EquipSlot.Hands => FullEquipType.Hands, + EquipSlot.Legs => FullEquipType.Legs, + EquipSlot.Feet => FullEquipType.Feet, + EquipSlot.Ears => FullEquipType.Ears, + EquipSlot.Neck => FullEquipType.Neck, + EquipSlot.Wrists => FullEquipType.Wrists, + EquipSlot.RFinger => FullEquipType.Finger, + EquipSlot.LFinger => FullEquipType.Finger, + EquipSlot.HeadBody => FullEquipType.Body, + EquipSlot.BodyHandsLegsFeet => FullEquipType.Body, + EquipSlot.LegsFeet => FullEquipType.Legs, + EquipSlot.FullBody => FullEquipType.Body, + EquipSlot.BodyHands => FullEquipType.Body, + EquipSlot.BodyLegsFeet => FullEquipType.Body, + EquipSlot.ChestHands => FullEquipType.Body, + EquipSlot.MainHand => category.ToEquipType(), + EquipSlot.OffHand => category.ToEquipType(), + EquipSlot.BothHand => category.ToEquipType(), + _ => FullEquipType.Unknown, + }; + + public static FullEquipType ToEquipType(this WeaponCategory category) + => category switch + { + WeaponCategory.Pugilist => FullEquipType.Fists, + WeaponCategory.Gladiator => FullEquipType.Sword, + WeaponCategory.Marauder => FullEquipType.Axe, + WeaponCategory.Archer => FullEquipType.Bow, + WeaponCategory.Lancer => FullEquipType.Lance, + WeaponCategory.Thaumaturge1 => FullEquipType.Wand, + WeaponCategory.Thaumaturge2 => FullEquipType.Staff, + WeaponCategory.Conjurer1 => FullEquipType.Wand, + WeaponCategory.Conjurer2 => FullEquipType.Staff, + WeaponCategory.Arcanist => FullEquipType.Book, + WeaponCategory.Shield => FullEquipType.Shield, + WeaponCategory.CarpenterMain => FullEquipType.Saw, + WeaponCategory.CarpenterOff => FullEquipType.ClawHammer, + WeaponCategory.BlacksmithMain => FullEquipType.CrossPeinHammer, + WeaponCategory.BlacksmithOff => FullEquipType.File, + WeaponCategory.ArmorerMain => FullEquipType.RaisingHammer, + WeaponCategory.ArmorerOff => FullEquipType.Pliers, + WeaponCategory.GoldsmithMain => FullEquipType.LapidaryHammer, + WeaponCategory.GoldsmithOff => FullEquipType.GrindingWheel, + WeaponCategory.LeatherworkerMain => FullEquipType.Knife, + WeaponCategory.LeatherworkerOff => FullEquipType.Awl, + WeaponCategory.WeaverMain => FullEquipType.Needle, + WeaponCategory.WeaverOff => FullEquipType.SpinningWheel, + WeaponCategory.AlchemistMain => FullEquipType.Alembic, + WeaponCategory.AlchemistOff => FullEquipType.Mortar, + WeaponCategory.CulinarianMain => FullEquipType.Frypan, + WeaponCategory.CulinarianOff => FullEquipType.CulinaryKnife, + WeaponCategory.MinerMain => FullEquipType.Pickaxe, + WeaponCategory.MinerOff => FullEquipType.Sledgehammer, + WeaponCategory.BotanistMain => FullEquipType.Hatchet, + WeaponCategory.BotanistOff => FullEquipType.GardenScythe, + WeaponCategory.FisherMain => FullEquipType.FishingRod, + WeaponCategory.Rogue => FullEquipType.Gig, + WeaponCategory.DarkKnight => FullEquipType.Broadsword, + WeaponCategory.Machinist => FullEquipType.Gun, + WeaponCategory.Astrologian => FullEquipType.Orrery, + WeaponCategory.Samurai => FullEquipType.Katana, + WeaponCategory.RedMage => FullEquipType.Rapier, + WeaponCategory.Scholar => FullEquipType.Book, + WeaponCategory.FisherOff => FullEquipType.Gig, + WeaponCategory.BlueMage => FullEquipType.Cane, + WeaponCategory.Gunbreaker => FullEquipType.Gunblade, + WeaponCategory.Dancer => FullEquipType.Glaives, + WeaponCategory.Reaper => FullEquipType.Scythe, + WeaponCategory.Sage => FullEquipType.Nouliths, + _ => FullEquipType.Unknown, + }; + + public static FullEquipType Offhand(this FullEquipType type) + => type switch + { + FullEquipType.Fists => FullEquipType.Fists, + FullEquipType.Sword => FullEquipType.Shield, + FullEquipType.Wand => FullEquipType.Shield, + FullEquipType.Daggers => FullEquipType.Daggers, + FullEquipType.Gun => FullEquipType.Gun, + FullEquipType.Orrery => FullEquipType.Orrery, + FullEquipType.Rapier => FullEquipType.Rapier, + FullEquipType.Glaives => FullEquipType.Glaives, + _ => FullEquipType.Unknown, + }; + + public static readonly IReadOnlyList WeaponTypes + = Enum.GetValues().Where(v => v.IsWeapon()).ToArray(); + + public static readonly IReadOnlyList ToolTypes + = Enum.GetValues().Where(v => v.IsTool()).ToArray(); + + public static readonly IReadOnlyList EquipmentTypes + = Enum.GetValues().Where(v => v.IsEquipment()).ToArray(); + + public static readonly IReadOnlyList AccessoryTypes + = Enum.GetValues().Where(v => v.IsAccessory()).ToArray(); +} diff --git a/Penumbra.GameData/Enums/WeaponCategory.cs b/Penumbra.GameData/Enums/WeaponCategory.cs index f7b1fc91..b40fa48a 100644 --- a/Penumbra.GameData/Enums/WeaponCategory.cs +++ b/Penumbra.GameData/Enums/WeaponCategory.cs @@ -50,160 +50,4 @@ public enum WeaponCategory : byte Dancer = 107, Reaper = 108, Sage = 109, -} - -public static class WeaponCategoryExtensions -{ - public static WeaponCategory AllowsOffHand( this WeaponCategory category ) - => category switch - { - WeaponCategory.Pugilist => WeaponCategory.Pugilist, - WeaponCategory.Gladiator => WeaponCategory.Shield, - WeaponCategory.Marauder => WeaponCategory.Unknown, - WeaponCategory.Archer => WeaponCategory.Unknown, - WeaponCategory.Lancer => WeaponCategory.Unknown, - WeaponCategory.Thaumaturge1 => WeaponCategory.Shield, - WeaponCategory.Thaumaturge2 => WeaponCategory.Unknown, - WeaponCategory.Conjurer1 => WeaponCategory.Shield, - WeaponCategory.Conjurer2 => WeaponCategory.Unknown, - WeaponCategory.Arcanist => WeaponCategory.Unknown, - WeaponCategory.Shield => WeaponCategory.Unknown, - WeaponCategory.CarpenterMain => WeaponCategory.CarpenterOff, - WeaponCategory.CarpenterOff => WeaponCategory.Unknown, - WeaponCategory.BlacksmithMain => WeaponCategory.BlacksmithOff, - WeaponCategory.BlacksmithOff => WeaponCategory.Unknown, - WeaponCategory.ArmorerMain => WeaponCategory.ArmorerOff, - WeaponCategory.ArmorerOff => WeaponCategory.Unknown, - WeaponCategory.GoldsmithMain => WeaponCategory.GoldsmithOff, - WeaponCategory.GoldsmithOff => WeaponCategory.Unknown, - WeaponCategory.LeatherworkerMain => WeaponCategory.LeatherworkerOff, - WeaponCategory.LeatherworkerOff => WeaponCategory.Unknown, - WeaponCategory.WeaverMain => WeaponCategory.WeaverOff, - WeaponCategory.WeaverOff => WeaponCategory.Unknown, - WeaponCategory.AlchemistMain => WeaponCategory.AlchemistOff, - WeaponCategory.AlchemistOff => WeaponCategory.Unknown, - WeaponCategory.CulinarianMain => WeaponCategory.CulinarianOff, - WeaponCategory.CulinarianOff => WeaponCategory.Unknown, - WeaponCategory.MinerMain => WeaponCategory.MinerOff, - WeaponCategory.MinerOff => WeaponCategory.Unknown, - WeaponCategory.BotanistMain => WeaponCategory.BotanistOff, - WeaponCategory.BotanistOff => WeaponCategory.Unknown, - WeaponCategory.FisherMain => WeaponCategory.FisherOff, - WeaponCategory.Rogue => WeaponCategory.Rogue, - WeaponCategory.DarkKnight => WeaponCategory.Unknown, - WeaponCategory.Machinist => WeaponCategory.Machinist, - WeaponCategory.Astrologian => WeaponCategory.Astrologian, - WeaponCategory.Samurai => WeaponCategory.Unknown, - WeaponCategory.RedMage => WeaponCategory.RedMage, - WeaponCategory.Scholar => WeaponCategory.Unknown, - WeaponCategory.FisherOff => WeaponCategory.Unknown, - WeaponCategory.BlueMage => WeaponCategory.Unknown, - WeaponCategory.Gunbreaker => WeaponCategory.Unknown, - WeaponCategory.Dancer => WeaponCategory.Dancer, - WeaponCategory.Reaper => WeaponCategory.Unknown, - WeaponCategory.Sage => WeaponCategory.Unknown, - _ => WeaponCategory.Unknown, - }; - - public static EquipSlot ToSlot( this WeaponCategory category ) - => category switch - { - WeaponCategory.Pugilist => EquipSlot.MainHand, - WeaponCategory.Gladiator => EquipSlot.MainHand, - WeaponCategory.Marauder => EquipSlot.MainHand, - WeaponCategory.Archer => EquipSlot.MainHand, - WeaponCategory.Lancer => EquipSlot.MainHand, - WeaponCategory.Thaumaturge1 => EquipSlot.MainHand, - WeaponCategory.Thaumaturge2 => EquipSlot.MainHand, - WeaponCategory.Conjurer1 => EquipSlot.MainHand, - WeaponCategory.Conjurer2 => EquipSlot.MainHand, - WeaponCategory.Arcanist => EquipSlot.MainHand, - WeaponCategory.Shield => EquipSlot.OffHand, - WeaponCategory.CarpenterMain => EquipSlot.MainHand, - WeaponCategory.CarpenterOff => EquipSlot.OffHand, - WeaponCategory.BlacksmithMain => EquipSlot.MainHand, - WeaponCategory.BlacksmithOff => EquipSlot.OffHand, - WeaponCategory.ArmorerMain => EquipSlot.MainHand, - WeaponCategory.ArmorerOff => EquipSlot.OffHand, - WeaponCategory.GoldsmithMain => EquipSlot.MainHand, - WeaponCategory.GoldsmithOff => EquipSlot.OffHand, - WeaponCategory.LeatherworkerMain => EquipSlot.MainHand, - WeaponCategory.LeatherworkerOff => EquipSlot.OffHand, - WeaponCategory.WeaverMain => EquipSlot.MainHand, - WeaponCategory.WeaverOff => EquipSlot.OffHand, - WeaponCategory.AlchemistMain => EquipSlot.MainHand, - WeaponCategory.AlchemistOff => EquipSlot.OffHand, - WeaponCategory.CulinarianMain => EquipSlot.MainHand, - WeaponCategory.CulinarianOff => EquipSlot.OffHand, - WeaponCategory.MinerMain => EquipSlot.MainHand, - WeaponCategory.MinerOff => EquipSlot.OffHand, - WeaponCategory.BotanistMain => EquipSlot.MainHand, - WeaponCategory.BotanistOff => EquipSlot.OffHand, - WeaponCategory.FisherMain => EquipSlot.MainHand, - WeaponCategory.Rogue => EquipSlot.MainHand, - WeaponCategory.DarkKnight => EquipSlot.MainHand, - WeaponCategory.Machinist => EquipSlot.MainHand, - WeaponCategory.Astrologian => EquipSlot.MainHand, - WeaponCategory.Samurai => EquipSlot.MainHand, - WeaponCategory.RedMage => EquipSlot.MainHand, - WeaponCategory.Scholar => EquipSlot.MainHand, - WeaponCategory.FisherOff => EquipSlot.OffHand, - WeaponCategory.BlueMage => EquipSlot.MainHand, - WeaponCategory.Gunbreaker => EquipSlot.MainHand, - WeaponCategory.Dancer => EquipSlot.MainHand, - WeaponCategory.Reaper => EquipSlot.MainHand, - WeaponCategory.Sage => EquipSlot.MainHand, - _ => EquipSlot.Unknown, - }; - - public static int ToIndex( this WeaponCategory category ) - => category switch - { - WeaponCategory.Pugilist => 0, - WeaponCategory.Gladiator => 1, - WeaponCategory.Marauder => 2, - WeaponCategory.Archer => 3, - WeaponCategory.Lancer => 4, - WeaponCategory.Thaumaturge1 => 5, - WeaponCategory.Thaumaturge2 => 6, - WeaponCategory.Conjurer1 => 7, - WeaponCategory.Conjurer2 => 8, - WeaponCategory.Arcanist => 9, - WeaponCategory.Shield => 10, - WeaponCategory.CarpenterMain => 11, - WeaponCategory.CarpenterOff => 12, - WeaponCategory.BlacksmithMain => 13, - WeaponCategory.BlacksmithOff => 14, - WeaponCategory.ArmorerMain => 15, - WeaponCategory.ArmorerOff => 16, - WeaponCategory.GoldsmithMain => 17, - WeaponCategory.GoldsmithOff => 18, - WeaponCategory.LeatherworkerMain => 19, - WeaponCategory.LeatherworkerOff => 20, - WeaponCategory.WeaverMain => 21, - WeaponCategory.WeaverOff => 22, - WeaponCategory.AlchemistMain => 23, - WeaponCategory.AlchemistOff => 24, - WeaponCategory.CulinarianMain => 25, - WeaponCategory.CulinarianOff => 26, - WeaponCategory.MinerMain => 27, - WeaponCategory.MinerOff => 28, - WeaponCategory.BotanistMain => 29, - WeaponCategory.BotanistOff => 30, - WeaponCategory.FisherMain => 31, - WeaponCategory.Rogue => 32, - WeaponCategory.DarkKnight => 33, - WeaponCategory.Machinist => 34, - WeaponCategory.Astrologian => 35, - WeaponCategory.Samurai => 36, - WeaponCategory.RedMage => 37, - WeaponCategory.Scholar => 38, - WeaponCategory.FisherOff => 39, - WeaponCategory.BlueMage => 40, - WeaponCategory.Gunbreaker => 41, - WeaponCategory.Dancer => 42, - WeaponCategory.Reaper => 43, - WeaponCategory.Sage => 44, - _ => -1, - }; } \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs b/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs deleted file mode 100644 index 70847af8..00000000 --- a/Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; -using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Files; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Mods.ItemSwap; - -public class EquipmentDataContainer -{ - public Item Item; - public EquipSlot Slot; - public SetId ModelId; - public byte Variant; - - public ImcManipulation ImcData; - - public EqpManipulation EqpData; - public GmpManipulation GmpData; - - // Example: Abyssos Helm / Body - public string AvfxPath = string.Empty; - - // Example: Dodore Doublet, but unknown what it does? - public string SoundPath = string.Empty; - - // Example: Crimson Standard Bracelet - public string DecalPath = string.Empty; - - // Example: The Howling Spirit and The Wailing Spirit, but unknown what it does. - public string AnimationPath = string.Empty; - - public Dictionary< GenderRace, GenderRaceContainer > Files = new(); - - public struct GenderRaceContainer - { - public EqdpManipulation Eqdp; - public GenderRace ModelRace; - public GenderRace MaterialRace; - public EstManipulation Est; - public string MdlPath; - public MtrlContainer[] MtrlPaths; - } - - public struct MtrlContainer - { - public string MtrlPath; - public string[] Textures; - public string Shader; - - public MtrlContainer( string mtrlPath ) - { - MtrlPath = mtrlPath; - var file = Dalamud.GameData.GetFile( mtrlPath ); - if( file != null ) - { - var mtrl = new MtrlFile( file.Data ); - Textures = mtrl.Textures.Select( t => t.Path ).ToArray(); - Shader = $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}"; - } - else - { - Textures = Array.Empty< string >(); - Shader = string.Empty; - } - } - } - - - private static EstManipulation GetEstEntry( GenderRace genderRace, SetId setId, EquipSlot slot ) - { - if( slot == EquipSlot.Head ) - { - var entry = EstFile.GetDefault( EstManipulation.EstType.Head, genderRace, setId.Value ); - return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Head, setId.Value, entry ); - } - - if( slot == EquipSlot.Body ) - { - var entry = EstFile.GetDefault( EstManipulation.EstType.Body, genderRace, setId.Value ); - return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Body, setId.Value, entry ); - } - - return default; - } - - private static GenderRaceContainer GetGenderRace( GenderRace genderRace, SetId modelId, EquipSlot slot, ushort materialId ) - { - var ret = new GenderRaceContainer() - { - Eqdp = GetEqdpEntry( genderRace, modelId, slot ), - Est = GetEstEntry( genderRace, modelId, slot ), - }; - ( ret.ModelRace, ret.MaterialRace ) = TraverseEqdpTree( genderRace, modelId, slot ); - ret.MdlPath = GamePaths.Equipment.Mdl.Path( modelId, ret.ModelRace, slot ); - ret.MtrlPaths = MtrlPaths( ret.MdlPath, ret.MaterialRace, modelId, materialId ); - return ret; - } - - private static EqdpManipulation GetEqdpEntry( GenderRace genderRace, SetId modelId, EquipSlot slot ) - { - var entry = ExpandedEqdpFile.GetDefault( genderRace, slot.IsAccessory(), modelId.Value ); - return new EqdpManipulation( entry, slot, genderRace.Split().Item1, genderRace.Split().Item2, modelId.Value ); - } - - private static MtrlContainer[] MtrlPaths( string mdlPath, GenderRace mtrlRace, SetId modelId, ushort materialId ) - { - var file = Dalamud.GameData.GetFile( mdlPath ); - if( file == null ) - { - return Array.Empty< MtrlContainer >(); - } - - var mdl = new MdlFile( Dalamud.GameData.GetFile( mdlPath )!.Data ); - var basePath = GamePaths.Equipment.Mtrl.FolderPath( modelId, ( byte )materialId ); - var equipPart = $"e{modelId.Value:D4}"; - var racePart = $"c{mtrlRace.ToRaceCode()}"; - - return mdl.Materials - .Where( m => m.Contains( equipPart ) ) - .Select( m => new MtrlContainer( $"{basePath}{m.Replace( "c0101", racePart )}" ) ) - .ToArray(); - } - - private static (GenderRace, GenderRace) TraverseEqdpTree( GenderRace genderRace, SetId modelId, EquipSlot slot ) - { - var model = GenderRace.Unknown; - var material = GenderRace.Unknown; - var accessory = slot.IsAccessory(); - foreach( var gr in genderRace.Dependencies() ) - { - var entry = ExpandedEqdpFile.GetDefault( gr, accessory, modelId.Value ); - var (b1, b2) = entry.ToBits( slot ); - if( b1 && material == GenderRace.Unknown ) - { - material = gr; - if( model != GenderRace.Unknown ) - { - return ( model, material ); - } - } - - if( b2 && model == GenderRace.Unknown ) - { - model = gr; - if( material != GenderRace.Unknown ) - { - return ( model, material ); - } - } - } - - return ( GenderRace.MidlanderMale, GenderRace.MidlanderMale ); - } - - - public EquipmentDataContainer( Item i ) - { - Item = i; - LookupItem( i, out Slot, out ModelId, out Variant ); - LookupImc( ModelId, Variant, Slot ); - EqpData = new EqpManipulation( ExpandedEqpFile.GetDefault( ModelId.Value ), Slot, ModelId.Value ); - GmpData = Slot == EquipSlot.Head ? new GmpManipulation( ExpandedGmpFile.GetDefault( ModelId.Value ), ModelId.Value ) : default; - - - foreach( var genderRace in Enum.GetValues< GenderRace >() ) - { - if( CharacterUtility.EqdpIdx( genderRace, Slot.IsAccessory() ) < 0 ) - { - continue; - } - - Files[ genderRace ] = GetGenderRace( genderRace, ModelId, Slot, ImcData.Entry.MaterialId ); - } - } - - - private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant ) - { - slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); - if( !slot.IsEquipment() ) - { - throw new ItemSwap.InvalidItemTypeException(); - } - - modelId = ( ( Quad )i.ModelMain ).A; - variant = ( byte )( ( Quad )i.ModelMain ).B; - } - - - - private void LookupImc( SetId modelId, byte variant, EquipSlot slot ) - { - var imc = ImcFile.GetDefault( GamePaths.Equipment.Imc.Path( modelId ), slot, variant, out var exists ); - if( !exists ) - { - throw new ItemSwap.InvalidImcException(); - } - - ImcData = new ImcManipulation( slot, variant, modelId.Value, imc ); - if( imc.DecalId != 0 ) - { - DecalPath = GamePaths.Equipment.Decal.Path( imc.DecalId ); - } - - // TODO: Figure out how this works. - if( imc.SoundId != 0 ) - { - SoundPath = string.Empty; - } - - if( imc.VfxId != 0 ) - { - AvfxPath = GamePaths.Equipment.Avfx.Path( modelId, imc.VfxId ); - } - - // TODO: Figure out how this works. - if( imc.MaterialAnimationId != 0 ) - { - AnimationPath = string.Empty; - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index f065d6be..301108f2 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -252,9 +252,13 @@ public static class EquipmentSwap return false; } + // IMC also controls sound, Example: Dodore Doublet, but unknown what it does? + // IMC also controls some material animation, Example: The Howling Spirit and The Wailing Spirit, but unknown what it does. + return true; } - + + // Example: Crimson Standard Bracelet public static bool AddDecal( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, byte decalId, MetaSwap imc ) { if( decalId != 0 ) @@ -271,6 +275,8 @@ public static class EquipmentSwap return true; } + + // Example: Abyssos Helm / Body public static bool AddAvfx( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId, MetaSwap imc ) { if( vfxId != 0 ) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 8027e7d1..eae8a60b 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -37,11 +37,12 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps ) + public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 ) { var convertedManips = new HashSet< MetaManipulation >( Swaps.Count ); var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); var convertedSwaps = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); + directory ??= mod.ModPath; try { foreach( var swap in Swaps.SelectMany( s => s.WithChildren() ) ) @@ -62,7 +63,7 @@ public class ItemSwapContainer } else { - var path = file.GetNewPath( mod.ModPath.FullName ); + var path = file.GetNewPath( directory.FullName ); var bytes = file.FileData.Write(); Directory.CreateDirectory( Path.GetDirectoryName( path )! ); File.WriteAllBytes( path, bytes ); @@ -80,9 +81,9 @@ public class ItemSwapContainer } } - Penumbra.ModManager.OptionSetFiles( mod, -1, 0, convertedFiles ); - Penumbra.ModManager.OptionSetFileSwaps( mod, -1, 0, convertedSwaps ); - Penumbra.ModManager.OptionSetManipulations( mod, -1, 0, convertedManips ); + Penumbra.ModManager.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles ); + Penumbra.ModManager.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps ); + Penumbra.ModManager.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips ); return true; } catch( Exception e ) @@ -120,7 +121,7 @@ public class ItemSwapContainer Loaded = true; return ret; } - catch( Exception e ) + catch { Swaps.Clear(); Loaded = false; diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index afb59c94..40cc9d2f 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -47,8 +47,10 @@ public partial class Mod return new DirectoryInfo( newModFolder ); } - // Create the name for a group or option subfolder based on its parent folder and given name. - // subFolderName should never be empty, and the result is unique and contains no invalid symbols. + /// + /// Create the name for a group or option subfolder based on its parent folder and given name. + /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. + /// internal static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) { var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b2550a69..91c24705 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,8 +22,13 @@ using Penumbra.Util; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Actors; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; +using Penumbra.Meta.Files; using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -63,6 +68,7 @@ public class Penumbra : IDalamudPlugin public static IObjectIdentifier Identifier { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!; public static StainManager StainManager { get; private set; } = null!; + public static ItemData ItemData { get; private set; } = null!; public static readonly List< Exception > ImcExceptions = new(); @@ -92,6 +98,7 @@ public class Penumbra : IDalamudPlugin Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); + ItemData = new ItemData( Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language ); Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ); Framework = new FrameworkManager(); @@ -289,6 +296,7 @@ public class Penumbra : IDalamudPlugin Api?.Dispose(); _commandHandler?.Dispose(); StainManager?.Dispose(); + ItemData?.Dispose(); Actors?.Dispose(); Identifier?.Dispose(); Framework?.Dispose(); diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 2d23abc4..2e59b540 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; @@ -14,46 +17,48 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.ItemSwap; +using Penumbra.Util; namespace Penumbra.UI.Classes; public class ItemSwapWindow : IDisposable { - private class EquipSelector : FilterComboCache< Item > + private enum SwapType { - public EquipSelector() - : base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i - => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsEquipment() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) ) - { } - - protected override string ToString( Item obj ) - => obj.Name.ToString(); + Hat, + Top, + Gloves, + Pants, + Shoes, + Earrings, + Necklace, + Bracelet, + Ring, + Hair, + Face, + Ears, + Tail, + Weapon, } - private class AccessorySelector : FilterComboCache< Item > + private class ItemSelector : FilterComboCache< (string, Item) > { - public AccessorySelector() - : base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i - => ( ( EquipSlot )i.EquipSlotCategory.Row ).IsAccessory() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) ) + public ItemSelector( FullEquipType type ) + : base( () => Penumbra.ItemData[ type ].Select( i => ( i.Name.ToDalamudString().TextValue, i ) ).ToArray() ) { } - protected override string ToString( Item obj ) - => obj.Name.ToString(); + protected override string ToString( (string, Item) obj ) + => obj.Item1; } - private class SlotSelector : FilterComboCache< Item > + private class WeaponSelector : FilterComboCache< FullEquipType > { - public readonly EquipSlot CurrentSlot; + public WeaponSelector() + : base( FullEquipTypeExtensions.WeaponTypes.Concat( FullEquipTypeExtensions.ToolTypes ) ) + { } - public SlotSelector( EquipSlot slot ) - : base( () => Dalamud.GameData.GetExcelSheet< Item >()!.Where( i - => ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot() == slot && i.ModelMain != 0 && i.Name.RawData.Length > 0 ).ToList() ) - { - CurrentSlot = slot; - } - - protected override string ToString( Item obj ) - => obj.Name.ToString(); + protected override string ToString( FullEquipType type ) + => type.ToName(); } public ItemSwapWindow() @@ -68,30 +73,44 @@ public class ItemSwapWindow : IDisposable Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; } - private readonly EquipSelector _equipSelector = new(); - private readonly AccessorySelector _accessorySelector = new(); - private SlotSelector? _slotSelector; - private readonly ItemSwapContainer _swapData = new(); + private readonly Dictionary< SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo) > _selectors = new() + { + [ SwapType.Hat ] = ( new ItemSelector( FullEquipType.Head ), new ItemSelector( FullEquipType.Head ), "Take this Hat", "and put it on this one" ), + [ SwapType.Top ] = ( new ItemSelector( FullEquipType.Body ), new ItemSelector( FullEquipType.Body ), "Take this Top", "and put it on this one" ), + [ SwapType.Gloves ] = ( new ItemSelector( FullEquipType.Hands ), new ItemSelector( FullEquipType.Hands ), "Take these Gloves", "and put them on these" ), + [ SwapType.Pants ] = ( new ItemSelector( FullEquipType.Legs ), new ItemSelector( FullEquipType.Legs ), "Take these Pants", "and put them on these" ), + [ SwapType.Shoes ] = ( new ItemSelector( FullEquipType.Feet ), new ItemSelector( FullEquipType.Feet ), "Take these Shoes", "and put them on these" ), + [ SwapType.Earrings ] = ( new ItemSelector( FullEquipType.Ears ), new ItemSelector( FullEquipType.Ears ), "Take these Earrings", "and put them on these" ), + [ SwapType.Necklace ] = ( new ItemSelector( FullEquipType.Neck ), new ItemSelector( FullEquipType.Neck ), "Take this Necklace", "and put it on this one" ), + [ SwapType.Bracelet ] = ( new ItemSelector( FullEquipType.Wrists ), new ItemSelector( FullEquipType.Wrists ), "Take these Bracelets", "and put them on these" ), + [ SwapType.Ring ] = ( new ItemSelector( FullEquipType.Finger ), new ItemSelector( FullEquipType.Finger ), "Take this Ring", "and put it on this one" ), + }; + + private ItemSelector? _weaponSource = null; + private ItemSelector? _weaponTarget = null; + private readonly WeaponSelector _slotSelector = new(); + private readonly ItemSwapContainer _swapData = new(); private Mod? _mod; private ModSettings? _modSettings; private bool _dirty; - private SwapType _lastTab = SwapType.Equipment; - private Gender _currentGender = Gender.Male; - private ModelRace _currentRace = ModelRace.Midlander; - private int _targetId = 0; - private int _sourceId = 0; - private Exception? _loadException = null; + private SwapType _lastTab = SwapType.Hair; + private Gender _currentGender = Gender.Male; + private ModelRace _currentRace = ModelRace.Midlander; + private int _targetId = 0; + private int _sourceId = 0; + private Exception? _loadException = null; - private string _newModName = string.Empty; - private string _newGroupName = "Swaps"; - private string _newOptionName = string.Empty; - private bool _useFileSwaps = true; + private string _newModName = string.Empty; + private string _newGroupName = "Swaps"; + private string _newOptionName = string.Empty; + private IModGroup? _selectedGroup = null; + private bool _subModValid = false; + private bool _useFileSwaps = true; private Item[]? _affectedItems; - public void UpdateMod( Mod mod, ModSettings? settings ) { if( mod == _mod && settings == _modSettings ) @@ -108,6 +127,7 @@ public class ItemSwapWindow : IDisposable _mod = mod; _modSettings = settings; _swapData.LoadMod( _mod, _modSettings ); + UpdateOption(); _dirty = true; } @@ -120,15 +140,26 @@ public class ItemSwapWindow : IDisposable _swapData.Clear(); _loadException = null; + _affectedItems = null; try { switch( _lastTab ) { - case SwapType.Equipment when _slotSelector?.CurrentSelection != null && _equipSelector.CurrentSelection != null: - _affectedItems = _swapData.LoadEquipment( _equipSelector.CurrentSelection, _slotSelector.CurrentSelection ); - break; - case SwapType.Accessory when _slotSelector?.CurrentSelection != null && _accessorySelector.CurrentSelection != null: - _affectedItems = _swapData.LoadEquipment( _accessorySelector.CurrentSelection, _slotSelector.CurrentSelection ); + case SwapType.Hat: + case SwapType.Top: + case SwapType.Gloves: + case SwapType.Pants: + case SwapType.Shoes: + case SwapType.Earrings: + case SwapType.Necklace: + case SwapType.Bracelet: + case SwapType.Ring: + var values = _selectors[ _lastTab ]; + if( values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null ) + { + _affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2 ); + } + break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); @@ -143,8 +174,6 @@ public class ItemSwapWindow : IDisposable _swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId ); break; case SwapType.Weapon: break; - case SwapType.Minion: break; - case SwapType.Mount: break; } } catch( Exception e ) @@ -169,6 +198,93 @@ public class ItemSwapWindow : IDisposable private string CreateDescription() => $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + private void UpdateOption() + { + _selectedGroup = _mod?.Groups.FirstOrDefault( g => g.Name == _newGroupName ); + _subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 && ( _selectedGroup?.All( o => o.Name != _newOptionName ) ?? true ); + } + + private void CreateMod() + { + var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); + Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); + Mod.CreateDefaultFiles( newDir ); + Penumbra.ModManager.AddMod( newDir ); + if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) + { + Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 ); + } + } + + private void CreateOption() + { + if( _mod == null || !_subModValid ) + { + return; + } + + var groupCreated = false; + var dirCreated = false; + var optionCreated = false; + DirectoryInfo? optionFolderName = null; + try + { + optionFolderName = Mod.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName ); + if( optionFolderName?.Exists == true ) + { + throw new Exception( $"The folder {optionFolderName.FullName} for the option already exists." ); + } + + if( optionFolderName != null ) + { + if( _selectedGroup == null ) + { + Penumbra.ModManager.AddModGroup( _mod, GroupType.Multi, _newGroupName ); + _selectedGroup = _mod.Groups.Last(); + groupCreated = true; + } + + Penumbra.ModManager.AddOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _newOptionName ); + optionCreated = true; + optionFolderName = Directory.CreateDirectory( optionFolderName.FullName ); + dirCreated = true; + if( !_swapData.WriteMod( _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, + _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ) ) + { + throw new Exception( "Failure writing files for mod swap." ); + } + } + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error ); + try + { + if( optionCreated && _selectedGroup != null ) + { + Penumbra.ModManager.DeleteOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ); + } + + if( groupCreated ) + { + Penumbra.ModManager.DeleteModGroup( _mod, _mod.Groups.IndexOf( _selectedGroup! ) ); + _selectedGroup = null; + } + + if( dirCreated && optionFolderName != null ) + { + Directory.Delete( optionFolderName.FullName, true ); + } + } + catch + { + // ignored + } + } + + UpdateOption(); + } + private void DrawHeaderLine( float width ) { var newModAvailable = _loadException == null && _swapData.Loaded; @@ -178,34 +294,40 @@ public class ItemSwapWindow : IDisposable { } ImGui.SameLine(); - var tt = "Create a new mod of the given name containing only the swap."; + var tt = !newModAvailable + ? "No swap is currently loaded." + : _newModName.Length == 0 + ? "Please enter a name for your mod." + : "Create a new mod of the given name containing only the swap."; if( ImGuiUtil.DrawDisabledButton( "Create New Mod", new Vector2( width / 2, 0 ), tt, !newModAvailable || _newModName.Length == 0 ) ) { - var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); - Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); - Mod.CreateDefaultFiles( newDir ); - Penumbra.ModManager.AddMod( newDir ); - if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) - { - Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 ); - } + CreateMod(); } ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) ) - { } + { + UpdateOption(); + } ImGui.SameLine(); ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); if( ImGui.InputTextWithHint( "##optionName", "New Option Name...", ref _newOptionName, 32 ) ) - { } + { + UpdateOption(); + } ImGui.SameLine(); - tt = "Create a new option inside this mod containing only the swap."; - if( ImGuiUtil.DrawDisabledButton( "Create New Option (WIP)", new Vector2( width / 2, 0 ), tt, - true || !newModAvailable || _newGroupName.Length == 0 || _newOptionName.Length == 0 || _mod == null || _mod.AllSubMods.Any( m => m.Name == _newOptionName ) ) ) - { } + tt = !_subModValid + ? "An option with that name already exists in that group, or no name is specified." + : !newModAvailable + ? "Create a new option inside this mod containing only the swap." + : "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap."; + if( ImGuiUtil.DrawDisabledButton( "Create New Option", new Vector2( width / 2, 0 ), tt, !newModAvailable || !_subModValid ) ) + { + CreateOption(); + } ImGui.SameLine(); var newPos = new Vector2( ImGui.GetCursorPosX() + 10 * ImGuiHelpers.GlobalScale, ImGui.GetCursorPosY() - ( ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y ) / 2 ); @@ -214,25 +336,19 @@ public class ItemSwapWindow : IDisposable ImGuiUtil.HoverTooltip( "Use File Swaps." ); } - private enum SwapType - { - Equipment, - Accessory, - Hair, - Face, - Ears, - Tail, - Weapon, - Minion, - Mount, - } - private void DrawSwapBar() { using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None ); - DrawArmorSwap(); - DrawAccessorySwap(); + DrawEquipmentSwap( SwapType.Hat ); + DrawEquipmentSwap( SwapType.Top ); + DrawEquipmentSwap( SwapType.Gloves ); + DrawEquipmentSwap( SwapType.Pants ); + DrawEquipmentSwap( SwapType.Shoes ); + DrawEquipmentSwap( SwapType.Earrings ); + DrawEquipmentSwap( SwapType.Necklace ); + DrawEquipmentSwap( SwapType.Bracelet ); + DrawEquipmentSwap( SwapType.Ring ); DrawHairSwap(); DrawFaceSwap(); DrawEarSwap(); @@ -254,64 +370,39 @@ public class ItemSwapWindow : IDisposable return tab; } - private void DrawArmorSwap() + private void DrawEquipmentSwap( SwapType type ) { - using var tab = DrawTab( SwapType.Equipment ); + using var tab = DrawTab( type ); if( !tab ) { return; } + var (sourceSelector, targetSelector, text1, text2) = _selectors[ type ]; using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "Take this piece of equipment" ); + ImGui.TextUnformatted( text1 ); ImGui.TableNextColumn(); - if( _equipSelector.Draw( "##itemTarget", _equipSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) - { - var slot = ( ( EquipSlot )( _equipSelector.CurrentSelection?.EquipSlotCategory.Row ?? 0 ) ).ToSlot(); - if( slot != _slotSelector?.CurrentSlot ) - _slotSelector = new SlotSelector( slot ); - _dirty = true; - } + _dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "And put it on this one" ); + ImGui.TextUnformatted( text2 ); ImGui.TableNextColumn(); - _slotSelector ??= new SlotSelector( EquipSlot.Unknown ); - _dirty |= _slotSelector.Draw( "##itemSource", _slotSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + _dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + + if( _affectedItems is { Length: > 0 } ) + { + ImGui.SameLine(); + ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( string.Join( '\n', _affectedItems.Select( i => i.Name.ToDalamudString().TextValue ) ) ); + } + } } - - private void DrawAccessorySwap() - { - using var tab = DrawTab( SwapType.Accessory ); - if( !tab ) - { - return; - } - - using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "Take this accessory" ); - ImGui.TableNextColumn(); - if( _accessorySelector.Draw( "##itemTarget", _accessorySelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) - { - var slot = ( ( EquipSlot )( _accessorySelector.CurrentSelection?.EquipSlotCategory.Row ?? 0 ) ).ToSlot(); - if( slot != _slotSelector?.CurrentSlot ) - _slotSelector = new SlotSelector( slot ); - _dirty = true; - } - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( "And put it on this one" ); - ImGui.TableNextColumn(); - _slotSelector ??= new SlotSelector( EquipSlot.Unknown ); - _dirty |= _slotSelector.Draw( "##itemSource", _slotSelector.CurrentSelection?.Name.ToString() ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); - } - + private void DrawHairSwap() { using var tab = DrawTab( SwapType.Hair ); @@ -379,6 +470,36 @@ public class ItemSwapWindow : IDisposable { return; } + + using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "Select the weapon or tool you want" ); + ImGui.TableNextColumn(); + if( _slotSelector.Draw( "##weaponSlot", _slotSelector.CurrentSelection.ToName(), InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) + { + _dirty = true; + _weaponSource = new ItemSelector( _slotSelector.CurrentSelection ); + _weaponTarget = new ItemSelector( _slotSelector.CurrentSelection ); + } + else + { + _dirty = _weaponSource == null || _weaponTarget == null; + _weaponSource ??= new ItemSelector( _slotSelector.CurrentSelection ); + _weaponTarget ??= new ItemSelector( _slotSelector.CurrentSelection ); + } + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "and put this variant of it" ); + ImGui.TableNextColumn(); + _dirty |= _weaponSource.Draw( "##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( "onto this one" ); + ImGui.TableNextColumn(); + _dirty |= _weaponTarget.Draw( "##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); } private const float InputWidth = 120; @@ -444,16 +565,21 @@ public class ItemSwapWindow : IDisposable private string NonExistentText() => _lastTab switch { - SwapType.Equipment => "One of the selected pieces of equipment does not seem to exist.", - SwapType.Accessory => "One of the selected accessories does not seem to exist.", - SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.", - SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.", - SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.", - SwapType.Tail => "One of the selected tails does not seem to exist for this gender and race combo.", - SwapType.Weapon => "One of the selected weapons does not seem to exist.", - SwapType.Minion => "One of the selected minions does not seem to exist.", - SwapType.Mount => "One of the selected mounts does not seem to exist.", - _ => string.Empty, + SwapType.Hat => "One of the selected hats does not seem to exist.", + SwapType.Top => "One of the selected tops does not seem to exist.", + SwapType.Gloves => "One of the selected pairs of gloves does not seem to exist.", + SwapType.Pants => "One of the selected pants does not seem to exist.", + SwapType.Shoes => "One of the selected pairs of shoes does not seem to exist.", + SwapType.Earrings => "One of the selected earrings does not seem to exist.", + SwapType.Necklace => "One of the selected necklaces does not seem to exist.", + SwapType.Bracelet => "One of the selected bracelets does not seem to exist.", + SwapType.Ring => "One of the selected rings does not seem to exist.", + SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.", + SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.", + SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.", + SwapType.Tail => "One of the selected tails does not seem to exist for this gender and race combo.", + SwapType.Weapon => "One of the selected weapons or tools does not seem to exist.", + _ => string.Empty, };