diff --git a/Penumbra.GameData/Data/EquipmentIdentificationList.cs b/Penumbra.GameData/Data/EquipmentIdentificationList.cs index f26cfc1a..e4ba59d1 100644 --- a/Penumbra.GameData/Data/EquipmentIdentificationList.cs +++ b/Penumbra.GameData/Data/EquipmentIdentificationList.cs @@ -9,15 +9,15 @@ using Penumbra.GameData.Structs; namespace Penumbra.GameData.Data; -internal sealed class EquipmentIdentificationList : KeyList +internal sealed class EquipmentIdentificationList : KeyList { - private const string Tag = "EquipmentIdentification"; + private const string Tag = "EquipmentIdentification"; public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(gameData, language)) { } - public IEnumerable Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) + public IEnumerable Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) { if (slot == EquipSlot.Unknown) return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF)); @@ -33,15 +33,10 @@ internal sealed class EquipmentIdentificationList : KeyList public static ulong ToKey(SetId modelId, EquipSlot slot, byte variant) => ((ulong)modelId << 32) | ((ulong)slot << 16) | variant; - public static ulong ToKey(Item i) - { - var model = (SetId)((Lumina.Data.Parsing.Quad)i.ModelMain).A; - var slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); - var variant = (byte)((Lumina.Data.Parsing.Quad)i.ModelMain).B; - return ToKey(model, slot, variant); - } + public static ulong ToKey(EquipItem i) + => ToKey(i.ModelId, i.Slot, i.Variant); - protected override IEnumerable ToKeys(Item i) + protected override IEnumerable ToKeys(EquipItem i) { yield return ToKey(i); } @@ -49,12 +44,12 @@ internal sealed class EquipmentIdentificationList : KeyList protected override bool ValidKey(ulong key) => key != 0; - protected override int ValueKeySelector(Item data) - => (int)data.RowId; + protected override int ValueKeySelector(EquipItem data) + => (int)data.Id; - private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) + private static IEnumerable CreateEquipmentList(DataManager gameData, ClientLanguage language) { var items = gameData.GetExcelSheet(language)!; - return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()); + return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()).Select(EquipItem.FromArmor); } } diff --git a/Penumbra.GameData/Data/ItemData.cs b/Penumbra.GameData/Data/ItemData.cs index 96e305ad..7cbd819c 100644 --- a/Penumbra.GameData/Data/ItemData.cs +++ b/Penumbra.GameData/Data/ItemData.cs @@ -5,32 +5,41 @@ using System.Linq; using Dalamud; using Dalamud.Data; using Dalamud.Plugin; -using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Penumbra.GameData.Data; -public sealed class ItemData : DataSharer, IReadOnlyDictionary> +public sealed class ItemData : DataSharer, IReadOnlyDictionary> { - private readonly IReadOnlyList> _items; + private readonly IReadOnlyList> _items; - private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) + private static IReadOnlyList> CreateItems(DataManager dataManager, ClientLanguage language) { - var tmp = Enum.GetValues().Select(t => new List(1024)).ToArray(); + var tmp = Enum.GetValues().Select(_ => new List(1024)).ToArray(); var itemSheet = dataManager.GetExcelSheet(language)!; - foreach (var item in itemSheet) + foreach (var item in itemSheet.Where(i => i.Name.RawData.Length > 1)) { var type = item.ToEquipType(); - if (type != FullEquipType.Unknown && item.Name.RawData.Length > 1) - tmp[(int)type].Add(item); + if (type.IsWeapon()) + { + if (item.ModelMain != 0) + tmp[(int)type].Add(EquipItem.FromMainhand(item)); + if (item.ModelSub != 0) + tmp[(int)type].Add(EquipItem.FromOffhand(item)); + } + else if (type != FullEquipType.Unknown) + { + tmp[(int)type].Add(EquipItem.FromArmor(item)); + } } - var ret = new IReadOnlyList[tmp.Length]; - ret[0] = Array.Empty(); + 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(); + ret[i] = tmp[i].OrderBy(item => item.Name).ToArray(); return ret; } @@ -44,10 +53,10 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary DisposeTag("ItemList"); - public IEnumerator>> GetEnumerator() + public IEnumerator>> GetEnumerator() { for (var i = 1; i < _items.Count; ++i) - yield return new KeyValuePair>((FullEquipType)i, _items[i]); + yield return new KeyValuePair>((FullEquipType)i, _items[i]); } IEnumerator IEnumerable.GetEnumerator() @@ -59,7 +68,7 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary (int)key < _items.Count && key != FullEquipType.Unknown; - public bool TryGetValue(FullEquipType key, out IReadOnlyList value) + public bool TryGetValue(FullEquipType key, out IReadOnlyList value) { if (ContainsKey(key)) { @@ -71,12 +80,12 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary this[FullEquipType key] + public IReadOnlyList this[FullEquipType key] => TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException(); public IEnumerable Keys => Enum.GetValues().Skip(1); - public IEnumerable> Values + public IEnumerable> Values => _items.Skip(1); } diff --git a/Penumbra.GameData/Data/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs index e992ce43..8e789339 100644 --- a/Penumbra.GameData/Data/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -65,7 +65,7 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier return ret; } - public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) + public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) => slot switch { EquipSlot.MainHand => _weapons.Between(setId, weaponType, (byte)variant), diff --git a/Penumbra.GameData/Data/WeaponIdentificationList.cs b/Penumbra.GameData/Data/WeaponIdentificationList.cs index 1b58d39b..a5e4ddf8 100644 --- a/Penumbra.GameData/Data/WeaponIdentificationList.cs +++ b/Penumbra.GameData/Data/WeaponIdentificationList.cs @@ -9,7 +9,7 @@ using Penumbra.GameData.Structs; namespace Penumbra.GameData.Data; -internal sealed class WeaponIdentificationList : KeyList +internal sealed class WeaponIdentificationList : KeyList { private const string Tag = "WeaponIdentification"; private const int Version = 1; @@ -18,10 +18,10 @@ internal sealed class WeaponIdentificationList : KeyList : base(pi, Tag, language, Version, CreateWeaponList(gameData, language)) { } - public IEnumerable Between(SetId modelId) + public IEnumerable Between(SetId modelId) => Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); - public IEnumerable Between(SetId modelId, WeaponType type, byte variant = 0) + public IEnumerable Between(SetId modelId, WeaponType type, byte variant = 0) { if (type == 0) return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); @@ -37,38 +37,31 @@ internal sealed class WeaponIdentificationList : KeyList public static ulong ToKey(SetId modelId, WeaponType type, byte variant) => ((ulong)modelId << 32) | ((ulong)type << 16) | variant; - public static ulong ToKey(Item i, bool offhand) - { - var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain; - return ToKey(quad.A, quad.B, (byte)quad.C); - } + public static ulong ToKey(EquipItem i) + => ToKey(i.ModelId, i.WeaponType, i.Variant); - protected override IEnumerable ToKeys(Item i) + protected override IEnumerable ToKeys(EquipItem data) { - var key1 = 0ul; - if (i.ModelMain != 0) - { - key1 = ToKey(i, false); - yield return key1; - } - - if (i.ModelSub != 0) - { - var key2 = ToKey(i, true); - if (key1 != key2) - yield return key2; - } + yield return ToKey(data); } protected override bool ValidKey(ulong key) => key != 0; - protected override int ValueKeySelector(Item data) - => (int)data.RowId; + protected override int ValueKeySelector(EquipItem data) + => (int)data.Id; - private static IEnumerable CreateWeaponList(DataManager gameData, ClientLanguage language) + private static IEnumerable CreateWeaponList(DataManager gameData, ClientLanguage language) + => gameData.GetExcelSheet(language)!.SelectMany(ToEquipItems); + + private static IEnumerable ToEquipItems(Item item) { - var items = gameData.GetExcelSheet(language)!; - return items.Where(i => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand); + if ((EquipSlot)item.EquipSlotCategory.Row is not (EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) + yield break; + + if (item.ModelMain != 0) + yield return EquipItem.FromMainhand(item); + if (item.ModelSub != 0) + yield return EquipItem.FromOffhand(item); } } diff --git a/Penumbra.GameData/Enums/ChangedItemExtensions.cs b/Penumbra.GameData/Enums/ChangedItemExtensions.cs index 85fb078e..8dbd5bb6 100644 --- a/Penumbra.GameData/Enums/ChangedItemExtensions.cs +++ b/Penumbra.GameData/Enums/ChangedItemExtensions.cs @@ -1,33 +1,19 @@ -using System; -using Dalamud.Data; -using Lumina.Excel.GeneratedSheets; using Penumbra.Api.Enums; +using Penumbra.GameData.Structs; using Action = Lumina.Excel.GeneratedSheets.Action; namespace Penumbra.GameData.Enums; public static class ChangedItemExtensions { - public static (ChangedItemType, uint) ChangedItemToTypeAndId( object? item ) + public static (ChangedItemType, uint) ChangedItemToTypeAndId(object? item) { return item switch { - null => ( ChangedItemType.None, 0 ), - Item i => ( ChangedItemType.Item, i.RowId ), - Action a => ( ChangedItemType.Action, a.RowId ), - _ => ( ChangedItemType.Customization, 0 ), + null => (ChangedItemType.None, 0), + EquipItem i => (ChangedItemType.Item, i.Id), + Action a => (ChangedItemType.Action, a.RowId), + _ => (ChangedItemType.Customization, 0), }; } - - public static object? GetObject( this ChangedItemType type, DataManager manager, uint id ) - { - return type switch - { - ChangedItemType.None => null, - ChangedItemType.Item => manager.GetExcelSheet< Item >()?.GetRow( id ), - ChangedItemType.Action => manager.GetExcelSheet< Action >()?.GetRow( id ), - ChangedItemType.Customization => null, - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Enums/FullEquipType.cs b/Penumbra.GameData/Enums/FullEquipType.cs index 27c046ea..c3956ae2 100644 --- a/Penumbra.GameData/Enums/FullEquipType.cs +++ b/Penumbra.GameData/Enums/FullEquipType.cs @@ -20,26 +20,34 @@ public enum FullEquipType : byte 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 + Fists, // PGL, MNK + FistsOff, + Sword, // GLA, PLD Main + Axe, // MRD, WAR + Bow, // ARC, BRD + BowOff, + Lance, // LNC, DRG, + Staff, // THM, BLM, CNJ, WHM + Wand, // THM, BLM, CNJ, WHM Main + Book, // ACN, SMN, SCH + Daggers, // ROG, NIN + DaggersOff, 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 + GunOff, + Orrery, // AST, + OrreryOff, + Katana, // SAM + KatanaOff, + Rapier, // RDM + RapierOff, + Cane, // BLU + Gunblade, // GNB, + Glaives, // DNC, + GlaivesOff, + Scythe, // RPR, + Nouliths, // SGE + Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off Saw, // CRP CrossPeinHammer, // BSM @@ -68,7 +76,7 @@ public enum FullEquipType : byte public static class FullEquipTypeExtensions { - public static FullEquipType ToEquipType(this Item item) + internal static FullEquipType ToEquipType(this Item item) { var slot = (EquipSlot)item.EquipSlotCategory.Row; var weapon = (WeaponCategory)item.ItemUICategory.Row; @@ -152,22 +160,30 @@ public static class FullEquipTypeExtensions FullEquipType.Wrists => EquipSlot.Wrists.ToName(), FullEquipType.Finger => "Ring", FullEquipType.Fists => "Fist Weapon", + FullEquipType.FistsOff => "Fist Weapon (Offhand)", FullEquipType.Sword => "Sword", FullEquipType.Axe => "Axe", FullEquipType.Bow => "Bow", + FullEquipType.BowOff => "Quiver", FullEquipType.Lance => "Lance", FullEquipType.Staff => "Staff", FullEquipType.Wand => "Mace", FullEquipType.Book => "Book", FullEquipType.Daggers => "Dagger", + FullEquipType.DaggersOff => "Dagger (Offhand)", FullEquipType.Broadsword => "Broadsword", FullEquipType.Gun => "Gun", + FullEquipType.GunOff => "Aetherotransformer", FullEquipType.Orrery => "Orrery", + FullEquipType.OrreryOff => "Card Holder", FullEquipType.Katana => "Katana", + FullEquipType.KatanaOff => "Sheathe", FullEquipType.Rapier => "Rapier", + FullEquipType.RapierOff => "Focus", FullEquipType.Cane => "Cane", FullEquipType.Gunblade => "Gunblade", FullEquipType.Glaives => "Glaive", + FullEquipType.GlaivesOff => "Glaive (Offhand)", FullEquipType.Scythe => "Scythe", FullEquipType.Nouliths => "Nouliths", FullEquipType.Shield => "Shield", @@ -209,22 +225,30 @@ public static class FullEquipTypeExtensions FullEquipType.Wrists => EquipSlot.Wrists, FullEquipType.Finger => EquipSlot.RFinger, FullEquipType.Fists => EquipSlot.MainHand, + FullEquipType.FistsOff => EquipSlot.OffHand, FullEquipType.Sword => EquipSlot.MainHand, FullEquipType.Axe => EquipSlot.MainHand, FullEquipType.Bow => EquipSlot.MainHand, + FullEquipType.BowOff => EquipSlot.OffHand, FullEquipType.Lance => EquipSlot.MainHand, FullEquipType.Staff => EquipSlot.MainHand, FullEquipType.Wand => EquipSlot.MainHand, FullEquipType.Book => EquipSlot.MainHand, FullEquipType.Daggers => EquipSlot.MainHand, + FullEquipType.DaggersOff => EquipSlot.OffHand, FullEquipType.Broadsword => EquipSlot.MainHand, FullEquipType.Gun => EquipSlot.MainHand, + FullEquipType.GunOff => EquipSlot.OffHand, FullEquipType.Orrery => EquipSlot.MainHand, + FullEquipType.OrreryOff => EquipSlot.OffHand, FullEquipType.Katana => EquipSlot.MainHand, + FullEquipType.KatanaOff => EquipSlot.OffHand, FullEquipType.Rapier => EquipSlot.MainHand, + FullEquipType.RapierOff => EquipSlot.OffHand, FullEquipType.Cane => EquipSlot.MainHand, FullEquipType.Gunblade => EquipSlot.MainHand, FullEquipType.Glaives => EquipSlot.MainHand, + FullEquipType.GlaivesOff => EquipSlot.OffHand, FullEquipType.Scythe => EquipSlot.MainHand, FullEquipType.Nouliths => EquipSlot.MainHand, FullEquipType.Shield => EquipSlot.OffHand, @@ -253,7 +277,7 @@ public static class FullEquipTypeExtensions _ => EquipSlot.Unknown, }; - public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown) + public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown, bool mainhand = true) => slot switch { EquipSlot.Head => FullEquipType.Head, @@ -273,77 +297,101 @@ public static class FullEquipTypeExtensions EquipSlot.BodyHands => FullEquipType.Body, EquipSlot.BodyLegsFeet => FullEquipType.Body, EquipSlot.ChestHands => FullEquipType.Body, - EquipSlot.MainHand => category.ToEquipType(), - EquipSlot.OffHand => category.ToEquipType(), - EquipSlot.BothHand => category.ToEquipType(), + EquipSlot.MainHand => category.ToEquipType(mainhand), + EquipSlot.OffHand => category.ToEquipType(mainhand), + EquipSlot.BothHand => category.ToEquipType(mainhand), _ => FullEquipType.Unknown, }; - public static FullEquipType ToEquipType(this WeaponCategory category) + public static FullEquipType ToEquipType(this WeaponCategory category, bool mainhand = true) => 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, + WeaponCategory.Pugilist when mainhand => FullEquipType.Fists, + WeaponCategory.Pugilist => FullEquipType.FistsOff, + WeaponCategory.Gladiator => FullEquipType.Sword, + WeaponCategory.Marauder => FullEquipType.Axe, + WeaponCategory.Archer when mainhand => FullEquipType.Bow, + WeaponCategory.Archer => FullEquipType.BowOff, + 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.FisherOff => FullEquipType.Gig, + WeaponCategory.Rogue when mainhand => FullEquipType.DaggersOff, + WeaponCategory.Rogue => FullEquipType.Daggers, + WeaponCategory.DarkKnight => FullEquipType.Broadsword, + WeaponCategory.Machinist when mainhand => FullEquipType.Gun, + WeaponCategory.Machinist => FullEquipType.GunOff, + WeaponCategory.Astrologian when mainhand => FullEquipType.Orrery, + WeaponCategory.Astrologian => FullEquipType.OrreryOff, + WeaponCategory.Samurai when mainhand => FullEquipType.Katana, + WeaponCategory.Samurai => FullEquipType.KatanaOff, + WeaponCategory.RedMage when mainhand => FullEquipType.Rapier, + WeaponCategory.RedMage => FullEquipType.RapierOff, + WeaponCategory.Scholar => FullEquipType.Book, + WeaponCategory.BlueMage => FullEquipType.Cane, + WeaponCategory.Gunbreaker => FullEquipType.Gunblade, + WeaponCategory.Dancer when mainhand => FullEquipType.Glaives, + WeaponCategory.Dancer => FullEquipType.GlaivesOff, + WeaponCategory.Reaper => FullEquipType.Scythe, + WeaponCategory.Sage => FullEquipType.Nouliths, + _ => FullEquipType.Unknown, }; public static FullEquipType Offhand(this FullEquipType type) => type switch { - FullEquipType.Fists => FullEquipType.Fists, + FullEquipType.Fists => FullEquipType.FistsOff, 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.Daggers => FullEquipType.DaggersOff, + FullEquipType.Gun => FullEquipType.GunOff, + FullEquipType.Orrery => FullEquipType.OrreryOff, + FullEquipType.Rapier => FullEquipType.RapierOff, + FullEquipType.Glaives => FullEquipType.GlaivesOff, + FullEquipType.Bow => FullEquipType.BowOff, + FullEquipType.Katana => FullEquipType.KatanaOff, _ => FullEquipType.Unknown, }; + internal static string OffhandTypeSuffix(this FullEquipType type) + => type switch + { + FullEquipType.FistsOff => " (Offhand)", + FullEquipType.DaggersOff => " (Offhand)", + FullEquipType.GunOff => " (Aetherotransformer)", + FullEquipType.OrreryOff => " (Card Holder)", + FullEquipType.RapierOff => " (Focus)", + FullEquipType.GlaivesOff => " (Offhand)", + FullEquipType.BowOff => " (Quiver)", + FullEquipType.KatanaOff => " (Sheathe)", + _ => string.Empty, + }; + public static readonly IReadOnlyList WeaponTypes = Enum.GetValues().Where(v => v.IsWeapon()).ToArray(); diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index f5fc1d05..02f7d33b 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -58,10 +58,10 @@ public interface IObjectIdentifier : IDisposable /// The secondary model ID for weapons, WeaponType.Zero for equipment and accessories. /// The variant ID of the model. /// The equipment slot the piece of equipment uses. - public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); + public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); /// - public IEnumerable Identify(SetId setId, ushort variant, EquipSlot slot) + public IEnumerable Identify(SetId setId, ushort variant, EquipSlot slot) => Identify(setId, 0, variant, slot); } diff --git a/Penumbra.GameData/Structs/EquipItem.cs b/Penumbra.GameData/Structs/EquipItem.cs new file mode 100644 index 00000000..9e26b27c --- /dev/null +++ b/Penumbra.GameData/Structs/EquipItem.cs @@ -0,0 +1,88 @@ +using System.Runtime.InteropServices; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Enums; + +namespace Penumbra.GameData.Structs; + +[StructLayout(LayoutKind.Sequential)] +public readonly struct EquipItem +{ + public readonly string Name; + public readonly uint Id; + public readonly ushort IconId; + public readonly SetId ModelId; + public readonly WeaponType WeaponType; + public readonly byte Variant; + public readonly FullEquipType Type; + public readonly EquipSlot Slot; + + public bool Valid + => Type != FullEquipType.Unknown; + + public CharacterArmor Armor() + => new(ModelId, Variant, 0); + + public CharacterArmor Armor(StainId stain) + => new(ModelId, Variant, stain); + + public CharacterWeapon Weapon() + => new(ModelId, WeaponType, Variant, 0); + + public CharacterWeapon Weapon(StainId stain) + => new(ModelId, WeaponType, Variant, stain); + + public EquipItem() + => Name = string.Empty; + + public EquipItem(string name, uint id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type, + EquipSlot slot) + { + Name = name; + Id = id; + IconId = iconId; + ModelId = modelId; + WeaponType = weaponType; + Variant = variant; + Type = type; + Slot = slot; + } + + + public static EquipItem FromArmor(Item item) + { + var type = item.ToEquipType(); + var slot = type.ToSlot(); + var name = string.Intern(item.Name.ToDalamudString().TextValue); + var id = item.RowId; + var icon = item.Icon; + var model = (SetId)item.ModelMain; + var weapon = (WeaponType)0; + var variant = (byte)(item.ModelMain >> 16); + return new EquipItem(name, id, icon, model, weapon, variant, type, slot); + } + + public static EquipItem FromMainhand(Item item) + { + var type = item.ToEquipType(); + var name = string.Intern(item.Name.ToDalamudString().TextValue); + var id = item.RowId; + var icon = item.Icon; + var model = (SetId)item.ModelMain; + var weapon = (WeaponType)(item.ModelMain >> 16); + var variant = (byte)(item.ModelMain >> 32); + return new EquipItem(name, id, icon, model, weapon, variant, type, EquipSlot.MainHand); + } + + public static EquipItem FromOffhand(Item item) + { + var type = item.ToEquipType().Offhand(); + var name = string.Intern(item.Name.ToDalamudString().TextValue + type.OffhandTypeSuffix()); + var id = item.RowId; + var icon = item.Icon; + var model = (SetId)item.ModelSub; + var weapon = (WeaponType)(item.ModelSub >> 16); + var variant = (byte)(item.ModelSub >> 32); + return new EquipItem(name, id, icon, model, weapon, variant, type, EquipSlot.OffHand); + } +} diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 80ff8e75..01a5fd56 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -17,6 +17,9 @@ public sealed class ChangedItemClick : EventWrapper { /// Default = 0, + + /// + Link = 1, } public ChangedItemClick() diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index afedf8fd..c0736256 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -15,6 +15,9 @@ public sealed class ChangedItemHover : EventWrapper, ChangedItem { /// Default = 0, + + /// + Link = 1, } public ChangedItemHover() diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 83fd2e51..12c24033 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -91,6 +91,8 @@ public class Configuration : IPluginConfiguration, ISavable public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; public TabType SelectedTab { get; set; } = TabType.Settings; + public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags; + public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool FixMainWindow { get; set; } = false; public bool AutoDeduplicateOnImport { get; set; } = true; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index e7de1a4a..57fdf20e 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -53,7 +53,7 @@ public class ResourceTree if (imcNode != null) Nodes.Add(globalContext.WithNames ? imcNode.WithName(imcNode.Name ?? $"IMC #{i}") : imcNode); - var mdl = (RenderModel*)model->ModelArray[i]; + var mdl = (RenderModel*)model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 443376a6..902f5e08 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -46,9 +44,9 @@ public static class EquipmentSwap : Array.Empty(); } - public static Item[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, + public static EquipItem[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, Func redirections, Func manips, - EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo) + EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); @@ -104,9 +102,9 @@ public static class EquipmentSwap return affectedItems; } - public static Item[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, - Func redirections, Func manips, Item itemFrom, - Item itemTo, bool rFinger = true, bool lFinger = true) + public static EquipItem[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List swaps, + Func redirections, Func manips, EquipItem itemFrom, + EquipItem itemTo, bool rFinger = true, bool lFinger = true) { // Check actual ids, variants and slots. We only support using the same slot. LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); @@ -122,7 +120,7 @@ public static class EquipmentSwap if (gmp != null) swaps.Add(gmp); - var affectedItems = Array.Empty(); + var affectedItems = Array.Empty(); foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); @@ -242,22 +240,22 @@ public static class EquipmentSwap return mdl; } - private static void LookupItem(Item i, out EquipSlot slot, out SetId modelId, out byte variant) + private static void LookupItem(EquipItem i, out EquipSlot slot, out SetId modelId, out byte variant) { - slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); - if (!slot.IsEquipmentPiece()) + if (!i.Slot.IsEquipmentPiece()) throw new ItemSwap.InvalidItemTypeException(); - modelId = ((Quad)i.ModelMain).A; - variant = (byte)((Quad)i.ModelMain).B; + slot = i.Slot; + modelId = i.ModelId; + variant = i.Variant; } - private static (ImcFile, byte[], Item[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, + private static (ImcFile, byte[], EquipItem[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom) { var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default); var imc = new ImcFile(manager, entry); - Item[] items; + EquipItem[] items; byte[] variants; if (idFrom.Value == idTo.Value) { @@ -271,7 +269,7 @@ public static class EquipmentSwap { items = identifier.Identify(slotFrom.IsEquipment() ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) - : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType().ToArray(); + : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType().ToArray(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (byte)i).ToArray(); } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index c793c68f..00bbaac8 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -132,7 +132,7 @@ public class ItemSwapContainer return m => set.TryGetValue( m, out var a ) ? a : m; } - public Item[] LoadEquipment( Item from, Item to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true ) + public EquipItem[] LoadEquipment( EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true ) { Swaps.Clear(); Loaded = false; @@ -141,7 +141,7 @@ public class ItemSwapContainer return ret; } - public Item[] LoadTypeSwap( EquipSlot slotFrom, Item from, EquipSlot slotTo, Item to, ModCollection? collection = null ) + public EquipItem[] LoadTypeSwap( EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null ) { Swaps.Clear(); Loaded = false; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 545ae83c..ba64ef79 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Linq; using System.Text; -using System.Threading.Tasks; using Dalamud.Plugin; using ImGuiNET; using Lumina.Excel.GeneratedSheets; @@ -21,6 +20,8 @@ using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.UI.Tabs; +using ChangedItemClick = Penumbra.Communication.ChangedItemClick; +using ChangedItemHover = Penumbra.Communication.ChangedItemHover; namespace Penumbra; @@ -50,16 +51,17 @@ public class Penumbra : IDalamudPlugin public Penumbra(DalamudPluginInterface pluginInterface) { try - { + { var startTimer = new StartTracker(); - using var timer = startTimer.Measure(StartTimeType.Total); + using var timer = startTimer.Measure(StartTimeType.Total); _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); ChatService = _services.GetRequiredService(); _validityChecker = _services.GetRequiredService(); var startup = _services.GetRequiredService().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) ? s.ToString() : "Unknown"; - Log.Information($"Loading Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} with Waiting For Plugins: {startup}..."); + Log.Information( + $"Loading Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} with Waiting For Plugins: {startup}..."); _services.GetRequiredService(); // Initialize because not required anywhere else. _config = _services.GetRequiredService(); _characterUtility = _services.GetRequiredService(); @@ -72,7 +74,7 @@ public class Penumbra : IDalamudPlugin _redrawService = _services.GetRequiredService(); _communicatorService = _services.GetRequiredService(); _services.GetRequiredService(); // Initialize because not required anywhere else. - _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { @@ -91,8 +93,8 @@ public class Penumbra : IDalamudPlugin if (_characterUtility.Ready) _residentResources.Reload(); } - catch(Exception ex) - { + catch (Exception ex) + { Log.Error($"Error constructing Penumbra, Disposing again:\n{ex}"); Dispose(); throw; @@ -104,16 +106,17 @@ public class Penumbra : IDalamudPlugin using var timer = _services.GetRequiredService().Measure(StartTimeType.Api); var api = _services.GetRequiredService(); _services.GetRequiredService(); - api.ChangedItemTooltip += it => + _communicatorService.ChangedItemHover.Subscribe(it => { if (it is Item) ImGui.TextUnformatted("Left Click to create an item link in chat."); - }; - api.ChangedItemClicked += (button, it) => + }, ChangedItemHover.Priority.Link); + + _communicatorService.ChangedItemClick.Subscribe((button, it) => { if (button == MouseButton.Left && it is Item item) ChatService.LinkItem(item); - }; + }, ChangedItemClick.Priority.Link); } private void SetupInterface() diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 72283304..7ca83ec8 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -129,14 +129,14 @@ public class ItemSwapTab : IDisposable, ITab Weapon, } - private class ItemSelector : FilterComboCache<(string, Item)> + private class ItemSelector : FilterComboCache { public ItemSelector(ItemService data, FullEquipType type) - : base(() => data.AwaitedService[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray()) + : base(() => data.AwaitedService[type]) { } - protected override string ToString((string, Item) obj) - => obj.Item1; + protected override string ToString(EquipItem obj) + => obj.Name; } private class WeaponSelector : FilterComboCache @@ -179,7 +179,7 @@ public class ItemSwapTab : IDisposable, ITab private bool _useLeftRing = true; private bool _useRightRing = true; - private Item[]? _affectedItems; + private EquipItem[]? _affectedItems; private void UpdateState() { @@ -203,17 +203,16 @@ public class ItemSwapTab : IDisposable, ITab 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, + if (values.Source.CurrentSelection.Type != FullEquipType.Unknown && values.Target.CurrentSelection.Type != FullEquipType.Unknown) + _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); break; case SwapType.BetweenSlots: var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); - if (selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null) - _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, - selectorFrom.CurrentSelection.Item2, + if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid) + _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection, _slotFrom, selectorFrom.CurrentSelection, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: @@ -468,7 +467,7 @@ public class ItemSwapTab : IDisposable, ITab } ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Name ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); (article1, _, selector) = GetAccessorySelector(_slotTo, false); @@ -493,7 +492,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); if (_affectedItems is not { Length: > 1 }) return; @@ -502,8 +501,8 @@ public class ItemSwapTab : IDisposable, ITab ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, selector.CurrentSelection.Item2)) - .Select(i => i.Name.ToDalamudString().TextValue))); + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name)) + .Select(i => i.Name))); } private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source) @@ -534,7 +533,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text1); ImGui.TableNextColumn(); - _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) @@ -547,7 +546,7 @@ public class ItemSwapTab : IDisposable, ITab ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text2); ImGui.TableNextColumn(); - _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) { @@ -562,8 +561,8 @@ public class ItemSwapTab : IDisposable, ITab ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2)) - .Select(i => i.Name.ToDalamudString().TextValue))); + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name)) + .Select(i => i.Name))); } private void DrawHairSwap() @@ -647,14 +646,14 @@ public class ItemSwapTab : IDisposable, ITab ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("and put this variant of it"); ImGui.TableNextColumn(); - _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, + _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Name, 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, string.Empty, InputWidth * 2, + _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Name, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing()); } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index c52bd5b3..1c84d6b2 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -4,15 +4,19 @@ using System.Linq; using System.Numerics; using Dalamud.Data; using Dalamud.Interface; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using ImGuiScene; -using Lumina.Data.Parsing; +using Lumina.Data.Files; +using Lumina.Excel; using Lumina.Excel.GeneratedSheets; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Services; using Penumbra.UI.Classes; @@ -20,18 +24,42 @@ namespace Penumbra.UI; public class ChangedItemDrawer : IDisposable { - private const EquipSlot MonsterSlot = (EquipSlot)100; - private const EquipSlot DemihumanSlot = (EquipSlot)101; - private const EquipSlot CustomizationSlot = (EquipSlot)102; - private const EquipSlot ActionSlot = (EquipSlot)103; - - private readonly CommunicatorService _communicator; - private readonly Dictionary _icons = new(16); - - public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator) + [Flags] + public enum ChangedItemIcon : uint { + Head = 0x0001, + Body = 0x0002, + Hands = 0x0004, + Legs = 0x0008, + Feet = 0x0010, + Ears = 0x0020, + Neck = 0x0040, + Wrists = 0x0080, + Finger = 0x0100, + Monster = 0x0200, + Demihuman = 0x0400, + Customization = 0x0800, + Action = 0x1000, + Mainhand = 0x2000, + Offhand = 0x4000, + Unknown = 0x8000, + } + + public const ChangedItemIcon AllFlags = (ChangedItemIcon)0xFFFF; + public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; + + private readonly Configuration _config; + private readonly ExcelSheet _items; + private readonly CommunicatorService _communicator; + private readonly Dictionary _icons = new(16); + private float _smallestIconWidth = 0; + + public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator, Configuration config) + { + _items = gameData.GetExcelSheet()!; uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData), true); _communicator = communicator; + _config = config; } public void Dispose() @@ -41,37 +69,49 @@ public class ChangedItemDrawer : IDisposable _icons.Clear(); } - /// Apply Changed Item Counters to the Name if necessary. - public static string ChangedItemName(string name, object? data) - => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + /// Check if a changed item should be drawn based on its category. + public bool FilterChangedItem(string name, object? data, LowerString filter) + => (_config.ChangedItemFilter == AllFlags || _config.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data))) + && (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data))); - /// Add filterable information to the string. - public static string ChangedItemFilterName(string name, object? data) - => data switch + /// Draw the icon corresponding to the category of a changed item. + public void DrawCategoryIcon(string name, object? data) + { + var height = ImGui.GetFrameHeight(); + var iconType = GetCategoryIcon(name, data); + if (!_icons.TryGetValue(iconType, out var icon)) { - int counter => $"{counter} Files Manipulating {name}s", - Item it => $"{name}\0{((EquipSlot)it.EquipSlotCategory.Row).ToName()}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", - ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", - _ => name, - }; + ImGui.Dummy(new Vector2(height)); + return; + } + + ImGui.Image(icon.ImGuiHandle, new Vector2(height)); + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); + ImGui.SameLine(); + ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0); + } + } /// /// Draw a changed item, invoking the Api-Events for clicks and tooltips. /// Also draw the item Id in grey if requested. /// - public void DrawChangedItem(string name, object? data, bool drawId) + public void DrawChangedItem(string name, object? data) { name = ChangedItemName(name, data); - DrawCategoryIcon(name, data); - ImGui.SameLine(); using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) { - var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) ? MouseButton.Left : MouseButton.None; + var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) + ? MouseButton.Left + : MouseButton.None; ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, data); + _communicator.ChangedItemClick.Invoke(ret, Convert(data)); } if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) @@ -80,13 +120,17 @@ public class ChangedItemDrawer : IDisposable // Circumvent ugly blank tooltip with less-ugly useless tooltip. using var tt = ImRaii.Tooltip(); using var group = ImRaii.Group(); - _communicator.ChangedItemHover.Invoke(data); + _communicator.ChangedItemHover.Invoke(Convert(data)); group.Dispose(); if (ImGui.GetItemRectSize() == Vector2.Zero) ImGui.TextUnformatted("No actions available."); } + } - if (!drawId || !GetChangedItemObject(data, out var text)) + /// Draw the model information, right-justified. + public void DrawModelData(object? data) + { + if (!GetChangedItemObject(data, out var text)) return; ImGui.SameLine(ImGui.GetContentRegionAvail().X); @@ -94,58 +138,147 @@ public class ChangedItemDrawer : IDisposable ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); } - private void DrawCategoryIcon(string name, object? obj) + /// Draw a header line with the different icon types to filter them. + public void DrawTypeFilter() { - var height = ImGui.GetFrameHeight(); - var slot = EquipSlot.Unknown; - var desc = string.Empty; - if (obj is Item it) - { - slot = (EquipSlot)it.EquipSlotCategory.Row; - desc = slot.ToName(); - } - else if (obj is ModelChara m) - { - (slot, desc) = (CharacterBase.ModelType)m.Type switch + using var _ = ImRaii.PushId("ChangedItemIconFilter"); + var available = ImGui.GetContentRegionAvail().X; + var (numLines, size) = available / _icons.Count > ImGui.GetTextLineHeight() * 2 + ? (1, new Vector2(Math.Min(_smallestIconWidth, available / _icons.Count))) + : (2, new Vector2(Math.Min(_smallestIconWidth, 2 * available / _icons.Count))); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + var lines = numLines == 2 + ? new[] { - CharacterBase.ModelType.DemiHuman => (DemihumanSlot, "Demi-Human"), - CharacterBase.ModelType.Monster => (MonsterSlot, "Monster"), - _ => (EquipSlot.Unknown, string.Empty), + new[] + { + ChangedItemIcon.Head, + ChangedItemIcon.Body, + ChangedItemIcon.Hands, + ChangedItemIcon.Legs, + ChangedItemIcon.Feet, + ChangedItemIcon.Mainhand, + ChangedItemIcon.Offhand, + ChangedItemIcon.Unknown, + }, + new[] + { + ChangedItemIcon.Ears, + ChangedItemIcon.Neck, + ChangedItemIcon.Wrists, + ChangedItemIcon.Finger, + ChangedItemIcon.Customization, + ChangedItemIcon.Action, + ChangedItemIcon.Monster, + ChangedItemIcon.Demihuman, + }, + } + : new[] + { + new[] + { + ChangedItemIcon.Head, + ChangedItemIcon.Body, + ChangedItemIcon.Hands, + ChangedItemIcon.Legs, + ChangedItemIcon.Feet, + ChangedItemIcon.Ears, + ChangedItemIcon.Neck, + ChangedItemIcon.Wrists, + ChangedItemIcon.Finger, + ChangedItemIcon.Mainhand, + ChangedItemIcon.Offhand, + ChangedItemIcon.Customization, + ChangedItemIcon.Action, + ChangedItemIcon.Monster, + ChangedItemIcon.Demihuman, + ChangedItemIcon.Unknown, + }, }; - } - else if (name.StartsWith("Action: ")) + + void DrawIcon(ChangedItemIcon type) { - (slot, desc) = (ActionSlot, "Action"); - } - else if (name.StartsWith("Customization: ")) - { - (slot, desc) = (CustomizationSlot, "Customization"); + var icon = _icons[type]; + var flag = _config.ChangedItemFilter.HasFlag(type); + ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); + if (ImGui.IsItemClicked()) + { + _config.ChangedItemFilter = flag ? _config.ChangedItemFilter & ~type : _config.ChangedItemFilter | type; + _config.Save(); + } + + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); + ImGui.SameLine(); + ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0); + } } - if (!_icons.TryGetValue(slot, out var icon)) + foreach (var line in lines) { - ImGui.Dummy(new Vector2(height)); - return; - } + foreach (var iconType in line.SkipLast(1)) + { + DrawIcon(iconType); + ImGui.SameLine(); + } - ImGui.Image(icon.ImGuiHandle, new Vector2(height)); - if (ImGui.IsItemHovered() && icon.Height > height) - { - using var tt = ImRaii.Tooltip(); - ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); - ImGui.SameLine(); - ImGuiUtil.DrawTextButton(desc, new Vector2(0, icon.Height), 0); + DrawIcon(line.Last()); } } + /// Obtain the icon category corresponding to a changed item. + private static ChangedItemIcon GetCategoryIcon(string name, object? obj) + { + var iconType = ChangedItemIcon.Unknown; + switch (obj) + { + case EquipItem it: + iconType = it.Slot switch + { + EquipSlot.MainHand => ChangedItemIcon.Mainhand, + EquipSlot.OffHand => ChangedItemIcon.Offhand, + EquipSlot.Head => ChangedItemIcon.Head, + EquipSlot.Body => ChangedItemIcon.Body, + EquipSlot.Hands => ChangedItemIcon.Hands, + EquipSlot.Legs => ChangedItemIcon.Legs, + EquipSlot.Feet => ChangedItemIcon.Feet, + EquipSlot.Ears => ChangedItemIcon.Ears, + EquipSlot.Neck => ChangedItemIcon.Neck, + EquipSlot.Wrists => ChangedItemIcon.Wrists, + EquipSlot.RFinger => ChangedItemIcon.Finger, + _ => ChangedItemIcon.Unknown, + }; + break; + case ModelChara m: + iconType = (CharacterBase.ModelType)m.Type switch + { + CharacterBase.ModelType.DemiHuman => ChangedItemIcon.Demihuman, + CharacterBase.ModelType.Monster => ChangedItemIcon.Monster, + _ => ChangedItemIcon.Unknown, + }; + break; + default: + { + if (name.StartsWith("Action: ")) + iconType = ChangedItemIcon.Action; + else if (name.StartsWith("Customization: ")) + iconType = ChangedItemIcon.Customization; + break; + } + } + + return iconType; + } + /// Return more detailed object information in text, if it exists. - public static bool GetChangedItemObject(object? obj, out string text) + private static bool GetChangedItemObject(object? obj, out string text) { switch (obj) { - case Item it: - var quad = (Quad)it.ModelMain; - text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; + case EquipItem it: + text = it.WeaponType == 0 ? $"({it.ModelId.Value}-{it.Variant})" : $"({it.ModelId.Value}-{it.WeaponType.Value}-{it.Variant})"; return true; case ModelChara m: text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; @@ -156,6 +289,51 @@ public class ChangedItemDrawer : IDisposable } } + /// We need to transform the internal EquipItem type to the Lumina Item type for API-events. + private object? Convert(object? data) + { + if (data is EquipItem it) + return _items.GetRow(it.Id); + + return data; + } + + private static string ToDescription(ChangedItemIcon icon) + => icon switch + { + ChangedItemIcon.Head => EquipSlot.Head.ToName(), + ChangedItemIcon.Body => EquipSlot.Body.ToName(), + ChangedItemIcon.Hands => EquipSlot.Hands.ToName(), + ChangedItemIcon.Legs => EquipSlot.Legs.ToName(), + ChangedItemIcon.Feet => EquipSlot.Feet.ToName(), + ChangedItemIcon.Ears => EquipSlot.Ears.ToName(), + ChangedItemIcon.Neck => EquipSlot.Neck.ToName(), + ChangedItemIcon.Wrists => EquipSlot.Wrists.ToName(), + ChangedItemIcon.Finger => "Ring", + ChangedItemIcon.Monster => "Monster", + ChangedItemIcon.Demihuman => "Demi-Human", + ChangedItemIcon.Customization => "Customization", + ChangedItemIcon.Action => "Action", + ChangedItemIcon.Mainhand => "Weapon (Mainhand)", + ChangedItemIcon.Offhand => "Weapon (Offhand)", + _ => "Other", + }; + + /// Apply Changed Item Counters to the Name if necessary. + private static string ChangedItemName(string name, object? data) + => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + + /// Add filterable information to the string. + private static string ChangedItemFilterName(string name, object? data) + => data switch + { + int counter => $"{counter} Files Manipulating {name}s", + EquipItem it => $"{name}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", + ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", + _ => name, + }; + + /// Initialize the icons. private bool CreateEquipSlotIcons(UiBuilder uiBuilder, DataManager gameData) { using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); @@ -163,94 +341,40 @@ public class ChangedItemDrawer : IDisposable if (!equipTypeIcons.Valid) return false; - // Weapon - var tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0); - if (tex != null) + void Add(ChangedItemIcon icon, TextureWrap? tex) { - _icons.Add(EquipSlot.MainHand, tex); - _icons.Add(EquipSlot.BothHand, tex); + if (tex != null) + _icons.Add(icon, tex); } - // Hat - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1); - if (tex != null) - _icons.Add(EquipSlot.Head, tex); + Add(ChangedItemIcon.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); + Add(ChangedItemIcon.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); + Add(ChangedItemIcon.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); + Add(ChangedItemIcon.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); + Add(ChangedItemIcon.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); + Add(ChangedItemIcon.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); + Add(ChangedItemIcon.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); + Add(ChangedItemIcon.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); + Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); + Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); + Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); + Add(ChangedItemIcon.Monster, gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex")); + Add(ChangedItemIcon.Demihuman, gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex")); + Add(ChangedItemIcon.Customization, gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex")); + Add(ChangedItemIcon.Action, gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex")); - // Body - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2); - if (tex != null) - { - _icons.Add(EquipSlot.Body, tex); - _icons.Add(EquipSlot.BodyHands, tex); - _icons.Add(EquipSlot.BodyHandsLegsFeet, tex); - _icons.Add(EquipSlot.BodyLegsFeet, tex); - _icons.Add(EquipSlot.ChestHands, tex); - _icons.Add(EquipSlot.FullBody, tex); - _icons.Add(EquipSlot.HeadBody, tex); - } + var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); + if (unk == null) + return true; - // Hands - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3); - if (tex != null) - _icons.Add(EquipSlot.Hands, tex); + var image = unk.GetRgbaImageData(); + var bytes = new byte[unk.Header.Height * unk.Header.Height * 4]; + var diff = 2 * (unk.Header.Height - unk.Header.Width); + for (var y = 0; y < unk.Header.Height; ++y) + image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff)); + Add(ChangedItemIcon.Unknown, uiBuilder.LoadImageRaw(bytes, unk.Header.Height, unk.Header.Height, 4)); - // Pants - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5); - if (tex != null) - { - _icons.Add(EquipSlot.Legs, tex); - _icons.Add(EquipSlot.LegsFeet, tex); - } - - // Shoes - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6); - if (tex != null) - _icons.Add(EquipSlot.Feet, tex); - - // Offhand - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7); - if (tex != null) - _icons.Add(EquipSlot.OffHand, tex); - - // Earrings - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8); - if (tex != null) - _icons.Add(EquipSlot.Ears, tex); - - // Necklace - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9); - if (tex != null) - _icons.Add(EquipSlot.Neck, tex); - - // Bracelet - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10); - if (tex != null) - _icons.Add(EquipSlot.Wrists, tex); - - // Ring - tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11); - if (tex != null) - _icons.Add(EquipSlot.RFinger, tex); - - // Monster - tex = gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex"); - if (tex != null) - _icons.Add(MonsterSlot, tex); - - // Demihuman - tex = gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex"); - if (tex != null) - _icons.Add(DemihumanSlot, tex); - - // Customization - tex = gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex"); - if (tex != null) - _icons.Add(CustomizationSlot, tex); - - // Action - tex = gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex"); - if (tex != null) - _icons.Add(ActionSlot, tex); + _smallestIconWidth = _icons.Values.Min(i => i.Width); return true; } diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index 46d9f3bc..df2c905e 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; using ImGuiNET; using OtterGui; @@ -14,6 +15,8 @@ public class ModPanelChangedItemsTab : ITab private readonly ModFileSystemSelector _selector; private readonly ChangedItemDrawer _drawer; + private ChangedItemDrawer.ChangedItemIcon _filter = Enum.GetValues().Aggregate((a, b) => a | b); + public ReadOnlySpan Label => "Changed Items"u8; @@ -28,12 +31,30 @@ public class ModPanelChangedItemsTab : ITab public void DrawContent() { - using var list = ImRaii.ListBox("##changedItems", -Vector2.One); - if (!list) + _drawer.DrawTypeFilter(); + ImGui.Separator(); + using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, + new Vector2(ImGui.GetContentRegionAvail().X, -1)); + if (!table) return; var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); - var height = ImGui.GetFrameHeight(); - ImGuiClip.ClippedDraw(zipList, kvp => _drawer.DrawChangedItem(kvp.Item1, kvp.Item2, true), height); + var height = ImGui.GetFrameHeightWithSpacing(); + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(height); + var remainder = ImGuiClip.FilteredClippedDraw(zipList, skips, CheckFilter, DrawChangedItem); + ImGuiClip.DrawEndDummy(remainder, height); + } + + private bool CheckFilter((string Name, object? Data) kvp) + => _drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); + + private void DrawChangedItem((string Name, object? Data) kvp) + { + ImGui.TableNextColumn(); + _drawer.DrawCategoryIcon(kvp.Name, kvp.Data); + ImGui.SameLine(); + _drawer.DrawChangedItem(kvp.Name, kvp.Data); + _drawer.DrawModelData(kvp.Data); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 9b929c60..f85387f8 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -39,8 +39,9 @@ public class ChangedItemsTab : ITab public void DrawContent() { - _collectionHeader.Draw(true); - var varWidth = DrawFilters(); + _collectionHeader.Draw(true); + _drawer.DrawTypeFilter(); + var varWidth = DrawFilters(); using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); if (!child) return; @@ -57,9 +58,7 @@ public class ChangedItemsTab : ITab ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); var items = _collectionManager.Active.Current.ChangedItems; - var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty - ? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count) - : ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); + var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); ImGuiClip.DrawEndDummy(rest, height); } @@ -79,26 +78,21 @@ public class ChangedItemsTab : ITab /// Apply the current filters. private bool FilterChangedItem(KeyValuePair, object?)> item) - => (_changedItemFilter.IsEmpty - || ChangedItemDrawer.ChangedItemFilterName(item.Key, item.Value.Item2) - .Contains(_changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase)) + => _drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. private void DrawChangedItemColumn(KeyValuePair, object?)> item) { ImGui.TableNextColumn(); - _drawer.DrawChangedItem(item.Key, item.Value.Item2, false); + _drawer.DrawCategoryIcon(item.Key, item.Value.Item2); + ImGui.SameLine(); + _drawer.DrawChangedItem(item.Key, item.Value.Item2); ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - if (!ChangedItemDrawer.GetChangedItemObject(item.Value.Item2, out var text)) - return; - - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightAlign(text); + _drawer.DrawModelData(item.Value.Item2); } private void DrawModColumn(SingleArray mods) @@ -110,7 +104,7 @@ public class ChangedItemsTab : ITab using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) && ImGui.GetIO().KeyCtrl - && first is Mod mod) + && first is Mod mod) _communicator.SelectTab.Invoke(TabType.Mods, mod); if (ImGui.IsItemHovered())