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] [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; +}