using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; namespace Dalamud.Game.Inventory; /// /// Dalamud wrapper around a ClientStructs InventoryItem. /// [StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)] public unsafe struct GameInventoryItem : IEquatable { /// /// The actual data. /// [FieldOffset(0)] internal readonly InventoryItem InternalItem; private const int StructSizeInBytes = 0x38; /// /// The view of the backing data, in . /// [FieldOffset(0)] private fixed ulong dataUInt64[StructSizeInBytes / 0x8]; static GameInventoryItem() { Debug.Assert( sizeof(InventoryItem) == StructSizeInBytes, $"Definition of {nameof(InventoryItem)} has been changed. " + $"Update {nameof(StructSizeInBytes)} to {sizeof(InventoryItem)} to accommodate for the size change."); } /// /// Initializes a new instance of the struct. /// /// Inventory item to wrap. internal GameInventoryItem(InventoryItem item) => this.InternalItem = item; /// /// Gets a value indicating whether the this is empty. /// public bool IsEmpty => this.InternalItem.ItemID == 0; /// /// Gets the container inventory type. /// public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.Container; /// /// Gets the inventory slot index this item is in. /// public uint InventorySlot => (uint)this.InternalItem.Slot; /// /// Gets the item id. /// public uint ItemId => this.InternalItem.ItemID; /// /// Gets the quantity of items in this item stack. /// public uint Quantity => this.InternalItem.Quantity; /// /// Gets the spiritbond of this item. /// public uint Spiritbond => this.InternalItem.Spiritbond; /// /// Gets the repair condition of this item. /// public uint Condition => this.InternalItem.Condition; /// /// Gets a value indicating whether the item is High Quality. /// public bool IsHq => (this.InternalItem.Flags & InventoryItem.ItemFlags.HQ) != 0; /// /// Gets a value indicating whether the item has a company crest applied. /// public bool IsCompanyCrestApplied => (this.InternalItem.Flags & InventoryItem.ItemFlags.CompanyCrestApplied) != 0; /// /// Gets a value indicating whether the item is a relic. /// public bool IsRelic => (this.InternalItem.Flags & InventoryItem.ItemFlags.Relic) != 0; /// /// Gets a value indicating whether the is a collectable. /// public bool IsCollectable => (this.InternalItem.Flags & InventoryItem.ItemFlags.Collectable) != 0; /// /// Gets the array of materia types. /// public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.Materia[0])), 5); /// /// Gets the array of materia grades. /// // TODO: Replace with MateriaGradeBytes [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public ReadOnlySpan MateriaGrade => this.MateriaGradeBytes.ToArray().Select(g => (ushort)g).ToArray().AsSpan(); /// /// Gets the address of native inventory item in the game.
/// Can be 0 if this instance of does not point to a valid set of container type and slot.
/// Note that this instance of can be a snapshot; it may not necessarily match the /// data you can query from the game using this address value. ///
public nint Address { get { var s = GetReadOnlySpanOfInventory(this.ContainerType); if (s.IsEmpty) return 0; foreach (ref readonly var i in s) { if (i.InventorySlot == this.InventorySlot) return (nint)Unsafe.AsPointer(ref Unsafe.AsRef(in i)); } return 0; } } /// /// Gets the color used for this item. /// public byte Stain => this.InternalItem.Stain; /// /// Gets the glamour id for this item. /// public uint GlamourId => this.InternalItem.GlamourID; /// /// 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; private ReadOnlySpan MateriaGradeBytes => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5); public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r); public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r); /// readonly bool IEquatable.Equals(GameInventoryItem other) => this.Equals(other); /// Indicates whether the current object is equal to another object of the same type. /// An object to compare with this object. /// true if the current object is equal to the parameter; otherwise, false. public readonly bool Equals(in GameInventoryItem other) { for (var i = 0; i < StructSizeInBytes / 8; i++) { if (this.dataUInt64[i] != other.dataUInt64[i]) return false; } return true; } /// public override bool Equals(object obj) => obj is GameInventoryItem gii && this.Equals(gii); /// public override int GetHashCode() { var k = 0x5a8447b91aff51b4UL; for (var i = 0; i < StructSizeInBytes / 8; i++) k ^= this.dataUInt64[i]; return unchecked((int)(k ^ (k >> 32))); } /// public override string ToString() => this.IsEmpty ? "empty" : $"item({this.ItemId}@{this.ContainerType}#{this.InventorySlot})"; /// /// Gets a view of s, wrapped as . /// /// The inventory type. /// The span. internal static ReadOnlySpan GetReadOnlySpanOfInventory(GameInventoryType type) { var inventoryManager = InventoryManager.Instance(); if (inventoryManager is null) return default; var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); if (inventory is null) return default; return new ReadOnlySpan(inventory->Items, (int)inventory->Size); } }