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
This commit is contained in:
Haselnussbomber 2025-03-28 17:00:14 +01:00 committed by GitHub
parent f5b3d85066
commit 6160252418
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 145 additions and 89 deletions

View file

@ -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)
{

View file

@ -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;
/// <summary>
@ -24,6 +28,14 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
[FieldOffset(0)]
private fixed ulong dataUInt64[InventoryItem.StructSize / 0x8];
/// <summary>
/// Initializes a new instance of the <see cref="GameInventoryItem"/> struct.
/// </summary>
public GameInventoryItem()
{
this.InternalItem.Ctor();
}
/// <summary>
/// Initializes a new instance of the <see cref="GameInventoryItem"/> struct.
/// </summary>
@ -33,32 +45,37 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// <summary>
/// Gets a value indicating whether the this <see cref="GameInventoryItem"/> is empty.
/// </summary>
public bool IsEmpty => this.InternalItem.ItemId == 0;
public bool IsEmpty => this.InternalItem.IsEmpty();
/// <summary>
/// Gets the container inventory type.
/// </summary>
public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.Container;
public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.GetInventoryType();
/// <summary>
/// Gets the inventory slot index this item is in.
/// </summary>
public uint InventorySlot => (uint)this.InternalItem.Slot;
public uint InventorySlot => this.InternalItem.GetSlot();
/// <summary>
/// Gets the item id.
/// </summary>
public uint ItemId => this.InternalItem.ItemId;
public uint ItemId => this.InternalItem.GetItemId();
/// <summary>
/// Gets the base item id (without HQ or Collectible offset applied).
/// </summary>
public uint BaseItemId => ItemUtil.GetBaseId(this.ItemId).ItemId;
/// <summary>
/// Gets the quantity of items in this item stack.
/// </summary>
public int Quantity => this.InternalItem.Quantity;
public int Quantity => (int)this.InternalItem.GetQuantity();
/// <summary>
/// Gets the spiritbond or collectability of this item.
/// </summary>
public uint SpiritbondOrCollectability => this.InternalItem.SpiritbondOrCollectability;
public uint SpiritbondOrCollectability => this.InternalItem.GetSpiritbondOrCollectability();
/// <summary>
/// Gets the spiritbond of this item.
@ -69,37 +86,89 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// <summary>
/// Gets the repair condition of this item.
/// </summary>
public uint Condition => this.InternalItem.Condition;
public uint Condition => this.InternalItem.GetCondition(); // Note: This will be the Breeding Capacity of Race Chocobos
/// <summary>
/// Gets a value indicating whether the item is High Quality.
/// </summary>
public bool IsHq => (this.InternalItem.Flags & InventoryItem.ItemFlags.HighQuality) != 0;
public bool IsHq => this.InternalItem.GetFlags().HasFlag(InventoryItem.ItemFlags.HighQuality);
/// <summary>
/// Gets a value indicating whether the item has a company crest applied.
/// </summary>
public bool IsCompanyCrestApplied => (this.InternalItem.Flags & InventoryItem.ItemFlags.CompanyCrestApplied) != 0;
public bool IsCompanyCrestApplied => this.InternalItem.GetFlags().HasFlag(InventoryItem.ItemFlags.CompanyCrestApplied);
/// <summary>
/// Gets a value indicating whether the item is a relic.
/// </summary>
public bool IsRelic => (this.InternalItem.Flags & InventoryItem.ItemFlags.Relic) != 0;
public bool IsRelic => this.InternalItem.GetFlags().HasFlag(InventoryItem.ItemFlags.Relic);
/// <summary>
/// Gets a value indicating whether the is a collectable.
/// </summary>
public bool IsCollectable => (this.InternalItem.Flags & InventoryItem.ItemFlags.Collectable) != 0;
public bool IsCollectable => this.InternalItem.GetFlags().HasFlag(InventoryItem.ItemFlags.Collectable);
/// <summary>
/// Gets the array of materia types.
/// </summary>
public ReadOnlySpan<ushort> Materia => new(Unsafe.AsPointer(ref this.InternalItem.Materia[0]), 5);
public ReadOnlySpan<ushort> Materia
{
get
{
var baseItemId = this.BaseItemId;
if (ItemUtil.IsEventItem(baseItemId) || this.IsMateriaUsedForDate)
return [];
var dataManager = Service<DataManager>.Get();
if (!dataManager.GetExcelSheet<Item>().TryGetRow(baseItemId, out var item) || item.MateriaSlotCount == 0)
return [];
Span<ushort> materiaIds = new ushort[item.MateriaSlotCount];
var materiaRowCount = dataManager.GetExcelSheet<Materia>().Count;
for (byte i = 0; i < item.MateriaSlotCount; i++)
{
var materiaId = this.InternalItem.GetMateriaId(i);
if (materiaId < materiaRowCount)
materiaIds[i] = materiaId;
}
return materiaIds;
}
}
/// <summary>
/// Gets the array of materia grades.
/// </summary>
public ReadOnlySpan<byte> MateriaGrade => new(Unsafe.AsPointer(ref this.InternalItem.MateriaGrades[0]), 5);
public ReadOnlySpan<byte> MateriaGrade
{
get
{
var baseItemId = this.BaseItemId;
if (ItemUtil.IsEventItem(baseItemId) || this.IsMateriaUsedForDate)
return [];
var dataManager = Service<DataManager>.Get();
if (!dataManager.GetExcelSheet<Item>().TryGetRow(baseItemId, out var item) || item.MateriaSlotCount == 0)
return [];
Span<byte> materiaGrades = new byte[item.MateriaSlotCount];
var materiaGradeRowCount = dataManager.GetExcelSheet<MateriaGrade>().Count;
for (byte i = 0; i < item.MateriaSlotCount; i++)
{
var materiaGrade = this.InternalItem.GetMateriaGrade(i);
if (materiaGrade < materiaGradeRowCount)
materiaGrades[i] = materiaGrade;
}
return materiaGrades;
}
}
/// <summary>
/// Gets the address of native inventory item in the game.<br />
@ -128,18 +197,60 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// <summary>
/// Gets the color used for this item.
/// </summary>
public ReadOnlySpan<byte> Stains => new(Unsafe.AsPointer(ref this.InternalItem.Stains[0]), 2);
public ReadOnlySpan<byte> Stains
{
get
{
var baseItemId = this.BaseItemId;
if (ItemUtil.IsEventItem(baseItemId))
return [];
var dataManager = Service<DataManager>.Get();
if (!dataManager.GetExcelSheet<Item>().TryGetRow(baseItemId, out var item) || item.DyeCount == 0)
return [];
Span<byte> stainIds = new byte[item.DyeCount];
var stainRowCount = dataManager.GetExcelSheet<Stain>().Count;
for (byte i = 0; i < item.DyeCount; i++)
{
var stainId = this.InternalItem.GetStain(i);
if (stainId < stainRowCount)
stainIds[i] = stainId;
}
return stainIds;
}
}
/// <summary>
/// Gets the glamour id for this item.
/// </summary>
public uint GlamourId => this.InternalItem.GlamourId;
public uint GlamourId => this.InternalItem.GetGlamourId();
/// <summary>
/// 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.
/// </summary>
internal ulong CrafterContentId => this.InternalItem.CrafterContentId;
internal ulong CrafterContentId => this.InternalItem.GetCrafterContentId();
/// <summary>
/// Gets a value indicating whether the Materia fields are used to store a date.
/// </summary>
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);

View file

@ -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<Item>().TryGetRow(item.ItemId, out var itemRow))
if (ItemUtil.IsNormalItem(item.ItemId) && this.dataManager.Excel.GetSheet<Item>().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<EventItem>().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<Item>().TryGetRow(itemId, out var itemRow)
? StripSoftHypen(itemRow.Name.ExtractText())
: $"Item#{itemId}";
}
private string GetStainName(uint stainId)
{
return this.dataManager.Excel.GetSheet<Stain>().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<Item>().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<Item>().TryGetRow(this.GetBaseItemId(itemId), out var item))
if (!this.dataManager.Excel.GetSheet<Item>().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<UIColor>().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<EventItem>().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<Item>().TryGetRow(itemId, out var item) ? item.Icon : 0u;