From 92f4df625feda6b8049c0cdd6f4a32298550455b Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:15:34 -0800 Subject: [PATCH 01/21] [GameInventory] Service Prototype --- Dalamud/Game/Inventory/GameInventory.cs | 268 +++++++++++++ .../Game/Inventory/GameInventoryChangelog.cs | 28 ++ .../Inventory/GameInventoryChangelogState.cs | 17 + Dalamud/Game/Inventory/GameInventoryItem.cs | 98 +++++ Dalamud/Game/Inventory/GameInventoryType.cs | 351 ++++++++++++++++++ Dalamud/Plugin/Services/IGameInventory.cs | 69 ++++ 6 files changed, 831 insertions(+) create mode 100644 Dalamud/Game/Inventory/GameInventory.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryChangelog.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryChangelogState.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryItem.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryType.cs create mode 100644 Dalamud/Plugin/Services/IGameInventory.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs new file mode 100644 index 000000000..7cd2556e2 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -0,0 +1,268 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace Dalamud.Game.Inventory; + +/// +/// This class provides events for the players in-game inventory. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal class GameInventory : IDisposable, IServiceType, IGameInventory +{ + private static readonly ModuleLog Log = new("GameInventory"); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + private readonly Dictionary> inventoryCache; + + [ServiceManager.ServiceConstructor] + private GameInventory() + { + this.inventoryCache = new Dictionary>(); + + foreach (var inventoryType in Enum.GetValues()) + { + this.inventoryCache.Add(inventoryType, new Dictionary()); + } + + 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 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) + { + foreach (var item in this.GetItemsForInventory(inventoryType)) + { + if (cachedInventoryItems.TryGetValue(item.Slot, out var inventoryItem)) + { + // 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; + } + } + else + { + cachedInventoryItems.Add(item.Slot, item); + } + } + } + + // 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)) + { + var hasAdd = false; + var hasRemove = false; + + foreach (var log in itemGroup) + { + switch (log.State) + { + case GameInventoryChangelogState.Added: + hasAdd = true; + break; + + case GameInventoryChangelogState.Removed: + hasRemove = true; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + var itemMoved = hasAdd && hasRemove; + if (itemMoved) + { + 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}"); + } + } + + 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); +} + +/// +/// Plugin-scoped version of a GameInventory service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory +{ + [ServiceManager.ServiceDependency] + private readonly GameInventory gameInventoryService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + public GameInventoryPluginScoped() + { + this.gameInventoryService.ItemMoved += this.OnItemMovedForward; + this.gameInventoryService.ItemRemoved += this.OnItemRemovedForward; + this.gameInventoryService.ItemAdded += this.OnItemAddedForward; + this.gameInventoryService.ItemChanged += this.OnItemChangedForward; + } + + /// + public event IGameInventory.OnItemMovedDelegate? ItemMoved; + + /// + public event IGameInventory.OnItemRemovedDelegate? ItemRemoved; + + /// + public event IGameInventory.OnItemAddedDelegate? ItemAdded; + + /// + public event IGameInventory.OnItemChangedDelegate? ItemChanged; + + /// + 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; + } + + 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); +} diff --git a/Dalamud/Game/Inventory/GameInventoryChangelog.cs b/Dalamud/Game/Inventory/GameInventoryChangelog.cs new file mode 100644 index 000000000..52ada81e0 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryChangelog.cs @@ -0,0 +1,28 @@ +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 new file mode 100644 index 000000000..23e972419 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryChangelogState.cs @@ -0,0 +1,17 @@ +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/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs new file mode 100644 index 000000000..286104c43 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -0,0 +1,98 @@ +using System.Runtime.CompilerServices; + +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace Dalamud.Game.Inventory; + +/// +/// Dalamud wrapper around a ClientStructs InventoryItem. +/// +public unsafe class GameInventoryItem +{ + private InventoryItem internalItem; + + /// + /// Initializes a new instance of the class. + /// + /// Inventory item to wrap. + internal GameInventoryItem(InventoryItem item) + { + this.internalItem = item; + } + + /// + /// 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.HasFlag(InventoryItem.ItemFlags.HQ); + + /// + /// Gets a value indicating whether the item has a company crest applied. + /// + public bool IsCompanyCrestApplied => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.CompanyCrestApplied); + + /// + /// Gets a value indicating whether the item is a relic. + /// + public bool IsRelic => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.Relic); + + /// + /// Gets a value indicating whether the is a collectable. + /// + public bool IsCollectable => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.Collectable); + + /// + /// Gets the array of materia types. + /// + public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref this.internalItem.Materia[0]), 5); + + /// + /// Gets the array of materia grades. + /// + public ReadOnlySpan MateriaGrade => new(Unsafe.AsPointer(ref this.internalItem.MateriaGrade[0]), 5); + + /// + /// Gets the color used for this item. + /// + public byte Stain => this.internalItem.Stain; + + /// + /// Gets the glamour id for this item. + /// + 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; +} diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs new file mode 100644 index 000000000..733af32d3 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -0,0 +1,351 @@ +namespace Dalamud.Game.Inventory; + +/// +/// Enum representing various player inventories. +/// +public enum GameInventoryType : uint +{ + /// + /// First panel of main player inventory. + /// + Inventory1 = 0, + + /// + /// Second panel of main player inventory. + /// + Inventory2 = 1, + + /// + /// Third panel of main player inventory. + /// + Inventory3 = 2, + + /// + /// Fourth panel of main player inventory. + /// + Inventory4 = 3, + + /// + /// Items that are currently equipped by the player. + /// + EquippedItems = 1000, + + /// + /// Player currency container. + /// ie, gil, serpent seals, sacks of nuts. + /// + Currency = 2000, + + /// + /// Crystal container. + /// + Crystals = 2001, + + /// + /// Mail container. + /// + Mail = 2003, + + /// + /// Key item container. + /// + KeyItems = 2004, + + /// + /// Quest item hand-in inventory. + /// + HandIn = 2005, + + /// + /// DamagedGear container. + /// + DamagedGear = 2007, + + /// + /// Examine window container. + /// + Examine = 2009, + + /// + /// Doman Enclave Reconstruction Reclamation Box. + /// + ReconstructionBuyback = 2013, + + /// + /// Armory off-hand weapon container. + /// + ArmoryOffHand = 3200, + + /// + /// Armory head container. + /// + ArmoryHead = 3201, + + /// + /// Armory body container. + /// + ArmoryBody = 3202, + + /// + /// Armory hand/gloves container. + /// + ArmoryHands = 3203, + + /// + /// Armory waist container. + /// + /// This container should be unused as belt items were removed from the game in Shadowbringers. + /// + /// + ArmoryWaist = 3204, + + /// + /// Armory legs/pants/skirt container. + /// + ArmoryLegs = 3205, + + /// + /// Armory feet/boots/shoes container. + /// + ArmoryFeets = 3206, + + /// + /// Armory earring container. + /// + ArmoryEar = 3207, + + /// + /// Armory necklace container. + /// + ArmoryNeck = 3208, + + /// + /// Armory bracelet container. + /// + ArmoryWrist = 3209, + + /// + /// Armory ring container. + /// + ArmoryRings = 3300, + + /// + /// Armory soul crystal container. + /// + ArmorySoulCrystal = 3400, + + /// + /// Armory main-hand weapon container. + /// + ArmoryMainHand = 3500, + + /// + /// First panel of saddelbag inventory. + /// + SaddleBag1 = 4000, + + /// + /// Second panel of Saddlebag inventory. + /// + SaddleBag2 = 4001, + + /// + /// First panel of premium saddlebag inventory. + /// + PremiumSaddleBag1 = 4100, + + /// + /// Second panel of premium saddlebag inventory. + /// + PremiumSaddleBag2 = 4101, + + /// + /// First panel of retainer inventory. + /// + RetainerPage1 = 10000, + + /// + /// Second panel of retainer inventory. + /// + RetainerPage2 = 10001, + + /// + /// Third panel of retainer inventory. + /// + RetainerPage3 = 10002, + + /// + /// Fourth panel of retainer inventory. + /// + RetainerPage4 = 10003, + + /// + /// Fifth panel of retainer inventory. + /// + RetainerPage5 = 10004, + + /// + /// Sixth panel of retainer inventory. + /// + RetainerPage6 = 10005, + + /// + /// Seventh panel of retainer inventory. + /// + RetainerPage7 = 10006, + + /// + /// Retainer equipment container. + /// + RetainerEquippedItems = 11000, + + /// + /// Retainer currency container. + /// + RetainerGil = 12000, + + /// + /// Retainer crystal container. + /// + RetainerCrystals = 12001, + + /// + /// Retainer market item container. + /// + RetainerMarket = 12002, + + /// + /// First panel of Free Company inventory. + /// + FreeCompanyPage1 = 20000, + + /// + /// Second panel of Free Company inventory. + /// + FreeCompanyPage2 = 20001, + + /// + /// Third panel of Free Company inventory. + /// + FreeCompanyPage3 = 20002, + + /// + /// Fourth panel of Free Company inventory. + /// + FreeCompanyPage4 = 20003, + + /// + /// Fifth panel of Free Company inventory. + /// + FreeCompanyPage5 = 20004, + + /// + /// Free Company currency container. + /// + FreeCompanyGil = 22000, + + /// + /// Free Company crystal container. + /// + FreeCompanyCrystals = 22001, + + /// + /// Housing exterior appearance container. + /// + HousingExteriorAppearance = 25000, + + /// + /// Housing exterior placed items container. + /// + HousingExteriorPlacedItems = 25001, + + /// + /// Housing interior appearance container. + /// + HousingInteriorAppearance = 25002, + + /// + /// First panel of housing interior inventory. + /// + HousingInteriorPlacedItems1 = 25003, + + /// + /// Second panel of housing interior inventory. + /// + HousingInteriorPlacedItems2 = 25004, + + /// + /// Third panel of housing interior inventory. + /// + HousingInteriorPlacedItems3 = 25005, + + /// + /// Fourth panel of housing interior inventory. + /// + HousingInteriorPlacedItems4 = 25006, + + /// + /// Fifth panel of housing interior inventory. + /// + HousingInteriorPlacedItems5 = 25007, + + /// + /// Sixth panel of housing interior inventory. + /// + HousingInteriorPlacedItems6 = 25008, + + /// + /// Seventh panel of housing interior inventory. + /// + HousingInteriorPlacedItems7 = 25009, + + /// + /// Eighth panel of housing interior inventory. + /// + HousingInteriorPlacedItems8 = 25010, + + /// + /// Housing exterior storeroom inventory. + /// + HousingExteriorStoreroom = 27000, + + /// + /// First panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom1 = 27001, + + /// + /// Second panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom2 = 27002, + + /// + /// Third panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom3 = 27003, + + /// + /// Fourth panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom4 = 27004, + + /// + /// Fifth panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom5 = 27005, + + /// + /// Sixth panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom6 = 27006, + + /// + /// Seventh panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom7 = 27007, + + /// + /// Eighth panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom8 = 27008, +} diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs new file mode 100644 index 000000000..0e796e8d8 --- /dev/null +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -0,0 +1,69 @@ +using Dalamud.Game.Inventory; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides events for the in-game inventory. +/// +public interface IGameInventory +{ + /// + /// Delegate function for when an item is moved from one inventory to the next. + /// + /// 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; + + /// + /// Event that is fired when an item is removed from one inventory. + /// + /// + /// This event will also be fired when an item is moved from one inventory to another. + /// + public event OnItemRemovedDelegate ItemRemoved; + + /// + /// Event that is fired when an item is added to one inventory. + /// + /// + /// 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; +} From 805615d9f4dcd6655efcc7bbbd848c0545b7f23e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:40:36 -0800 Subject: [PATCH 02/21] Fix incorrect equality operator --- Dalamud/Game/Inventory/GameInventory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 7cd2556e2..c9285b246 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -197,10 +197,10 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } private unsafe bool IsMateriaChanged(InventoryItem a, InventoryItem b) - => new ReadOnlySpan(a.Materia, 5) == new ReadOnlySpan(b.Materia, 5); + => 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); + => new ReadOnlySpan(a.MateriaGrade, 5) != new ReadOnlySpan(b.MateriaGrade, 5); } /// From 5204bb723d824072bf415759b9e4f23c84d8c9d0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 16:47:54 +0900 Subject: [PATCH 03/21] 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 From 000d16c553801e06c320e2099eb708f1e4625550 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 13:15:19 +0900 Subject: [PATCH 04/21] Assume the size of inventory does not change once it's set --- Dalamud/Game/Inventory/GameInventory.cs | 140 +++++++----------------- 1 file changed, 37 insertions(+), 103 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index cfb22ca0d..cac7d5266 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.IoC; @@ -26,22 +25,13 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory private readonly Framework framework = Service.Get(); private readonly GameInventoryType[] inventoryTypes; - private readonly GameInventoryItem[][] inventoryItems; - private readonly unsafe GameInventoryItem*[] inventoryItemsPointers; + private readonly GameInventoryItem[]?[] inventoryItems; [ServiceManager.ServiceConstructor] - private unsafe GameInventory() + private GameInventory() { this.inventoryTypes = 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.inventoryItems[i] = GC.AllocateArray(1, true); - this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref this.inventoryItems[i][0]); - } this.framework.Update += this.OnFrameworkUpdate; } @@ -70,69 +60,25 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory 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) - { - var i = 0; - for (; i < span.Length; i++) - { - 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) + private 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;) + for (var i = 0; i < this.inventoryTypes.Length; i++) { - var oldItemsArray = this.inventoryItems[i]; - var oldItemsLength = oldItemsArray.Length; - var oldItemsPointer = this.inventoryItemsPointers[i]; + var newItems = GetItemsForInventory(this.inventoryTypes[i]); + if (newItems.IsEmpty) + continue; - var resizeRequired = 0; - foreach (ref var newItem in GetItemsForInventory(this.inventoryTypes[i])) + // 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]; + + foreach (ref var newItem in newItems) { - var slot = newItem.InternalItem.Slot; - if (slot >= oldItemsLength) - { - resizeRequired = Math.Max(resizeRequired, slot + 1); - continue; - } - - // We already checked the range above. Go raw. - ref var oldItem = ref oldItemsPointer[slot]; + ref var oldItem = ref oldItems[newItem.InternalItem.Slot]; if (oldItem.IsEmpty) { @@ -153,21 +99,6 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory 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++; - } } // Was there any change? If not, stop further processing. @@ -179,65 +110,68 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // From this point, the size of changelog shall not change. var span = CollectionsMarshal.AsSpan(this.changelog); + // Ensure that changelog is in order of Added, Removed, and then Changed. 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); + + var removedFrom = 0; + while (removedFrom < span.Length && span[removedFrom].Type != GameInventoryEvent.Removed) + removedFrom++; + + var changedFrom = removedFrom; + while (changedFrom < span.Length && span[changedFrom].Type != GameInventoryEvent.Changed) + changedFrom++; + + var addedSpan = span[..removedFrom]; + var removedSpan = span[removedFrom..changedFrom]; + var changedSpan = span[changedFrom..]; // Resolve changelog for item moved, from 1 added + 1 removed - for (var iAdded = addedFrom; iAdded < removedFrom; iAdded++) + foreach (ref var added in addedSpan) { - ref var added = ref span[iAdded]; - for (var iRemoved = removedFrom; iRemoved < changedFrom; iRemoved++) + foreach (ref var removed in removedSpan) { - 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"); + Log.Verbose($"Move: reinterpreting {removed} + {added}"); + added = new(GameInventoryEvent.Moved, removed.Source, added.Target); + removed = default; break; } } } // Resolve changelog for item moved, from 2 changeds - for (var i = changedFrom; i < this.changelog.Count; i++) + for (var i = 0; i < changedSpan.Length; i++) { if (span[i].IsEmpty) continue; - ref var e1 = ref span[i]; - for (var j = i + 1; j < this.changelog.Count; j++) + ref var e1 = ref changedSpan[i]; + for (var j = i + 1; j < changedSpan.Length; j++) { - ref var e2 = ref span[j]; + ref var e2 = ref changedSpan[j]; if (e1.Target.ItemId == e2.Source.ItemId && e1.Source.ItemId == e2.Target.ItemId) { if (e1.Target.IsEmpty) { // e1 got moved to e2 + Log.Verbose($"Move: reinterpreting {e1} + {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 + Log.Verbose($"Move: reinterpreting {e2} + {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 + Log.Verbose($"Move(Swap): reinterpreting {e1} + {e2}"); (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}"); } } } From 40575e1a8897a650f275280ce171053e81d00747 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 30 Nov 2023 21:28:37 -0800 Subject: [PATCH 05/21] Use ReadOnlySpan --- Dalamud/Game/Inventory/GameInventory.cs | 26 +++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index cac7d5266..d370574d7 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -50,7 +50,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory /// /// The inventory type. /// The span. - private static unsafe Span GetItemsForInventory(GameInventoryType type) + private static unsafe ReadOnlySpan GetItemsForInventory(GameInventoryType type) { var inventoryManager = InventoryManager.Instance(); if (inventoryManager is null) return default; @@ -58,15 +58,11 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); if (inventory is null) return default; - return new(inventory->Items, (int)inventory->Size); + return new ReadOnlySpan(inventory->Items, (int)inventory->Size); } private 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; i++) { var newItems = GetItemsForInventory(this.inventoryTypes[i]); @@ -76,7 +72,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // 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]; - foreach (ref var newItem in newItems) + foreach (ref readonly var newItem in newItems) { ref var oldItem = ref oldItems[newItem.InternalItem.Slot]; @@ -84,14 +80,14 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { if (newItem.IsEmpty) continue; - this.changelog.Add(new(GameInventoryEvent.Added, default, newItem)); + this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Added, default, newItem)); } else { if (newItem.IsEmpty) - this.changelog.Add(new(GameInventoryEvent.Removed, oldItem, default)); + this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Removed, oldItem, default)); else if (!oldItem.Equals(newItem)) - this.changelog.Add(new(GameInventoryEvent.Changed, oldItem, newItem)); + this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Changed, oldItem, newItem)); else continue; } @@ -133,7 +129,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory if (added.Target.ItemId == removed.Source.ItemId) { Log.Verbose($"Move: reinterpreting {removed} + {added}"); - added = new(GameInventoryEvent.Moved, removed.Source, added.Target); + added = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, removed.Source, added.Target); removed = default; break; } @@ -156,22 +152,22 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { // e1 got moved to e2 Log.Verbose($"Move: reinterpreting {e1} + {e2}"); - e1 = new(GameInventoryEvent.Moved, e1.Source, e2.Target); + e1 = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e1.Source, e2.Target); e2 = default; } else if (e2.Target.IsEmpty) { // e2 got moved to e1 Log.Verbose($"Move: reinterpreting {e2} + {e1}"); - e1 = new(GameInventoryEvent.Moved, e2.Source, e1.Target); + e1 = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e2.Source, e1.Target); e2 = default; } else { // e1 and e2 got swapped Log.Verbose($"Move(Swap): reinterpreting {e1} + {e2}"); - (e1, e2) = (new(GameInventoryEvent.Moved, e1.Target, e2.Target), - new(GameInventoryEvent.Moved, e2.Target, e1.Target)); + (e1, e2) = (new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e1.Target, e2.Target), + new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e2.Target, e1.Target)); } } } From 7c6f98dc9fe6e7fbe5b97e82dbd6c46becff2630 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:18:33 -0800 Subject: [PATCH 06/21] Proposed API Surface --- Dalamud/Game/Inventory/GameInventory.cs | 178 +++++++++++++++--- Dalamud/Game/Inventory/GameInventoryEvent.cs | 2 +- Dalamud/Game/Inventory/GameInventoryItem.cs | 2 +- Dalamud/Game/Inventory/GameInventoryType.cs | 2 +- .../InventoryEventArgs.cs | 29 +++ .../InventoryItemAddedArgs.cs | 20 ++ .../InventoryItemChangedArgs.cs | 26 +++ .../InventoryItemMovedArgs.cs | 30 +++ .../InventoryItemRemovedArgs.cs | 20 ++ Dalamud/Plugin/Services/IGameInventory.cs | 95 +++------- 10 files changed, 311 insertions(+), 93 deletions(-) create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index d370574d7..c2603f1bf 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -8,7 +8,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; -namespace Dalamud.Game.Inventory; +namespace Dalamud.Game.GameInventory; /// /// This class provides events for the players in-game inventory. @@ -19,7 +19,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { private static readonly ModuleLog Log = new("GameInventory"); - private readonly List changelog = new(); + private readonly List changelog = new(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -37,7 +37,19 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } /// - public event IGameInventory.InventoryChangeDelegate? InventoryChanged; + public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemAdded; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemRemoved; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMoved; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemChanged; /// public void Dispose() @@ -80,16 +92,39 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { if (newItem.IsEmpty) continue; - this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Added, default, newItem)); + + this.changelog.Add(new InventoryItemAddedArgs + { + Item = newItem, + Inventory = newItem.ContainerType, + Slot = newItem.InventorySlot, + }); } else { if (newItem.IsEmpty) - this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Removed, oldItem, default)); + { + this.changelog.Add(new InventoryItemRemovedArgs + { + Item = oldItem, + Inventory = oldItem.ContainerType, + Slot = oldItem.InventorySlot, + }); + } else if (!oldItem.Equals(newItem)) - this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Changed, oldItem, newItem)); + { + this.changelog.Add(new InventoryItemChangedArgs + { + OldItemState = oldItem, + Item = newItem, + Inventory = newItem.ContainerType, + Slot = newItem.InventorySlot, + }); + } else + { continue; + } } Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}"); @@ -126,48 +161,86 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { foreach (ref var removed in removedSpan) { - if (added.Target.ItemId == removed.Source.ItemId) + if (added.Item.ItemId == removed.Item.ItemId) { Log.Verbose($"Move: reinterpreting {removed} + {added}"); - added = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, removed.Source, added.Target); + added = new InventoryItemMovedArgs + { + Item = removed.Item, + SourceInventory = removed.Item.ContainerType, + SourceSlot = removed.Item.InventorySlot, + TargetInventory = added.Item.ContainerType, + TargetSlot = added.Item.InventorySlot, + }; removed = default; break; } } } - // Resolve changelog for item moved, from 2 changeds + // Resolve changelog for item moved, from 2 changes for (var i = 0; i < changedSpan.Length; i++) { - if (span[i].IsEmpty) + if (span[i].Type is GameInventoryEvent.Empty) continue; ref var e1 = ref changedSpan[i]; for (var j = i + 1; j < changedSpan.Length; j++) { ref var e2 = ref changedSpan[j]; - if (e1.Target.ItemId == e2.Source.ItemId && e1.Source.ItemId == e2.Target.ItemId) + if (e1.Item.ItemId == e2.Item.ItemId && e1.Item.ItemId == e2.Item.ItemId) { - if (e1.Target.IsEmpty) + if (e1.Item.IsEmpty) { // e1 got moved to e2 Log.Verbose($"Move: reinterpreting {e1} + {e2}"); - e1 = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e1.Source, e2.Target); + e1 = new InventoryItemMovedArgs + { + Item = e2.Item, + SourceInventory = e1.Item.ContainerType, + SourceSlot = e1.Item.InventorySlot, + TargetInventory = e2.Item.ContainerType, + TargetSlot = e2.Item.InventorySlot, + }; e2 = default; } - else if (e2.Target.IsEmpty) + else if (e2.Item.IsEmpty) { // e2 got moved to e1 Log.Verbose($"Move: reinterpreting {e2} + {e1}"); - e1 = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e2.Source, e1.Target); + e1 = new InventoryItemMovedArgs + { + Item = e1.Item, + SourceInventory = e2.Item.ContainerType, + SourceSlot = e2.Item.InventorySlot, + TargetInventory = e1.Item.ContainerType, + TargetSlot = e1.Item.InventorySlot, + }; e2 = default; } else { // e1 and e2 got swapped Log.Verbose($"Move(Swap): reinterpreting {e1} + {e2}"); - (e1, e2) = (new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e1.Target, e2.Target), - new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e2.Target, e1.Target)); + var newEvent1 = new InventoryItemMovedArgs + { + Item = e2.Item, + SourceInventory = e1.Item.ContainerType, + SourceSlot = e1.Item.InventorySlot, + TargetInventory = e2.Item.ContainerType, + TargetSlot = e2.Item.InventorySlot, + }; + + var newEvent2 = new InventoryItemMovedArgs + { + Item = e1.Item, + SourceInventory = e2.Item.ContainerType, + SourceSlot = e2.Item.InventorySlot, + TargetInventory = e1.Item.ContainerType, + TargetSlot = e1.Item.InventorySlot, + }; + + (e1, e2) = (newEvent1, newEvent2); } } } @@ -177,7 +250,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // We do not care about the order of items in the changelog anymore. for (var i = 0; i < span.Length;) { - if (span[i].IsEmpty) + if (span[i] is null || span[i].Type is GameInventoryEvent.Empty) { span[i] = span[^1]; span = span[..^1]; @@ -190,7 +263,31 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // Actually broadcast the changes to subscribers. if (!span.IsEmpty) + { this.InventoryChanged?.Invoke(span); + + foreach (var change in span) + { + switch (change) + { + case InventoryItemAddedArgs: + this.ItemAdded?.Invoke(GameInventoryEvent.Added, change); + break; + + case InventoryItemRemovedArgs: + this.ItemRemoved?.Invoke(GameInventoryEvent.Removed, change); + break; + + case InventoryItemMovedArgs: + this.ItemMoved?.Invoke(GameInventoryEvent.Moved, change); + break; + + case InventoryItemChangedArgs: + this.ItemChanged?.Invoke(GameInventoryEvent.Changed, change); + break; + } + } + } } finally { @@ -219,18 +316,55 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public GameInventoryPluginScoped() { this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward; + this.gameInventoryService.ItemAdded += this.OnInventoryItemAddedForward; + this.gameInventoryService.ItemRemoved += this.OnInventoryItemRemovedForward; + this.gameInventoryService.ItemMoved += this.OnInventoryItemMovedForward; + this.gameInventoryService.ItemChanged += this.OnInventoryItemChangedForward; } - + /// - public event IGameInventory.InventoryChangeDelegate? InventoryChanged; - + public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemAdded; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemRemoved; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMoved; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemChanged; + /// public void Dispose() { this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward; + this.gameInventoryService.ItemAdded -= this.OnInventoryItemAddedForward; + this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; + this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; + this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; + this.InventoryChanged = null; + this.ItemAdded = null; + this.ItemRemoved = null; + this.ItemMoved = null; + this.ItemChanged = null; } - private void OnInventoryChangedForward(ReadOnlySpan events) + private void OnInventoryChangedForward(ReadOnlySpan events) => this.InventoryChanged?.Invoke(events); + + private void OnInventoryItemAddedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemAdded?.Invoke(type, data); + + private void OnInventoryItemRemovedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemRemoved?.Invoke(type, data); + + private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemMoved?.Invoke(type, data); + + private void OnInventoryItemChangedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemChanged?.Invoke(type, data); } diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs index c23d79f30..805306671 100644 --- a/Dalamud/Game/Inventory/GameInventoryEvent.cs +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory; +namespace Dalamud.Game.GameInventory; /// /// Class representing a item's changelog state. diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 9073073cb..794785e5c 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Game; -namespace Dalamud.Game.Inventory; +namespace Dalamud.Game.GameInventory; /// /// Dalamud wrapper around a ClientStructs InventoryItem. diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs index c982fa80f..0eeeebe20 100644 --- a/Dalamud/Game/Inventory/GameInventoryType.cs +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory; +namespace Dalamud.Game.GameInventory; /// /// Enum representing various player inventories. diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs new file mode 100644 index 000000000..a427dc840 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs @@ -0,0 +1,29 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Abstract base class representing inventory changed events. +/// +public abstract class InventoryEventArgs +{ + /// + /// Gets the type of event for these args. + /// + public abstract GameInventoryEvent Type { get; } + + /// + /// Gets the item associated with this event. + /// This is a copy of the item data. + /// + required public GameInventoryItem Item { get; init; } + + /// + public override string ToString() => this.Type switch + { + GameInventoryEvent.Empty => $"<{this.Type}>", + GameInventoryEvent.Added => $"<{this.Type}> ({this.Item})", + GameInventoryEvent.Removed => $"<{this.Type}> ({this.Item})", + GameInventoryEvent.Changed => $"<{this.Type}> ({this.Item})", + GameInventoryEvent.Moved when this is InventoryItemMovedArgs args => $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {args.SourceSlot} in {args.SourceInventory}) to (slot {args.TargetSlot} in {args.TargetInventory})", + _ => $" {this.Item}", + }; +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs new file mode 100644 index 000000000..8d3e99823 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Represents the data associated with an item being added to an inventory. +/// +public class InventoryItemAddedArgs : InventoryEventArgs +{ + /// + public override GameInventoryEvent Type => GameInventoryEvent.Added; + + /// + /// Gets the inventory this item was added to. + /// + required public GameInventoryType Inventory { get; init; } + + /// + /// Gets the slot this item was added to. + /// + required public uint Slot { get; init; } +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs new file mode 100644 index 000000000..1e2632722 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs @@ -0,0 +1,26 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Represents the data associated with an items properties being changed. +/// This also includes an items stack count changing. +/// +public class InventoryItemChangedArgs : InventoryEventArgs +{ + /// + public override GameInventoryEvent Type => GameInventoryEvent.Changed; + + /// + /// Gets the inventory this item is in. + /// + required public GameInventoryType Inventory { get; init; } + + /// + /// Gets the inventory slot this item is in. + /// + required public uint Slot { get; init; } + + /// + /// Gets the state of the item from before it was changed. + /// + required public GameInventoryItem OldItemState { get; init; } +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs new file mode 100644 index 000000000..655f43445 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -0,0 +1,30 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Represents the data associated with an item being moved from one inventory and added to another. +/// +public class InventoryItemMovedArgs : InventoryEventArgs +{ + /// + public override GameInventoryEvent Type => GameInventoryEvent.Moved; + + /// + /// Gets the inventory this item was moved from. + /// + required public GameInventoryType SourceInventory { get; init; } + + /// + /// Gets the inventory this item was moved to. + /// + required public GameInventoryType TargetInventory { get; init; } + + /// + /// Gets the slot this item was moved from. + /// + required public uint SourceSlot { get; init; } + + /// + /// Gets the slot this item was moved to. + /// + required public uint TargetSlot { get; init; } +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs new file mode 100644 index 000000000..2d4db2384 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Represents the data associated with an item being removed from an inventory. +/// +public class InventoryItemRemovedArgs : InventoryEventArgs +{ + /// + public override GameInventoryEvent Type => GameInventoryEvent.Removed; + + /// + /// Gets the inventory this item was removed from. + /// + required public GameInventoryType Inventory { get; init; } + + /// + /// Gets the slot this item was removed from. + /// + required public uint Slot { get; init; } +} diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index b2ffe64d0..40b4bd84f 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.Inventory; +using Dalamud.Game.GameInventory; namespace Dalamud.Plugin.Services; @@ -9,82 +9,41 @@ public interface IGameInventory { /// /// Delegate function to be called when inventories have been changed. + /// This delegate sends the entire set of changes recorded. /// /// The events. - public delegate void InventoryChangeDelegate(ReadOnlySpan events); + public delegate void InventoryChangelogDelegate(ReadOnlySpan events); + + /// + /// Delegate function to be called for each change to inventories. + /// This delegate sends individual events for changes. + /// + /// The event try that triggered this message. + /// Data for the triggered event. + public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data); /// /// Event that is fired when the inventory has been changed. /// - public event InventoryChangeDelegate InventoryChanged; - + public event InventoryChangelogDelegate InventoryChanged; + /// - /// Argument for . + /// Event that is fired when an item is added to an inventory. /// - public readonly struct GameInventoryEventArgs - { - /// - /// The type of the event. - /// - public readonly GameInventoryEvent Type; + public event InventoryChangedDelegate ItemAdded; - /// - /// 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; + /// + /// Event that is fired when an item is removed from an inventory. + /// + public event InventoryChangedDelegate ItemRemoved; - /// - /// 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; - } + /// + /// Event that is fired when an item is moved from one inventory into another. + /// + public event InventoryChangedDelegate ItemMoved; - /// - /// 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}", - }; - } + /// + /// Event that is fired when an items properties are changed. + /// + public event InventoryChangedDelegate ItemChanged; } From 34e3adb3f25028bac795c83e71d641d884dfd20d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 18:10:09 +0900 Subject: [PATCH 07/21] wip; needs testing and more thinking --- Dalamud/Game/Inventory/GameInventory.cs | 338 +++++++++--------- Dalamud/Game/Inventory/GameInventoryEvent.cs | 2 +- Dalamud/Game/Inventory/GameInventoryItem.cs | 2 +- Dalamud/Game/Inventory/GameInventoryType.cs | 2 +- .../InventoryEventArgs.cs | 30 +- .../InventoryItemAddedArgs.cs | 20 +- .../InventoryItemChangedArgs.cs | 26 +- .../InventoryItemMovedArgs.cs | 46 ++- .../InventoryItemRemovedArgs.cs | 18 +- Dalamud/Plugin/Services/IGameInventory.cs | 34 +- 10 files changed, 286 insertions(+), 232 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index c2603f1bf..4ee66ffaf 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; -using System.Runtime.InteropServices; +using Dalamud.Configuration.Internal; +using Dalamud.Game.Inventory.InventoryChangeArgsTypes; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -8,7 +9,9 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; -namespace Dalamud.Game.GameInventory; +using Serilog.Events; + +namespace Dalamud.Game.Inventory; /// /// This class provides events for the players in-game inventory. @@ -19,10 +22,17 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { private static readonly ModuleLog Log = new("GameInventory"); - private readonly List changelog = new(); + private readonly List allEvents = new(); + private readonly List addedEvents = new(); + private readonly List removedEvents = new(); + private readonly List changedEvents = new(); + private readonly List movedEvents = new(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; @@ -39,6 +49,9 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory /// public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; + /// + public event IGameInventory.InventoryChangelogDelegate? InventoryChangedRaw; + /// public event IGameInventory.InventoryChangedDelegate? ItemAdded; @@ -72,6 +85,32 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory return new ReadOnlySpan(inventory->Items, (int)inventory->Size); } + + private static void InvokeSafely( + IGameInventory.InventoryChangelogDelegate? cb, + IReadOnlyCollection data) + { + try + { + cb?.Invoke(data); + } + catch (Exception e) + { + Log.Error(e, "Exception during batch callback"); + } + } + + private static void InvokeSafely(IGameInventory.InventoryChangedDelegate? cb, InventoryEventArgs arg) + { + try + { + cb?.Invoke(arg.Type, arg); + } + catch (Exception e) + { + Log.Error(e, "Exception during {argType} callback", arg.Type); + } + } private void OnFrameworkUpdate(IFramework framework1) { @@ -90,208 +129,146 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory if (oldItem.IsEmpty) { - if (newItem.IsEmpty) - continue; - - this.changelog.Add(new InventoryItemAddedArgs + if (!newItem.IsEmpty) { - Item = newItem, - Inventory = newItem.ContainerType, - Slot = newItem.InventorySlot, - }); + this.addedEvents.Add(new(newItem)); + oldItem = newItem; + } } else { if (newItem.IsEmpty) { - this.changelog.Add(new InventoryItemRemovedArgs - { - Item = oldItem, - Inventory = oldItem.ContainerType, - Slot = oldItem.InventorySlot, - }); + this.removedEvents.Add(new(oldItem)); + oldItem = newItem; } else if (!oldItem.Equals(newItem)) { - this.changelog.Add(new InventoryItemChangedArgs - { - OldItemState = oldItem, - Item = newItem, - Inventory = newItem.ContainerType, - Slot = newItem.InventorySlot, - }); - } - else - { - continue; + this.changedEvents.Add(new(oldItem, newItem)); + oldItem = newItem; } } - - Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}"); - oldItem = newItem; } } // Was there any change? If not, stop further processing. - if (this.changelog.Count == 0) + // Note that... + // * this.movedEvents is not checked; it will be populated after this check. + // * this.allEvents is not checked; it is a temporary list to be used after this check. + if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0) return; try { - // From this point, the size of changelog shall not change. - var span = CollectionsMarshal.AsSpan(this.changelog); + // Broadcast InventoryChangedRaw, if necessary. + if (this.InventoryChangedRaw is not null) + { + this.allEvents.Clear(); + this.allEvents.EnsureCapacity( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count); + this.allEvents.AddRange(this.addedEvents); + this.allEvents.AddRange(this.removedEvents); + this.allEvents.AddRange(this.changedEvents); + InvokeSafely(this.InventoryChangedRaw, this.allEvents); + } - // Ensure that changelog is in order of Added, Removed, and then Changed. - span.Sort((a, b) => a.Type.CompareTo(b.Type)); + // Resolve changelog for item moved, from 1 added + 1 removed event. + for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) + { + var added = this.addedEvents[iAdded]; + for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved) + { + var removed = this.removedEvents[iRemoved]; + if (added.Item.ItemId != removed.Item.ItemId) + continue; + + this.movedEvents.Add(new(removed, added)); + + // Remove the reinterpreted entries. + this.addedEvents.RemoveAt(iAdded); + this.removedEvents.RemoveAt(iRemoved); + break; + } + } + + // Resolve changelog for item moved, from 2 changed events. + for (var i = this.changedEvents.Count - 1; i >= 0; --i) + { + var e1 = this.changedEvents[i]; + for (var j = i - 1; j >= 0; --j) + { + var e2 = this.changedEvents[j]; + if (e1.Item.ItemId != e2.Item.ItemId || e1.Item.ItemId != e2.Item.ItemId) + continue; + + // move happened, and e2 has an item + if (!e2.Item.IsEmpty) + this.movedEvents.Add(new(e1, e2)); + + // move happened, and e1 has an item + if (!e1.Item.IsEmpty) + this.movedEvents.Add(new(e2, e1)); + + // Remove the reinterpreted entries. Note that i > j. + this.changedEvents.RemoveAt(i); + this.changedEvents.RemoveAt(j); + break; + } + } + + // Log only if it matters. + if (this.dalamudConfiguration.LogLevel >= LogEventLevel.Verbose) + { + foreach (var x in this.addedEvents) + Log.Verbose($"{x}"); - var removedFrom = 0; - while (removedFrom < span.Length && span[removedFrom].Type != GameInventoryEvent.Removed) - removedFrom++; + foreach (var x in this.removedEvents) + Log.Verbose($"{x}"); - var changedFrom = removedFrom; - while (changedFrom < span.Length && span[changedFrom].Type != GameInventoryEvent.Changed) - changedFrom++; - - var addedSpan = span[..removedFrom]; - var removedSpan = span[removedFrom..changedFrom]; - var changedSpan = span[changedFrom..]; - - // Resolve changelog for item moved, from 1 added + 1 removed - foreach (ref var added in addedSpan) - { - foreach (ref var removed in removedSpan) - { - if (added.Item.ItemId == removed.Item.ItemId) - { - Log.Verbose($"Move: reinterpreting {removed} + {added}"); - added = new InventoryItemMovedArgs - { - Item = removed.Item, - SourceInventory = removed.Item.ContainerType, - SourceSlot = removed.Item.InventorySlot, - TargetInventory = added.Item.ContainerType, - TargetSlot = added.Item.InventorySlot, - }; - removed = default; - break; - } - } + foreach (var x in this.changedEvents) + Log.Verbose($"{x}"); + + foreach (var x in this.movedEvents) + Log.Verbose($"{x} (({x.SourceEvent}) + ({x.TargetEvent}))"); } - // Resolve changelog for item moved, from 2 changes - for (var i = 0; i < changedSpan.Length; i++) + // Broadcast InventoryChanged, if necessary. + if (this.InventoryChanged is not null) { - if (span[i].Type is GameInventoryEvent.Empty) - continue; - - ref var e1 = ref changedSpan[i]; - for (var j = i + 1; j < changedSpan.Length; j++) - { - ref var e2 = ref changedSpan[j]; - if (e1.Item.ItemId == e2.Item.ItemId && e1.Item.ItemId == e2.Item.ItemId) - { - if (e1.Item.IsEmpty) - { - // e1 got moved to e2 - Log.Verbose($"Move: reinterpreting {e1} + {e2}"); - e1 = new InventoryItemMovedArgs - { - Item = e2.Item, - SourceInventory = e1.Item.ContainerType, - SourceSlot = e1.Item.InventorySlot, - TargetInventory = e2.Item.ContainerType, - TargetSlot = e2.Item.InventorySlot, - }; - e2 = default; - } - else if (e2.Item.IsEmpty) - { - // e2 got moved to e1 - Log.Verbose($"Move: reinterpreting {e2} + {e1}"); - e1 = new InventoryItemMovedArgs - { - Item = e1.Item, - SourceInventory = e2.Item.ContainerType, - SourceSlot = e2.Item.InventorySlot, - TargetInventory = e1.Item.ContainerType, - TargetSlot = e1.Item.InventorySlot, - }; - e2 = default; - } - else - { - // e1 and e2 got swapped - Log.Verbose($"Move(Swap): reinterpreting {e1} + {e2}"); - var newEvent1 = new InventoryItemMovedArgs - { - Item = e2.Item, - SourceInventory = e1.Item.ContainerType, - SourceSlot = e1.Item.InventorySlot, - TargetInventory = e2.Item.ContainerType, - TargetSlot = e2.Item.InventorySlot, - }; - - var newEvent2 = new InventoryItemMovedArgs - { - Item = e1.Item, - SourceInventory = e2.Item.ContainerType, - SourceSlot = e2.Item.InventorySlot, - TargetInventory = e1.Item.ContainerType, - TargetSlot = e1.Item.InventorySlot, - }; - - (e1, e2) = (newEvent1, newEvent2); - } - } - } + this.allEvents.Clear(); + this.allEvents.EnsureCapacity( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count + + this.movedEvents.Count); + this.allEvents.AddRange(this.addedEvents); + this.allEvents.AddRange(this.removedEvents); + this.allEvents.AddRange(this.changedEvents); + this.allEvents.AddRange(this.movedEvents); + InvokeSafely(this.InventoryChanged, this.allEvents); } - // 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] is null || span[i].Type is GameInventoryEvent.Empty) - { - span[i] = span[^1]; - span = span[..^1]; - } - else - { - i++; - } - } - - // Actually broadcast the changes to subscribers. - if (!span.IsEmpty) - { - this.InventoryChanged?.Invoke(span); - - foreach (var change in span) - { - switch (change) - { - case InventoryItemAddedArgs: - this.ItemAdded?.Invoke(GameInventoryEvent.Added, change); - break; - - case InventoryItemRemovedArgs: - this.ItemRemoved?.Invoke(GameInventoryEvent.Removed, change); - break; - - case InventoryItemMovedArgs: - this.ItemMoved?.Invoke(GameInventoryEvent.Moved, change); - break; - - case InventoryItemChangedArgs: - this.ItemChanged?.Invoke(GameInventoryEvent.Changed, change); - break; - } - } - } + // Broadcast the rest. + foreach (var x in this.addedEvents) + InvokeSafely(this.ItemAdded, x); + + foreach (var x in this.removedEvents) + InvokeSafely(this.ItemRemoved, x); + + foreach (var x in this.changedEvents) + InvokeSafely(this.ItemChanged, x); + + foreach (var x in this.movedEvents) + InvokeSafely(this.ItemMoved, x); } finally { - this.changelog.Clear(); + this.addedEvents.Clear(); + this.removedEvents.Clear(); + this.changedEvents.Clear(); + this.movedEvents.Clear(); } } } @@ -316,6 +293,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public GameInventoryPluginScoped() { this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward; + this.gameInventoryService.InventoryChangedRaw += this.OnInventoryChangedRawForward; this.gameInventoryService.ItemAdded += this.OnInventoryItemAddedForward; this.gameInventoryService.ItemRemoved += this.OnInventoryItemRemovedForward; this.gameInventoryService.ItemMoved += this.OnInventoryItemMovedForward; @@ -325,6 +303,9 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; + /// + public event IGameInventory.InventoryChangelogDelegate? InventoryChangedRaw; + /// public event IGameInventory.InventoryChangedDelegate? ItemAdded; @@ -341,20 +322,25 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public void Dispose() { this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward; + this.gameInventoryService.InventoryChangedRaw -= this.OnInventoryChangedRawForward; this.gameInventoryService.ItemAdded -= this.OnInventoryItemAddedForward; this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; this.InventoryChanged = null; + this.InventoryChangedRaw = null; this.ItemAdded = null; this.ItemRemoved = null; this.ItemMoved = null; this.ItemChanged = null; } - private void OnInventoryChangedForward(ReadOnlySpan events) + private void OnInventoryChangedForward(IReadOnlyCollection events) => this.InventoryChanged?.Invoke(events); + + private void OnInventoryChangedRawForward(IReadOnlyCollection events) + => this.InventoryChangedRaw?.Invoke(events); private void OnInventoryItemAddedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemAdded?.Invoke(type, data); diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs index 805306671..c23d79f30 100644 --- a/Dalamud/Game/Inventory/GameInventoryEvent.cs +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory; /// /// Class representing a item's changelog state. diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 794785e5c..9073073cb 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Game; -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory; /// /// Dalamud wrapper around a ClientStructs InventoryItem. diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs index 0eeeebe20..c982fa80f 100644 --- a/Dalamud/Game/Inventory/GameInventoryType.cs +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory; /// /// Enum representing various player inventories. diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs index a427dc840..070d8a8db 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs @@ -1,29 +1,35 @@ -namespace Dalamud.Game.GameInventory; +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Abstract base class representing inventory changed events. /// +[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "It literally says , , and then . required is not an access modifier.")] public abstract class InventoryEventArgs { + /// + /// Initializes a new instance of the class. + /// + /// Type of the event. + /// Item about the event. + protected InventoryEventArgs(GameInventoryEvent type, in GameInventoryItem item) + { + this.Type = type; + this.Item = item; + } + /// /// Gets the type of event for these args. /// - public abstract GameInventoryEvent Type { get; } + public GameInventoryEvent Type { get; } /// /// Gets the item associated with this event. /// This is a copy of the item data. /// - required public GameInventoryItem Item { get; init; } + public GameInventoryItem Item { get; } /// - public override string ToString() => this.Type switch - { - GameInventoryEvent.Empty => $"<{this.Type}>", - GameInventoryEvent.Added => $"<{this.Type}> ({this.Item})", - GameInventoryEvent.Removed => $"<{this.Type}> ({this.Item})", - GameInventoryEvent.Changed => $"<{this.Type}> ({this.Item})", - GameInventoryEvent.Moved when this is InventoryItemMovedArgs args => $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {args.SourceSlot} in {args.SourceInventory}) to (slot {args.TargetSlot} in {args.TargetInventory})", - _ => $" {this.Item}", - }; + public override string ToString() => $"<{this.Type}> ({this.Item})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs index 8d3e99823..f68b23106 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs @@ -1,20 +1,26 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an item being added to an inventory. /// public class InventoryItemAddedArgs : InventoryEventArgs { - /// - public override GameInventoryEvent Type => GameInventoryEvent.Added; - + /// + /// Initializes a new instance of the class. + /// + /// The item. + internal InventoryItemAddedArgs(in GameInventoryItem item) + : base(GameInventoryEvent.Added, item) + { + } + /// /// Gets the inventory this item was added to. /// - required public GameInventoryType Inventory { get; init; } - + public GameInventoryType Inventory => this.Item.ContainerType; + /// /// Gets the slot this item was added to. /// - required public uint Slot { get; init; } + public uint Slot => this.Item.InventorySlot; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs index 1e2632722..1c47d3b83 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an items properties being changed. @@ -6,21 +6,29 @@ /// public class InventoryItemChangedArgs : InventoryEventArgs { - /// - public override GameInventoryEvent Type => GameInventoryEvent.Changed; - + /// + /// Initializes a new instance of the class. + /// + /// The item before change. + /// The item after change. + internal InventoryItemChangedArgs(in GameInventoryItem oldItem, in GameInventoryItem newItem) + : base(GameInventoryEvent.Changed, newItem) + { + this.OldItemState = oldItem; + } + /// /// Gets the inventory this item is in. /// - required public GameInventoryType Inventory { get; init; } - + public GameInventoryType Inventory => this.Item.ContainerType; + /// /// Gets the inventory slot this item is in. /// - required public uint Slot { get; init; } - + public uint Slot => this.Item.InventorySlot; + /// /// Gets the state of the item from before it was changed. /// - required public GameInventoryItem OldItemState { get; init; } + public GameInventoryItem OldItemState { get; init; } } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs index 655f43445..2f1113b02 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -1,30 +1,56 @@ -namespace Dalamud.Game.GameInventory; +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an item being moved from one inventory and added to another. /// +[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "It literally says , , and then . required is not an access modifier.")] public class InventoryItemMovedArgs : InventoryEventArgs { - /// - public override GameInventoryEvent Type => GameInventoryEvent.Moved; - + /// + /// Initializes a new instance of the class. + /// + /// The item at before slot. + /// The item at after slot. + internal InventoryItemMovedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) + : base(GameInventoryEvent.Moved, targetEvent.Item) + { + this.SourceEvent = sourceEvent; + this.TargetEvent = targetEvent; + } + /// /// Gets the inventory this item was moved from. /// - required public GameInventoryType SourceInventory { get; init; } - + public GameInventoryType SourceInventory => this.SourceEvent.Item.ContainerType; + /// /// Gets the inventory this item was moved to. /// - required public GameInventoryType TargetInventory { get; init; } + public GameInventoryType TargetInventory => this.Item.ContainerType; /// /// Gets the slot this item was moved from. /// - required public uint SourceSlot { get; init; } - + public uint SourceSlot => this.SourceEvent.Item.InventorySlot; + /// /// Gets the slot this item was moved to. /// - required public uint TargetSlot { get; init; } + public uint TargetSlot => this.Item.InventorySlot; + + /// + /// Gets the associated source event. + /// + internal InventoryEventArgs SourceEvent { get; } + + /// + /// Gets the associated target event. + /// + internal InventoryEventArgs TargetEvent { get; } + + /// + public override string ToString() => + $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {this.SourceSlot} in {this.SourceInventory}) to (slot {this.TargetSlot} in {this.TargetInventory})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs index 2d4db2384..bd982d702 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs @@ -1,20 +1,26 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an item being removed from an inventory. /// public class InventoryItemRemovedArgs : InventoryEventArgs { - /// - public override GameInventoryEvent Type => GameInventoryEvent.Removed; - + /// + /// Initializes a new instance of the class. + /// + /// The item. + internal InventoryItemRemovedArgs(in GameInventoryItem item) + : base(GameInventoryEvent.Removed, item) + { + } + /// /// Gets the inventory this item was removed from. /// - required public GameInventoryType Inventory { get; init; } + public GameInventoryType Inventory => this.Item.ContainerType; /// /// Gets the slot this item was removed from. /// - required public uint Slot { get; init; } + public uint Slot => this.Item.InventorySlot; } diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 40b4bd84f..979e2d6a6 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -1,4 +1,7 @@ -using Dalamud.Game.GameInventory; +using System.Collections.Generic; + +using Dalamud.Game.Inventory; +using Dalamud.Game.Inventory.InventoryChangeArgsTypes; namespace Dalamud.Plugin.Services; @@ -12,7 +15,7 @@ public interface IGameInventory /// This delegate sends the entire set of changes recorded. /// /// The events. - public delegate void InventoryChangelogDelegate(ReadOnlySpan events); + public delegate void InventoryChangelogDelegate(IReadOnlyCollection events); /// /// Delegate function to be called for each change to inventories. @@ -26,24 +29,37 @@ public interface IGameInventory /// Event that is fired when the inventory has been changed. /// public event InventoryChangelogDelegate InventoryChanged; + + /// + /// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes + /// as a move event as appropriate.
+ /// In other words, does not fire in this event. + ///
+ public event InventoryChangelogDelegate InventoryChangedRaw; /// - /// Event that is fired when an item is added to an inventory. + /// Event that is fired when an item is added to an inventory.
+ /// If an accompanying item remove event happens, then will be called instead.
+ /// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemAdded; /// - /// Event that is fired when an item is removed from an inventory. + /// Event that is fired when an item is removed from an inventory.
+ /// If an accompanying item add event happens, then will be called instead.
+ /// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemRemoved; + /// + /// Event that is fired when an items properties are changed.
+ /// If an accompanying item change event happens, then will be called instead.
+ /// Use if you do not want such reinterpretation. + ///
+ public event InventoryChangedDelegate ItemChanged; + /// /// Event that is fired when an item is moved from one inventory into another. /// public event InventoryChangedDelegate ItemMoved; - - /// - /// Event that is fired when an items properties are changed. - /// - public event InventoryChangedDelegate ItemChanged; } From 35f4ff5c94674b823f7629953bb0e6f3dd29eac7 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 20:55:46 +0900 Subject: [PATCH 08/21] wip --- Dalamud/Game/Inventory/GameInventory.cs | 250 +++++++++--------- Dalamud/Game/Inventory/GameInventoryEvent.cs | 17 +- Dalamud/Game/Inventory/GameInventoryItem.cs | 15 +- Dalamud/Game/Inventory/GameInventoryType.cs | 120 ++++----- .../InventoryEventArgs.cs | 16 +- .../InventoryItemChangedArgs.cs | 8 +- .../InventoryItemMovedArgs.cs | 9 +- .../InventoryItemRemovedArgs.cs | 4 +- Dalamud/Plugin/Services/IGameInventory.cs | 4 +- 9 files changed, 226 insertions(+), 217 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 4ee66ffaf..5842996b6 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using Dalamud.Configuration.Internal; using Dalamud.Game.Inventory.InventoryChangeArgsTypes; @@ -22,21 +24,20 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { private static readonly ModuleLog Log = new("GameInventory"); - private readonly List allEvents = new(); private readonly List addedEvents = new(); private readonly List removedEvents = new(); private readonly List changedEvents = new(); private readonly List movedEvents = new(); - + [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - + [ServiceManager.ServiceDependency] private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; - + [ServiceManager.ServiceConstructor] private GameInventory() { @@ -59,10 +60,10 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory public event IGameInventory.InventoryChangedDelegate? ItemRemoved; /// - public event IGameInventory.InventoryChangedDelegate? ItemMoved; + public event IGameInventory.InventoryChangedDelegate? ItemChanged; /// - public event IGameInventory.InventoryChangedDelegate? ItemChanged; + public event IGameInventory.InventoryChangedDelegate? ItemMoved; /// public void Dispose() @@ -111,7 +112,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory Log.Error(e, "Exception during {argType} callback", arg.Type); } } - + private void OnFrameworkUpdate(IFramework framework1) { for (var i = 0; i < this.inventoryTypes.Length; i++) @@ -152,124 +153,135 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } // Was there any change? If not, stop further processing. - // Note that... - // * this.movedEvents is not checked; it will be populated after this check. - // * this.allEvents is not checked; it is a temporary list to be used after this check. + // Note that this.movedEvents is not checked; it will be populated after this check. if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0) return; - try + // Broadcast InventoryChangedRaw. + InvokeSafely( + this.InventoryChangedRaw, + new DeferredReadOnlyCollection( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count, + () => Array.Empty() + .Concat(this.addedEvents) + .Concat(this.removedEvents) + .Concat(this.changedEvents))); + + // Resolve changelog for item moved, from 1 added + 1 removed event. + for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) { - // Broadcast InventoryChangedRaw, if necessary. - if (this.InventoryChangedRaw is not null) + var added = this.addedEvents[iAdded]; + for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved) { - this.allEvents.Clear(); - this.allEvents.EnsureCapacity( - this.addedEvents.Count - + this.removedEvents.Count - + this.changedEvents.Count); - this.allEvents.AddRange(this.addedEvents); - this.allEvents.AddRange(this.removedEvents); - this.allEvents.AddRange(this.changedEvents); - InvokeSafely(this.InventoryChangedRaw, this.allEvents); - } + var removed = this.removedEvents[iRemoved]; + if (added.Item.ItemId != removed.Item.ItemId) + continue; - // Resolve changelog for item moved, from 1 added + 1 removed event. - for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) + this.movedEvents.Add(new(removed, added)); + + // Remove the reinterpreted entries. + this.addedEvents.RemoveAt(iAdded); + this.removedEvents.RemoveAt(iRemoved); + break; + } + } + + // Resolve changelog for item moved, from 2 changed events. + for (var i = this.changedEvents.Count - 1; i >= 0; --i) + { + var e1 = this.changedEvents[i]; + for (var j = i - 1; j >= 0; --j) { - var added = this.addedEvents[iAdded]; - for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved) - { - var removed = this.removedEvents[iRemoved]; - if (added.Item.ItemId != removed.Item.ItemId) - continue; + var e2 = this.changedEvents[j]; + if (e1.Item.ItemId != e2.Item.ItemId || e1.Item.ItemId != e2.Item.ItemId) + continue; - this.movedEvents.Add(new(removed, added)); - - // Remove the reinterpreted entries. - this.addedEvents.RemoveAt(iAdded); - this.removedEvents.RemoveAt(iRemoved); - break; - } + // move happened, and e2 has an item + if (!e2.Item.IsEmpty) + this.movedEvents.Add(new(e1, e2)); + + // move happened, and e1 has an item + if (!e1.Item.IsEmpty) + this.movedEvents.Add(new(e2, e1)); + + // Remove the reinterpreted entries. Note that i > j. + this.changedEvents.RemoveAt(i); + this.changedEvents.RemoveAt(j); + break; } + } - // Resolve changelog for item moved, from 2 changed events. - for (var i = this.changedEvents.Count - 1; i >= 0; --i) - { - var e1 = this.changedEvents[i]; - for (var j = i - 1; j >= 0; --j) - { - var e2 = this.changedEvents[j]; - if (e1.Item.ItemId != e2.Item.ItemId || e1.Item.ItemId != e2.Item.ItemId) - continue; - - // move happened, and e2 has an item - if (!e2.Item.IsEmpty) - this.movedEvents.Add(new(e1, e2)); - - // move happened, and e1 has an item - if (!e1.Item.IsEmpty) - this.movedEvents.Add(new(e2, e1)); - - // Remove the reinterpreted entries. Note that i > j. - this.changedEvents.RemoveAt(i); - this.changedEvents.RemoveAt(j); - break; - } - } - - // Log only if it matters. - if (this.dalamudConfiguration.LogLevel >= LogEventLevel.Verbose) - { - foreach (var x in this.addedEvents) - Log.Verbose($"{x}"); - - foreach (var x in this.removedEvents) - Log.Verbose($"{x}"); - - foreach (var x in this.changedEvents) - Log.Verbose($"{x}"); - - foreach (var x in this.movedEvents) - Log.Verbose($"{x} (({x.SourceEvent}) + ({x.TargetEvent}))"); - } - - // Broadcast InventoryChanged, if necessary. - if (this.InventoryChanged is not null) - { - this.allEvents.Clear(); - this.allEvents.EnsureCapacity( - this.addedEvents.Count - + this.removedEvents.Count - + this.changedEvents.Count - + this.movedEvents.Count); - this.allEvents.AddRange(this.addedEvents); - this.allEvents.AddRange(this.removedEvents); - this.allEvents.AddRange(this.changedEvents); - this.allEvents.AddRange(this.movedEvents); - InvokeSafely(this.InventoryChanged, this.allEvents); - } - - // Broadcast the rest. + // Log only if it matters. + if (this.dalamudConfiguration.LogLevel <= LogEventLevel.Verbose) + { foreach (var x in this.addedEvents) - InvokeSafely(this.ItemAdded, x); - + Log.Verbose($"{x}"); + foreach (var x in this.removedEvents) - InvokeSafely(this.ItemRemoved, x); - + Log.Verbose($"{x}"); + foreach (var x in this.changedEvents) - InvokeSafely(this.ItemChanged, x); - + Log.Verbose($"{x}"); + foreach (var x in this.movedEvents) - InvokeSafely(this.ItemMoved, x); + Log.Verbose($"{x} (({x.SourceEvent}) + ({x.TargetEvent}))"); } - finally + + // Broadcast the rest. + InvokeSafely( + this.InventoryChanged, + new DeferredReadOnlyCollection( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count + + this.movedEvents.Count, + () => Array.Empty() + .Concat(this.addedEvents) + .Concat(this.removedEvents) + .Concat(this.changedEvents) + .Concat(this.movedEvents))); + + foreach (var x in this.addedEvents) + InvokeSafely(this.ItemAdded, x); + + foreach (var x in this.removedEvents) + InvokeSafely(this.ItemRemoved, x); + + foreach (var x in this.changedEvents) + InvokeSafely(this.ItemChanged, x); + + foreach (var x in this.movedEvents) + InvokeSafely(this.ItemMoved, x); + + // We're done using the lists. Clean them up. + this.addedEvents.Clear(); + this.removedEvents.Clear(); + this.changedEvents.Clear(); + this.movedEvents.Clear(); + } + + /// + /// A view of , so that the number of items + /// contained within can be known in advance, and it can be enumerated multiple times. + /// + /// The type of elements being enumerated. + private class DeferredReadOnlyCollection : IReadOnlyCollection + { + private readonly Func> enumerableGenerator; + + public DeferredReadOnlyCollection(int count, Func> enumerableGenerator) { - this.addedEvents.Clear(); - this.removedEvents.Clear(); - this.changedEvents.Clear(); - this.movedEvents.Clear(); + this.enumerableGenerator = enumerableGenerator; + this.Count = count; } + + public int Count { get; } + + public IEnumerator GetEnumerator() => this.enumerableGenerator().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.enumerableGenerator().GetEnumerator(); } } @@ -313,10 +325,10 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public event IGameInventory.InventoryChangedDelegate? ItemRemoved; /// - public event IGameInventory.InventoryChangedDelegate? ItemMoved; + public event IGameInventory.InventoryChangedDelegate? ItemChanged; /// - public event IGameInventory.InventoryChangedDelegate? ItemChanged; + public event IGameInventory.InventoryChangedDelegate? ItemMoved; /// public void Dispose() @@ -325,15 +337,15 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.InventoryChangedRaw -= this.OnInventoryChangedRawForward; this.gameInventoryService.ItemAdded -= this.OnInventoryItemAddedForward; this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; - this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; - + this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; + this.InventoryChanged = null; this.InventoryChangedRaw = null; this.ItemAdded = null; this.ItemRemoved = null; - this.ItemMoved = null; this.ItemChanged = null; + this.ItemMoved = null; } private void OnInventoryChangedForward(IReadOnlyCollection events) @@ -341,16 +353,16 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven private void OnInventoryChangedRawForward(IReadOnlyCollection events) => this.InventoryChangedRaw?.Invoke(events); - + private void OnInventoryItemAddedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemAdded?.Invoke(type, data); - + private void OnInventoryItemRemovedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemRemoved?.Invoke(type, data); - private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemMoved?.Invoke(type, data); - private void OnInventoryItemChangedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemChanged?.Invoke(type, data); + + private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemMoved?.Invoke(type, data); } diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs index c23d79f30..6a4bb86e2 100644 --- a/Dalamud/Game/Inventory/GameInventoryEvent.cs +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -3,7 +3,6 @@ /// /// Class representing a item's changelog state. /// -[Flags] public enum GameInventoryEvent { /// @@ -11,24 +10,24 @@ public enum GameInventoryEvent /// 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, - + Added = 1, + /// /// Item was removed from an inventory. /// - Removed = 1 << 1, - + Removed = 2, + /// /// Properties are changed for an item in an inventory. /// - Changed = 1 << 2, - + Changed = 3, + /// /// Item has been moved, possibly across different inventories. /// - Moved = 1 << 3, + Moved = 4, } diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 9073073cb..52a5c5e3c 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -12,11 +12,6 @@ namespace Dalamud.Game.Inventory; [StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)] public unsafe struct GameInventoryItem : IEquatable { - /// - /// An empty instance of . - /// - internal static readonly GameInventoryItem Empty = default; - /// /// The actual data. /// @@ -104,7 +99,7 @@ public unsafe struct GameInventoryItem : IEquatable /// 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. /// @@ -119,8 +114,8 @@ public unsafe struct GameInventoryItem : IEquatable /// /// Gets the glamour id for this item. /// - public uint GlmaourId => this.InternalItem.GlamourID; - + 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. @@ -163,6 +158,6 @@ public unsafe struct GameInventoryItem : IEquatable /// public override string ToString() => this.IsEmpty - ? "" - : $"Item #{this.ItemId} at slot {this.InventorySlot} in {this.ContainerType}"; + ? "no item" + : $"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 c982fa80f..00c65046f 100644 --- a/Dalamud/Game/Inventory/GameInventoryType.cs +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -9,17 +9,17 @@ public enum GameInventoryType : ushort /// First panel of main player inventory. /// Inventory1 = 0, - + /// /// Second panel of main player inventory. /// Inventory2 = 1, - + /// /// Third panel of main player inventory. /// Inventory3 = 2, - + /// /// Fourth panel of main player inventory. /// @@ -40,32 +40,32 @@ public enum GameInventoryType : ushort /// Crystal container. ///
Crystals = 2001, - + /// /// Mail container. /// Mail = 2003, - + /// /// Key item container. /// KeyItems = 2004, - + /// /// Quest item hand-in inventory. /// HandIn = 2005, - + /// /// DamagedGear container. /// DamagedGear = 2007, - + /// /// Examine window container. /// Examine = 2009, - + /// /// Doman Enclave Reconstruction Reclamation Box. /// @@ -75,22 +75,22 @@ public enum GameInventoryType : ushort /// Armory off-hand weapon container. ///
ArmoryOffHand = 3200, - + /// /// Armory head container. /// ArmoryHead = 3201, - + /// /// Armory body container. /// ArmoryBody = 3202, - + /// /// Armory hand/gloves container. /// ArmoryHands = 3203, - + /// /// Armory waist container. /// @@ -98,42 +98,42 @@ public enum GameInventoryType : ushort /// /// ArmoryWaist = 3204, - + /// /// Armory legs/pants/skirt container. /// ArmoryLegs = 3205, - + /// /// Armory feet/boots/shoes container. /// ArmoryFeets = 3206, - + /// /// Armory earring container. /// ArmoryEar = 3207, - + /// /// Armory necklace container. /// ArmoryNeck = 3208, - + /// /// Armory bracelet container. /// ArmoryWrist = 3209, - + /// /// Armory ring container. /// ArmoryRings = 3300, - + /// /// Armory soul crystal container. /// ArmorySoulCrystal = 3400, - + /// /// Armory main-hand weapon container. /// @@ -143,17 +143,17 @@ public enum GameInventoryType : ushort /// First panel of saddelbag inventory. ///
SaddleBag1 = 4000, - + /// /// Second panel of Saddlebag inventory. /// SaddleBag2 = 4001, - + /// /// First panel of premium saddlebag inventory. /// PremiumSaddleBag1 = 4100, - + /// /// Second panel of premium saddlebag inventory. /// @@ -163,52 +163,52 @@ public enum GameInventoryType : ushort /// First panel of retainer inventory. ///
RetainerPage1 = 10000, - + /// /// Second panel of retainer inventory. /// RetainerPage2 = 10001, - + /// /// Third panel of retainer inventory. /// RetainerPage3 = 10002, - + /// /// Fourth panel of retainer inventory. /// RetainerPage4 = 10003, - + /// /// Fifth panel of retainer inventory. /// RetainerPage5 = 10004, - + /// /// Sixth panel of retainer inventory. /// RetainerPage6 = 10005, - + /// /// Seventh panel of retainer inventory. /// RetainerPage7 = 10006, - + /// /// Retainer equipment container. /// RetainerEquippedItems = 11000, - + /// /// Retainer currency container. /// RetainerGil = 12000, - + /// /// Retainer crystal container. /// RetainerCrystals = 12001, - + /// /// Retainer market item container. /// @@ -218,32 +218,32 @@ public enum GameInventoryType : ushort /// First panel of Free Company inventory. ///
FreeCompanyPage1 = 20000, - + /// /// Second panel of Free Company inventory. /// FreeCompanyPage2 = 20001, - + /// /// Third panel of Free Company inventory. /// FreeCompanyPage3 = 20002, - + /// /// Fourth panel of Free Company inventory. /// FreeCompanyPage4 = 20003, - + /// /// Fifth panel of Free Company inventory. /// FreeCompanyPage5 = 20004, - + /// /// Free Company currency container. /// FreeCompanyGil = 22000, - + /// /// Free Company crystal container. /// @@ -253,102 +253,102 @@ public enum GameInventoryType : ushort /// Housing exterior appearance container. ///
HousingExteriorAppearance = 25000, - + /// /// Housing exterior placed items container. /// HousingExteriorPlacedItems = 25001, - + /// /// Housing interior appearance container. /// HousingInteriorAppearance = 25002, - + /// /// First panel of housing interior inventory. /// HousingInteriorPlacedItems1 = 25003, - + /// /// Second panel of housing interior inventory. /// HousingInteriorPlacedItems2 = 25004, - + /// /// Third panel of housing interior inventory. /// HousingInteriorPlacedItems3 = 25005, - + /// /// Fourth panel of housing interior inventory. /// HousingInteriorPlacedItems4 = 25006, - + /// /// Fifth panel of housing interior inventory. /// HousingInteriorPlacedItems5 = 25007, - + /// /// Sixth panel of housing interior inventory. /// HousingInteriorPlacedItems6 = 25008, - + /// /// Seventh panel of housing interior inventory. /// HousingInteriorPlacedItems7 = 25009, - + /// /// Eighth panel of housing interior inventory. /// HousingInteriorPlacedItems8 = 25010, - + /// /// Housing exterior storeroom inventory. /// HousingExteriorStoreroom = 27000, - + /// /// First panel of housing interior storeroom inventory. /// HousingInteriorStoreroom1 = 27001, - + /// /// Second panel of housing interior storeroom inventory. /// HousingInteriorStoreroom2 = 27002, - + /// /// Third panel of housing interior storeroom inventory. /// HousingInteriorStoreroom3 = 27003, - + /// /// Fourth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom4 = 27004, - + /// /// Fifth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom5 = 27005, - + /// /// Sixth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom6 = 27006, - + /// /// Seventh panel of housing interior storeroom inventory. /// HousingInteriorStoreroom7 = 27007, - + /// /// Eighth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom8 = 27008, - + /// /// An invalid value. /// diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs index 070d8a8db..301715bf2 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs @@ -1,13 +1,12 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Abstract base class representing inventory changed events. /// -[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "It literally says , , and then . required is not an access modifier.")] public abstract class InventoryEventArgs { + private readonly GameInventoryItem item; + /// /// Initializes a new instance of the class. /// @@ -16,7 +15,7 @@ public abstract class InventoryEventArgs protected InventoryEventArgs(GameInventoryEvent type, in GameInventoryItem item) { this.Type = type; - this.Item = item; + this.item = item; } /// @@ -28,8 +27,11 @@ public abstract class InventoryEventArgs /// Gets the item associated with this event. /// This is a copy of the item data. /// - public GameInventoryItem Item { get; } - + // impl note: we return a ref readonly view, to avoid making copies every time this property is accessed. + // see: https://devblogs.microsoft.com/premier-developer/avoiding-struct-and-readonly-reference-performance-pitfalls-with-errorprone-net/ + // "Consider using ref readonly locals and ref return for library code" + public ref readonly GameInventoryItem Item => ref this.item; + /// public override string ToString() => $"<{this.Type}> ({this.Item})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs index 1c47d3b83..1682ae32d 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs @@ -6,6 +6,8 @@ /// public class InventoryItemChangedArgs : InventoryEventArgs { + private readonly GameInventoryItem oldItemState; + /// /// Initializes a new instance of the class. /// @@ -14,7 +16,7 @@ public class InventoryItemChangedArgs : InventoryEventArgs internal InventoryItemChangedArgs(in GameInventoryItem oldItem, in GameInventoryItem newItem) : base(GameInventoryEvent.Changed, newItem) { - this.OldItemState = oldItem; + this.oldItemState = oldItem; } /// @@ -29,6 +31,8 @@ public class InventoryItemChangedArgs : InventoryEventArgs /// /// Gets the state of the item from before it was changed. + /// This is a copy of the item data. /// - public GameInventoryItem OldItemState { get; init; } + // impl note: see InventoryEventArgs.Item. + public ref readonly GameInventoryItem OldItemState => ref this.oldItemState; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs index 2f1113b02..b6f490a2c 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -1,11 +1,8 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an item being moved from one inventory and added to another. /// -[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "It literally says , , and then . required is not an access modifier.")] public class InventoryItemMovedArgs : InventoryEventArgs { /// @@ -29,7 +26,7 @@ public class InventoryItemMovedArgs : InventoryEventArgs /// Gets the inventory this item was moved to. /// public GameInventoryType TargetInventory => this.Item.ContainerType; - + /// /// Gets the slot this item was moved from. /// @@ -49,7 +46,7 @@ public class InventoryItemMovedArgs : InventoryEventArgs /// Gets the associated target event. /// internal InventoryEventArgs TargetEvent { get; } - + /// public override string ToString() => $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {this.SourceSlot} in {this.SourceInventory}) to (slot {this.TargetSlot} in {this.TargetInventory})"; diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs index bd982d702..41ca9d380 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs @@ -13,12 +13,12 @@ public class InventoryItemRemovedArgs : InventoryEventArgs : base(GameInventoryEvent.Removed, item) { } - + /// /// Gets the inventory this item was removed from. /// public GameInventoryType Inventory => this.Item.ContainerType; - + /// /// Gets the slot this item was removed from. /// diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 979e2d6a6..058bcbd27 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -24,12 +24,12 @@ public interface IGameInventory /// The event try that triggered this message. /// Data for the triggered event. public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data); - + /// /// Event that is fired when the inventory has been changed. /// public event InventoryChangelogDelegate InventoryChanged; - + /// /// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes /// as a move event as appropriate.
From f8dff15fe07847cc6a05a54215191443518dc11f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 22:02:08 +0900 Subject: [PATCH 09/21] fix bugs --- Dalamud/Game/Inventory/GameInventory.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 5842996b6..b5e3029b9 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -22,7 +22,7 @@ namespace Dalamud.Game.Inventory; [ServiceManager.BlockingEarlyLoadedService] internal class GameInventory : IDisposable, IServiceType, IGameInventory { - private static readonly ModuleLog Log = new("GameInventory"); + private static readonly ModuleLog Log = new(nameof(GameInventory)); private readonly List addedEvents = new(); private readonly List removedEvents = new(); @@ -195,20 +195,23 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory for (var j = i - 1; j >= 0; --j) { var e2 = this.changedEvents[j]; - if (e1.Item.ItemId != e2.Item.ItemId || e1.Item.ItemId != e2.Item.ItemId) + if (e1.Item.ItemId != e2.OldItemState.ItemId || e1.OldItemState.ItemId != e2.Item.ItemId) continue; - // move happened, and e2 has an item + // Move happened, and e2 has an item. if (!e2.Item.IsEmpty) this.movedEvents.Add(new(e1, e2)); - // move happened, and e1 has an item + // Move happened, and e1 has an item. if (!e1.Item.IsEmpty) this.movedEvents.Add(new(e2, e1)); // Remove the reinterpreted entries. Note that i > j. this.changedEvents.RemoveAt(i); this.changedEvents.RemoveAt(j); + + // We've removed two. Adjust the outer counter. + --i; break; } } From 6dd34ebda46d4984300ee00f0e21301fb3966201 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 22:35:44 +0900 Subject: [PATCH 10/21] support merge/split events --- Dalamud/Game/Inventory/GameInventory.cs | 108 +++++++++++++++--- Dalamud/Game/Inventory/GameInventoryEvent.cs | 10 ++ Dalamud/Game/Inventory/GameInventoryItem.cs | 4 +- .../InventoryComplexEventArgs.cs | 54 +++++++++ .../InventoryEventArgs.cs | 2 +- .../InventoryItemAddedArgs.cs | 2 +- .../InventoryItemChangedArgs.cs | 2 +- .../InventoryItemMergedArgs.cs | 26 +++++ .../InventoryItemMovedArgs.cs | 39 +------ .../InventoryItemRemovedArgs.cs | 2 +- .../InventoryItemSplitArgs.cs | 26 +++++ Dalamud/Plugin/Services/IGameInventory.cs | 19 ++- 12 files changed, 234 insertions(+), 60 deletions(-) create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index b5e3029b9..36e6756bf 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -28,6 +28,8 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory private readonly List removedEvents = new(); private readonly List changedEvents = new(); private readonly List movedEvents = new(); + private readonly List splitEvents = new(); + private readonly List mergedEvents = new(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -45,6 +47,21 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; this.framework.Update += this.OnFrameworkUpdate; + + // Separate log logic as an event handler. + this.InventoryChanged += events => + { + if (this.dalamudConfiguration.LogLevel > LogEventLevel.Verbose) + return; + + foreach (var e in events) + { + if (e is InventoryComplexEventArgs icea) + Log.Verbose($"{icea}\n\t├ {icea.SourceEvent}\n\t└ {icea.TargetEvent}"); + else + Log.Verbose($"{e}"); + } + }; } /// @@ -65,6 +82,12 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory /// public event IGameInventory.InventoryChangedDelegate? ItemMoved; + /// + public event IGameInventory.InventoryChangedDelegate? ItemSplit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMerged; + /// public void Dispose() { @@ -153,11 +176,12 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } // Was there any change? If not, stop further processing. - // Note that this.movedEvents is not checked; it will be populated after this check. + // Note that only these three are checked; the rest will be populated after this check. if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0) return; // Broadcast InventoryChangedRaw. + // Same reason with the above on why are there 3 lists of events involved. InvokeSafely( this.InventoryChangedRaw, new DeferredReadOnlyCollection( @@ -169,7 +193,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory .Concat(this.removedEvents) .Concat(this.changedEvents))); - // Resolve changelog for item moved, from 1 added + 1 removed event. + // Resolve moved items, from 1 added + 1 removed event. for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) { var added = this.addedEvents[iAdded]; @@ -188,7 +212,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } } - // Resolve changelog for item moved, from 2 changed events. + // Resolve moved items, from 2 changed events. for (var i = this.changedEvents.Count - 1; i >= 0; --i) { var e1 = this.changedEvents[i]; @@ -209,27 +233,49 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // Remove the reinterpreted entries. Note that i > j. this.changedEvents.RemoveAt(i); this.changedEvents.RemoveAt(j); - + // We've removed two. Adjust the outer counter. --i; break; } } - // Log only if it matters. - if (this.dalamudConfiguration.LogLevel <= LogEventLevel.Verbose) + // Resolve split items, from 1 added + 1 changed event. + for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) { - foreach (var x in this.addedEvents) - Log.Verbose($"{x}"); + var added = this.addedEvents[iAdded]; + for (var iChanged = this.changedEvents.Count - 1; iChanged >= 0; --iChanged) + { + var changed = this.changedEvents[iChanged]; + if (added.Item.ItemId != changed.Item.ItemId || added.Item.ItemId != changed.OldItemState.ItemId) + continue; - foreach (var x in this.removedEvents) - Log.Verbose($"{x}"); + this.splitEvents.Add(new(changed, added)); - foreach (var x in this.changedEvents) - Log.Verbose($"{x}"); + // Remove the reinterpreted entries. + this.addedEvents.RemoveAt(iAdded); + this.changedEvents.RemoveAt(iChanged); + break; + } + } - foreach (var x in this.movedEvents) - Log.Verbose($"{x} (({x.SourceEvent}) + ({x.TargetEvent}))"); + // Resolve merged items, from 1 removed + 1 changed event. + for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved) + { + var removed = this.removedEvents[iRemoved]; + for (var iChanged = this.changedEvents.Count - 1; iChanged >= 0; --iChanged) + { + var changed = this.changedEvents[iChanged]; + if (removed.Item.ItemId != changed.Item.ItemId || removed.Item.ItemId != changed.OldItemState.ItemId) + continue; + + this.mergedEvents.Add(new(removed, changed)); + + // Remove the reinterpreted entries. + this.removedEvents.RemoveAt(iRemoved); + this.changedEvents.RemoveAt(iChanged); + break; + } } // Broadcast the rest. @@ -239,12 +285,16 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory this.addedEvents.Count + this.removedEvents.Count + this.changedEvents.Count + - this.movedEvents.Count, + this.movedEvents.Count + + this.splitEvents.Count + + this.mergedEvents.Count, () => Array.Empty() .Concat(this.addedEvents) .Concat(this.removedEvents) .Concat(this.changedEvents) - .Concat(this.movedEvents))); + .Concat(this.movedEvents) + .Concat(this.splitEvents) + .Concat(this.mergedEvents))); foreach (var x in this.addedEvents) InvokeSafely(this.ItemAdded, x); @@ -258,11 +308,19 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory foreach (var x in this.movedEvents) InvokeSafely(this.ItemMoved, x); + foreach (var x in this.splitEvents) + InvokeSafely(this.ItemSplit, x); + + foreach (var x in this.mergedEvents) + InvokeSafely(this.ItemMerged, x); + // We're done using the lists. Clean them up. this.addedEvents.Clear(); this.removedEvents.Clear(); this.changedEvents.Clear(); this.movedEvents.Clear(); + this.splitEvents.Clear(); + this.mergedEvents.Clear(); } /// @@ -313,6 +371,8 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.ItemRemoved += this.OnInventoryItemRemovedForward; this.gameInventoryService.ItemMoved += this.OnInventoryItemMovedForward; this.gameInventoryService.ItemChanged += this.OnInventoryItemChangedForward; + this.gameInventoryService.ItemSplit += this.OnInventoryItemSplitForward; + this.gameInventoryService.ItemMerged += this.OnInventoryItemMergedForward; } /// @@ -333,6 +393,12 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public event IGameInventory.InventoryChangedDelegate? ItemMoved; + /// + public event IGameInventory.InventoryChangedDelegate? ItemSplit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMerged; + /// public void Dispose() { @@ -342,6 +408,8 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; + this.gameInventoryService.ItemSplit -= this.OnInventoryItemSplitForward; + this.gameInventoryService.ItemMerged -= this.OnInventoryItemMergedForward; this.InventoryChanged = null; this.InventoryChangedRaw = null; @@ -349,6 +417,8 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.ItemRemoved = null; this.ItemChanged = null; this.ItemMoved = null; + this.ItemSplit = null; + this.ItemMerged = null; } private void OnInventoryChangedForward(IReadOnlyCollection events) @@ -368,4 +438,10 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemMoved?.Invoke(type, data); + + private void OnInventoryItemSplitForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemSplit?.Invoke(type, data); + + private void OnInventoryItemMergedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemMerged?.Invoke(type, data); } diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs index 6a4bb86e2..16efab648 100644 --- a/Dalamud/Game/Inventory/GameInventoryEvent.cs +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -30,4 +30,14 @@ public enum GameInventoryEvent /// Item has been moved, possibly across different inventories. /// Moved = 4, + + /// + /// Item has been split into two stacks from one, possibly across different inventories. + /// + Split = 5, + + /// + /// Item has been merged into one stack from two, possibly across different inventories. + /// + Merged = 6, } diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 52a5c5e3c..4958574aa 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -158,6 +158,6 @@ public unsafe struct GameInventoryItem : IEquatable /// public override string ToString() => this.IsEmpty - ? "no item" - : $"item #{this.ItemId} at slot {this.InventorySlot} in {this.ContainerType}"; + ? "empty" + : $"item({this.ItemId}@{this.ContainerType}#{this.InventorySlot})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs new file mode 100644 index 000000000..c44bfb991 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs @@ -0,0 +1,54 @@ +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; + +/// +/// Represents the data associated with an item being affected across different slots, possibly in different containers. +/// +public abstract class InventoryComplexEventArgs : InventoryEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// Type of the event. + /// The item at before slot. + /// The item at after slot. + internal InventoryComplexEventArgs( + GameInventoryEvent type, InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) + : base(type, targetEvent.Item) + { + this.SourceEvent = sourceEvent; + this.TargetEvent = targetEvent; + } + + /// + /// Gets the inventory this item was at. + /// + public GameInventoryType SourceInventory => this.SourceEvent.Item.ContainerType; + + /// + /// Gets the inventory this item now is. + /// + public GameInventoryType TargetInventory => this.Item.ContainerType; + + /// + /// Gets the slot this item was at. + /// + public uint SourceSlot => this.SourceEvent.Item.InventorySlot; + + /// + /// Gets the slot this item now is. + /// + public uint TargetSlot => this.Item.InventorySlot; + + /// + /// Gets the associated source event. + /// + public InventoryEventArgs SourceEvent { get; } + + /// + /// Gets the associated target event. + /// + public InventoryEventArgs TargetEvent { get; } + + /// + public override string ToString() => $"{this.Type}({this.SourceEvent}, {this.TargetEvent})"; +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs index 301715bf2..8197e28f5 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs @@ -33,5 +33,5 @@ public abstract class InventoryEventArgs public ref readonly GameInventoryItem Item => ref this.item; /// - public override string ToString() => $"<{this.Type}> ({this.Item})"; + public override string ToString() => $"{this.Type}({this.Item})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs index f68b23106..45a35739a 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs @@ -3,7 +3,7 @@ /// /// Represents the data associated with an item being added to an inventory. /// -public class InventoryItemAddedArgs : InventoryEventArgs +public sealed class InventoryItemAddedArgs : InventoryEventArgs { /// /// Initializes a new instance of the class. diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs index 1682ae32d..191cfa1d8 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs @@ -4,7 +4,7 @@ /// Represents the data associated with an items properties being changed. /// This also includes an items stack count changing. /// -public class InventoryItemChangedArgs : InventoryEventArgs +public sealed class InventoryItemChangedArgs : InventoryEventArgs { private readonly GameInventoryItem oldItemState; diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs new file mode 100644 index 000000000..0f088f24b --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs @@ -0,0 +1,26 @@ +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; + +/// +/// Represents the data associated with an item being merged from two stacks into one. +/// +public sealed class InventoryItemMergedArgs : InventoryComplexEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The item at before slot. + /// The item at after slot. + internal InventoryItemMergedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) + : base(GameInventoryEvent.Merged, sourceEvent, targetEvent) + { + } + + /// + public override string ToString() => + this.TargetEvent is InventoryItemChangedArgs iica + ? $"{this.Type}(" + + $"item({this.Item.ItemId}), " + + $"{this.SourceInventory}#{this.SourceSlot}({this.SourceEvent.Item.Quantity} to 0), " + + $"{this.TargetInventory}#{this.TargetSlot}({iica.OldItemState.Quantity} to {iica.Item.Quantity}))" + : base.ToString(); +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs index b6f490a2c..ac33acd0d 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -3,7 +3,7 @@ /// /// Represents the data associated with an item being moved from one inventory and added to another. /// -public class InventoryItemMovedArgs : InventoryEventArgs +public sealed class InventoryItemMovedArgs : InventoryComplexEventArgs { /// /// Initializes a new instance of the class. @@ -11,43 +11,14 @@ public class InventoryItemMovedArgs : InventoryEventArgs /// The item at before slot. /// The item at after slot. internal InventoryItemMovedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) - : base(GameInventoryEvent.Moved, targetEvent.Item) + : base(GameInventoryEvent.Moved, sourceEvent, targetEvent) { - this.SourceEvent = sourceEvent; - this.TargetEvent = targetEvent; } - /// - /// Gets the inventory this item was moved from. - /// - public GameInventoryType SourceInventory => this.SourceEvent.Item.ContainerType; - - /// - /// Gets the inventory this item was moved to. - /// - public GameInventoryType TargetInventory => this.Item.ContainerType; - - /// - /// Gets the slot this item was moved from. - /// - public uint SourceSlot => this.SourceEvent.Item.InventorySlot; - - /// - /// Gets the slot this item was moved to. - /// - public uint TargetSlot => this.Item.InventorySlot; - - /// - /// Gets the associated source event. - /// - internal InventoryEventArgs SourceEvent { get; } - - /// - /// Gets the associated target event. - /// - internal InventoryEventArgs TargetEvent { get; } + // /// + // public override string ToString() => $"{this.Type}({this.SourceEvent}, {this.TargetEvent})"; /// public override string ToString() => - $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {this.SourceSlot} in {this.SourceInventory}) to (slot {this.TargetSlot} in {this.TargetInventory})"; + $"{this.Type}(item({this.Item.ItemId}) from {this.SourceInventory}#{this.SourceSlot} to {this.TargetInventory}#{this.TargetSlot})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs index 41ca9d380..fe40c870b 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs @@ -3,7 +3,7 @@ /// /// Represents the data associated with an item being removed from an inventory. /// -public class InventoryItemRemovedArgs : InventoryEventArgs +public sealed class InventoryItemRemovedArgs : InventoryEventArgs { /// /// Initializes a new instance of the class. diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs new file mode 100644 index 000000000..2a3d41c09 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs @@ -0,0 +1,26 @@ +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; + +/// +/// Represents the data associated with an item being split from one stack into two. +/// +public sealed class InventoryItemSplitArgs : InventoryComplexEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The item at before slot. + /// The item at after slot. + internal InventoryItemSplitArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) + : base(GameInventoryEvent.Split, sourceEvent, targetEvent) + { + } + + /// + public override string ToString() => + this.SourceEvent is InventoryItemChangedArgs iica + ? $"{this.Type}(" + + $"item({this.Item.ItemId}), " + + $"{this.SourceInventory}#{this.SourceSlot}({iica.OldItemState.Quantity} to {iica.Item.Quantity}), " + + $"{this.TargetInventory}#{this.TargetSlot}(0 to {this.Item.Quantity}))" + : base.ToString(); +} diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 058bcbd27..a6f4b4adf 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -33,27 +33,28 @@ public interface IGameInventory /// /// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes /// as a move event as appropriate.
- /// In other words, does not fire in this event. + /// In other words, , , and + /// do not fire in this event. ///
public event InventoryChangelogDelegate InventoryChangedRaw; /// /// Event that is fired when an item is added to an inventory.
- /// If an accompanying item remove event happens, then will be called instead.
+ /// If this event is a part of multi-step event, then this event will not be called.
/// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemAdded; /// /// Event that is fired when an item is removed from an inventory.
- /// If an accompanying item add event happens, then will be called instead.
+ /// If this event is a part of multi-step event, then this event will not be called.
/// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemRemoved; /// /// Event that is fired when an items properties are changed.
- /// If an accompanying item change event happens, then will be called instead.
+ /// If this event is a part of multi-step event, then this event will not be called.
/// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemChanged; @@ -62,4 +63,14 @@ public interface IGameInventory /// Event that is fired when an item is moved from one inventory into another. ///
public event InventoryChangedDelegate ItemMoved; + + /// + /// Event that is fired when an item is split from one stack into two. + /// + public event InventoryChangedDelegate ItemSplit; + + /// + /// Event that is fired when an item is merged from two stacks into one. + /// + public event InventoryChangedDelegate ItemMerged; } From 1039c1eb8a6bdbc4b7a8f8a53e6f22e8bcf38564 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:22:07 +0900 Subject: [PATCH 11/21] Cleanup --- Dalamud/Game/Inventory/GameInventory.cs | 20 +--------- Dalamud/Game/Inventory/GameInventoryItem.cs | 38 +++++++++++++++++++ .../InventoryItemMovedArgs.cs | 3 -- .../Internal/Windows/ConsoleWindow.cs | 3 ++ Dalamud/Plugin/Services/IGameInventory.cs | 8 +++- 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 36e6756bf..b8e81ced7 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -9,8 +9,6 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game; - using Serilog.Events; namespace Dalamud.Game.Inventory; @@ -94,22 +92,6 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory this.framework.Update -= this.OnFrameworkUpdate; } - /// - /// Gets a view of s, wrapped as . - /// - /// The inventory type. - /// The span. - private static unsafe ReadOnlySpan 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 ReadOnlySpan(inventory->Items, (int)inventory->Size); - } - private static void InvokeSafely( IGameInventory.InventoryChangelogDelegate? cb, IReadOnlyCollection data) @@ -140,7 +122,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { for (var i = 0; i < this.inventoryTypes.Length; i++) { - var newItems = GetItemsForInventory(this.inventoryTypes[i]); + var newItems = GameInventoryItem.GetReadOnlySpanOfInventory(this.inventoryTypes[i]); if (newItems.IsEmpty) continue; diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 4958574aa..1e71f6914 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -106,6 +106,28 @@ public unsafe struct GameInventoryItem : IEquatable public ReadOnlySpan MateriaGrade => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5); + /// + /// 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. + ///
+ public nint NativeAddress + { + 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. /// @@ -160,4 +182,20 @@ public unsafe struct GameInventoryItem : IEquatable 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); + } } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs index ac33acd0d..6a59d1304 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -15,9 +15,6 @@ public sealed class InventoryItemMovedArgs : InventoryComplexEventArgs { } - // /// - // public override string ToString() => $"{this.Type}({this.SourceEvent}, {this.TargetEvent})"; - /// public override string ToString() => $"{this.Type}(item({this.Item.ItemId}) from {this.SourceInventory}#{this.SourceSlot} to {this.TargetInventory}#{this.TargetSlot})"; diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index b285520d4..89dd153cc 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -679,6 +679,9 @@ internal class ConsoleWindow : Window, IDisposable private bool IsFilterApplicable(LogEntry entry) { + if (this.regexError) + return false; + try { // If this entry is below a newly set minimum level, fail it diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index a6f4b4adf..6e84e780a 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -26,7 +26,11 @@ public interface IGameInventory public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data); /// - /// Event that is fired when the inventory has been changed. + /// Event that is fired when the inventory has been changed.
+ /// Note that some events, such as , , and + /// currently is subject to reinterpretation as , , and + /// .
+ /// Use if you do not want such reinterpretation. ///
public event InventoryChangelogDelegate InventoryChanged; @@ -34,7 +38,7 @@ public interface IGameInventory /// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes /// as a move event as appropriate.
/// In other words, , , and - /// do not fire in this event. + /// currently do not fire in this event. ///
public event InventoryChangelogDelegate InventoryChangedRaw; From e4370ed5d3e9d08c4f0d69ac723c96a5594a8f4d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:23:36 +0900 Subject: [PATCH 12/21] Extra note --- Dalamud/Game/Inventory/GameInventoryItem.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 1e71f6914..970f75081 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -108,7 +108,9 @@ public unsafe struct GameInventoryItem : IEquatable /// /// 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. + /// 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 NativeAddress { From 05820ad9c714471cb9bd67d35366316a67c3fa9a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:25:10 +0900 Subject: [PATCH 13/21] Rename --- Dalamud/Game/Inventory/GameInventoryItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 970f75081..912b91f53 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -112,7 +112,7 @@ public unsafe struct GameInventoryItem : IEquatable /// 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 NativeAddress + public nint Address { get { From b2fc0c4ad249ae1d6121b5437ae32abc2f37d8f7 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:33:20 +0900 Subject: [PATCH 14/21] Adjust namespaces --- .../{InventoryChangeArgsTypes => }/InventoryComplexEventArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryEventArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemAddedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemChangedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemMergedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemMovedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemRemovedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemSplitArgs.cs | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryComplexEventArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryEventArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemAddedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemChangedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemMergedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemMovedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemRemovedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemSplitArgs.cs (100%) diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs b/Dalamud/Game/Inventory/InventoryComplexEventArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs rename to Dalamud/Game/Inventory/InventoryComplexEventArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryItemAddedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemAddedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryItemChangedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemChangedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs b/Dalamud/Game/Inventory/InventoryItemMergedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemMergedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryItemMovedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemMovedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryItemRemovedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemRemovedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs b/Dalamud/Game/Inventory/InventoryItemSplitArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs rename to Dalamud/Game/Inventory/InventoryItemSplitArgs.cs From 35b0d53e801ff56a89d6b8396c62be448cedde69 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:59:13 +0900 Subject: [PATCH 15/21] Add typed event variants --- Dalamud/Game/Inventory/GameInventory.cs | 97 +++++++++++++++++++++++ Dalamud/Plugin/Services/IGameInventory.cs | 26 ++++++ 2 files changed, 123 insertions(+) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index b8e81ced7..fffe95d53 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -86,6 +86,24 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory /// public event IGameInventory.InventoryChangedDelegate? ItemMerged; + /// + public event IGameInventory.InventoryChangedDelegate? ItemAddedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemRemovedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemChangedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMovedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemSplitExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; + /// public void Dispose() { @@ -118,6 +136,19 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } } + private static void InvokeSafely(IGameInventory.InventoryChangedDelegate? cb, T arg) + where T : InventoryEventArgs + { + try + { + cb?.Invoke(arg); + } + catch (Exception e) + { + Log.Error(e, "Exception during {argType} callback", arg.Type); + } + } + private void OnFrameworkUpdate(IFramework framework1) { for (var i = 0; i < this.inventoryTypes.Length; i++) @@ -279,22 +310,40 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory .Concat(this.mergedEvents))); foreach (var x in this.addedEvents) + { InvokeSafely(this.ItemAdded, x); + InvokeSafely(this.ItemAddedExplicit, x); + } foreach (var x in this.removedEvents) + { InvokeSafely(this.ItemRemoved, x); + InvokeSafely(this.ItemRemovedExplicit, x); + } foreach (var x in this.changedEvents) + { InvokeSafely(this.ItemChanged, x); + InvokeSafely(this.ItemChangedExplicit, x); + } foreach (var x in this.movedEvents) + { InvokeSafely(this.ItemMoved, x); + InvokeSafely(this.ItemMovedExplicit, x); + } foreach (var x in this.splitEvents) + { InvokeSafely(this.ItemSplit, x); + InvokeSafely(this.ItemSplitExplicit, x); + } foreach (var x in this.mergedEvents) + { InvokeSafely(this.ItemMerged, x); + InvokeSafely(this.ItemMergedExplicit, x); + } // We're done using the lists. Clean them up. this.addedEvents.Clear(); @@ -381,6 +430,24 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public event IGameInventory.InventoryChangedDelegate? ItemMerged; + /// + public event IGameInventory.InventoryChangedDelegate? ItemAddedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemRemovedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemChangedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMovedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemSplitExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; + /// public void Dispose() { @@ -392,6 +459,12 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; this.gameInventoryService.ItemSplit -= this.OnInventoryItemSplitForward; this.gameInventoryService.ItemMerged -= this.OnInventoryItemMergedForward; + this.gameInventoryService.ItemAddedExplicit -= this.OnInventoryItemAddedExplicitForward; + this.gameInventoryService.ItemRemovedExplicit -= this.OnInventoryItemRemovedExplicitForward; + this.gameInventoryService.ItemChangedExplicit -= this.OnInventoryItemChangedExplicitForward; + this.gameInventoryService.ItemMovedExplicit -= this.OnInventoryItemMovedExplicitForward; + this.gameInventoryService.ItemSplitExplicit -= this.OnInventoryItemSplitExplicitForward; + this.gameInventoryService.ItemMergedExplicit -= this.OnInventoryItemMergedExplicitForward; this.InventoryChanged = null; this.InventoryChangedRaw = null; @@ -401,6 +474,12 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.ItemMoved = null; this.ItemSplit = null; this.ItemMerged = null; + this.ItemAddedExplicit = null; + this.ItemRemovedExplicit = null; + this.ItemChangedExplicit = null; + this.ItemMovedExplicit = null; + this.ItemSplitExplicit = null; + this.ItemMergedExplicit = null; } private void OnInventoryChangedForward(IReadOnlyCollection events) @@ -426,4 +505,22 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven private void OnInventoryItemMergedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemMerged?.Invoke(type, data); + + private void OnInventoryItemAddedExplicitForward(InventoryItemAddedArgs data) + => this.ItemAddedExplicit?.Invoke(data); + + private void OnInventoryItemRemovedExplicitForward(InventoryItemRemovedArgs data) + => this.ItemRemovedExplicit?.Invoke(data); + + private void OnInventoryItemChangedExplicitForward(InventoryItemChangedArgs data) + => this.ItemChangedExplicit?.Invoke(data); + + private void OnInventoryItemMovedExplicitForward(InventoryItemMovedArgs data) + => this.ItemMovedExplicit?.Invoke(data); + + private void OnInventoryItemSplitExplicitForward(InventoryItemSplitArgs data) + => this.ItemSplitExplicit?.Invoke(data); + + private void OnInventoryItemMergedExplicitForward(InventoryItemMergedArgs data) + => this.ItemMergedExplicit?.Invoke(data); } diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 6e84e780a..cd289bc54 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -25,6 +25,14 @@ public interface IGameInventory /// Data for the triggered event. public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data); + /// + /// Delegate function to be called for each change to inventories. + /// This delegate sends individual events for changes. + /// + /// The event arg type. + /// Data for the triggered event. + public delegate void InventoryChangedDelegate(T data) where T : InventoryEventArgs; + /// /// Event that is fired when the inventory has been changed.
/// Note that some events, such as , , and @@ -77,4 +85,22 @@ public interface IGameInventory /// Event that is fired when an item is merged from two stacks into one. ///
public event InventoryChangedDelegate ItemMerged; + + /// + public event InventoryChangedDelegate ItemAddedExplicit; + + /// + public event InventoryChangedDelegate ItemRemovedExplicit; + + /// + public event InventoryChangedDelegate ItemChangedExplicit; + + /// + public event InventoryChangedDelegate ItemMovedExplicit; + + /// + public event InventoryChangedDelegate ItemSplitExplicit; + + /// + public event InventoryChangedDelegate ItemMergedExplicit; } From e7afde82b23457cf695d686533f9fc42207370a1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 11:02:37 +0900 Subject: [PATCH 16/21] fix strange change --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 090e0c244..cc6687524 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 090e0c244df668454616026188c1363e5d25a1bc +Subproject commit cc668752416a8459a3c23345c51277e359803de8 From 6b4094d89a0aff26a2468d79fa3f9cafa45fc413 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 11:06:11 +0900 Subject: [PATCH 17/21] Fix missing event handler registration --- Dalamud/Game/Inventory/GameInventory.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index fffe95d53..fba950c09 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -404,6 +404,12 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.ItemChanged += this.OnInventoryItemChangedForward; this.gameInventoryService.ItemSplit += this.OnInventoryItemSplitForward; this.gameInventoryService.ItemMerged += this.OnInventoryItemMergedForward; + this.gameInventoryService.ItemAddedExplicit += this.OnInventoryItemAddedExplicitForward; + this.gameInventoryService.ItemRemovedExplicit += this.OnInventoryItemRemovedExplicitForward; + this.gameInventoryService.ItemChangedExplicit += this.OnInventoryItemChangedExplicitForward; + this.gameInventoryService.ItemMovedExplicit += this.OnInventoryItemMovedExplicitForward; + this.gameInventoryService.ItemSplitExplicit += this.OnInventoryItemSplitExplicitForward; + this.gameInventoryService.ItemMergedExplicit += this.OnInventoryItemMergedExplicitForward; } /// From 5f0b65a6c4cbe2b3ed272391a5bb6a35b2c8d45a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 11:08:12 +0900 Subject: [PATCH 18/21] last --- .../{ => InventoryEventArgTypes}/InventoryComplexEventArgs.cs | 0 .../Inventory/{ => InventoryEventArgTypes}/InventoryEventArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemAddedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemChangedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemMergedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemMovedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemRemovedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemSplitArgs.cs | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryComplexEventArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryEventArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemAddedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemChangedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemMergedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemMovedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemRemovedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemSplitArgs.cs (100%) diff --git a/Dalamud/Game/Inventory/InventoryComplexEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryComplexEventArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryEventArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemAddedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemChangedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemMergedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemMergedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemMovedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemRemovedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemSplitArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemSplitArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs From e594d59986123c438a364daaef908097e13a9409 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 12:58:55 +0900 Subject: [PATCH 19/21] Enable tracking only when there exists a subscriber --- Dalamud/Game/Inventory/GameInventory.cs | 433 +++++++++--------- .../InventoryComplexEventArgs.cs | 2 +- .../InventoryEventArgs.cs | 2 +- .../InventoryItemAddedArgs.cs | 2 +- .../InventoryItemChangedArgs.cs | 2 +- .../InventoryItemMergedArgs.cs | 2 +- .../InventoryItemMovedArgs.cs | 2 +- .../InventoryItemRemovedArgs.cs | 2 +- .../InventoryItemSplitArgs.cs | 2 +- .../Internal/Windows/Data/DataWindow.cs | 1 + .../Windows/Data/GameInventoryTestWidget.cs | 163 +++++++ Dalamud/Plugin/Services/IGameInventory.cs | 2 +- 12 files changed, 381 insertions(+), 234 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index fba950c09..9a0388113 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -2,15 +2,13 @@ using System.Collections.Generic; using System.Linq; -using Dalamud.Configuration.Internal; -using Dalamud.Game.Inventory.InventoryChangeArgsTypes; +using Dalamud.Game.Inventory.InventoryEventArgTypes; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; -using Serilog.Events; - namespace Dalamud.Game.Inventory; /// @@ -18,9 +16,10 @@ namespace Dalamud.Game.Inventory; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal class GameInventory : IDisposable, IServiceType, IGameInventory +internal class GameInventory : IDisposable, IServiceType { - private static readonly ModuleLog Log = new(nameof(GameInventory)); + private readonly List subscribersPendingChange = new(); + private readonly List subscribers = new(); private readonly List addedEvents = new(); private readonly List removedEvents = new(); @@ -32,120 +31,58 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); - private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; + private bool subscribersChanged; + [ServiceManager.ServiceConstructor] private GameInventory() { this.inventoryTypes = Enum.GetValues(); this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; - - this.framework.Update += this.OnFrameworkUpdate; - - // Separate log logic as an event handler. - this.InventoryChanged += events => - { - if (this.dalamudConfiguration.LogLevel > LogEventLevel.Verbose) - return; - - foreach (var e in events) - { - if (e is InventoryComplexEventArgs icea) - Log.Verbose($"{icea}\n\t├ {icea.SourceEvent}\n\t└ {icea.TargetEvent}"); - else - Log.Verbose($"{e}"); - } - }; } - /// - public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; - - /// - public event IGameInventory.InventoryChangelogDelegate? InventoryChangedRaw; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemAdded; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemRemoved; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemChanged; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemMoved; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemSplit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemMerged; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemAddedExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemRemovedExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemChangedExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemMovedExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemSplitExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; - /// public void Dispose() { - this.framework.Update -= this.OnFrameworkUpdate; - } - - private static void InvokeSafely( - IGameInventory.InventoryChangelogDelegate? cb, - IReadOnlyCollection data) - { - try + lock (this.subscribersPendingChange) { - cb?.Invoke(data); - } - catch (Exception e) - { - Log.Error(e, "Exception during batch callback"); + this.subscribers.Clear(); + this.subscribersPendingChange.Clear(); + this.subscribersChanged = false; + this.framework.Update -= this.OnFrameworkUpdate; } } - private static void InvokeSafely(IGameInventory.InventoryChangedDelegate? cb, InventoryEventArgs arg) + /// + /// Subscribe to events. + /// + /// The event target. + public void Subscribe(GameInventoryPluginScoped s) { - try + lock (this.subscribersPendingChange) { - cb?.Invoke(arg.Type, arg); - } - catch (Exception e) - { - Log.Error(e, "Exception during {argType} callback", arg.Type); + this.subscribersPendingChange.Add(s); + this.subscribersChanged = true; + if (this.subscribersPendingChange.Count == 1) + this.framework.Update += this.OnFrameworkUpdate; } } - private static void InvokeSafely(IGameInventory.InventoryChangedDelegate? cb, T arg) - where T : InventoryEventArgs + /// + /// Unsubscribe from events. + /// + /// The event target. + public void Unsubscribe(GameInventoryPluginScoped s) { - try + lock (this.subscribersPendingChange) { - cb?.Invoke(arg); - } - catch (Exception e) - { - Log.Error(e, "Exception during {argType} callback", arg.Type); + if (!this.subscribersPendingChange.Remove(s)) + return; + this.subscribersChanged = true; + if (this.subscribersPendingChange.Count == 0) + this.framework.Update -= this.OnFrameworkUpdate; } } @@ -193,18 +130,40 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0) return; + // Make a copy of subscribers, to accommodate self removal during the loop. + if (this.subscribersChanged) + { + bool isNew; + lock (this.subscribersPendingChange) + { + isNew = this.subscribersPendingChange.Any() && !this.subscribers.Any(); + this.subscribers.Clear(); + this.subscribers.AddRange(this.subscribersPendingChange); + this.subscribersChanged = false; + } + + // Is this the first time (resuming) scanning for changes? Then discard the "changes". + if (isNew) + { + this.addedEvents.Clear(); + this.removedEvents.Clear(); + this.changedEvents.Clear(); + return; + } + } + // Broadcast InventoryChangedRaw. // Same reason with the above on why are there 3 lists of events involved. - InvokeSafely( - this.InventoryChangedRaw, - new DeferredReadOnlyCollection( - this.addedEvents.Count + - this.removedEvents.Count + - this.changedEvents.Count, - () => Array.Empty() - .Concat(this.addedEvents) - .Concat(this.removedEvents) - .Concat(this.changedEvents))); + var allRawEventsCollection = new DeferredReadOnlyCollection( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count, + () => Array.Empty() + .Concat(this.addedEvents) + .Concat(this.removedEvents) + .Concat(this.changedEvents)); + foreach (var s in this.subscribers) + s.InvokeChangedRaw(allRawEventsCollection); // Resolve moved items, from 1 added + 1 removed event. for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) @@ -291,58 +250,32 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } } + // Create a collection view of all events. + var allEventsCollection = new DeferredReadOnlyCollection( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count + + this.movedEvents.Count + + this.splitEvents.Count + + this.mergedEvents.Count, + () => Array.Empty() + .Concat(this.addedEvents) + .Concat(this.removedEvents) + .Concat(this.changedEvents) + .Concat(this.movedEvents) + .Concat(this.splitEvents) + .Concat(this.mergedEvents)); + // Broadcast the rest. - InvokeSafely( - this.InventoryChanged, - new DeferredReadOnlyCollection( - this.addedEvents.Count + - this.removedEvents.Count + - this.changedEvents.Count + - this.movedEvents.Count + - this.splitEvents.Count + - this.mergedEvents.Count, - () => Array.Empty() - .Concat(this.addedEvents) - .Concat(this.removedEvents) - .Concat(this.changedEvents) - .Concat(this.movedEvents) - .Concat(this.splitEvents) - .Concat(this.mergedEvents))); - - foreach (var x in this.addedEvents) + foreach (var s in this.subscribers) { - InvokeSafely(this.ItemAdded, x); - InvokeSafely(this.ItemAddedExplicit, x); - } - - foreach (var x in this.removedEvents) - { - InvokeSafely(this.ItemRemoved, x); - InvokeSafely(this.ItemRemovedExplicit, x); - } - - foreach (var x in this.changedEvents) - { - InvokeSafely(this.ItemChanged, x); - InvokeSafely(this.ItemChangedExplicit, x); - } - - foreach (var x in this.movedEvents) - { - InvokeSafely(this.ItemMoved, x); - InvokeSafely(this.ItemMovedExplicit, x); - } - - foreach (var x in this.splitEvents) - { - InvokeSafely(this.ItemSplit, x); - InvokeSafely(this.ItemSplitExplicit, x); - } - - foreach (var x in this.mergedEvents) - { - InvokeSafely(this.ItemMerged, x); - InvokeSafely(this.ItemMergedExplicit, x); + s.InvokeChanged(allEventsCollection); + s.Invoke(this.addedEvents); + s.Invoke(this.removedEvents); + s.Invoke(this.changedEvents); + s.Invoke(this.movedEvents); + s.Invoke(this.splitEvents); + s.Invoke(this.mergedEvents); } // We're done using the lists. Clean them up. @@ -388,29 +321,15 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory #pragma warning restore SA1015 internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory { + private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped)); + [ServiceManager.ServiceDependency] private readonly GameInventory gameInventoryService = Service.Get(); /// /// Initializes a new instance of the class. /// - public GameInventoryPluginScoped() - { - this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward; - this.gameInventoryService.InventoryChangedRaw += this.OnInventoryChangedRawForward; - this.gameInventoryService.ItemAdded += this.OnInventoryItemAddedForward; - this.gameInventoryService.ItemRemoved += this.OnInventoryItemRemovedForward; - this.gameInventoryService.ItemMoved += this.OnInventoryItemMovedForward; - this.gameInventoryService.ItemChanged += this.OnInventoryItemChangedForward; - this.gameInventoryService.ItemSplit += this.OnInventoryItemSplitForward; - this.gameInventoryService.ItemMerged += this.OnInventoryItemMergedForward; - this.gameInventoryService.ItemAddedExplicit += this.OnInventoryItemAddedExplicitForward; - this.gameInventoryService.ItemRemovedExplicit += this.OnInventoryItemRemovedExplicitForward; - this.gameInventoryService.ItemChangedExplicit += this.OnInventoryItemChangedExplicitForward; - this.gameInventoryService.ItemMovedExplicit += this.OnInventoryItemMovedExplicitForward; - this.gameInventoryService.ItemSplitExplicit += this.OnInventoryItemSplitExplicitForward; - this.gameInventoryService.ItemMergedExplicit += this.OnInventoryItemMergedExplicitForward; - } + public GameInventoryPluginScoped() => this.gameInventoryService.Subscribe(this); /// public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; @@ -457,20 +376,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public void Dispose() { - this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward; - this.gameInventoryService.InventoryChangedRaw -= this.OnInventoryChangedRawForward; - this.gameInventoryService.ItemAdded -= this.OnInventoryItemAddedForward; - this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; - this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; - this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; - this.gameInventoryService.ItemSplit -= this.OnInventoryItemSplitForward; - this.gameInventoryService.ItemMerged -= this.OnInventoryItemMergedForward; - this.gameInventoryService.ItemAddedExplicit -= this.OnInventoryItemAddedExplicitForward; - this.gameInventoryService.ItemRemovedExplicit -= this.OnInventoryItemRemovedExplicitForward; - this.gameInventoryService.ItemChangedExplicit -= this.OnInventoryItemChangedExplicitForward; - this.gameInventoryService.ItemMovedExplicit -= this.OnInventoryItemMovedExplicitForward; - this.gameInventoryService.ItemSplitExplicit -= this.OnInventoryItemSplitExplicitForward; - this.gameInventoryService.ItemMergedExplicit -= this.OnInventoryItemMergedExplicitForward; + this.gameInventoryService.Unsubscribe(this); this.InventoryChanged = null; this.InventoryChangedRaw = null; @@ -488,45 +394,122 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.ItemMergedExplicit = null; } - private void OnInventoryChangedForward(IReadOnlyCollection events) - => this.InventoryChanged?.Invoke(events); + /// + /// Invoke . + /// + /// The data. + internal void InvokeChanged(IReadOnlyCollection data) + { + try + { + this.InventoryChanged?.Invoke(data); + } + catch (Exception e) + { + Log.Error( + e, + "[{plugin}] Exception during {argType} callback", + Service.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)", + nameof(this.InventoryChanged)); + } + } - private void OnInventoryChangedRawForward(IReadOnlyCollection events) - => this.InventoryChangedRaw?.Invoke(events); + /// + /// Invoke . + /// + /// The data. + internal void InvokeChangedRaw(IReadOnlyCollection data) + { + try + { + this.InventoryChangedRaw?.Invoke(data); + } + catch (Exception e) + { + Log.Error( + e, + "[{plugin}] Exception during {argType} callback", + Service.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)", + nameof(this.InventoryChangedRaw)); + } + } + + // Note below: using List instead of IEnumerable, since List has a specialized lightweight enumerator. - private void OnInventoryItemAddedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemAdded?.Invoke(type, data); + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemAdded, this.ItemAddedExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemRemoved, this.ItemRemovedExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemChanged, this.ItemChangedExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemMoved, this.ItemMovedExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemSplit, this.ItemSplitExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemMerged, this.ItemMergedExplicit, events); + + private static void Invoke( + IGameInventory.InventoryChangedDelegate? cb, + IGameInventory.InventoryChangedDelegate? cbt, + List events) where T : InventoryEventArgs + { + foreach (var evt in events) + { + try + { + cb?.Invoke(evt.Type, evt); + } + catch (Exception e) + { + Log.Error( + e, + "[{plugin}] Exception during untyped callback for {evt}", + Service.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)", + evt); + } - private void OnInventoryItemRemovedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemRemoved?.Invoke(type, data); - - private void OnInventoryItemChangedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemChanged?.Invoke(type, data); - - private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemMoved?.Invoke(type, data); - - private void OnInventoryItemSplitForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemSplit?.Invoke(type, data); - - private void OnInventoryItemMergedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemMerged?.Invoke(type, data); - - private void OnInventoryItemAddedExplicitForward(InventoryItemAddedArgs data) - => this.ItemAddedExplicit?.Invoke(data); - - private void OnInventoryItemRemovedExplicitForward(InventoryItemRemovedArgs data) - => this.ItemRemovedExplicit?.Invoke(data); - - private void OnInventoryItemChangedExplicitForward(InventoryItemChangedArgs data) - => this.ItemChangedExplicit?.Invoke(data); - - private void OnInventoryItemMovedExplicitForward(InventoryItemMovedArgs data) - => this.ItemMovedExplicit?.Invoke(data); - - private void OnInventoryItemSplitExplicitForward(InventoryItemSplitArgs data) - => this.ItemSplitExplicit?.Invoke(data); - - private void OnInventoryItemMergedExplicitForward(InventoryItemMergedArgs data) - => this.ItemMergedExplicit?.Invoke(data); + try + { + cbt?.Invoke(evt); + } + catch (Exception e) + { + Log.Error( + e, + "[{plugin}] Exception during typed callback for {evt}", + Service.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)", + evt); + } + } + } } diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs index c44bfb991..95d7e8238 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being affected across different slots, possibly in different containers. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs index 8197e28f5..198e0395b 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Abstract base class representing inventory changed events. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs index 45a35739a..ceb64c6f9 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being added to an inventory. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs index 191cfa1d8..372418793 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an items properties being changed. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs index 0f088f24b..d7056356e 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being merged from two stacks into one. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs index 6a59d1304..8d0bbca17 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being moved from one inventory and added to another. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs index fe40c870b..5677e3cc4 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being removed from an inventory. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs index 2a3d41c09..5f717cf60 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being split from one stack into two. diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index e9d4152a5..20c3d6d01 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -33,6 +33,7 @@ internal class DataWindow : Window new FateTableWidget(), new FlyTextWidget(), new FontAwesomeTestWidget(), + new GameInventoryTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), diff --git a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs new file mode 100644 index 000000000..c19f56654 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; + +using Dalamud.Configuration.Internal; +using Dalamud.Game.Inventory; +using Dalamud.Game.Inventory.InventoryEventArgTypes; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +using Serilog.Events; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Tester for . +/// +internal class GameInventoryTestWidget : IDataWindowWidget +{ + private static readonly ModuleLog Log = new(nameof(GameInventoryTestWidget)); + + private GameInventoryPluginScoped? scoped; + private bool standardEnabled; + private bool rawEnabled; + + /// + public string[]? CommandShortcuts { get; init; } = { "gameinventorytest" }; + + /// + public string DisplayName { get; init; } = "GameInventory Test"; + + /// + public bool Ready { get; set; } + + /// + public void Load() => this.Ready = true; + + /// + public void Draw() + { + if (Service.Get().LogLevel > LogEventLevel.Information) + { + ImGuiHelpers.SafeTextColoredWrapped( + ImGuiColors.DalamudRed, + "Enable LogLevel=Information display to see the logs."); + } + + using var table = ImRaii.Table(this.DisplayName, 3, ImGuiTableFlags.SizingFixedFit); + if (!table.Success) + return; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Standard Logging"); + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(this.standardEnabled)) + { + if (ImGui.Button("Enable##standard-enable") && !this.standardEnabled) + { + this.scoped ??= new(); + this.scoped.InventoryChanged += ScopedOnInventoryChanged; + this.standardEnabled = true; + } + } + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(!this.standardEnabled)) + { + if (ImGui.Button("Disable##standard-disable") && this.scoped is not null && this.standardEnabled) + { + this.scoped.InventoryChanged -= ScopedOnInventoryChanged; + this.standardEnabled = false; + if (!this.rawEnabled) + { + this.scoped.Dispose(); + this.scoped = null; + } + } + } + + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Raw Logging"); + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(this.rawEnabled)) + { + if (ImGui.Button("Enable##raw-enable") && !this.rawEnabled) + { + this.scoped ??= new(); + this.scoped.InventoryChangedRaw += ScopedOnInventoryChangedRaw; + this.rawEnabled = true; + } + } + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(!this.rawEnabled)) + { + if (ImGui.Button("Disable##raw-disable") && this.scoped is not null && this.rawEnabled) + { + this.scoped.InventoryChangedRaw -= ScopedOnInventoryChangedRaw; + this.rawEnabled = false; + if (!this.standardEnabled) + { + this.scoped.Dispose(); + this.scoped = null; + } + } + } + + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("All"); + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(this.standardEnabled && this.rawEnabled)) + { + if (ImGui.Button("Enable##all-enable")) + { + this.scoped ??= new(); + if (!this.standardEnabled) + this.scoped.InventoryChanged += ScopedOnInventoryChanged; + if (!this.rawEnabled) + this.scoped.InventoryChangedRaw += ScopedOnInventoryChangedRaw; + this.standardEnabled = this.rawEnabled = true; + } + } + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(this.scoped is null)) + { + if (ImGui.Button("Disable##all-disable")) + { + this.scoped?.Dispose(); + this.scoped = null; + this.standardEnabled = this.rawEnabled = false; + } + } + } + + private static void ScopedOnInventoryChangedRaw(IReadOnlyCollection events) + { + var i = 0; + foreach (var e in events) + Log.Information($"[{++i}/{events.Count}] Raw: {e}"); + } + + private static void ScopedOnInventoryChanged(IReadOnlyCollection events) + { + var i = 0; + foreach (var e in events) + { + if (e is InventoryComplexEventArgs icea) + Log.Information($"[{++i}/{events.Count}] {icea}\n\t├ {icea.SourceEvent}\n\t└ {icea.TargetEvent}"); + else + Log.Information($"[{++i}/{events.Count}] {e}"); + } + } +} diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index cd289bc54..a1b1114d7 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Dalamud.Game.Inventory; -using Dalamud.Game.Inventory.InventoryChangeArgsTypes; +using Dalamud.Game.Inventory.InventoryEventArgTypes; namespace Dalamud.Plugin.Services; From 841c47e1866db19b87a4dad81cc527bea4d53313 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 13:44:28 +0900 Subject: [PATCH 20/21] Use RaptureAtkModule.Update as a cue for checking inventory changes --- Dalamud/Game/Inventory/GameInventory.cs | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 9a0388113..4dc7d7251 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -3,12 +3,15 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Game.Inventory.InventoryEventArgTypes; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.UI; + namespace Dalamud.Game.Inventory; /// @@ -31,18 +34,30 @@ internal class GameInventory : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + private readonly Hook raptureAtkModuleUpdateHook; + private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; private bool subscribersChanged; + private bool inventoriesMightBeChanged; [ServiceManager.ServiceConstructor] private GameInventory() { this.inventoryTypes = Enum.GetValues(); this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; + + unsafe + { + this.raptureAtkModuleUpdateHook = Hook.FromFunctionPointerVariable( + new(&((RaptureAtkModule.RaptureAtkModuleVTable*)RaptureAtkModule.StaticAddressPointers.VTable)->Update), + this.RaptureAtkModuleUpdateDetour); + } } + private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); + /// public void Dispose() { @@ -52,6 +67,7 @@ internal class GameInventory : IDisposable, IServiceType this.subscribersPendingChange.Clear(); this.subscribersChanged = false; this.framework.Update -= this.OnFrameworkUpdate; + this.raptureAtkModuleUpdateHook.Dispose(); } } @@ -66,7 +82,11 @@ internal class GameInventory : IDisposable, IServiceType this.subscribersPendingChange.Add(s); this.subscribersChanged = true; if (this.subscribersPendingChange.Count == 1) + { + this.inventoriesMightBeChanged = true; this.framework.Update += this.OnFrameworkUpdate; + this.raptureAtkModuleUpdateHook.Enable(); + } } } @@ -82,12 +102,20 @@ internal class GameInventory : IDisposable, IServiceType return; this.subscribersChanged = true; if (this.subscribersPendingChange.Count == 0) + { this.framework.Update -= this.OnFrameworkUpdate; + this.raptureAtkModuleUpdateHook.Disable(); + } } } private void OnFrameworkUpdate(IFramework framework1) { + if (!this.inventoriesMightBeChanged) + return; + + this.inventoriesMightBeChanged = false; + for (var i = 0; i < this.inventoryTypes.Length; i++) { var newItems = GameInventoryItem.GetReadOnlySpanOfInventory(this.inventoryTypes[i]); @@ -287,6 +315,12 @@ internal class GameInventory : IDisposable, IServiceType this.mergedEvents.Clear(); } + private unsafe void RaptureAtkModuleUpdateDetour(RaptureAtkModule* ram, float f1) + { + this.inventoriesMightBeChanged |= ram->AgentUpdateFlag != 0; + this.raptureAtkModuleUpdateHook.Original(ram, f1); + } + /// /// A view of , so that the number of items /// contained within can be known in advance, and it can be enumerated multiple times. From ba5e3407d62db4ef88e6640cb50f3948d944bce4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 13:53:00 +0900 Subject: [PATCH 21/21] Permaenable raptureAtkModuleUpdateHook --- Dalamud/Game/Inventory/GameInventory.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 4dc7d7251..1c7f3e3bf 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -54,6 +54,8 @@ internal class GameInventory : IDisposable, IServiceType new(&((RaptureAtkModule.RaptureAtkModuleVTable*)RaptureAtkModule.StaticAddressPointers.VTable)->Update), this.RaptureAtkModuleUpdateDetour); } + + this.raptureAtkModuleUpdateHook.Enable(); } private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); @@ -85,7 +87,6 @@ internal class GameInventory : IDisposable, IServiceType { this.inventoriesMightBeChanged = true; this.framework.Update += this.OnFrameworkUpdate; - this.raptureAtkModuleUpdateHook.Enable(); } } } @@ -102,10 +103,7 @@ internal class GameInventory : IDisposable, IServiceType return; this.subscribersChanged = true; if (this.subscribersPendingChange.Count == 0) - { this.framework.Update -= this.OnFrameworkUpdate; - this.raptureAtkModuleUpdateHook.Disable(); - } } }