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);