From 6160252418658972e35ba978ca28fa10bb490ffc Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 28 Mar 2025 17:00:14 +0100 Subject: [PATCH] Update GameInventoryItem (#2219) * Update GameInventoryItem - Resolve symbolic InventoryItem, used in HandIn - Harden Materia/MateriaGrade/Stains results - Make sure GameInventoryItem is constructed correctly * Remove some duplicate code from InventoryWidget * Fix null check --- Dalamud/Game/Inventory/GameInventory.cs | 4 +- Dalamud/Game/Inventory/GameInventoryItem.cs | 145 ++++++++++++++++-- .../Windows/Data/Widgets/InventoryWidget.cs | 85 ++-------- 3 files changed, 145 insertions(+), 89 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 02412c551..27f14bf79 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -121,6 +121,8 @@ internal class GameInventory : IInternalDisposableService // Assumption: newItems is sorted by slots, and the last item has the highest slot number. var oldItems = this.inventoryItems[i] ??= new GameInventoryItem[newItems[^1].InternalItem.Slot + 1]; + if (this.inventoryItems[i] == null) + oldItems.Initialize(); foreach (ref readonly var newItem in newItems) { diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 32eb9911b..5a0ec0c74 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -1,9 +1,13 @@ -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Dalamud.Data; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.Game; +using Lumina.Excel.Sheets; + namespace Dalamud.Game.Inventory; /// @@ -24,6 +28,14 @@ public unsafe struct GameInventoryItem : IEquatable [FieldOffset(0)] private fixed ulong dataUInt64[InventoryItem.StructSize / 0x8]; + /// + /// Initializes a new instance of the struct. + /// + public GameInventoryItem() + { + this.InternalItem.Ctor(); + } + /// /// Initializes a new instance of the struct. /// @@ -33,32 +45,37 @@ public unsafe struct GameInventoryItem : IEquatable /// /// Gets a value indicating whether the this is empty. /// - public bool IsEmpty => this.InternalItem.ItemId == 0; + public bool IsEmpty => this.InternalItem.IsEmpty(); /// /// Gets the container inventory type. /// - public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.Container; + public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.GetInventoryType(); /// /// Gets the inventory slot index this item is in. /// - public uint InventorySlot => (uint)this.InternalItem.Slot; + public uint InventorySlot => this.InternalItem.GetSlot(); /// /// Gets the item id. /// - public uint ItemId => this.InternalItem.ItemId; + public uint ItemId => this.InternalItem.GetItemId(); + + /// + /// Gets the base item id (without HQ or Collectible offset applied). + /// + public uint BaseItemId => ItemUtil.GetBaseId(this.ItemId).ItemId; /// /// Gets the quantity of items in this item stack. /// - public int Quantity => this.InternalItem.Quantity; + public int Quantity => (int)this.InternalItem.GetQuantity(); /// /// Gets the spiritbond or collectability of this item. /// - public uint SpiritbondOrCollectability => this.InternalItem.SpiritbondOrCollectability; + public uint SpiritbondOrCollectability => this.InternalItem.GetSpiritbondOrCollectability(); /// /// Gets the spiritbond of this item. @@ -69,37 +86,89 @@ public unsafe struct GameInventoryItem : IEquatable /// /// Gets the repair condition of this item. /// - public uint Condition => this.InternalItem.Condition; + public uint Condition => this.InternalItem.GetCondition(); // Note: This will be the Breeding Capacity of Race Chocobos /// /// Gets a value indicating whether the item is High Quality. /// - public bool IsHq => (this.InternalItem.Flags & InventoryItem.ItemFlags.HighQuality) != 0; + public bool IsHq => this.InternalItem.GetFlags().HasFlag(InventoryItem.ItemFlags.HighQuality); /// /// Gets a value indicating whether the item has a company crest applied. /// - public bool IsCompanyCrestApplied => (this.InternalItem.Flags & InventoryItem.ItemFlags.CompanyCrestApplied) != 0; + public bool IsCompanyCrestApplied => this.InternalItem.GetFlags().HasFlag(InventoryItem.ItemFlags.CompanyCrestApplied); /// /// Gets a value indicating whether the item is a relic. /// - public bool IsRelic => (this.InternalItem.Flags & InventoryItem.ItemFlags.Relic) != 0; + public bool IsRelic => this.InternalItem.GetFlags().HasFlag(InventoryItem.ItemFlags.Relic); /// /// Gets a value indicating whether the is a collectable. /// - public bool IsCollectable => (this.InternalItem.Flags & InventoryItem.ItemFlags.Collectable) != 0; + public bool IsCollectable => this.InternalItem.GetFlags().HasFlag(InventoryItem.ItemFlags.Collectable); /// /// Gets the array of materia types. /// - public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref this.InternalItem.Materia[0]), 5); + public ReadOnlySpan Materia + { + get + { + var baseItemId = this.BaseItemId; + + if (ItemUtil.IsEventItem(baseItemId) || this.IsMateriaUsedForDate) + return []; + + var dataManager = Service.Get(); + + if (!dataManager.GetExcelSheet().TryGetRow(baseItemId, out var item) || item.MateriaSlotCount == 0) + return []; + + Span materiaIds = new ushort[item.MateriaSlotCount]; + var materiaRowCount = dataManager.GetExcelSheet().Count; + + for (byte i = 0; i < item.MateriaSlotCount; i++) + { + var materiaId = this.InternalItem.GetMateriaId(i); + if (materiaId < materiaRowCount) + materiaIds[i] = materiaId; + } + + return materiaIds; + } + } /// /// Gets the array of materia grades. /// - public ReadOnlySpan MateriaGrade => new(Unsafe.AsPointer(ref this.InternalItem.MateriaGrades[0]), 5); + public ReadOnlySpan MateriaGrade + { + get + { + var baseItemId = this.BaseItemId; + + if (ItemUtil.IsEventItem(baseItemId) || this.IsMateriaUsedForDate) + return []; + + var dataManager = Service.Get(); + + if (!dataManager.GetExcelSheet().TryGetRow(baseItemId, out var item) || item.MateriaSlotCount == 0) + return []; + + Span materiaGrades = new byte[item.MateriaSlotCount]; + var materiaGradeRowCount = dataManager.GetExcelSheet().Count; + + for (byte i = 0; i < item.MateriaSlotCount; i++) + { + var materiaGrade = this.InternalItem.GetMateriaGrade(i); + if (materiaGrade < materiaGradeRowCount) + materiaGrades[i] = materiaGrade; + } + + return materiaGrades; + } + } /// /// Gets the address of native inventory item in the game.
@@ -128,18 +197,60 @@ public unsafe struct GameInventoryItem : IEquatable /// /// Gets the color used for this item. /// - public ReadOnlySpan Stains => new(Unsafe.AsPointer(ref this.InternalItem.Stains[0]), 2); + public ReadOnlySpan Stains + { + get + { + var baseItemId = this.BaseItemId; + + if (ItemUtil.IsEventItem(baseItemId)) + return []; + + var dataManager = Service.Get(); + + if (!dataManager.GetExcelSheet().TryGetRow(baseItemId, out var item) || item.DyeCount == 0) + return []; + + Span stainIds = new byte[item.DyeCount]; + var stainRowCount = dataManager.GetExcelSheet().Count; + + for (byte i = 0; i < item.DyeCount; i++) + { + var stainId = this.InternalItem.GetStain(i); + if (stainId < stainRowCount) + stainIds[i] = stainId; + } + + return stainIds; + } + } /// /// Gets the glamour id for this item. /// - public uint GlamourId => this.InternalItem.GlamourId; + public uint GlamourId => this.InternalItem.GetGlamourId(); /// /// Gets the items crafter's content id. /// NOTE: I'm not sure if this is a good idea to include or not in the dalamud api. Marked internal for now. /// - internal ulong CrafterContentId => this.InternalItem.CrafterContentId; + internal ulong CrafterContentId => this.InternalItem.GetCrafterContentId(); + + /// + /// Gets a value indicating whether the Materia fields are used to store a date. + /// + private bool IsMateriaUsedForDate => this.BaseItemId + // Race Chocobo related items + is 9560 // Proof of Covering + + // Wedding related items + or 8575 // Eternity Ring + or 8693 // Promise of Innocence + or 8694 // Promise of Passion + or 8695 // Promise of Devotion + or 8696 // (Unknown/unused) + or 8698 // Blank Invitation + or 8699; // Ceremony Invitation public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index d532c2cdc..f8aa4e500 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs @@ -9,6 +9,7 @@ using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; @@ -145,12 +146,9 @@ internal class InventoryWidget : IDataWindowWidget ImGui.TableNextColumn(); // Item if (item.ItemId != 0 && item.Quantity != 0) { - var itemName = this.GetItemName(item.ItemId); + var itemName = ItemUtil.GetItemName(item.ItemId).ExtractText(); var iconId = this.GetItemIconId(item.ItemId); - if (item.IsHq) - itemName += " " + SeIconChar.HighQuality.ToIconString(); - if (this.textureManager.Shared.TryGetFromGameIcon(new GameIconLookup(iconId, item.IsHq), out var tex) && tex.TryGetWrap(out var texture, out _)) { ImGui.Image(texture.ImGuiHandle, new Vector2(ImGui.GetTextLineHeight())); @@ -217,7 +215,7 @@ internal class InventoryWidget : IDataWindowWidget AddKeyValueRow("Quantity", item.Quantity.ToString()); AddKeyValueRow("GlamourId", item.GlamourId.ToString()); - if (!this.IsEventItem(item.ItemId)) + if (!ItemUtil.IsEventItem(item.ItemId)) { AddKeyValueRow(item.IsCollectable ? "Collectability" : "Spiritbond", item.SpiritbondOrCollectability.ToString()); @@ -261,9 +259,9 @@ internal class InventoryWidget : IDataWindowWidget AddKeyValueRow("Flags", flagsBuilder.ToString()); - if (this.IsNormalItem(item.ItemId) && this.dataManager.Excel.GetSheet().TryGetRow(item.ItemId, out var itemRow)) + if (ItemUtil.IsNormalItem(item.ItemId) && this.dataManager.Excel.GetSheet().TryGetRow(item.ItemId, out var itemRow)) { - if (itemRow.DyeCount > 0) + if (itemRow.DyeCount > 0 && item.Stains.Length > 0) { ImGui.TableNextRow(); ImGui.TableNextColumn(); @@ -279,11 +277,12 @@ internal class InventoryWidget : IDataWindowWidget for (var i = 0; i < itemRow.DyeCount; i++) { - AddValueValueRow(item.Stains[i].ToString(), this.GetStainName(item.Stains[i])); + var stainId = item.Stains[i]; + AddValueValueRow(stainId.ToString(), this.GetStainName(stainId)); } } - if (itemRow.MateriaSlotCount > 0) + if (itemRow.MateriaSlotCount > 0 && item.Materia.Length > 0) { ImGui.TableNextRow(); ImGui.TableNextColumn(); @@ -307,45 +306,6 @@ internal class InventoryWidget : IDataWindowWidget } } - private bool IsEventItem(uint itemId) => itemId is > 2_000_000; - - private bool IsHighQuality(uint itemId) => itemId is > 1_000_000 and < 2_000_000; - - private bool IsCollectible(uint itemId) => itemId is > 500_000 and < 1_000_000; - - private bool IsNormalItem(uint itemId) => itemId is < 500_000; - - private uint GetBaseItemId(uint itemId) - { - if (this.IsEventItem(itemId)) return itemId; // uses EventItem sheet - if (this.IsHighQuality(itemId)) return itemId - 1_000_000; - if (this.IsCollectible(itemId)) return itemId - 500_000; - return itemId; - } - - private string GetItemName(uint itemId) - { - // EventItem - if (this.IsEventItem(itemId)) - { - return this.dataManager.Excel.GetSheet().TryGetRow(itemId, out var eventItemRow) - ? StripSoftHypen(eventItemRow.Name.ExtractText()) - : $"EventItem#{itemId}"; - } - - // HighQuality - if (this.IsHighQuality(itemId)) - itemId -= 1_000_000; - - // Collectible - if (this.IsCollectible(itemId)) - itemId -= 500_000; - - return this.dataManager.Excel.GetSheet().TryGetRow(itemId, out var itemRow) - ? StripSoftHypen(itemRow.Name.ExtractText()) - : $"Item#{itemId}"; - } - private string GetStainName(uint stainId) { return this.dataManager.Excel.GetSheet().TryGetRow(stainId, out var stainRow) @@ -353,32 +313,15 @@ internal class InventoryWidget : IDataWindowWidget : $"Stain#{stainId}"; } - private uint GetItemRarityColorType(Item item, bool isEdgeColor = false) - { - return (isEdgeColor ? 548u : 547u) + item.Rarity * 2u; - } - - private uint GetItemRarityColorType(uint itemId, bool isEdgeColor = false) - { - // EventItem - if (this.IsEventItem(itemId)) - return this.GetItemRarityColorType(1, isEdgeColor); - - if (!this.dataManager.Excel.GetSheet().TryGetRow(this.GetBaseItemId(itemId), out var item)) - return this.GetItemRarityColorType(1, isEdgeColor); - - return this.GetItemRarityColorType(item, isEdgeColor); - } - private uint GetItemRarityColor(uint itemId, bool isEdgeColor = false) { - if (this.IsEventItem(itemId)) + if (ItemUtil.IsEventItem(itemId)) return isEdgeColor ? 0xFF000000 : 0xFFFFFFFF; - if (!this.dataManager.Excel.GetSheet().TryGetRow(this.GetBaseItemId(itemId), out var item)) + if (!this.dataManager.Excel.GetSheet().TryGetRow(ItemUtil.GetBaseId(itemId).ItemId, out var item)) return isEdgeColor ? 0xFF000000 : 0xFFFFFFFF; - var rowId = this.GetItemRarityColorType(item, isEdgeColor); + var rowId = ItemUtil.GetItemRarityColorType(item.RowId, isEdgeColor); return this.dataManager.Excel.GetSheet().TryGetRow(rowId, out var color) ? BinaryPrimitives.ReverseEndianness(color.Dark) | 0xFF000000 : 0xFFFFFFFF; @@ -387,15 +330,15 @@ internal class InventoryWidget : IDataWindowWidget private uint GetItemIconId(uint itemId) { // EventItem - if (this.IsEventItem(itemId)) + if (ItemUtil.IsEventItem(itemId)) return this.dataManager.Excel.GetSheet().TryGetRow(itemId, out var eventItem) ? eventItem.Icon : 0u; // HighQuality - if (this.IsHighQuality(itemId)) + if (ItemUtil.IsHighQuality(itemId)) itemId -= 1_000_000; // Collectible - if (this.IsCollectible(itemId)) + if (ItemUtil.IsCollectible(itemId)) itemId -= 500_000; return this.dataManager.Excel.GetSheet().TryGetRow(itemId, out var item) ? item.Icon : 0u;