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