From 5204bb723d824072bf415759b9e4f23c84d8c9d0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 16:47:54 +0900 Subject: [PATCH] Optimizations --- Dalamud/Game/Inventory/GameInventory.cs | 396 ++++++++++-------- .../Game/Inventory/GameInventoryChangelog.cs | 28 -- .../Inventory/GameInventoryChangelogState.cs | 17 - Dalamud/Game/Inventory/GameInventoryEvent.cs | 34 ++ Dalamud/Game/Inventory/GameInventoryItem.cs | 118 ++++-- Dalamud/Game/Inventory/GameInventoryType.cs | 7 +- Dalamud/Plugin/Services/IGameInventory.cs | 123 +++--- lib/FFXIVClientStructs | 2 +- 8 files changed, 424 insertions(+), 301 deletions(-) delete mode 100644 Dalamud/Game/Inventory/GameInventoryChangelog.cs delete mode 100644 Dalamud/Game/Inventory/GameInventoryChangelogState.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryEvent.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index c9285b246..cfb22ca0d 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; + using FFXIVClientStructs.FFXIV.Client.Game; namespace Dalamud.Game.Inventory; @@ -14,193 +15,258 @@ namespace Dalamud.Game.Inventory; /// This class provides events for the players in-game inventory. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal class GameInventory : IDisposable, IServiceType, IGameInventory { private static readonly ModuleLog Log = new("GameInventory"); + + private readonly List changelog = new(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - private readonly Dictionary> inventoryCache; + private readonly GameInventoryType[] inventoryTypes; + private readonly GameInventoryItem[][] inventoryItems; + private readonly unsafe GameInventoryItem*[] inventoryItemsPointers; [ServiceManager.ServiceConstructor] - private GameInventory() + private unsafe GameInventory() { - this.inventoryCache = new Dictionary>(); + this.inventoryTypes = Enum.GetValues(); - foreach (var inventoryType in Enum.GetValues()) + // Using GC.AllocateArray(pinned: true), so that Unsafe.AsPointer(ref array[0]) does not fail. + this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; + this.inventoryItemsPointers = new GameInventoryItem*[this.inventoryTypes.Length]; + for (var i = 0; i < this.inventoryItems.Length; i++) { - this.inventoryCache.Add(inventoryType, new Dictionary()); + this.inventoryItems[i] = GC.AllocateArray(1, true); + this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref this.inventoryItems[i][0]); } - + this.framework.Update += this.OnFrameworkUpdate; } /// - public event IGameInventory.OnItemMovedDelegate? ItemMoved; - - /// - public event IGameInventory.OnItemRemovedDelegate? ItemRemoved; - - /// - public event IGameInventory.OnItemAddedDelegate? ItemAdded; - - /// - public event IGameInventory.OnItemChangedDelegate? ItemChanged; + public event IGameInventory.InventoryChangeDelegate? InventoryChanged; /// public void Dispose() { this.framework.Update -= this.OnFrameworkUpdate; } - - private void OnFrameworkUpdate(IFramework framework1) - { - // If no one is listening for event's then we don't need to track anything. - if (!this.AnyListeners()) return; - var performanceMonitor = Stopwatch.StartNew(); - - var changelog = new List(); - - foreach (var (inventoryType, cachedInventoryItems) in this.inventoryCache) + /// + /// Gets a view of s, wrapped as . + /// + /// The inventory type. + /// The span. + private static unsafe Span GetItemsForInventory(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(inventory->Items, (int)inventory->Size); + } + + /// + /// Looks for the first index of , or the supposed position one should be if none could be found. + /// + /// The span to look in. + /// The type. + /// The index. + private static int FindTypeIndex(Span span, GameInventoryEvent type) + { + // Use linear lookup if span size is small enough + if (span.Length < 64) { - foreach (var item in this.GetItemsForInventory(inventoryType)) + var i = 0; + for (; i < span.Length; i++) { - if (cachedInventoryItems.TryGetValue(item.Slot, out var inventoryItem)) + if (type <= span[i].Type) + break; + } + + return i; + } + + var lo = 0; + var hi = span.Length - 1; + while (lo <= hi) + { + var i = lo + ((hi - lo) >> 1); + var type2 = span[i].Type; + if (type == type2) + return i; + if (type < type2) + lo = i + 1; + else + hi = i - 1; + } + + return lo; + } + + private unsafe void OnFrameworkUpdate(IFramework framework1) + { + // TODO: Uncomment this + // // If no one is listening for event's then we don't need to track anything. + // if (this.InventoryChanged is null) return; + + for (var i = 0; i < this.inventoryTypes.Length;) + { + var oldItemsArray = this.inventoryItems[i]; + var oldItemsLength = oldItemsArray.Length; + var oldItemsPointer = this.inventoryItemsPointers[i]; + + var resizeRequired = 0; + foreach (ref var newItem in GetItemsForInventory(this.inventoryTypes[i])) + { + var slot = newItem.InternalItem.Slot; + if (slot >= oldItemsLength) { - // Gained Item - // If the item we have cached has an item id of 0, then we expect it to be an empty slot. - // However, if the item we see in the game data has an item id that is not 0, then it now has an item. - if (inventoryItem.ItemID is 0 && item.ItemID is not 0) - { - var gameInventoryItem = new GameInventoryItem(item); - this.ItemAdded?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); - changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Added, gameInventoryItem)); - - Log.Verbose($"New Item Added to {inventoryType}: {item.ItemID}"); - this.inventoryCache[inventoryType][item.Slot] = item; - } - - // Removed Item - // If the item we have cached has an item id of not 0, then we expect it to have an item. - // However, if the item we see in the game data has an item id that is 0, then it was removed from this inventory. - if (inventoryItem.ItemID is not 0 && item.ItemID is 0) - { - var gameInventoryItem = new GameInventoryItem(inventoryItem); - this.ItemRemoved?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); - changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Removed, gameInventoryItem)); - - Log.Verbose($"Item Removed from {inventoryType}: {inventoryItem.ItemID}"); - this.inventoryCache[inventoryType][item.Slot] = item; - } - - // Changed Item - // If the item we have cached, does not match the item that we see in the game data - // AND if neither item is empty, then the item has been changed. - if (this.IsItemChanged(inventoryItem, item) && inventoryItem.ItemID is not 0 && item.ItemID is not 0) - { - var gameInventoryItem = new GameInventoryItem(inventoryItem); - this.ItemChanged?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); - - Log.Verbose($"Item Changed {inventoryType}: {inventoryItem.ItemID}"); - this.inventoryCache[inventoryType][item.Slot] = item; - } + resizeRequired = Math.Max(resizeRequired, slot + 1); + continue; + } + + // We already checked the range above. Go raw. + ref var oldItem = ref oldItemsPointer[slot]; + + if (oldItem.IsEmpty) + { + if (newItem.IsEmpty) + continue; + this.changelog.Add(new(GameInventoryEvent.Added, default, newItem)); } else { - cachedInventoryItems.Add(item.Slot, item); + if (newItem.IsEmpty) + this.changelog.Add(new(GameInventoryEvent.Removed, oldItem, default)); + else if (!oldItem.Equals(newItem)) + this.changelog.Add(new(GameInventoryEvent.Changed, oldItem, newItem)); + else + continue; } + + Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}"); + oldItem = newItem; + } + + // Did the max slot number get changed? + if (resizeRequired != 0) + { + // Resize our buffer, and then try again. + var oldItemsExpanded = GC.AllocateArray(resizeRequired, true); + oldItemsArray.CopyTo(oldItemsExpanded, 0); + this.inventoryItems[i] = oldItemsExpanded; + this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref oldItemsExpanded[0]); + } + else + { + // Proceed to the next inventory. + i++; } } - - // Resolve changelog for item moved - // Group all changelogs that have the same itemId, and check if there was an add and a remove event for that item. - foreach (var itemGroup in changelog.GroupBy(log => log.Item.ItemId)) + + // Was there any change? If not, stop further processing. + if (this.changelog.Count == 0) + return; + + try { - var hasAdd = false; - var hasRemove = false; - - foreach (var log in itemGroup) + // From this point, the size of changelog shall not change. + var span = CollectionsMarshal.AsSpan(this.changelog); + + span.Sort((a, b) => a.Type.CompareTo(b.Type)); + var addedFrom = FindTypeIndex(span, GameInventoryEvent.Added); + var removedFrom = FindTypeIndex(span, GameInventoryEvent.Removed); + var changedFrom = FindTypeIndex(span, GameInventoryEvent.Changed); + + // Resolve changelog for item moved, from 1 added + 1 removed + for (var iAdded = addedFrom; iAdded < removedFrom; iAdded++) { - switch (log.State) + ref var added = ref span[iAdded]; + for (var iRemoved = removedFrom; iRemoved < changedFrom; iRemoved++) { - case GameInventoryChangelogState.Added: - hasAdd = true; + ref var removed = ref span[iRemoved]; + if (added.Target.ItemId == removed.Source.ItemId) + { + span[iAdded] = new(GameInventoryEvent.Moved, span[iRemoved].Source, span[iAdded].Target); + span[iRemoved] = default; + Log.Verbose($"[{iAdded}] Interpreting instead as: {span[iAdded]}"); + Log.Verbose($"[{iRemoved}] Discarding"); break; - - case GameInventoryChangelogState.Removed: - hasRemove = true; - break; - - default: - throw new ArgumentOutOfRangeException(); + } } } - var itemMoved = hasAdd && hasRemove; - if (itemMoved) + // Resolve changelog for item moved, from 2 changeds + for (var i = changedFrom; i < this.changelog.Count; i++) { - var added = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Added); - var removed = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Removed); - if (added is null || removed is null) continue; - - this.ItemMoved?.Invoke(removed.Item.ContainerType, removed.Item.InventorySlot, added.Item.ContainerType, added.Item.InventorySlot, added.Item); - - Log.Verbose($"Item Moved {removed.Item.ContainerType}:{removed.Item.InventorySlot} -> {added.Item.ContainerType}:{added.Item.InventorySlot}: {added.Item.ItemId}"); + if (span[i].IsEmpty) + continue; + + ref var e1 = ref span[i]; + for (var j = i + 1; j < this.changelog.Count; j++) + { + ref var e2 = ref span[j]; + if (e1.Target.ItemId == e2.Source.ItemId && e1.Source.ItemId == e2.Target.ItemId) + { + if (e1.Target.IsEmpty) + { + // e1 got moved to e2 + e1 = new(GameInventoryEvent.Moved, e1.Source, e2.Target); + e2 = default; + Log.Verbose($"[{i}] Interpreting instead as: {e1}"); + Log.Verbose($"[{j}] Discarding"); + } + else if (e2.Target.IsEmpty) + { + // e2 got moved to e1 + e1 = new(GameInventoryEvent.Moved, e2.Source, e1.Target); + e2 = default; + Log.Verbose($"[{i}] Interpreting instead as: {e1}"); + Log.Verbose($"[{j}] Discarding"); + } + else + { + // e1 and e2 got swapped + (e1, e2) = (new(GameInventoryEvent.Moved, e1.Target, e2.Target), + new(GameInventoryEvent.Moved, e2.Target, e1.Target)); + + Log.Verbose($"[{i}] Interpreting instead as: {e1}"); + Log.Verbose($"[{j}] Interpreting instead as: {e2}"); + } + } + } } + + // Filter out the emptied out entries. + // We do not care about the order of items in the changelog anymore. + for (var i = 0; i < span.Length;) + { + if (span[i].IsEmpty) + { + span[i] = span[^1]; + span = span[..^1]; + } + else + { + i++; + } + } + + // Actually broadcast the changes to subscribers. + if (!span.IsEmpty) + this.InventoryChanged?.Invoke(span); + } + finally + { + this.changelog.Clear(); } - - var elapsed = performanceMonitor.Elapsed; - - Log.Verbose($"Processing Time: {elapsed.Ticks}ticks :: {elapsed.TotalMilliseconds}ms"); } - - private bool AnyListeners() - { - if (this.ItemMoved is not null) return true; - if (this.ItemRemoved is not null) return true; - if (this.ItemAdded is not null) return true; - if (this.ItemChanged is not null) return true; - - return false; - } - - private unsafe ReadOnlySpan GetItemsForInventory(GameInventoryType type) - { - var inventoryManager = InventoryManager.Instance(); - if (inventoryManager is null) return ReadOnlySpan.Empty; - - var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); - if (inventory is null) return ReadOnlySpan.Empty; - - return new ReadOnlySpan(inventory->Items, (int)inventory->Size); - } - - private bool IsItemChanged(InventoryItem a, InventoryItem b) - { - if (a.Container != b.Container) return true; // Shouldn't be possible, but shouldn't hurt. - if (a.Slot != b.Slot) return true; // Shouldn't be possible, but shouldn't hurt. - if (a.ItemID != b.ItemID) return true; - if (a.Quantity != b.Quantity) return true; - if (a.Spiritbond != b.Spiritbond) return true; - if (a.Condition != b.Condition) return true; - if (a.Flags != b.Flags) return true; - if (a.CrafterContentID != b.CrafterContentID) return true; - if (this.IsMateriaChanged(a, b)) return true; - if (this.IsMateriaGradeChanged(a, b)) return true; - if (a.Stain != b.Stain) return true; - if (a.GlamourID != b.GlamourID) return true; - - return false; - } - - private unsafe bool IsMateriaChanged(InventoryItem a, InventoryItem b) - => new ReadOnlySpan(a.Materia, 5) != new ReadOnlySpan(b.Materia, 5); - - private unsafe bool IsMateriaGradeChanged(InventoryItem a, InventoryItem b) - => new ReadOnlySpan(a.MateriaGrade, 5) != new ReadOnlySpan(b.MateriaGrade, 5); } /// @@ -222,47 +288,19 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public GameInventoryPluginScoped() { - this.gameInventoryService.ItemMoved += this.OnItemMovedForward; - this.gameInventoryService.ItemRemoved += this.OnItemRemovedForward; - this.gameInventoryService.ItemAdded += this.OnItemAddedForward; - this.gameInventoryService.ItemChanged += this.OnItemChangedForward; + this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward; } /// - public event IGameInventory.OnItemMovedDelegate? ItemMoved; - - /// - public event IGameInventory.OnItemRemovedDelegate? ItemRemoved; - - /// - public event IGameInventory.OnItemAddedDelegate? ItemAdded; - - /// - public event IGameInventory.OnItemChangedDelegate? ItemChanged; + public event IGameInventory.InventoryChangeDelegate? InventoryChanged; /// public void Dispose() { - this.gameInventoryService.ItemMoved -= this.OnItemMovedForward; - this.gameInventoryService.ItemRemoved -= this.OnItemRemovedForward; - this.gameInventoryService.ItemAdded -= this.OnItemAddedForward; - this.gameInventoryService.ItemChanged -= this.OnItemChangedForward; - - this.ItemMoved = null; - this.ItemRemoved = null; - this.ItemAdded = null; - this.ItemChanged = null; + this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward; + this.InventoryChanged = null; } - private void OnItemMovedForward(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item) - => this.ItemMoved?.Invoke(source, sourceSlot, destination, destinationSlot, item); - - private void OnItemRemovedForward(GameInventoryType source, uint sourceSlot, GameInventoryItem item) - => this.ItemRemoved?.Invoke(source, sourceSlot, item); - - private void OnItemAddedForward(GameInventoryType destination, uint destinationSlot, GameInventoryItem item) - => this.ItemAdded?.Invoke(destination, destinationSlot, item); - - private void OnItemChangedForward(GameInventoryType inventory, uint slot, GameInventoryItem item) - => this.ItemChanged?.Invoke(inventory, slot, item); + private void OnInventoryChangedForward(ReadOnlySpan events) + => this.InventoryChanged?.Invoke(events); } diff --git a/Dalamud/Game/Inventory/GameInventoryChangelog.cs b/Dalamud/Game/Inventory/GameInventoryChangelog.cs deleted file mode 100644 index 52ada81e0..000000000 --- a/Dalamud/Game/Inventory/GameInventoryChangelog.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Dalamud.Game.Inventory; - -/// -/// Class representing an inventory item change event. -/// -internal class GameInventoryItemChangelog -{ - /// - /// Initializes a new instance of the class. - /// - /// Item state. - /// Item. - internal GameInventoryItemChangelog(GameInventoryChangelogState state, GameInventoryItem item) - { - this.State = state; - this.Item = item; - } - - /// - /// Gets the state of this changelog event. - /// - internal GameInventoryChangelogState State { get; } - - /// - /// Gets the item for this changelog event. - /// - internal GameInventoryItem Item { get; } -} diff --git a/Dalamud/Game/Inventory/GameInventoryChangelogState.cs b/Dalamud/Game/Inventory/GameInventoryChangelogState.cs deleted file mode 100644 index 23e972419..000000000 --- a/Dalamud/Game/Inventory/GameInventoryChangelogState.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dalamud.Game.Inventory; - -/// -/// Class representing a item's changelog state. -/// -internal enum GameInventoryChangelogState -{ - /// - /// Item was added to an inventory. - /// - Added, - - /// - /// Item was removed from an inventory. - /// - Removed, -} diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs new file mode 100644 index 000000000..c23d79f30 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -0,0 +1,34 @@ +namespace Dalamud.Game.Inventory; + +/// +/// Class representing a item's changelog state. +/// +[Flags] +public enum GameInventoryEvent +{ + /// + /// A value indicating that there was no event.
+ /// You should not see this value, unless you explicitly used it yourself, or APIs using this enum say otherwise. + ///
+ Empty = 0, + + /// + /// Item was added to an inventory. + /// + Added = 1 << 0, + + /// + /// Item was removed from an inventory. + /// + Removed = 1 << 1, + + /// + /// Properties are changed for an item in an inventory. + /// + Changed = 1 << 2, + + /// + /// Item has been moved, possibly across different inventories. + /// + Moved = 1 << 3, +} diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 286104c43..9073073cb 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -1,4 +1,6 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Game; @@ -7,92 +9,160 @@ namespace Dalamud.Game.Inventory; /// /// Dalamud wrapper around a ClientStructs InventoryItem. /// -public unsafe class GameInventoryItem +[StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)] +public unsafe struct GameInventoryItem : IEquatable { - private InventoryItem internalItem; + /// + /// An empty instance of . + /// + internal static readonly GameInventoryItem Empty = default; /// - /// Initializes a new instance of the class. + /// 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; - } + 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; + public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.Container; /// /// Gets the inventory slot index this item is in. /// - public uint InventorySlot => (uint)this.internalItem.Slot; + public uint InventorySlot => (uint)this.InternalItem.Slot; /// /// Gets the item id. /// - public uint ItemId => this.internalItem.ItemID; + public uint ItemId => this.InternalItem.ItemID; /// /// Gets the quantity of items in this item stack. /// - public uint Quantity => this.internalItem.Quantity; + public uint Quantity => this.InternalItem.Quantity; /// /// Gets the spiritbond of this item. /// - public uint Spiritbond => this.internalItem.Spiritbond; + public uint Spiritbond => this.InternalItem.Spiritbond; /// /// Gets the repair condition of this item. /// - public uint Condition => this.internalItem.Condition; + public uint Condition => this.InternalItem.Condition; /// /// Gets a value indicating whether the item is High Quality. /// - public bool IsHq => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.HQ); + 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.HasFlag(InventoryItem.ItemFlags.CompanyCrestApplied); - + 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.HasFlag(InventoryItem.ItemFlags.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.HasFlag(InventoryItem.ItemFlags.Collectable); + public bool IsCollectable => (this.InternalItem.Flags & InventoryItem.ItemFlags.Collectable) != 0; /// /// Gets the array of materia types. /// - public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref this.internalItem.Materia[0]), 5); + public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.Materia[0])), 5); /// /// Gets the array of materia grades. /// - public ReadOnlySpan MateriaGrade => new(Unsafe.AsPointer(ref this.internalItem.MateriaGrade[0]), 5); + public ReadOnlySpan MateriaGrade => + new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5); /// /// Gets the color used for this item. /// - public byte Stain => this.internalItem.Stain; + public byte Stain => this.InternalItem.Stain; /// /// Gets the glamour id for this item. /// - public uint GlmaourId => this.internalItem.GlamourID; + public uint GlmaourId => 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; + internal ulong CrafterContentId => this.InternalItem.CrafterContentID; + + 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 + ? "" + : $"Item #{this.ItemId} at slot {this.InventorySlot} in {this.ContainerType}"; } diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs index 733af32d3..c982fa80f 100644 --- a/Dalamud/Game/Inventory/GameInventoryType.cs +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -3,7 +3,7 @@ /// /// Enum representing various player inventories. /// -public enum GameInventoryType : uint +public enum GameInventoryType : ushort { /// /// First panel of main player inventory. @@ -348,4 +348,9 @@ public enum GameInventoryType : uint /// Eighth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom8 = 27008, + + /// + /// An invalid value. + /// + Invalid = ushort.MaxValue, } diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 0e796e8d8..b2ffe64d0 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -8,62 +8,83 @@ namespace Dalamud.Plugin.Services; public interface IGameInventory { /// - /// Delegate function for when an item is moved from one inventory to the next. + /// Delegate function to be called when inventories have been changed. /// - /// Which inventory the item was moved from. - /// The slot this item was moved from. - /// Which inventory the item was moved to. - /// The slot this item was moved to. - /// The item moved. - public delegate void OnItemMovedDelegate(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item); - - /// - /// Delegate function for when an item is removed from an inventory. - /// - /// Which inventory the item was removed from. - /// The slot this item was removed from. - /// The item removed. - public delegate void OnItemRemovedDelegate(GameInventoryType source, uint sourceSlot, GameInventoryItem item); - - /// - /// Delegate function for when an item is added to an inventory. - /// - /// Which inventory the item was added to. - /// The slot this item was added to. - /// The item added. - public delegate void OnItemAddedDelegate(GameInventoryType destination, uint destinationSlot, GameInventoryItem item); - - /// - /// Delegate function for when an items properties are changed. - /// - /// Which inventory the item that was changed is in. - /// The slot the item that was changed is in. - /// The item changed. - public delegate void OnItemChangedDelegate(GameInventoryType inventory, uint slot, GameInventoryItem item); - - /// - /// Event that is fired when an item is moved from one inventory to another. - /// - public event OnItemMovedDelegate ItemMoved; + /// The events. + public delegate void InventoryChangeDelegate(ReadOnlySpan events); /// - /// Event that is fired when an item is removed from one inventory. + /// Event that is fired when the inventory has been changed. /// - /// - /// This event will also be fired when an item is moved from one inventory to another. - /// - public event OnItemRemovedDelegate ItemRemoved; + public event InventoryChangeDelegate InventoryChanged; /// - /// Event that is fired when an item is added to one inventory. + /// Argument for . /// - /// - /// This event will also be fired when an item is moved from one inventory to another. - /// - public event OnItemAddedDelegate ItemAdded; - - /// - /// Event that is fired when an items properties are changed. - /// - public event OnItemChangedDelegate ItemChanged; + public readonly struct GameInventoryEventArgs + { + /// + /// The type of the event. + /// + public readonly GameInventoryEvent Type; + + /// + /// The content of the item in the source inventory.
+ /// Relevant if is , , or . + ///
+ public readonly GameInventoryItem Source; + + /// + /// The content of the item in the target inventory
+ /// Relevant if is , , or . + ///
+ public readonly GameInventoryItem Target; + + /// + /// Initializes a new instance of the struct. + /// + /// The type of the event. + /// The source inventory item. + /// The target inventory item. + public GameInventoryEventArgs(GameInventoryEvent type, GameInventoryItem source, GameInventoryItem target) + { + this.Type = type; + this.Source = source; + this.Target = target; + } + + /// + /// Gets a value indicating whether this instance of contains no information. + /// + public bool IsEmpty => this.Type == GameInventoryEvent.Empty; + + // TODO: are the following two aliases useful? + + /// + /// Gets the type of the source inventory.
+ /// Relevant for and . + ///
+ public GameInventoryType SourceType => this.Source.ContainerType; + + /// + /// Gets the type of the target inventory.
+ /// Relevant for , , and + /// . + ///
+ public GameInventoryType TargetType => this.Target.ContainerType; + + /// + public override string ToString() => this.Type switch + { + GameInventoryEvent.Empty => + $"<{this.Type}>", + GameInventoryEvent.Added => + $"<{this.Type}> ({this.Target})", + GameInventoryEvent.Removed => + $"<{this.Type}> ({this.Source})", + GameInventoryEvent.Changed or GameInventoryEvent.Moved => + $"<{this.Type}> ({this.Source}) to ({this.Target})", + _ => $" {this.Source} => {this.Target}", + }; + } } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index cc6687524..090e0c244 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cc668752416a8459a3c23345c51277e359803de8 +Subproject commit 090e0c244df668454616026188c1363e5d25a1bc