From d52118b3ad366a61216129c80c0fa250c885abac Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Wed, 29 Nov 2023 00:57:51 +0100 Subject: [PATCH 01/51] chore: bump up timeout to 120 seconds for now --- Dalamud/ServiceManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 46a6ba509..21c08ce72 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -194,7 +194,7 @@ internal static class ServiceManager try { var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); - while (await Task.WhenAny(whenBlockingComplete, Task.Delay(30000)) != whenBlockingComplete) + while (await Task.WhenAny(whenBlockingComplete, Task.Delay(120000)) != whenBlockingComplete) { if (NativeFunctions.MessageBoxW( IntPtr.Zero, 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 02/51] [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 03/51] 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 04/51] 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 05/51] 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 06/51] 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 07/51] 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 08/51] 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 09/51] 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 10/51] 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 11/51] 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 12/51] 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 13/51] 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 14/51] 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 15/51] 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 16/51] 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 17/51] 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 18/51] 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 19/51] 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 20/51] 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 21/51] 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 22/51] 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(); - } } } From 37bcff84b17714343ad52c9d3aa583874ecbc53c Mon Sep 17 00:00:00 2001 From: Sirius902 <10891979+Sirius902@users.noreply.github.com> Date: Sat, 2 Dec 2023 20:25:43 -0800 Subject: [PATCH 23/51] Fix Dalamud trying to unload IServiceType and crashing (#1557) --- Dalamud/ServiceManager.cs | 2 +- Dalamud/Service{T}.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 21c08ce72..00447da9e 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -336,7 +336,7 @@ internal static class ServiceManager foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - if (!serviceType.IsAssignableTo(typeof(IServiceType))) + if (serviceType.IsAbstract || !serviceType.IsAssignableTo(typeof(IServiceType))) continue; // Scoped services shall never be unloaded here. diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 9c7f0411d..08c362433 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -176,7 +176,7 @@ internal static class Service where T : IServiceType { foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - if (!serviceType.IsAssignableTo(typeof(IServiceType))) + if (serviceType.IsAbstract || !serviceType.IsAssignableTo(typeof(IServiceType))) continue; if (serviceType == typeof(PluginManager)) From 70249a4db00a462c62a88786eae6c72366b23fdf Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 3 Dec 2023 13:57:53 +0900 Subject: [PATCH 24/51] Revert "Fix Dalamud trying to unload IServiceType and crashing (#1557)" (#1559) This reverts commit 37bcff84b17714343ad52c9d3aa583874ecbc53c. --- Dalamud/ServiceManager.cs | 2 +- Dalamud/Service{T}.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 00447da9e..21c08ce72 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -336,7 +336,7 @@ internal static class ServiceManager foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - if (serviceType.IsAbstract || !serviceType.IsAssignableTo(typeof(IServiceType))) + if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; // Scoped services shall never be unloaded here. diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 08c362433..9c7f0411d 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -176,7 +176,7 @@ internal static class Service where T : IServiceType { foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - if (serviceType.IsAbstract || !serviceType.IsAssignableTo(typeof(IServiceType))) + if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; if (serviceType == typeof(PluginManager)) From 5777745ab3cc0a52ee1c7f799aae19c312513095 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 7 Dec 2023 14:06:39 +0900 Subject: [PATCH 25/51] Add injector option to not apply any exception handlers (#1541) * Add injector option to not apply any exception handlers * Log as warning if NoExceptionHandlers is set --- Dalamud.Boot/DalamudStartInfo.cpp | 1 + Dalamud.Boot/DalamudStartInfo.h | 1 + Dalamud.Boot/dllmain.cpp | 4 +++- Dalamud.Common/DalamudStartInfo.cs | 37 ++++-------------------------- Dalamud.Injector/EntryPoint.cs | 6 +++-- Dalamud/EntryPoint.cs | 6 +++-- 6 files changed, 18 insertions(+), 37 deletions(-) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 15faf82ad..e2fed1beb 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -103,6 +103,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { } config.CrashHandlerShow = json.value("CrashHandlerShow", config.CrashHandlerShow); + config.NoExceptionHandlers = json.value("NoExceptionHandlers", config.NoExceptionHandlers); } void DalamudStartInfo::from_envvars() { diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 66109abf7..73a1a0d34 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -49,6 +49,7 @@ struct DalamudStartInfo { std::set BootUnhookDlls{}; bool CrashHandlerShow = false; + bool NoExceptionHandlers = false; friend void from_json(const nlohmann::json&, DalamudStartInfo&); void from_envvars(); diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 94f1c7d0f..8ffef40b0 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -133,7 +133,9 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { // ============================== VEH ======================================== // logging::I("Initializing VEH..."); - if (utils::is_running_on_wine()) { + if (g_startInfo.NoExceptionHandlers) { + logging::W("=> Exception handlers are disabled from DalamudStartInfo."); + } else if (utils::is_running_on_wine()) { logging::I("=> VEH was disabled, running on wine"); } else if (g_startInfo.BootVehEnabled) { if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory)) diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index 069a0ef9f..5126fe3a4 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -17,38 +17,6 @@ public record DalamudStartInfo // ignored } - /// - /// Initializes a new instance of the class. - /// - /// Object to copy values from. - public DalamudStartInfo(DalamudStartInfo other) - { - this.WorkingDirectory = other.WorkingDirectory; - this.ConfigurationPath = other.ConfigurationPath; - this.LogPath = other.LogPath; - this.LogName = other.LogName; - this.PluginDirectory = other.PluginDirectory; - this.AssetDirectory = other.AssetDirectory; - this.Language = other.Language; - this.GameVersion = other.GameVersion; - this.DelayInitializeMs = other.DelayInitializeMs; - this.TroubleshootingPackData = other.TroubleshootingPackData; - this.NoLoadPlugins = other.NoLoadPlugins; - this.NoLoadThirdPartyPlugins = other.NoLoadThirdPartyPlugins; - this.BootLogPath = other.BootLogPath; - this.BootShowConsole = other.BootShowConsole; - this.BootDisableFallbackConsole = other.BootDisableFallbackConsole; - this.BootWaitMessageBox = other.BootWaitMessageBox; - this.BootWaitDebugger = other.BootWaitDebugger; - this.BootVehEnabled = other.BootVehEnabled; - this.BootVehFull = other.BootVehFull; - this.BootEnableEtw = other.BootEnableEtw; - this.BootDotnetOpenProcessHookMode = other.BootDotnetOpenProcessHookMode; - this.BootEnabledGameFixes = other.BootEnabledGameFixes; - this.BootUnhookDlls = other.BootUnhookDlls; - this.CrashHandlerShow = other.CrashHandlerShow; - } - /// /// Gets or sets the working directory of the XIVLauncher installations. /// @@ -169,4 +137,9 @@ public record DalamudStartInfo /// Gets or sets a value indicating whether to show crash handler console window. /// public bool CrashHandlerShow { get; set; } + + /// + /// Gets or sets a value indicating whether to disable all kinds of global exception handlers. + /// + public bool NoExceptionHandlers { get; set; } } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index bd9fa87f8..3ffb7ba18 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -96,6 +96,7 @@ namespace Dalamud.Injector args.Remove("--no-plugin"); args.Remove("--no-3rd-plugin"); args.Remove("--crash-handler-console"); + args.Remove("--no-exception-handlers"); var mainCommand = args[1].ToLowerInvariant(); if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand) @@ -393,6 +394,7 @@ namespace Dalamud.Injector startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-3rd-plugin"); // startInfo.BootUnhookDlls = new List() { "kernel32.dll", "ntdll.dll", "user32.dll" }; startInfo.CrashHandlerShow = args.Contains("--crash-handler-console"); + startInfo.NoExceptionHandlers = args.Contains("--no-exception-handlers"); return startInfo; } @@ -434,7 +436,7 @@ namespace Dalamud.Injector Console.WriteLine("Verbose logging:\t[-v]"); Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]"); Console.WriteLine("Enable ETW:\t[--etw]"); - Console.WriteLine("Enable VEH:\t[--veh], [--veh-full]"); + Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--no-exception-handlers]"); Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]"); Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]"); Console.WriteLine("Logging:\t[--logname=] [--logpath=]"); @@ -889,7 +891,7 @@ namespace Dalamud.Injector var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); var gameVer = GameVersion.Parse(gameVerStr); - return new DalamudStartInfo(startInfo) + return startInfo with { GameVersion = gameVer, }; diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index c9537eda6..d0f9e8845 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -147,7 +147,8 @@ public sealed class EntryPoint LogLevelSwitch.MinimumLevel = configuration.LogLevel; // Log any unhandled exception. - AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + if (!info.NoExceptionHandlers) + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; var unloadFailed = false; @@ -196,7 +197,8 @@ public sealed class EntryPoint finally { TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; - AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; + if (!info.NoExceptionHandlers) + AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; Log.Information("Session has ended."); Log.CloseAndFlush(); From a0f4baf8fa81dae18c3d975b07ff4b60e4f4f8d5 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 7 Dec 2023 14:29:46 +0900 Subject: [PATCH 26/51] Less footguns in service dependency handling (#1560) --- .../Game/Addon/Events/AddonEventManager.cs | 8 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 20 +- Dalamud/Game/ClientState/ClientState.cs | 8 +- .../Game/ClientState/Conditions/Condition.cs | 23 +- .../Game/ClientState/GamePad/GamepadState.cs | 7 +- Dalamud/Game/DutyState/DutyState.cs | 8 +- Dalamud/Game/Framework.cs | 10 +- Dalamud/Game/Gui/ChatGui.cs | 12 +- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 8 +- Dalamud/Game/Gui/GameGui.cs | 22 +- Dalamud/Game/Gui/Internal/DalamudIME.cs | 2 +- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 7 +- Dalamud/Game/Gui/Toast/ToastGui.cs | 12 +- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 12 +- Dalamud/Game/Network/GameNetwork.cs | 10 +- .../Interface/Internal/InterfaceManager.cs | 10 +- .../Windows/Data/Widgets/ServicesWidget.cs | 306 +++++++++++++++++- Dalamud/Plugin/Internal/PluginManager.cs | 89 ++++- .../Plugin/Internal/StartupPluginLoader.cs | 50 --- Dalamud/ServiceManager.cs | 127 ++++++-- Dalamud/Service{T}.cs | 165 +++++----- Dalamud/Storage/Assets/DalamudAssetManager.cs | 35 +- Dalamud/Utility/ArrayExtensions.cs | 10 + 23 files changed, 659 insertions(+), 302 deletions(-) delete mode 100644 Dalamud/Plugin/Internal/StartupPluginLoader.cs diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index d8f3427ef..23f3b1a6d 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -57,6 +57,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize); this.addonLifecycle.RegisterListener(this.finalizeEventListener); + + this.onUpdateCursor.Enable(); } private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); @@ -149,12 +151,6 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.onUpdateCursor.Enable(); - } - /// /// When an addon finalizes, check it for any registered events, and unregister them. /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 08a2d59ef..3528de562 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -58,6 +58,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); this.onAddonRefreshHook = Hook.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh); this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); + + this.onAddonSetupHook.Enable(); + this.onAddonSetup2Hook.Enable(); + this.onAddonFinalizeHook.Enable(); + this.onAddonDrawHook.Enable(); + this.onAddonUpdateHook.Enable(); + this.onAddonRefreshHook.Enable(); + this.onAddonRequestedUpdateHook.Enable(); } private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values); @@ -181,18 +189,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.onAddonSetupHook.Enable(); - this.onAddonSetup2Hook.Enable(); - this.onAddonFinalizeHook.Enable(); - this.onAddonDrawHook.Enable(); - this.onAddonUpdateHook.Enable(); - this.onAddonRefreshHook.Enable(); - this.onAddonRequestedUpdateHook.Enable(); - } - private void RegisterReceiveEventHook(AtkUnitBase* addon) { // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 3b3f65128..d387c2e2d 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -58,6 +58,8 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState this.framework.Update += this.FrameworkOnOnUpdateEvent; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; + + this.setupTerritoryTypeHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -120,12 +122,6 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.setupTerritoryTypeHook.Enable(); - } - private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) { this.TerritoryType = terriType; diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 2db47ea4d..a298b1502 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -16,6 +16,9 @@ internal sealed partial class Condition : IServiceType, ICondition /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// internal const int MaxConditionEntries = 104; + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); private readonly bool[] cache = new bool[MaxConditionEntries]; @@ -24,6 +27,12 @@ internal sealed partial class Condition : IServiceType, ICondition { var resolver = clientState.AddressResolver; this.Address = resolver.ConditionFlags; + + // Initialization + for (var i = 0; i < MaxConditionEntries; i++) + this.cache[i] = this[i]; + + this.framework.Update += this.FrameworkUpdate; } /// @@ -80,17 +89,7 @@ internal sealed partial class Condition : IServiceType, ICondition return false; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(Framework framework) - { - // Initialization - for (var i = 0; i < MaxConditionEntries; i++) - this.cache[i] = this[i]; - - framework.Update += this.FrameworkUpdate; - } - - private void FrameworkUpdate(IFramework framework) + private void FrameworkUpdate(IFramework unused) { for (var i = 0; i < MaxConditionEntries; i++) { @@ -144,7 +143,7 @@ internal sealed partial class Condition : IDisposable if (disposing) { - Service.Get().Update -= this.FrameworkUpdate; + this.framework.Update -= this.FrameworkUpdate; } this.isDisposed = true; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index b03db6df2..40e632113 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -38,6 +38,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState var resolver = clientState.AddressResolver; Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}"); this.gamepadPoll = Hook.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour); + this.gamepadPoll?.Enable(); } private delegate int ControllerPoll(IntPtr controllerInput); @@ -114,12 +115,6 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState GC.SuppressFinalize(this); } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.gamepadPoll?.Enable(); - } - private int GamepadPollDetour(IntPtr gamepadInput) { var original = this.gamepadPoll!.Original(gamepadInput); diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 66356033b..c4bda0d19 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -37,6 +37,8 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState this.framework.Update += this.FrameworkOnUpdateEvent; this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent; + + this.contentDirectorNetworkMessageHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -67,12 +69,6 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.contentDirectorNetworkMessageHook.Enable(); - } - private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3) { var category = *a3; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 6db9f7312..ce34f2c06 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -58,6 +58,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.updateHook = Hook.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); this.destroyHook = Hook.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); + + this.updateHook.Enable(); + this.destroyHook.Enable(); } /// @@ -330,13 +333,6 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.updateHook.Enable(); - this.destroyHook.Enable(); - } - private void RunPendingTickTasks() { if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 50c5b2908..8f2a617cf 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -50,6 +50,10 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui this.printMessageHook = Hook.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour); this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); this.interactableLinkClickedHook = Hook.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); + + this.printMessageHook.Enable(); + this.populateItemLinkHook.Enable(); + this.interactableLinkClickedHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -182,14 +186,6 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui this.dalamudLinkHandlers.Remove((pluginName, commandId)); } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.printMessageHook.Enable(); - this.populateItemLinkHook.Enable(); - this.interactableLinkClickedHook.Enable(); - } - private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color) { var builder = new SeStringBuilder(); diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 36056883e..2383b4e53 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -36,6 +36,8 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer(this.Address.AddFlyText); this.createFlyTextHook = Hook.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour); + + this.createFlyTextHook.Enable(); } /// @@ -143,12 +145,6 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui return terminated; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(GameGui gameGui) - { - this.createFlyTextHook.Enable(); - } - private IntPtr CreateFlyTextDetour( IntPtr addonFlyText, FlyTextKind kind, diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index a1a17436e..a97e19a0a 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -75,6 +75,15 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.toggleUiHideHook = Hook.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.utf8StringFromSequenceHook = Hook.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); + + this.setGlobalBgmHook.Enable(); + this.handleItemHoverHook.Enable(); + this.handleItemOutHook.Enable(); + this.handleImmHook.Enable(); + this.toggleUiHideHook.Enable(); + this.handleActionHoverHook.Enable(); + this.handleActionOutHook.Enable(); + this.utf8StringFromSequenceHook.Enable(); } // Marshaled delegates @@ -376,19 +385,6 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.GameUiHidden = false; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.setGlobalBgmHook.Enable(); - this.handleItemHoverHook.Enable(); - this.handleItemOutHook.Enable(); - this.handleImmHook.Enable(); - this.toggleUiHideHook.Enable(); - this.handleActionHoverHook.Enable(); - this.handleActionOutHook.Enable(); - this.utf8StringFromSequenceHook.Enable(); - } - private IntPtr HandleSetGlobalBgmDetour(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6) { var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6); diff --git a/Dalamud/Game/Gui/Internal/DalamudIME.cs b/Dalamud/Game/Gui/Internal/DalamudIME.cs index 37c072806..a9f6991ae 100644 --- a/Dalamud/Game/Gui/Internal/DalamudIME.cs +++ b/Dalamud/Game/Gui/Internal/DalamudIME.cs @@ -253,7 +253,7 @@ internal unsafe class DalamudIME : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady] + [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) { try diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 61c0f62e4..4a8332d24 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -35,6 +35,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize); this.receiveListingHook = Hook.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour); + this.receiveListingHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -60,12 +61,6 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(GameGui gameGui) - { - this.receiveListingHook.Enable(); - } - private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data) { try diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 362edb3be..7491b7f13 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -41,6 +41,10 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui this.showNormalToastHook = Hook.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour); this.showQuestToastHook = Hook.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour); this.showErrorToastHook = Hook.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour); + + this.showNormalToastHook.Enable(); + this.showQuestToastHook.Enable(); + this.showErrorToastHook.Enable(); } #region Marshal delegates @@ -109,14 +113,6 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui return terminated; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(GameGui gameGui) - { - this.showNormalToastHook.Enable(); - this.showQuestToastHook.Enable(); - this.showErrorToastHook.Enable(); - } - private SeString ParseString(IntPtr text) { var bytes = new List(); diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 0013dca4d..4eb605a76 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -63,6 +63,10 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings"); // this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened; + + this.hookAgentHudOpenSystemMenu.Enable(); + this.hookUiModuleRequestMainCommand.Enable(); + this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); } private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize); @@ -75,14 +79,6 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5); - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(DalamudInterface dalamudInterface) - { - this.hookAgentHudOpenSystemMenu.Enable(); - this.hookUiModuleRequestMainCommand.Enable(); - this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); - } - /* private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args) { diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 9ea3e491e..4099f228e 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -44,6 +44,9 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork this.processZonePacketDownHook = Hook.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour); this.processZonePacketUpHook = Hook.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); + + this.processZonePacketDownHook.Enable(); + this.processZonePacketUpHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -62,13 +65,6 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork this.processZonePacketUpHook.Dispose(); } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.processZonePacketDownHook.Enable(); - this.processZonePacketUpHook.Enable(); - } - private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr) { this.baseAddress = a; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 52e849c0e..1b12fd853 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1063,14 +1063,10 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction( - TargetSigScanner sigScanner, - DalamudAssetManager dalamudAssetManager, - DalamudConfiguration configuration) + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) { - dalamudAssetManager.WaitForAllRequiredAssets().Wait(); - this.address.Setup(sigScanner); this.framework.RunOnFrameworkThread(() => { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs index 49f3c1b90..22b53cdaa 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; @@ -13,6 +15,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class ServicesWidget : IDataWindowWidget { + private readonly Dictionary nodeRects = new(); + private readonly HashSet selectedNodes = new(); + private readonly HashSet tempRelatedNodes = new(); + + private bool includeUnloadDependencies; + private List>? dependencyNodes; + /// public string[]? CommandShortcuts { get; init; } = { "services" }; @@ -33,27 +42,294 @@ internal class ServicesWidget : IDataWindowWidget { var container = Service.Get(); - foreach (var instance in container.Instances) + if (ImGui.CollapsingHeader("Dependencies")) { - var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); - var isPublic = instance.Key.IsPublic; + if (ImGui.Button("Clear selection")) + this.selectedNodes.Clear(); - ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); - - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) + ImGui.SameLine(); + switch (this.includeUnloadDependencies) { - ImGui.Text(hasInterface - ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" - : "\t => NO INTERFACE!!!"); + case true when ImGui.Button("Show load-time dependencies"): + this.includeUnloadDependencies = false; + this.dependencyNodes = null; + break; + case false when ImGui.Button("Show unload-time dependencies"): + this.includeUnloadDependencies = true; + this.dependencyNodes = null; + break; } - if (isPublic) + this.dependencyNodes ??= ServiceDependencyNode.CreateTreeByLevel(this.includeUnloadDependencies); + var cellPad = ImGui.CalcTextSize("WW"); + var margin = ImGui.CalcTextSize("W\nW\nW"); + var rowHeight = cellPad.Y * 3; + var width = ImGui.GetContentRegionAvail().X; + if (ImGui.BeginChild( + "dependency-graph", + new(width, (this.dependencyNodes.Count * (rowHeight + margin.Y)) + cellPad.Y), + false, + ImGuiWindowFlags.HorizontalScrollbar)) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.Text("\t => PUBLIC!!!"); + const uint rectBaseBorderColor = 0xFFFFFFFF; + const uint rectHoverFillColor = 0xFF404040; + const uint rectHoverRelatedFillColor = 0xFF802020; + const uint rectSelectedFillColor = 0xFF20A020; + const uint rectSelectedRelatedFillColor = 0xFF204020; + const uint lineBaseColor = 0xFF808080; + const uint lineHoverColor = 0xFFFF8080; + const uint lineHoverNotColor = 0xFF404040; + const uint lineSelectedColor = 0xFF80FF00; + const uint lineInvalidColor = 0xFFFF0000; + + ServiceDependencyNode? hoveredNode = null; + + var pos = ImGui.GetCursorScreenPos(); + var dl = ImGui.GetWindowDrawList(); + var mouse = ImGui.GetMousePos(); + var maxRowWidth = 0f; + + // 1. Layout + for (var level = 0; level < this.dependencyNodes.Count; level++) + { + var levelNodes = this.dependencyNodes[level]; + + var rowWidth = 0f; + foreach (var node in levelNodes) + rowWidth += ImGui.CalcTextSize(node.TypeName).X + cellPad.X + margin.X; + + var off = cellPad / 2; + if (rowWidth < width) + off.X += ImGui.GetScrollX() + ((width - rowWidth) / 2); + else if (rowWidth - ImGui.GetScrollX() < width) + off.X += width - (rowWidth - ImGui.GetScrollX()); + off.Y = (rowHeight + margin.Y) * level; + + foreach (var node in levelNodes) + { + var textSize = ImGui.CalcTextSize(node.TypeName); + var cellSize = textSize + cellPad; + + var rc = new Vector4(pos + off, pos.X + off.X + cellSize.X, pos.Y + off.Y + cellSize.Y); + this.nodeRects[node] = rc; + if (rc.X <= mouse.X && mouse.X < rc.Z && rc.Y <= mouse.Y && mouse.Y < rc.W) + { + hoveredNode = node; + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + if (this.selectedNodes.Contains(node.Type)) + this.selectedNodes.Remove(node.Type); + else + this.selectedNodes.Add(node.Type); + } + } + + off.X += cellSize.X + margin.X; + } + + maxRowWidth = Math.Max(maxRowWidth, rowWidth); + } + + // 2. Draw non-hovered lines + foreach (var levelNodes in this.dependencyNodes) + { + foreach (var node in levelNodes) + { + var rect = this.nodeRects[node]; + var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y); + + foreach (var parent in node.InvalidParents) + { + rect = this.nodeRects[parent]; + var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W); + if (node == hoveredNode || parent == hoveredNode) + continue; + + dl.AddLine(point1, point2, lineInvalidColor, 2f * ImGuiHelpers.GlobalScale); + } + + foreach (var parent in node.Parents) + { + rect = this.nodeRects[parent]; + var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W); + if (node == hoveredNode || parent == hoveredNode) + continue; + + var isSelected = this.selectedNodes.Contains(node.Type) || + this.selectedNodes.Contains(parent.Type); + dl.AddLine( + point1, + point2, + isSelected + ? lineSelectedColor + : hoveredNode is not null + ? lineHoverNotColor + : lineBaseColor); + } + } + } + + // 3. Draw boxes + foreach (var levelNodes in this.dependencyNodes) + { + foreach (var node in levelNodes) + { + var textSize = ImGui.CalcTextSize(node.TypeName); + var cellSize = textSize + cellPad; + + var rc = this.nodeRects[node]; + if (hoveredNode == node) + dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectHoverFillColor); + else if (this.selectedNodes.Contains(node.Type)) + dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectSelectedFillColor); + else if (node.Relatives.Any(x => this.selectedNodes.Contains(x.Type))) + dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectSelectedRelatedFillColor); + else if (hoveredNode?.Relatives.Select(x => x.Type).Contains(node.Type) is true) + dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectHoverRelatedFillColor); + + dl.AddRect(new(rc.X, rc.Y), new(rc.Z, rc.W), rectBaseBorderColor); + ImGui.SetCursorPos((new Vector2(rc.X, rc.Y) - pos) + ((cellSize - textSize) / 2)); + ImGui.TextUnformatted(node.TypeName); + } + } + + // 4. Draw hovered lines + if (hoveredNode is not null) + { + foreach (var levelNodes in this.dependencyNodes) + { + foreach (var node in levelNodes) + { + var rect = this.nodeRects[node]; + var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y); + foreach (var parent in node.Parents) + { + if (node == hoveredNode || parent == hoveredNode) + { + rect = this.nodeRects[parent]; + var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W); + dl.AddLine( + point1, + point2, + lineHoverColor, + 2 * ImGuiHelpers.GlobalScale); + } + } + } + } + } + + ImGui.SetCursorPos(default); + ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight)); + ImGui.EndChild(); } - - ImGuiHelpers.ScaledDummy(2); + } + + if (ImGui.CollapsingHeader("Plugin-facing Services")) + { + foreach (var instance in container.Instances) + { + var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); + var isPublic = instance.Key.IsPublic; + + ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); + + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) + { + ImGui.Text( + hasInterface + ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" + : "\t => NO INTERFACE!!!"); + } + + if (isPublic) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.Text("\t => PUBLIC!!!"); + } + + ImGuiHelpers.ScaledDummy(2); + } + } + } + + private class ServiceDependencyNode + { + private readonly List parents = new(); + private readonly List children = new(); + private readonly List invalidParents = new(); + + private ServiceDependencyNode(Type t) => this.Type = t; + + public Type Type { get; } + + public string TypeName => this.Type.Name; + + public IReadOnlyList Parents => this.parents; + + public IReadOnlyList Children => this.children; + + public IReadOnlyList InvalidParents => this.invalidParents; + + public IEnumerable Relatives => + this.parents.Concat(this.children).Concat(this.invalidParents); + + public int Level { get; private set; } + + public static List CreateTree(bool includeUnloadDependencies) + { + var nodes = new Dictionary(); + foreach (var t in ServiceManager.GetConcreteServiceTypes()) + nodes.Add(typeof(Service<>).MakeGenericType(t), new(t)); + foreach (var t in ServiceManager.GetConcreteServiceTypes()) + { + var st = typeof(Service<>).MakeGenericType(t); + var node = nodes[st]; + foreach (var depType in ServiceHelpers.GetDependencies(st, includeUnloadDependencies)) + { + var depServiceType = typeof(Service<>).MakeGenericType(depType); + var depNode = nodes[depServiceType]; + if (node.IsAncestorOf(depType)) + { + node.invalidParents.Add(depNode); + } + else + { + depNode.UpdateNodeLevel(1); + node.UpdateNodeLevel(depNode.Level + 1); + node.parents.Add(depNode); + depNode.children.Add(node); + } + } + } + + return nodes.Values.OrderBy(x => x.Level).ThenBy(x => x.Type.Name).ToList(); + } + + public static List> CreateTreeByLevel(bool includeUnloadDependencies) + { + var res = new List>(); + foreach (var n in CreateTree(includeUnloadDependencies)) + { + while (res.Count <= n.Level) + res.Add(new()); + res[n.Level].Add(n); + } + + return res; + } + + private bool IsAncestorOf(Type type) => + this.children.Any(x => x.Type == type) || this.children.Any(x => x.IsAncestorOf(type)); + + private void UpdateNodeLevel(int newLevel) + { + if (this.Level >= newLevel) + return; + + this.Level = newLevel; + foreach (var c in this.children) + c.UpdateNodeLevel(newLevel + 1); } } } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 363d01f26..0ef3d49f8 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -21,6 +21,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; +using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Networking.Http; @@ -29,6 +30,7 @@ using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc.Internal; +using Dalamud.Support; using Dalamud.Utility; using Dalamud.Utility.Timing; using Newtonsoft.Json; @@ -93,7 +95,9 @@ internal partial class PluginManager : IDisposable, IServiceType } [ServiceManager.ServiceConstructor] - private PluginManager() + private PluginManager( + ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker, + ServiceManager.RegisterUnloadAfterDelegate registerUnloadAfter) { this.pluginDirectory = new DirectoryInfo(this.dalamud.StartInfo.PluginDirectory!); @@ -142,6 +146,14 @@ internal partial class PluginManager : IDisposable, IServiceType this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient); this.ApplyPatches(); + + registerStartupBlocker( + Task.Run(this.LoadAndStartLoadSyncPlugins), + "Waiting for plugins that asked to be loaded before the game."); + + registerUnloadAfter( + ResolvePossiblePluginDependencyServices(), + "See the attached comment for the called function."); } /// @@ -1201,6 +1213,49 @@ internal partial class PluginManager : IDisposable, IServiceType /// The calling plugin, or null. public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace()); + /// + /// Resolves the services that a plugin may have a dependency on.
+ /// This is required, as the lifetime of a plugin cannot be longer than PluginManager, + /// and we want to ensure that dependency services to be kept alive at least until all the plugins, and thus + /// PluginManager to be gone. + ///
+ /// The dependency services. + private static IEnumerable ResolvePossiblePluginDependencyServices() + { + foreach (var serviceType in ServiceManager.GetConcreteServiceTypes()) + { + if (serviceType == typeof(PluginManager)) + continue; + + // Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away. + // Nonetheless, their direct dependencies must be considered. + if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService) + { + var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); + var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT, false); + ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count); + + foreach (var scopedDep in dependencies) + { + if (scopedDep == typeof(PluginManager)) + throw new Exception("Scoped plugin services cannot depend on PluginManager."); + + ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!); + yield return scopedDep; + } + + continue; + } + + var pluginInterfaceAttribute = serviceType.GetCustomAttribute(true); + if (pluginInterfaceAttribute == null) + continue; + + ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!); + yield return serviceType; + } + } + private async Task DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) { var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; @@ -1590,6 +1645,38 @@ internal partial class PluginManager : IDisposable, IServiceType } } + private void LoadAndStartLoadSyncPlugins() + { + try + { + using (Timings.Start("PM Load Plugin Repos")) + { + _ = this.SetPluginReposFromConfigAsync(false); + this.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting); + + Log.Information("[T3] PM repos OK!"); + } + + using (Timings.Start("PM Cleanup Plugins")) + { + this.CleanupPlugins(); + Log.Information("[T3] PMC OK!"); + } + + using (Timings.Start("PM Load Sync Plugins")) + { + this.LoadAllPlugins().Wait(); + Log.Information("[T3] PML OK!"); + } + + _ = Task.Run(Troubleshooting.LogTroubleshooting); + } + catch (Exception ex) + { + Log.Error(ex, "Plugin load failed"); + } + } + private static class Locs { public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version); diff --git a/Dalamud/Plugin/Internal/StartupPluginLoader.cs b/Dalamud/Plugin/Internal/StartupPluginLoader.cs deleted file mode 100644 index 4f68d39fc..000000000 --- a/Dalamud/Plugin/Internal/StartupPluginLoader.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Support; -using Dalamud.Utility.Timing; - -namespace Dalamud.Plugin.Internal; - -/// -/// Class responsible for loading plugins on startup. -/// -[ServiceManager.BlockingEarlyLoadedService] -public class StartupPluginLoader : IServiceType -{ - private static readonly ModuleLog Log = new("SPL"); - - [ServiceManager.ServiceConstructor] - private StartupPluginLoader(PluginManager pluginManager) - { - try - { - using (Timings.Start("PM Load Plugin Repos")) - { - _ = pluginManager.SetPluginReposFromConfigAsync(false); - pluginManager.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting); - - Log.Information("[T3] PM repos OK!"); - } - - using (Timings.Start("PM Cleanup Plugins")) - { - pluginManager.CleanupPlugins(); - Log.Information("[T3] PMC OK!"); - } - - using (Timings.Start("PM Load Sync Plugins")) - { - pluginManager.LoadAllPlugins().Wait(); - Log.Information("[T3] PML OK!"); - } - - Task.Run(Troubleshooting.LogTroubleshooting); - } - catch (Exception ex) - { - Log.Error(ex, "Plugin load failed"); - } - } -} diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 21c08ce72..3ff7cde76 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -11,6 +11,7 @@ using Dalamud.Game; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Storage; +using Dalamud.Utility; using Dalamud.Utility.Timing; using JetBrains.Annotations; @@ -21,7 +22,7 @@ namespace Dalamud; // - Visualize/output .dot or imgui thing /// -/// Class to initialize Service<T>s. +/// Class to initialize . /// internal static class ServiceManager { @@ -43,6 +44,26 @@ internal static class ServiceManager private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static ManualResetEvent unloadResetEvent = new(false); + + /// + /// Delegate for registering startup blocker task.
+ /// Do not use this delegate outside the constructor. + ///
+ /// The blocker task. + /// The justification for using this feature. + [InjectableType] + public delegate void RegisterStartupBlockerDelegate(Task t, string justification); + + /// + /// Delegate for registering services that should be unloaded before self.
+ /// Intended for use with . If you think you need to use this outside + /// of that, consider having a discussion first.
+ /// Do not use this delegate outside the constructor. + ///
+ /// Services that should be unloaded first. + /// The justification for using this feature. + [InjectableType] + public delegate void RegisterUnloadAfterDelegate(IEnumerable unloadAfter, string justification); /// /// Kinds of services. @@ -125,6 +146,15 @@ internal static class ServiceManager #endif } + /// + /// Gets the concrete types of services, i.e. the non-abstract non-interface types. + /// + /// The enumerable of service types, that may be enumerated only once per call. + public static IEnumerable GetConcreteServiceTypes() => + Assembly.GetExecutingAssembly() + .GetTypes() + .Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract); + /// /// Kicks off construction of services that can handle early loading. /// @@ -141,7 +171,7 @@ internal static class ServiceManager var serviceContainer = Service.Get(); - foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract)) + foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind"); @@ -157,7 +187,7 @@ internal static class ServiceManager var getTask = (Task)genericWrappedServiceType .InvokeMember( - "GetAsync", + nameof(Service.GetAsync), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, @@ -184,17 +214,42 @@ internal static class ServiceManager } var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); - dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT) + dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, false) .Select(x => typeof(Service<>).MakeGenericType(x)) .ToList(); } + var blockerTasks = new List(); _ = Task.Run(async () => { try { - var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); - while (await Task.WhenAny(whenBlockingComplete, Task.Delay(120000)) != whenBlockingComplete) + // Wait for all blocking constructors to complete first. + await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); + + // All the BlockingEarlyLoadedService constructors have been run, + // and blockerTasks now will not change. Now wait for them. + // Note that ServiceManager.CallWhenServicesReady does not get to register a blocker. + await WaitWithTimeoutConsent(blockerTasks); + + BlockingServicesLoadedTaskCompletionSource.SetResult(); + Timings.Event("BlockingServices Initialized"); + } + catch (Exception e) + { + BlockingServicesLoadedTaskCompletionSource.SetException(e); + } + + return; + + async Task WaitWithTimeoutConsent(IEnumerable tasksEnumerable) + { + var tasks = tasksEnumerable.AsReadOnlyCollection(); + if (tasks.Count == 0) + return; + + var aggregatedTask = Task.WhenAll(tasks); + while (await Task.WhenAny(aggregatedTask, Task.Delay(120000)) != aggregatedTask) { if (NativeFunctions.MessageBoxW( IntPtr.Zero, @@ -208,13 +263,6 @@ internal static class ServiceManager "and the user chose to continue without Dalamud."); } } - - BlockingServicesLoadedTaskCompletionSource.SetResult(); - Timings.Event("BlockingServices Initialized"); - } - catch (Exception e) - { - BlockingServicesLoadedTaskCompletionSource.SetException(e); } }).ConfigureAwait(false); @@ -249,6 +297,25 @@ internal static class ServiceManager if (!hasDeps) continue; + // This object will be used in a task. Each task must receive a new object. + var startLoaderArgs = new List(); + if (serviceType.GetCustomAttribute() is not null) + { + startLoaderArgs.Add( + new RegisterStartupBlockerDelegate( + (task, justification) => + { +#if DEBUG + if (CurrentConstructorServiceType.Value != serviceType) + throw new InvalidOperationException("Forbidden."); +#endif + blockerTasks.Add(task); + + // No need to store the justification; the fact that the reason is specified is good enough. + _ = justification; + })); + } + tasks.Add((Task)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( @@ -256,7 +323,7 @@ internal static class ServiceManager BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, - null)); + new object[] { startLoaderArgs })); servicesToLoad.Remove(serviceType); #if DEBUG @@ -328,13 +395,13 @@ internal static class ServiceManager unloadResetEvent.Reset(); - var dependencyServicesMap = new Dictionary>(); + var dependencyServicesMap = new Dictionary>(); var allToUnload = new HashSet(); var unloadOrder = new List(); Log.Information("==== COLLECTING SERVICES TO UNLOAD ===="); - foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) + foreach (var serviceType in GetConcreteServiceTypes()) { if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; @@ -347,7 +414,7 @@ internal static class ServiceManager Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!); var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); - dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT); + dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, true); allToUnload.Add(serviceType); } @@ -541,11 +608,35 @@ internal static class ServiceManager } /// - /// Indicates that the method should be called when the services given in the constructor are ready. + /// Indicates that the method should be called when the services given in the marked method's parameters are ready. + /// This will be executed immediately after the constructor has run, if all services specified as its parameters + /// are already ready, or no parameter is given. /// [AttributeUsage(AttributeTargets.Method)] [MeansImplicitUse] public class CallWhenServicesReady : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// Specify the reason here. + public CallWhenServicesReady(string justification) + { + // No need to store the justification; the fact that the reason is specified is good enough. + _ = justification; + } + } + + /// + /// Indicates that something is a candidate for being considered as an injected parameter for constructors. + /// + [AttributeUsage( + AttributeTargets.Delegate + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Enum + | AttributeTargets.Interface)] + public class InjectableTypeAttribute : Attribute { } } diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 9c7f0411d..08f592826 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Dalamud.IoC; using Dalamud.IoC.Internal; -using Dalamud.Plugin.Internal; using Dalamud.Utility.Timing; using JetBrains.Annotations; @@ -25,6 +24,7 @@ internal static class Service where T : IServiceType private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(); private static List? dependencyServices; + private static List? dependencyServicesForUnload; static Service() { @@ -95,7 +95,7 @@ internal static class Service where T : IServiceType if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService && ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType) { - var deps = ServiceHelpers.GetDependencies(currentServiceType); + var deps = ServiceHelpers.GetDependencies(typeof(Service<>).MakeGenericType(currentServiceType), false); if (!deps.Contains(typeof(T))) { throw new InvalidOperationException( @@ -115,7 +115,6 @@ internal static class Service where T : IServiceType /// Pull the instance out of the service locator, waiting if necessary. /// /// The object. - [UsedImplicitly] public static Task GetAsync() => instanceTcs.Task; /// @@ -141,11 +140,15 @@ internal static class Service where T : IServiceType /// /// Gets an enumerable containing s that are required for this Service to initialize /// without blocking. + /// These are NOT returned as types; raw types will be returned. /// + /// Whether to include the unload dependencies. /// List of dependency services. - [UsedImplicitly] - public static List GetDependencyServices() + public static IReadOnlyCollection GetDependencyServices(bool includeUnloadDependencies) { + if (includeUnloadDependencies && dependencyServicesForUnload is not null) + return dependencyServicesForUnload; + if (dependencyServices is not null) return dependencyServices; @@ -158,7 +161,8 @@ internal static class Service where T : IServiceType { res.AddRange(ctor .GetParameters() - .Select(x => x.ParameterType)); + .Select(x => x.ParameterType) + .Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None)); } res.AddRange(typeof(T) @@ -171,50 +175,8 @@ internal static class Service where T : IServiceType .OfType() .Select(x => x.GetType().GetGenericArguments().First())); - // HACK: PluginManager needs to depend on ALL plugin exposed services - if (typeof(T) == typeof(PluginManager)) - { - foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) - { - if (!serviceType.IsAssignableTo(typeof(IServiceType))) - continue; - - if (serviceType == typeof(PluginManager)) - continue; - - // Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away. - // Nonetheless, their direct dependencies must be considered. - if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService) - { - var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); - var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT); - ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count); - - foreach (var scopedDep in dependencies) - { - if (scopedDep == typeof(PluginManager)) - throw new Exception("Scoped plugin services cannot depend on PluginManager."); - - ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!); - res.Add(scopedDep); - } - - continue; - } - - var pluginInterfaceAttribute = serviceType.GetCustomAttribute(true); - if (pluginInterfaceAttribute == null) - continue; - - ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!); - res.Add(serviceType); - } - } - foreach (var type in res) - { ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name); - } var deps = res .Distinct() @@ -244,8 +206,9 @@ internal static class Service where T : IServiceType /// /// Starts the service loader. Only to be called from . /// + /// Additional objects available to constructors. /// The loader task. - internal static Task StartLoader() + internal static Task StartLoader(IReadOnlyCollection additionalProvidedTypedObjects) { if (instanceTcs.Task.IsCompleted) throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed."); @@ -256,10 +219,27 @@ internal static class Service where T : IServiceType return Task.Run(Timings.AttachTimingHandle(async () => { + var ctorArgs = new List(additionalProvidedTypedObjects.Count + 1); + ctorArgs.AddRange(additionalProvidedTypedObjects); + ctorArgs.Add( + new ServiceManager.RegisterUnloadAfterDelegate( + (additionalDependencies, justification) => + { +#if DEBUG + if (ServiceManager.CurrentConstructorServiceType.Value != typeof(T)) + throw new InvalidOperationException("Forbidden."); +#endif + dependencyServicesForUnload ??= new(GetDependencyServices(false)); + dependencyServicesForUnload.AddRange(additionalDependencies); + + // No need to store the justification; the fact that the reason is specified is good enough. + _ = justification; + })); + ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name); try { - var instance = await ConstructObject(); + var instance = await ConstructObject(ctorArgs).ConfigureAwait(false); instanceTcs.SetResult(instance); List? tasks = null; @@ -270,8 +250,17 @@ internal static class Service where T : IServiceType continue; ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name); - var args = await Task.WhenAll(method.GetParameters().Select( - x => ResolveServiceFromTypeAsync(x.ParameterType))); + var args = await ResolveInjectedParameters( + method.GetParameters(), + Array.Empty()).ConfigureAwait(false); + if (args.Length == 0) + { + ServiceManager.Log.Warning( + "Service<{0}>: Method {1} does not have any arguments. Consider merging it with the ctor.", + typeof(T).Name, + method.Name); + } + try { if (method.Invoke(instance, args) is Task task) @@ -331,24 +320,6 @@ internal static class Service where T : IServiceType instanceTcs.SetException(new UnloadedException()); } - private static async Task ResolveServiceFromTypeAsync(Type type) - { - var task = (Task)typeof(Service<>) - .MakeGenericType(type) - .InvokeMember( - "GetAsync", - BindingFlags.InvokeMethod | - BindingFlags.Static | - BindingFlags.Public, - null, - null, - null)!; - await task; - return typeof(Task<>).MakeGenericType(type) - .GetProperty("Result", BindingFlags.Instance | BindingFlags.Public)! - .GetValue(task); - } - private static ConstructorInfo? GetServiceConstructor() { const BindingFlags ctorBindingFlags = @@ -359,18 +330,18 @@ internal static class Service where T : IServiceType .SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any()); } - private static async Task ConstructObject() + private static async Task ConstructObject(IReadOnlyCollection additionalProvidedTypedObjects) { var ctor = GetServiceConstructor(); if (ctor == null) throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); - var args = await Task.WhenAll( - ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType))); + var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects) + .ConfigureAwait(false); using (Timings.Start($"{typeof(T).Name} Construct")) { #if DEBUG - ServiceManager.CurrentConstructorServiceType.Value = typeof(Service); + ServiceManager.CurrentConstructorServiceType.Value = typeof(T); try { return (T)ctor.Invoke(args)!; @@ -385,6 +356,43 @@ internal static class Service where T : IServiceType } } + private static Task ResolveInjectedParameters( + IReadOnlyList argDefs, + IReadOnlyCollection additionalProvidedTypedObjects) + { + var argTasks = new Task[argDefs.Count]; + for (var i = 0; i < argDefs.Count; i++) + { + var argType = argDefs[i].ParameterType; + ref var argTask = ref argTasks[i]; + + if (argType.GetCustomAttribute() is not null) + { + argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType)); + continue; + } + + argTask = (Task)typeof(Service<>) + .MakeGenericType(argType) + .InvokeMember( + nameof(GetAsyncAsObject), + BindingFlags.InvokeMethod | + BindingFlags.Static | + BindingFlags.NonPublic, + null, + null, + null)!; + } + + return Task.WhenAll(argTasks); + } + + /// + /// Pull the instance out of the service locator, waiting if necessary. + /// + /// The object. + private static Task GetAsyncAsObject() => instanceTcs.Task.ContinueWith(r => (object)r.Result); + /// /// Exception thrown when service is attempted to be retrieved when it's unloaded. /// @@ -407,11 +415,12 @@ internal static class ServiceHelpers { /// /// Get a list of dependencies for a service. Only accepts types. - /// These are returned as types. + /// These are NOT returned as types; raw types will be returned. /// /// The dependencies for this service. + /// Whether to include the unload dependencies. /// A list of dependencies. - public static List GetDependencies(Type serviceType) + public static IReadOnlyCollection GetDependencies(Type serviceType, bool includeUnloadDependencies) { #if DEBUG if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>)) @@ -422,12 +431,12 @@ internal static class ServiceHelpers } #endif - return (List)serviceType.InvokeMember( + return (IReadOnlyCollection)serviceType.InvokeMember( nameof(Service.GetDependencyServices), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, - null) ?? new List(); + new object?[] { includeUnloadDependencies }) ?? new List(); } /// diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 30441f479..70a91c4bf 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -44,7 +44,10 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA private bool isDisposed; [ServiceManager.ServiceConstructor] - private DalamudAssetManager(Dalamud dalamud, HappyHttpClient httpClient) + private DalamudAssetManager( + Dalamud dalamud, + HappyHttpClient httpClient, + ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker) { this.dalamud = dalamud; this.httpClient = httpClient; @@ -55,8 +58,17 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.fileStreams = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); this.textureWraps = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); + // Block until all the required assets to be ready. var loadTimings = Timings.Start("DAM LoadAll"); - this.WaitForAllRequiredAssets().ContinueWith(_ => loadTimings.Dispose()); + registerStartupBlocker( + Task.WhenAll( + Enum.GetValues() + .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.Required is true) + .Select(this.CreateStreamAsync) + .Select(x => x.ToContentDisposedTask())) + .ContinueWith(_ => loadTimings.Dispose()), + "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); } /// @@ -83,25 +95,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.scopedFinalizer.Dispose(); } - /// - /// Waits for all the required assets to be ready. Will result in a faulted task, if any of the required assets - /// has failed to load. - /// - /// The task. - [Pure] - public Task WaitForAllRequiredAssets() - { - lock (this.syncRoot) - { - return Task.WhenAll( - Enum.GetValues() - .Where(x => x is not DalamudAsset.Empty4X4) - .Where(x => x.GetAttribute()?.Required is true) - .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())); - } - } - /// [Pure] public bool IsStreamImmediatelyAvailable(DalamudAsset asset) => diff --git a/Dalamud/Utility/ArrayExtensions.cs b/Dalamud/Utility/ArrayExtensions.cs index afb1511e3..fa6e3dbe9 100644 --- a/Dalamud/Utility/ArrayExtensions.cs +++ b/Dalamud/Utility/ArrayExtensions.cs @@ -87,4 +87,14 @@ internal static class ArrayExtensions result = default; return false; } + + /// + /// Interprets the given array as an , so that you can enumerate it multiple + /// times, and know the number of elements within. + /// + /// The enumerable. + /// The element type. + /// casted as a if it is one; otherwise the result of . + public static IReadOnlyCollection AsReadOnlyCollection(this IEnumerable array) => + array as IReadOnlyCollection ?? array.ToArray(); } From 30c2872400ec21360ea21334ab5ff9a2521c7b11 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 8 Dec 2023 08:08:43 +0900 Subject: [PATCH 27/51] Fix ChatGui race condition (#1563) --- Dalamud/Game/Gui/ChatGui.cs | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 8f2a617cf..5e8b4f3a2 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Runtime.InteropServices; @@ -40,6 +40,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui private readonly LibcFunction libcFunction = Service.Get(); private IntPtr baseAddress = IntPtr.Zero; + private ImmutableDictionary<(string PluginName, uint CommandId), Action>? dalamudLinkHandlersCopy; [ServiceManager.ServiceConstructor] private ChatGui(TargetSigScanner sigScanner) @@ -84,7 +85,21 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui public byte LastLinkedItemFlags { get; private set; } /// - public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers => this.dalamudLinkHandlers; + public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers + { + get + { + var copy = this.dalamudLinkHandlersCopy; + if (copy is not null) + return copy; + + lock (this.dalamudLinkHandlers) + { + return this.dalamudLinkHandlersCopy ??= + this.dalamudLinkHandlers.ToImmutableDictionary(x => x.Key, x => x.Value); + } + } + } /// /// Dispose of managed and unmanaged resources. @@ -160,7 +175,12 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action commandAction) { var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId }; - this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction); + lock (this.dalamudLinkHandlers) + { + this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction); + this.dalamudLinkHandlersCopy = null; + } + return payload; } @@ -170,9 +190,14 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui /// The name of the plugin handling the links. internal void RemoveChatLinkHandler(string pluginName) { - foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName)) + lock (this.dalamudLinkHandlers) { - this.dalamudLinkHandlers.Remove(handler); + var changed = false; + + foreach (var handler in this.RegisteredLinkHandlers.Keys.Where(k => k.PluginName == pluginName)) + changed |= this.dalamudLinkHandlers.Remove(handler); + if (changed) + this.dalamudLinkHandlersCopy = null; } } @@ -183,7 +208,11 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui /// The ID of the command to be removed. internal void RemoveChatLinkHandler(string pluginName, uint commandId) { - this.dalamudLinkHandlers.Remove((pluginName, commandId)); + lock (this.dalamudLinkHandlers) + { + if (this.dalamudLinkHandlers.Remove((pluginName, commandId))) + this.dalamudLinkHandlersCopy = null; + } } private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color) @@ -391,7 +420,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui var linkPayload = payloads[0]; if (linkPayload is DalamudLinkPayload link) { - if (this.dalamudLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value)) + if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value)) { Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}"); value.Invoke(link.CommandId, new SeString(payloads)); From 6637bd82073fb3c9e84038026900db7fee3df6ce Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:10:15 +0100 Subject: [PATCH 28/51] Use the substitution provider for textures in the ULD Wrapper. (#1553) --- Dalamud/Interface/UldWrapper.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index e78546ed9..127ea85ec 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using Dalamud.Data; using Dalamud.Interface.Internal; using Dalamud.Utility; -using ImGuiScene; using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; @@ -155,20 +155,27 @@ public class UldWrapper : IDisposable // Try to load HD textures first. var hrPath = texturePath.Replace(".tex", "_hr1.tex"); + var substitution = Service.Get(); + hrPath = substitution.GetSubstitutedPath(hrPath); var hd = true; - var file = this.data.GetFile(hrPath); - if (file == null) + var tex = Path.IsPathRooted(hrPath) + ? this.data.GameData.GetFileFromDisk(hrPath) + : this.data.GetFile(hrPath); + if (tex == null) { hd = false; - file = this.data.GetFile(texturePath); + texturePath = substitution.GetSubstitutedPath(texturePath); + tex = Path.IsPathRooted(texturePath) + ? this.data.GameData.GetFileFromDisk(texturePath) + : this.data.GetFile(texturePath); // Neither texture could be loaded. - if (file == null) + if (tex == null) { return null; } } - return (id, file.Header.Width, file.Header.Height, hd, file.GetRgbaImageData()); + return (id, tex.Header.Width, tex.Header.Height, hd, tex.GetRgbaImageData()); } } From 9489c4ec20c98c01b76fd1c5bd359a881fd505ee Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 8 Dec 2023 00:22:09 +0100 Subject: [PATCH 29/51] Refactor ChatGui internals to use CS additions (#1520) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud/Game/Gui/ChatGui.cs | 121 +++++++-------------- Dalamud/Game/Gui/ChatGuiAddressResolver.cs | 73 ------------- 2 files changed, 41 insertions(+), 153 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 5e8b4f3a2..1214850b0 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -4,26 +4,34 @@ using System.Linq; using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; -using Dalamud.Game.Libc; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Memory; using Dalamud.Plugin.Services; using Dalamud.Utility; -using Serilog; +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; namespace Dalamud.Game.Gui; +// TODO(api10): Update IChatGui, ChatGui and XivChatEntry to use correct types and names: +// "uint SenderId" should be "int Timestamp". +// "IntPtr Parameters" should be something like "bool Silent". It suppresses new message sounds in certain channels. + /// /// This class handles interacting with the native chat UI. /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class ChatGui : IDisposable, IServiceType, IChatGui +internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui { + private static readonly ModuleLog Log = new("ChatGui"); + private readonly ChatGuiAddressResolver address; private readonly Queue chatQueue = new(); @@ -36,10 +44,6 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly LibcFunction libcFunction = Service.Get(); - - private IntPtr baseAddress = IntPtr.Zero; private ImmutableDictionary<(string PluginName, uint CommandId), Action>? dalamudLinkHandlersCopy; [ServiceManager.ServiceConstructor] @@ -48,7 +52,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui this.address = new ChatGuiAddressResolver(); this.address.Setup(sigScanner); - this.printMessageHook = Hook.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour); + this.printMessageHook = Hook.FromAddress((nint)RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour); this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); this.interactableLinkClickedHook = Hook.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); @@ -58,7 +62,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter); + private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, bool silent); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr); @@ -150,18 +154,13 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui { var chat = this.chatQueue.Dequeue(); - if (this.baseAddress == IntPtr.Zero) - { - continue; - } + var sender = Utf8String.FromSequence(chat.Name.Encode()); + var message = Utf8String.FromSequence(chat.Message.Encode()); - var senderRaw = (chat.Name ?? string.Empty).Encode(); - using var senderOwned = this.libcFunction.NewString(senderRaw); + this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, chat.Parameters != 0); - var messageRaw = (chat.Message ?? string.Empty).Encode(); - using var messageOwned = this.libcFunction.NewString(messageRaw); - - this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters); + sender->Dtor(true); + message->Dtor(true); } } @@ -279,29 +278,17 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui } } - private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chatType, IntPtr pSenderName, IntPtr pMessage, uint senderId, IntPtr parameter) + private uint HandlePrintMessageDetour(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, bool silent) { - var retVal = IntPtr.Zero; + var messageId = 0u; try { - var sender = StdString.ReadFromPointer(pSenderName); - var parsedSender = SeString.Parse(sender.RawData); - var originalSenderData = (byte[])sender.RawData.Clone(); - var oldEditedSender = parsedSender.Encode(); - var senderPtr = pSenderName; - OwnedStdString allocatedString = null; + var originalSenderData = sender->Span.ToArray(); + var originalMessageData = message->Span.ToArray(); - var message = StdString.ReadFromPointer(pMessage); - var parsedMessage = SeString.Parse(message.RawData); - var originalMessageData = (byte[])message.RawData.Clone(); - var oldEdited = parsedMessage.Encode(); - var messagePtr = pMessage; - OwnedStdString allocatedStringSender = null; - - // Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue); - - // Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}"); + var parsedSender = SeString.Parse(originalSenderData); + var parsedMessage = SeString.Parse(originalMessageData); // Call events var isHandled = false; @@ -312,7 +299,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui try { var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate; - messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled); + messageHandledDelegate!.Invoke(chatType, (uint)timestamp, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) { @@ -328,7 +315,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui try { var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate; - messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled); + messageHandledDelegate!.Invoke(chatType, (uint)timestamp, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) { @@ -337,61 +324,39 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui } } - var newEdited = parsedMessage.Encode(); - if (!Util.FastByteArrayCompare(oldEdited, newEdited)) + var possiblyModifiedSenderData = parsedSender.Encode(); + var possiblyModifiedMessageData = parsedMessage.Encode(); + + if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData)) { - Log.Verbose("SeString was edited, taking precedence over StdString edit."); - message.RawData = newEdited; - // Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}"); + Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(originalSenderData)} -> {parsedSender}"); + sender->SetString(possiblyModifiedSenderData); } - if (!Util.FastByteArrayCompare(originalMessageData, message.RawData)) + if (!Util.FastByteArrayCompare(originalMessageData, possiblyModifiedMessageData)) { - allocatedString = this.libcFunction.NewString(message.RawData); - Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})"); - messagePtr = allocatedString.Address; - } - - var newEditedSender = parsedSender.Encode(); - if (!Util.FastByteArrayCompare(oldEditedSender, newEditedSender)) - { - Log.Verbose("SeString was edited, taking precedence over StdString edit."); - sender.RawData = newEditedSender; - // Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}"); - } - - if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData)) - { - allocatedStringSender = this.libcFunction.NewString(sender.RawData); - Log.Debug( - $"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})"); - senderPtr = allocatedStringSender.Address; + Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(originalMessageData)} -> {parsedMessage}"); + message->SetString(possiblyModifiedMessageData); } // Print the original chat if it's handled. if (isHandled) { - this.ChatMessageHandled?.Invoke(chatType, senderId, parsedSender, parsedMessage); + this.ChatMessageHandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage); } else { - retVal = this.printMessageHook.Original(manager, chatType, senderPtr, messagePtr, senderId, parameter); - this.ChatMessageUnhandled?.Invoke(chatType, senderId, parsedSender, parsedMessage); + messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent); + this.ChatMessageUnhandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage); } - - if (this.baseAddress == IntPtr.Zero) - this.baseAddress = manager; - - allocatedString?.Dispose(); - allocatedStringSender?.Dispose(); } catch (Exception ex) { Log.Error(ex, "Exception on OnChatMessage hook."); - retVal = this.printMessageHook.Original(manager, chatType, pSenderName, pMessage, senderId, parameter); + messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent); } - return retVal; + return messageId; } private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr) @@ -409,11 +374,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10); - var messageSize = 0; - while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++; - var payloadBytes = new byte[messageSize]; - Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize); - var seStr = SeString.Parse(payloadBytes); + var seStr = MemoryHelper.ReadSeStringNullTerminated(messagePtr); var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator); var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads; if (payloads.Count == 0) return; diff --git a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs index d653ec146..ae53f90e9 100644 --- a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs @@ -5,11 +5,6 @@ namespace Dalamud.Game.Gui; /// internal sealed class ChatGuiAddressResolver : BaseAddressResolver { - /// - /// Gets the address of the native PrintMessage method. - /// - public IntPtr PrintMessage { get; private set; } - /// /// Gets the address of the native PopulateItemLinkObject method. /// @@ -20,77 +15,9 @@ internal sealed class ChatGuiAddressResolver : BaseAddressResolver /// public IntPtr InteractableLinkClicked { get; private set; } - /* - --- for reference: 4.57 --- - .text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal) - .text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near - .text:00000001405CD210 ; CODE XREF: sub_1401419F0+201↑p - .text:00000001405CD210 ; sub_140141D10+220↑p ... - .text:00000001405CD210 - .text:00000001405CD210 var_220 = qword ptr -220h - .text:00000001405CD210 var_218 = byte ptr -218h - .text:00000001405CD210 var_210 = word ptr -210h - .text:00000001405CD210 var_208 = byte ptr -208h - .text:00000001405CD210 var_200 = word ptr -200h - .text:00000001405CD210 var_1FC = dword ptr -1FCh - .text:00000001405CD210 var_1F8 = qword ptr -1F8h - .text:00000001405CD210 var_1F0 = qword ptr -1F0h - .text:00000001405CD210 var_1E8 = qword ptr -1E8h - .text:00000001405CD210 var_1E0 = dword ptr -1E0h - .text:00000001405CD210 var_1DC = word ptr -1DCh - .text:00000001405CD210 var_1DA = word ptr -1DAh - .text:00000001405CD210 var_1D8 = qword ptr -1D8h - .text:00000001405CD210 var_1D0 = byte ptr -1D0h - .text:00000001405CD210 var_1C8 = qword ptr -1C8h - .text:00000001405CD210 var_1B0 = dword ptr -1B0h - .text:00000001405CD210 var_1AC = dword ptr -1ACh - .text:00000001405CD210 var_1A8 = dword ptr -1A8h - .text:00000001405CD210 var_1A4 = dword ptr -1A4h - .text:00000001405CD210 var_1A0 = dword ptr -1A0h - .text:00000001405CD210 var_160 = dword ptr -160h - .text:00000001405CD210 var_15C = dword ptr -15Ch - .text:00000001405CD210 var_140 = dword ptr -140h - .text:00000001405CD210 var_138 = dword ptr -138h - .text:00000001405CD210 var_130 = byte ptr -130h - .text:00000001405CD210 var_C0 = byte ptr -0C0h - .text:00000001405CD210 var_50 = qword ptr -50h - .text:00000001405CD210 var_38 = qword ptr -38h - .text:00000001405CD210 var_30 = qword ptr -30h - .text:00000001405CD210 var_28 = qword ptr -28h - .text:00000001405CD210 var_20 = qword ptr -20h - .text:00000001405CD210 senderActorId = dword ptr 30h - .text:00000001405CD210 isLocal = byte ptr 38h - .text:00000001405CD210 - .text:00000001405CD210 ; __unwind { // __GSHandlerCheck - .text:00000001405CD210 push rbp - .text:00000001405CD212 push rdi - .text:00000001405CD213 push r14 - .text:00000001405CD215 push r15 - .text:00000001405CD217 lea rbp, [rsp-128h] - .text:00000001405CD21F sub rsp, 228h - .text:00000001405CD226 mov rax, cs:__security_cookie - .text:00000001405CD22D xor rax, rsp - .text:00000001405CD230 mov [rbp+140h+var_50], rax - .text:00000001405CD237 xor r10b, r10b - .text:00000001405CD23A mov [rsp+240h+var_1F8], rcx - .text:00000001405CD23F xor eax, eax - .text:00000001405CD241 mov r11, r9 - .text:00000001405CD244 mov r14, r8 - .text:00000001405CD247 mov r9d, eax - .text:00000001405CD24A movzx r15d, dx - .text:00000001405CD24E lea r8, [rcx+0C10h] - .text:00000001405CD255 mov rdi, rcx - */ - /// protected override void Setup64Bit(ISigScanner sig) { - // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1??? - this.PrintMessage = sig.ScanText("40 55 53 56 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC 20 02 00 00 48 8B 05"); - // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old - - // PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33"); - // PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); // PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0 From 496bed4c69118c945940d41b6fb19cf2ec14e637 Mon Sep 17 00:00:00 2001 From: grittyfrog <148605153+grittyfrog@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:30:11 +1100 Subject: [PATCH 30/51] Fix multi-line copy/paste between ImGui and XIV (#1525) --- .../Internal/ImGuiClipboardConfig.cs | 80 +++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 2 + 2 files changed, 82 insertions(+) create mode 100644 Dalamud/Interface/Internal/ImGuiClipboardConfig.cs diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs new file mode 100644 index 000000000..b3302add4 --- /dev/null +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -0,0 +1,80 @@ +using System.Runtime.InteropServices; +using ImGuiNET; + +namespace Dalamud.Interface.Internal; + +/// +/// Configures the ImGui clipboard behaviour to work nicely with XIV. +/// +/// +/// +/// XIV uses '\r' for line endings and will truncate all text after a '\n' character. +/// This means that copy/pasting multi-line text from ImGui to XIV will only copy the first line. +/// +/// +/// ImGui uses '\n' for line endings and will ignore '\r' entirely. +/// This means that copy/pasting multi-line text from XIV to ImGui will copy all the text +/// without line breaks. +/// +/// +/// To fix this we normalize all clipboard line endings entering/exiting ImGui to '\r\n' which +/// works for both ImGui and XIV. +/// +/// +internal static class ImGuiClipboardConfig +{ + private delegate void SetClipboardTextDelegate(IntPtr userData, string text); + private delegate string GetClipboardTextDelegate(); + + private static SetClipboardTextDelegate? _setTextOriginal = null; + private static GetClipboardTextDelegate? _getTextOriginal = null; + + // These must exist as variables to prevent them from being GC'd + private static SetClipboardTextDelegate? _setText = null; + private static GetClipboardTextDelegate? _getText = null; + + public static void Apply() + { + var io = ImGui.GetIO(); + if (_setTextOriginal == null) + { + _setTextOriginal = + Marshal.GetDelegateForFunctionPointer(io.SetClipboardTextFn); + } + + if (_getTextOriginal == null) + { + _getTextOriginal = + Marshal.GetDelegateForFunctionPointer(io.GetClipboardTextFn); + } + + _setText = new SetClipboardTextDelegate(SetClipboardText); + _getText = new GetClipboardTextDelegate(GetClipboardText); + + io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText); + io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText); + } + + public static void Unapply() + { + var io = ImGui.GetIO(); + if (_setTextOriginal != null) + { + io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal); + } + if (_getTextOriginal != null) + { + io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal); + } + } + + private static void SetClipboardText(IntPtr userData, string text) + { + _setTextOriginal!(userData, text.ReplaceLineEndings("\r\n")); + } + + private static string GetClipboardText() + { + return _getTextOriginal!().ReplaceLineEndings("\r\n"); + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 1b12fd853..7d164c01f 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -240,6 +240,7 @@ internal class InterfaceManager : IDisposable, IServiceType this.processMessageHook?.Dispose(); }).Wait(); + ImGuiClipboardConfig.Unapply(); this.scene?.Dispose(); } @@ -628,6 +629,7 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; this.SetupFonts(); + ImGuiClipboardConfig.Apply(); if (!configuration.IsDocking) { From e6f5801ab5793048db2edf9bacb8e144d8df0be4 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:35:45 +0100 Subject: [PATCH 31/51] [master] Update ClientStructs (#1556) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index cc6687524..dcc913975 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cc668752416a8459a3c23345c51277e359803de8 +Subproject commit dcc9139758bf5e2ff5c0b53d73a3566eb0eec4f0 From 0bfcc557749df00f92b824c3d062ee10cfbf5a13 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 8 Dec 2023 08:49:09 +0900 Subject: [PATCH 32/51] Reduce heap allocation every frame in AddonLifecycle (#1555) --- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 35 +++- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 8 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 24 ++- .../AddonArgTypes/AddonRefreshArgs.cs | 16 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 16 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 16 +- .../AddonArgTypes/AddonUpdateArgs.cs | 10 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 188 ++++++------------ .../AddonLifecycleReceiveEventListener.cs | 47 ++--- Dalamud/Memory/MemoryHelper.cs | 32 +++ 11 files changed, 212 insertions(+), 188 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 334542c71..077ca7c93 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -14,22 +14,51 @@ public abstract unsafe class AddonArgs public const string InvalidAddon = "NullAddon"; private string? addonName; + private IntPtr addon; /// /// Gets the name of the addon this args referrers to. /// public string AddonName => this.GetAddonName(); - + /// /// Gets the pointer to the addons AtkUnitBase. /// - public nint Addon { get; init; } - + public nint Addon + { + get => this.addon; + internal set + { + if (this.addon == value) + return; + + this.addon = value; + this.addonName = null; + } + } + /// /// Gets the type of these args. /// public abstract AddonArgsType Type { get; } + /// + /// Checks if addon name matches the given span of char. + /// + /// The name to check. + /// Whether it is the case. + internal bool IsAddon(ReadOnlySpan name) + { + if (this.Addon == nint.Zero) return false; + if (name.Length is 0 or > 0x20) + return false; + + var addonPointer = (AtkUnitBase*)this.Addon; + if (addonPointer->Name is null) return false; + + return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20); + } + /// /// Helper method for ensuring the name of the addon is valid. /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 10d46a573..1e1013dd5 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -3,8 +3,14 @@ /// /// Addon argument data for Draw events. /// -public class AddonDrawArgs : AddonArgs +public class AddonDrawArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Draw; + + /// + public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index caf422927..fc26a6c33 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -3,8 +3,14 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonFinalizeArgs : AddonArgs +public class AddonFinalizeArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Finalize; + + /// + public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index df75307f1..8f9003b4c 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,28 +3,34 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonReceiveEventArgs : AddonArgs +public class AddonReceiveEventArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.ReceiveEvent; /// - /// Gets the AtkEventType for this event message. + /// Gets or sets the AtkEventType for this event message. /// - public byte AtkEventType { get; init; } + public byte AtkEventType { get; set; } /// - /// Gets the event id for this event message. + /// Gets or sets the event id for this event message. /// - public int EventParam { get; init; } + public int EventParam { get; set; } /// - /// Gets the pointer to an AtkEvent for this event message. + /// Gets or sets the pointer to an AtkEvent for this event message. /// - public nint AtkEvent { get; init; } + public nint AtkEvent { get; set; } /// - /// Gets the pointer to a block of data for this event message. + /// Gets or sets the pointer to a block of data for this event message. /// - public nint Data { get; init; } + public nint Data { get; set; } + + /// + public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index b6ac6d8b6..bfcf02544 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -5,23 +5,29 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Refresh events. /// -public class AddonRefreshArgs : AddonArgs +public class AddonRefreshArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Refresh; /// - /// Gets the number of AtkValues. + /// Gets or sets the number of AtkValues. /// - public uint AtkValueCount { get; init; } + public uint AtkValueCount { get; set; } /// - /// Gets the address of the AtkValue array. + /// Gets or sets the address of the AtkValue array. /// - public nint AtkValues { get; init; } + public nint AtkValues { get; set; } /// /// Gets the AtkValues in the form of a span. /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index 1b743b31a..219288ccf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,18 +3,24 @@ /// /// Addon argument data for OnRequestedUpdate events. /// -public class AddonRequestedUpdateArgs : AddonArgs +public class AddonRequestedUpdateArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.RequestedUpdate; /// - /// Gets the NumberArrayData** for this event. + /// Gets or sets the NumberArrayData** for this event. /// - public nint NumberArrayData { get; init; } + public nint NumberArrayData { get; set; } /// - /// Gets the StringArrayData** for this event. + /// Gets or sets the StringArrayData** for this event. /// - public nint StringArrayData { get; init; } + public nint StringArrayData { get; set; } + + /// + public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index df2ec26be..bd60879b8 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -5,23 +5,29 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : AddonArgs +public class AddonSetupArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Setup; /// - /// Gets the number of AtkValues. + /// Gets or sets the number of AtkValues. /// - public uint AtkValueCount { get; init; } + public uint AtkValueCount { get; set; } /// - /// Gets the address of the AtkValue array. + /// Gets or sets the address of the AtkValue array. /// - public nint AtkValues { get; init; } + public nint AtkValues { get; set; } /// /// Gets the AtkValues in the form of a span. /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index 651fbcafb..b087ac15a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// -public class AddonUpdateArgs : AddonArgs +public class AddonUpdateArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Update; @@ -11,5 +11,11 @@ public class AddonUpdateArgs : AddonArgs /// /// Gets the time since the last update. /// - public float TimeDelta { get; init; } + public float TimeDelta { get; internal set; } + + /// + public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 3528de562..decb7a9f4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; @@ -40,6 +41,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); + // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet + // package, and these events are always called from the main thread, this is fine. + private readonly AddonSetupArgs recyclingSetupArgs = new(); + private readonly AddonFinalizeArgs recyclingFinalizeArgs = new(); + private readonly AddonDrawArgs recyclingDrawArgs = new(); + private readonly AddonUpdateArgs recyclingUpdateArgs = new(); + private readonly AddonRefreshArgs recyclingRefreshArgs = new(); + private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new(); + [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { @@ -132,12 +142,27 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// /// Event Type. /// AddonArgs. - internal void InvokeListeners(AddonEvent eventType, AddonArgs args) + /// What to blame on errors. + internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { - // Match on string.empty for listeners that want events for all addons. - foreach (var listener in this.EventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) + // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. + foreach (var listener in this.EventListeners) { - listener.FunctionDelegate.Invoke(eventType, args); + if (listener.EventType != eventType) + continue; + + // Match on string.empty for listeners that want events for all addons. + if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) + continue; + + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke."); + } } } @@ -249,20 +274,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); } - - try - { - this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); - } + + this.recyclingSetupArgs.Addon = (nint)addon; + this.recyclingSetupArgs.AtkValueCount = valueCount; + this.recyclingSetupArgs.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs); + valueCount = this.recyclingSetupArgs.AtkValueCount; + values = (AtkValue*)this.recyclingSetupArgs.AtkValues; try { @@ -273,19 +291,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup post-setup invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostSetup, this.recyclingSetupArgs); } private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) @@ -299,15 +305,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); } - - try - { - this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke."); - } + + this.recyclingFinalizeArgs.Addon = (nint)atkUnitBase[0]; + this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs); try { @@ -321,14 +321,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonDraw(AtkUnitBase* addon) { - try - { - this.InvokeListeners(AddonEvent.PreDraw, new AddonDrawArgs { Addon = (nint)addon }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); - } + this.recyclingDrawArgs.Addon = (nint)addon; + this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs); try { @@ -339,26 +333,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostDraw, new AddonDrawArgs { Addon = (nint)addon }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonDraw post-draw invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostDraw, this.recyclingDrawArgs); } private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - try - { - this.InvokeListeners(AddonEvent.PreUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); - } + this.recyclingUpdateArgs.Addon = (nint)addon; + this.recyclingUpdateArgs.TimeDelta = delta; + this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs); try { @@ -369,33 +351,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostUpdate, this.recyclingUpdateArgs); } private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { byte result = 0; - - try - { - this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); - } + + this.recyclingRefreshArgs.Addon = (nint)addon; + this.recyclingRefreshArgs.AtkValueCount = valueCount; + this.recyclingRefreshArgs.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs); + valueCount = this.recyclingRefreshArgs.AtkValueCount; + values = (AtkValue*)this.recyclingRefreshArgs.AtkValues; try { @@ -406,38 +374,18 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostRefresh, new AddonRefreshArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); - } - + this.InvokeListenersSafely(AddonEvent.PostRefresh, this.recyclingRefreshArgs); return result; } private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - try - { - this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonRequestedUpdateArgs - { - Addon = (nint)addon, - NumberArrayData = (nint)numberArrayData, - StringArrayData = (nint)stringArrayData, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke."); - } + this.recyclingRequestedUpdateArgs.Addon = (nint)addon; + this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData; + this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData; + this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs); + numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData; + stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData; try { @@ -448,19 +396,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonRequestedUpdateArgs - { - Addon = (nint)addon, - NumberArrayData = (nint)numberArrayData, - StringArrayData = (nint)stringArrayData, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.recyclingRequestedUpdateArgs); } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs index 10171eb16..1c138e447 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -16,6 +16,10 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable { private static readonly ModuleLog Log = new("AddonLifecycle"); + // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet + // package, and these events are always called from the main thread, this is fine. + private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new(); + /// /// Initializes a new instance of the class. /// @@ -74,22 +78,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable this.Hook!.Original(addon, eventType, eventParam, atkEvent, data); return; } - - try - { - this.AddonLifecycle.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs - { - Addon = (nint)addon, - AtkEventType = (byte)eventType, - EventParam = eventParam, - AtkEvent = (nint)atkEvent, - Data = data, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke."); - } + + this.recyclingReceiveEventArgs.Addon = (nint)addon; + this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType; + this.recyclingReceiveEventArgs.EventParam = eventParam; + this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent; + this.recyclingReceiveEventArgs.Data = data; + this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs); + eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType; + eventParam = this.recyclingReceiveEventArgs.EventParam; + atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent; + data = this.recyclingReceiveEventArgs.Data; try { @@ -100,20 +99,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.AddonLifecycle.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs - { - Addon = (nint)addon, - AtkEventType = (byte)eventType, - EventParam = eventParam, - AtkEvent = (nint)atkEvent, - Data = data, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke."); - } + this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs); } } diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index 3ceecf6a6..552817646 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -163,6 +163,38 @@ public static unsafe class MemoryHelper #region ReadString + /// + /// Compares if the given char span equals to the null-terminated string at . + /// + /// The character span. + /// The address of null-terminated string. + /// The encoding of the null-terminated string. + /// The maximum length of the null-terminated string. + /// Whether they are equal. + public static bool EqualsZeroTerminatedString( + ReadOnlySpan charSpan, + nint memoryAddress, + Encoding? encoding = null, + int maxLength = int.MaxValue) + { + encoding ??= Encoding.UTF8; + maxLength = Math.Min(maxLength, charSpan.Length + 4); + + var pmem = ((byte*)memoryAddress)!; + var length = 0; + while (length < maxLength && pmem[length] != 0) + length++; + + var mem = new Span(pmem, length); + var memCharCount = encoding.GetCharCount(mem); + if (memCharCount != charSpan.Length) + return false; + + Span chars = stackalloc char[memCharCount]; + encoding.GetChars(mem, chars); + return charSpan.SequenceEqual(chars); + } + /// /// Read a UTF-8 encoded string from a specified memory address. /// From b89df8b130b03c37714922fddd81933b59ab9d12 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:20:05 +0900 Subject: [PATCH 33/51] Prevent end comment aligning (Resharper/SA conflict) --- .editorconfig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 66e123f53..141e8c9c9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -104,13 +104,14 @@ resharper_can_use_global_alias = false resharper_csharp_align_multiline_parameter = true resharper_csharp_align_multiple_declaration = true resharper_csharp_empty_block_style = multiline -resharper_csharp_int_align_comments = true +resharper_csharp_int_align_comments = false resharper_csharp_new_line_before_while = true resharper_csharp_wrap_after_declaration_lpar = true resharper_csharp_wrap_after_invocation_lpar = true resharper_csharp_wrap_arguments_style = chop_if_long resharper_enforce_line_ending_style = true resharper_instance_members_qualify_declared_in = this_class, base_class +resharper_int_align = false resharper_member_can_be_private_global_highlighting = none resharper_member_can_be_private_local_highlighting = none resharper_new_line_before_finally = true From 0c3ebd4b5b969e2645f23c22d8547276b5528cf4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:20:25 +0900 Subject: [PATCH 34/51] Fix missing length modification in ImVectorWrapper.Insert --- Dalamud/Interface/Utility/ImVectorWrapper.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index 67b002179..d41ee0094 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -519,10 +519,11 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi if (index < 0 || index > this.LengthUnsafe) throw new IndexOutOfRangeException(); - this.EnsureCapacityExponential(this.CapacityUnsafe + 1); + this.EnsureCapacityExponential(this.LengthUnsafe + 1); var num = this.LengthUnsafe - index; Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T)); this.DataUnsafe[index] = item; + this.LengthUnsafe += 1; } /// @@ -535,6 +536,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + count, num * sizeof(T), num * sizeof(T)); foreach (var item in items) this.DataUnsafe[index++] = item; + this.LengthUnsafe += count; } else { @@ -551,6 +553,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T)); foreach (var item in items) this.DataUnsafe[index++] = item; + this.LengthUnsafe += items.Length; } /// @@ -566,6 +569,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi this.destroyer?.Invoke(&this.DataUnsafe[index]); Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T)); + this.LengthUnsafe -= 1; } /// From 8967174cd968d6993401fa706bc7682657127ed5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:28:12 +0900 Subject: [PATCH 35/51] Reimplement clipboard text normalizer to use the correct buffers --- .../Internal/ImGuiClipboardConfig.cs | 232 +++++++++++++++--- .../Interface/Internal/InterfaceManager.cs | 2 - 2 files changed, 193 insertions(+), 41 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index b3302add4..5dc04d736 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -1,4 +1,9 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal; @@ -21,60 +26,209 @@ namespace Dalamud.Interface.Internal; /// works for both ImGui and XIV. /// /// -internal static class ImGuiClipboardConfig +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable { - private delegate void SetClipboardTextDelegate(IntPtr userData, string text); - private delegate string GetClipboardTextDelegate(); + private readonly nint clipboardUserDataOriginal; + private readonly delegate* unmanaged setTextOriginal; + private readonly delegate* unmanaged getTextOriginal; - private static SetClipboardTextDelegate? _setTextOriginal = null; - private static GetClipboardTextDelegate? _getTextOriginal = null; + [ServiceManager.ServiceConstructor] + private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws) + { + // Effectively waiting for ImGui to become available. + _ = imws; + Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); - // These must exist as variables to prevent them from being GC'd - private static SetClipboardTextDelegate? _setText = null; - private static GetClipboardTextDelegate? _getText = null; + var io = ImGui.GetIO(); + this.setTextOriginal = (delegate* unmanaged)io.SetClipboardTextFn; + this.getTextOriginal = (delegate* unmanaged)io.GetClipboardTextFn; + this.clipboardUserDataOriginal = io.ClipboardUserData; + io.SetClipboardTextFn = (nint)(delegate* unmanaged)(&StaticSetClipboardTextImpl); + io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; + io.ClipboardUserData = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + return; - public static void Apply() + [UnmanagedCallersOnly] + static void StaticSetClipboardTextImpl(nint userData, byte* text) => + ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text); + + [UnmanagedCallersOnly] + static byte* StaticGetClipboardTextImpl(nint userData) => + ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); + } + + /// + /// Finalizes an instance of the class. + /// + ~ImGuiClipboardConfig() => this.ReleaseUnmanagedResources(); + + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] + private static ImVectorWrapper ImGuiCurrentContextClipboardHandlerData => + new((ImVector*)(ImGui.GetCurrentContext() + 0x5520)); + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private void ReleaseUnmanagedResources() { var io = ImGui.GetIO(); - if (_setTextOriginal == null) - { - _setTextOriginal = - Marshal.GetDelegateForFunctionPointer(io.SetClipboardTextFn); - } + if (io.ClipboardUserData == default) + return; - if (_getTextOriginal == null) - { - _getTextOriginal = - Marshal.GetDelegateForFunctionPointer(io.GetClipboardTextFn); - } - - _setText = new SetClipboardTextDelegate(SetClipboardText); - _getText = new GetClipboardTextDelegate(GetClipboardText); - - io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText); - io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText); + GCHandle.FromIntPtr(io.ClipboardUserData).Free(); + io.SetClipboardTextFn = (nint)this.setTextOriginal; + io.GetClipboardTextFn = (nint)this.getTextOriginal; + io.ClipboardUserData = this.clipboardUserDataOriginal; } - public static void Unapply() + private void SetClipboardTextImpl(byte* text) { - var io = ImGui.GetIO(); - if (_setTextOriginal != null) - { - io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal); - } - if (_getTextOriginal != null) - { - io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal); - } + var buffer = ImGuiCurrentContextClipboardHandlerData; + Utf8Utils.SetFromNullTerminatedBytes(ref buffer, text); + Utf8Utils.Normalize(ref buffer); + Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data); } - private static void SetClipboardText(IntPtr userData, string text) + private byte* GetClipboardTextImpl() { - _setTextOriginal!(userData, text.ReplaceLineEndings("\r\n")); + _ = this.getTextOriginal(this.clipboardUserDataOriginal); + + var buffer = ImGuiCurrentContextClipboardHandlerData; + Utf8Utils.TrimNullTerminator(ref buffer); + Utf8Utils.Normalize(ref buffer); + Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + return buffer.Data; } - private static string GetClipboardText() + private static class Utf8Utils { - return _getTextOriginal!().ReplaceLineEndings("\r\n"); + /// + /// Sets from , a null terminated UTF-8 string. + /// + /// The target buffer. It will not contain a null terminator. + /// The pointer to the null-terminated UTF-8 string. + public static void SetFromNullTerminatedBytes(ref ImVectorWrapper buf, byte* psz) + { + var len = 0; + while (psz[len] != 0) + len++; + + buf.Clear(); + buf.AddRange(new Span(psz, len)); + } + + /// + /// Removes the null terminator. + /// + /// The UTF-8 string buffer. + public static void TrimNullTerminator(ref ImVectorWrapper buf) + { + while (buf.Length > 0 && buf[^1] == 0) + buf.LengthUnsafe--; + } + + /// + /// Adds a null terminator to the buffer. + /// + /// The buffer. + public static void AddNullTerminatorIfMissing(ref ImVectorWrapper buf) + { + if (buf.Length > 0 && buf[^1] == 0) + return; + buf.Add(0); + } + + /// + /// Counts the number of bytes for the UTF-8 character. + /// + /// The bytes. + /// Available number of bytes. + /// Number of bytes taken, or -1 if the byte was invalid. + public static int CountBytes(byte* b, int avail) + { + if (avail <= 0) + return 0; + if ((b[0] & 0x80) == 0) + return 1; + if ((b[0] & 0xE0) == 0xC0 && avail >= 2) + return (b[1] & 0xC0) == 0x80 ? 2 : -1; + if ((b[0] & 0xF0) == 0xE0 && avail >= 3) + return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 ? 3 : -1; + if ((b[0] & 0xF8) == 0xF0 && avail >= 4) + return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80 ? 4 : -1; + return -1; + } + + /// + /// Gets the codepoint. + /// + /// The bytes. + /// The result from . + /// The codepoint, or \xFFFD replacement character if failed. + public static int GetCodepoint(byte* b, int cb) => cb switch + { + 1 => b[0], + 2 => ((b[0] & 0x8F) << 6) | (b[1] & 0x3F), + 3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F), + 4 => ((b[0] & 0x0F) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), + _ => 0xFFFD, + }; + + /// + /// Normalize the given text for our use case. + /// + /// The buffer. + public static void Normalize(ref ImVectorWrapper buf) + { + for (var i = 0; i < buf.Length;) + { + // Already correct? + if (buf[i] is 0x0D && buf[i + 1] is 0x0A) + { + i += 2; + continue; + } + + var cb = CountBytes(buf.Data + i, buf.Length - i); + var currInt = GetCodepoint(buf.Data + i, cb); + switch (currInt) + { + case 0xFFFF: // Simply invalid + case > char.MaxValue: // ImWchar is same size with char; does not support + case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support + // Replace with \uFFFD in UTF-8: EF BF BD + buf[i++] = 0xEF; + buf.Insert(i++, 0xBF); + buf.Insert(i++, 0xBD); + break; + + // See String.Manipulation.cs: IndexOfNewlineChar. + case '\r': // CR; Carriage Return + case '\n': // LF; Line Feed + case '\f': // FF; Form Feed + buf[i++] = 0x0D; + buf.Insert(i++, 0x0A); + break; + + case '\u0085': // NEL; Next Line + case '\u2028': // LS; Line Separator + case '\u2029': // PS; Paragraph Separator + buf[i++] = 0x0D; + buf[i++] = 0x0A; + break; + + default: + // Not a newline char. + i += cb; + break; + } + } + } } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 7d164c01f..1b12fd853 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -240,7 +240,6 @@ internal class InterfaceManager : IDisposable, IServiceType this.processMessageHook?.Dispose(); }).Wait(); - ImGuiClipboardConfig.Unapply(); this.scene?.Dispose(); } @@ -629,7 +628,6 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; this.SetupFonts(); - ImGuiClipboardConfig.Apply(); if (!configuration.IsDocking) { From 2521658a984f2b605c320952745ac9eae27a8b94 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:33:35 +0900 Subject: [PATCH 36/51] Better cleanup logic --- Dalamud/Interface/Internal/ImGuiClipboardConfig.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index 5dc04d736..db47d9734 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -32,6 +32,7 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable private readonly nint clipboardUserDataOriginal; private readonly delegate* unmanaged setTextOriginal; private readonly delegate* unmanaged getTextOriginal; + private GCHandle clipboardUserData; [ServiceManager.ServiceConstructor] private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws) @@ -46,7 +47,7 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable this.clipboardUserDataOriginal = io.ClipboardUserData; io.SetClipboardTextFn = (nint)(delegate* unmanaged)(&StaticSetClipboardTextImpl); io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; - io.ClipboardUserData = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this)); return; [UnmanagedCallersOnly] @@ -76,14 +77,15 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable private void ReleaseUnmanagedResources() { - var io = ImGui.GetIO(); - if (io.ClipboardUserData == default) + if (!this.clipboardUserData.IsAllocated) return; - GCHandle.FromIntPtr(io.ClipboardUserData).Free(); + var io = ImGui.GetIO(); io.SetClipboardTextFn = (nint)this.setTextOriginal; io.GetClipboardTextFn = (nint)this.getTextOriginal; io.ClipboardUserData = this.clipboardUserDataOriginal; + + this.clipboardUserData.Free(); } private void SetClipboardTextImpl(byte* text) From 683464ed2da8a50b2b1c296c7612efd8c5c640da Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:37:41 +0900 Subject: [PATCH 37/51] Fix normalization buffer offsetting --- .../Internal/ImGuiClipboardConfig.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index db47d9734..286f58a81 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -206,23 +206,40 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support // Replace with \uFFFD in UTF-8: EF BF BD buf[i++] = 0xEF; - buf.Insert(i++, 0xBF); - buf.Insert(i++, 0xBD); + + if (cb >= 2) + buf[i++] = 0xBF; + else + buf.Insert(i++, 0xBF); + + if (cb >= 3) + buf[i++] = 0xBD; + else + buf.Insert(i++, 0xBD); + + if (cb >= 4) + buf.RemoveAt(i); break; // See String.Manipulation.cs: IndexOfNewlineChar. case '\r': // CR; Carriage Return case '\n': // LF; Line Feed case '\f': // FF; Form Feed - buf[i++] = 0x0D; - buf.Insert(i++, 0x0A); - break; - case '\u0085': // NEL; Next Line case '\u2028': // LS; Line Separator case '\u2029': // PS; Paragraph Separator buf[i++] = 0x0D; - buf[i++] = 0x0A; + + if (cb >= 2) + buf[i++] = 0x0A; + else + buf.Insert(i++, 0x0A); + + if (cb >= 3) + buf.RemoveAt(i); + + if (cb >= 4) + buf.RemoveAt(i); break; default: From a0b7f53b0103a393f79b12f9038f2356f34ef6ab Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:45:07 +0900 Subject: [PATCH 38/51] Remove finalizer; cannot be finalized due to the use of GCHandle --- Dalamud/Interface/Internal/ImGuiClipboardConfig.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index 286f58a81..ab8730682 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -59,23 +59,12 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); } - /// - /// Finalizes an instance of the class. - /// - ~ImGuiClipboardConfig() => this.ReleaseUnmanagedResources(); - [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] private static ImVectorWrapper ImGuiCurrentContextClipboardHandlerData => new((ImVector*)(ImGui.GetCurrentContext() + 0x5520)); /// public void Dispose() - { - this.ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - private void ReleaseUnmanagedResources() { if (!this.clipboardUserData.IsAllocated) return; From ddee969d883f71f2fc5eec767a9ae1eaac6bff78 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 00:00:28 +0900 Subject: [PATCH 39/51] Rename service --- ...ipboardConfig.cs => ImGuiClipboardFunctionProvider.cs} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename Dalamud/Interface/Internal/{ImGuiClipboardConfig.cs => ImGuiClipboardFunctionProvider.cs} (95%) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs similarity index 95% rename from Dalamud/Interface/Internal/ImGuiClipboardConfig.cs rename to Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index ab8730682..a99064a9d 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -27,7 +27,7 @@ namespace Dalamud.Interface.Internal; /// /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable +internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable { private readonly nint clipboardUserDataOriginal; private readonly delegate* unmanaged setTextOriginal; @@ -35,7 +35,7 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable private GCHandle clipboardUserData; [ServiceManager.ServiceConstructor] - private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws) + private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws) { // Effectively waiting for ImGui to become available. _ = imws; @@ -52,11 +52,11 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable [UnmanagedCallersOnly] static void StaticSetClipboardTextImpl(nint userData, byte* text) => - ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text); + ((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text); [UnmanagedCallersOnly] static byte* StaticGetClipboardTextImpl(nint userData) => - ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); + ((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); } [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] From 3d51f88a335eb933206f79149e2661344610c65c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 00:03:10 +0900 Subject: [PATCH 40/51] fix --- Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index a99064a9d..20e49823a 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -165,9 +165,9 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis public static int GetCodepoint(byte* b, int cb) => cb switch { 1 => b[0], - 2 => ((b[0] & 0x8F) << 6) | (b[1] & 0x3F), + 2 => ((b[0] & 0x1F) << 6) | (b[1] & 0x3F), 3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F), - 4 => ((b[0] & 0x0F) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), + 4 => ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), _ => 0xFFFD, }; From ca321e59e402a5fb83191e0508ae19cafa167959 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 00:14:10 +0900 Subject: [PATCH 41/51] Make logic clearer --- .../ImGuiClipboardFunctionProvider.cs | 75 ++++++++++--------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 20e49823a..f14210b97 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -171,43 +171,61 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis _ => 0xFFFD, }; + /// + /// Replaces a sequence with another. + /// + /// The buffer. + /// Offset of the sequence to be replaced. + /// Length of the sequence to be replaced. + /// The replacement sequence. + /// The length of . + public static int ReplaceSequence( + ref ImVectorWrapper buf, + int offset, + int length, + ReadOnlySpan replacement) + { + var i = 0; + for (; i < replacement.Length; i++) + { + if (length >= i + 1) + buf[offset++] = replacement[i]; + else + buf.Insert(offset++, replacement[i]); + } + + for (; i < length; i++) + buf.RemoveAt(offset); + + return replacement.Length; + } + /// /// Normalize the given text for our use case. /// /// The buffer. public static void Normalize(ref ImVectorWrapper buf) { + // Ensure an implicit null after the end of the string. + buf.EnsureCapacity(buf.Length + 1); + buf.StorageSpan[buf.Length] = 0; + for (var i = 0; i < buf.Length;) { - // Already correct? - if (buf[i] is 0x0D && buf[i + 1] is 0x0A) - { - i += 2; - continue; - } - var cb = CountBytes(buf.Data + i, buf.Length - i); var currInt = GetCodepoint(buf.Data + i, cb); switch (currInt) { - case 0xFFFF: // Simply invalid + // Note that buf.Data[i + 1] is always defined. See the beginning of the function. + case '\r' when buf.Data[i + 1] == '\n': // Already CR LF? + i += 2; + continue; + + case 0xFFFE or 0xFFFF: // Simply invalid case > char.MaxValue: // ImWchar is same size with char; does not support case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support // Replace with \uFFFD in UTF-8: EF BF BD - buf[i++] = 0xEF; - - if (cb >= 2) - buf[i++] = 0xBF; - else - buf.Insert(i++, 0xBF); - - if (cb >= 3) - buf[i++] = 0xBD; - else - buf.Insert(i++, 0xBD); - - if (cb >= 4) - buf.RemoveAt(i); + i += ReplaceSequence(ref buf, i, cb, "\uFFFD"u8); break; // See String.Manipulation.cs: IndexOfNewlineChar. @@ -217,18 +235,7 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis case '\u0085': // NEL; Next Line case '\u2028': // LS; Line Separator case '\u2029': // PS; Paragraph Separator - buf[i++] = 0x0D; - - if (cb >= 2) - buf[i++] = 0x0A; - else - buf.Insert(i++, 0x0A); - - if (cb >= 3) - buf.RemoveAt(i); - - if (cb >= 4) - buf.RemoveAt(i); + i += ReplaceSequence(ref buf, i, cb, "\r\n"u8); break; default: From 6b65ee9940fa309b0cab7953c44d71669e63470f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 01:40:54 +0900 Subject: [PATCH 42/51] Extract functions to ImVectorWrapper --- .../ImGuiClipboardFunctionProvider.cs | 162 +------------- .../ImVectorWrapper.ZeroTerminatedSequence.cs | 207 ++++++++++++++++++ Dalamud/Interface/Utility/ImVectorWrapper.cs | 91 ++++++-- 3 files changed, 286 insertions(+), 174 deletions(-) create mode 100644 Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index f14210b97..265bc5812 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -80,9 +80,9 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis private void SetClipboardTextImpl(byte* text) { var buffer = ImGuiCurrentContextClipboardHandlerData; - Utf8Utils.SetFromNullTerminatedBytes(ref buffer, text); - Utf8Utils.Normalize(ref buffer); - Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + buffer.SetFromZeroTerminatedSequence(text); + buffer.Utf8Normalize(); + buffer.AddZeroTerminatorIfMissing(); this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data); } @@ -91,159 +91,9 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis _ = this.getTextOriginal(this.clipboardUserDataOriginal); var buffer = ImGuiCurrentContextClipboardHandlerData; - Utf8Utils.TrimNullTerminator(ref buffer); - Utf8Utils.Normalize(ref buffer); - Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + buffer.TrimZeroTerminator(); + buffer.Utf8Normalize(); + buffer.AddZeroTerminatorIfMissing(); return buffer.Data; } - - private static class Utf8Utils - { - /// - /// Sets from , a null terminated UTF-8 string. - /// - /// The target buffer. It will not contain a null terminator. - /// The pointer to the null-terminated UTF-8 string. - public static void SetFromNullTerminatedBytes(ref ImVectorWrapper buf, byte* psz) - { - var len = 0; - while (psz[len] != 0) - len++; - - buf.Clear(); - buf.AddRange(new Span(psz, len)); - } - - /// - /// Removes the null terminator. - /// - /// The UTF-8 string buffer. - public static void TrimNullTerminator(ref ImVectorWrapper buf) - { - while (buf.Length > 0 && buf[^1] == 0) - buf.LengthUnsafe--; - } - - /// - /// Adds a null terminator to the buffer. - /// - /// The buffer. - public static void AddNullTerminatorIfMissing(ref ImVectorWrapper buf) - { - if (buf.Length > 0 && buf[^1] == 0) - return; - buf.Add(0); - } - - /// - /// Counts the number of bytes for the UTF-8 character. - /// - /// The bytes. - /// Available number of bytes. - /// Number of bytes taken, or -1 if the byte was invalid. - public static int CountBytes(byte* b, int avail) - { - if (avail <= 0) - return 0; - if ((b[0] & 0x80) == 0) - return 1; - if ((b[0] & 0xE0) == 0xC0 && avail >= 2) - return (b[1] & 0xC0) == 0x80 ? 2 : -1; - if ((b[0] & 0xF0) == 0xE0 && avail >= 3) - return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 ? 3 : -1; - if ((b[0] & 0xF8) == 0xF0 && avail >= 4) - return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80 ? 4 : -1; - return -1; - } - - /// - /// Gets the codepoint. - /// - /// The bytes. - /// The result from . - /// The codepoint, or \xFFFD replacement character if failed. - public static int GetCodepoint(byte* b, int cb) => cb switch - { - 1 => b[0], - 2 => ((b[0] & 0x1F) << 6) | (b[1] & 0x3F), - 3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F), - 4 => ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), - _ => 0xFFFD, - }; - - /// - /// Replaces a sequence with another. - /// - /// The buffer. - /// Offset of the sequence to be replaced. - /// Length of the sequence to be replaced. - /// The replacement sequence. - /// The length of . - public static int ReplaceSequence( - ref ImVectorWrapper buf, - int offset, - int length, - ReadOnlySpan replacement) - { - var i = 0; - for (; i < replacement.Length; i++) - { - if (length >= i + 1) - buf[offset++] = replacement[i]; - else - buf.Insert(offset++, replacement[i]); - } - - for (; i < length; i++) - buf.RemoveAt(offset); - - return replacement.Length; - } - - /// - /// Normalize the given text for our use case. - /// - /// The buffer. - public static void Normalize(ref ImVectorWrapper buf) - { - // Ensure an implicit null after the end of the string. - buf.EnsureCapacity(buf.Length + 1); - buf.StorageSpan[buf.Length] = 0; - - for (var i = 0; i < buf.Length;) - { - var cb = CountBytes(buf.Data + i, buf.Length - i); - var currInt = GetCodepoint(buf.Data + i, cb); - switch (currInt) - { - // Note that buf.Data[i + 1] is always defined. See the beginning of the function. - case '\r' when buf.Data[i + 1] == '\n': // Already CR LF? - i += 2; - continue; - - case 0xFFFE or 0xFFFF: // Simply invalid - case > char.MaxValue: // ImWchar is same size with char; does not support - case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support - // Replace with \uFFFD in UTF-8: EF BF BD - i += ReplaceSequence(ref buf, i, cb, "\uFFFD"u8); - break; - - // See String.Manipulation.cs: IndexOfNewlineChar. - case '\r': // CR; Carriage Return - case '\n': // LF; Line Feed - case '\f': // FF; Form Feed - case '\u0085': // NEL; Next Line - case '\u2028': // LS; Line Separator - case '\u2029': // PS; Paragraph Separator - i += ReplaceSequence(ref buf, i, cb, "\r\n"u8); - break; - - default: - // Not a newline char. - i += cb; - break; - } - } - } - } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs b/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs new file mode 100644 index 000000000..507bdce20 --- /dev/null +++ b/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs @@ -0,0 +1,207 @@ +using System.Numerics; +using System.Text; + +namespace Dalamud.Interface.Utility; + +/// +/// Utility methods for . +/// +public static partial class ImVectorWrapper +{ + /// + /// Appends from , a zero terminated sequence. + /// + /// The element type. + /// The target buffer. + /// The pointer to the zero-terminated sequence. + public static unsafe void AppendZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) + where T : unmanaged, INumber + { + var len = 0; + while (psz[len] != default) + len++; + + buf.AddRange(new Span(psz, len)); + } + + /// + /// Sets from , a zero terminated sequence. + /// + /// The element type. + /// The target buffer. + /// The pointer to the zero-terminated sequence. + public static unsafe void SetFromZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) + where T : unmanaged, INumber + { + buf.Clear(); + buf.AppendZeroTerminatedSequence(psz); + } + + /// + /// Trims zero terminator(s). + /// + /// The element type. + /// The buffer. + public static void TrimZeroTerminator(this ref ImVectorWrapper buf) + where T : unmanaged, INumber + { + ref var len = ref buf.LengthUnsafe; + while (len > 0 && buf[len - 1] == default) + len--; + } + + /// + /// Adds a zero terminator to the buffer, if missing. + /// + /// The element type. + /// The buffer. + public static void AddZeroTerminatorIfMissing(this ref ImVectorWrapper buf) + where T : unmanaged, INumber + { + if (buf.Length > 0 && buf[^1] == default) + return; + buf.Add(default); + } + + /// + /// Gets the codepoint at the given offset. + /// + /// The buffer containing bytes in UTF-8. + /// The offset in bytes. + /// Number of bytes occupied by the character, invalid or not. + /// The fallback character, if no valid UTF-8 character could be found. + /// The parsed codepoint, or if it could not be parsed correctly. + public static unsafe int Utf8GetCodepoint( + this in ImVectorWrapper buf, + int offset, + out int numBytes, + int invalid = 0xFFFD) + { + var cb = buf.LengthUnsafe - offset; + if (cb <= 0) + { + numBytes = 0; + return invalid; + } + + numBytes = 1; + + var b = buf.DataUnsafe + offset; + if ((b[0] & 0x80) == 0) + return b[0]; + + if (cb < 2 || (b[1] & 0xC0) != 0x80) + return invalid; + if ((b[0] & 0xE0) == 0xC0) + { + numBytes = 2; + return ((b[0] & 0x1F) << 6) | (b[1] & 0x3F); + } + + if (cb < 3 || (b[2] & 0xC0) != 0x80) + return invalid; + if ((b[0] & 0xF0) == 0xE0) + { + numBytes = 3; + return ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F); + } + + if (cb < 4 || (b[3] & 0xC0) != 0x80) + return invalid; + if ((b[0] & 0xF8) == 0xF0) + { + numBytes = 4; + return ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F); + } + + return invalid; + } + + /// + /// Normalizes the given UTF-8 string.
+ /// Using the default values will ensure the best interop between the game, ImGui, and Windows. + ///
+ /// The buffer containing bytes in UTF-8. + /// The replacement line ending. If empty, CR LF will be used. + /// The replacement invalid character. If empty, U+FFFD REPLACEMENT CHARACTER will be used. + /// Specify whether to normalize the line endings. + /// Specify whether to replace invalid characters. + /// Specify whether to replace characters that requires the use of surrogate, when encoded in UTF-16. + /// Specify whether to make sense out of WTF-8. + public static unsafe void Utf8Normalize( + this ref ImVectorWrapper buf, + ReadOnlySpan lineEnding = default, + ReadOnlySpan invalidChar = default, + bool normalizeLineEndings = true, + bool sanitizeInvalidCharacters = true, + bool sanitizeNonUcs2Characters = true, + bool sanitizeSurrogates = true) + { + if (lineEnding.IsEmpty) + lineEnding = "\r\n"u8; + if (invalidChar.IsEmpty) + invalidChar = "\uFFFD"u8; + + // Ensure an implicit null after the end of the string. + buf.EnsureCapacity(buf.Length + 1); + buf.StorageSpan[buf.Length] = 0; + + Span charsBuf = stackalloc char[2]; + Span bytesBuf = stackalloc byte[4]; + for (var i = 0; i < buf.Length;) + { + var c1 = buf.Utf8GetCodepoint(i, out var cb, -1); + switch (c1) + { + // Note that buf.Data[i + 1] is always defined. See the beginning of the function. + case '\r' when buf.Data[i + 1] == '\n': + // If it's already CR LF, it passes all filters. + i += 2; + break; + + case >= 0xD800 and <= 0xDFFF when sanitizeSurrogates: + { + var c2 = buf.Utf8GetCodepoint(i + cb, out var cb2); + if (c1 is < 0xD800 or >= 0xDC00) + goto case -2; + if (c2 is < 0xDC00 or >= 0xE000) + goto case -2; + charsBuf[0] = unchecked((char)c1); + charsBuf[1] = unchecked((char)c2); + var bytesLen = Encoding.UTF8.GetBytes(charsBuf, bytesBuf); + buf.ReplaceRange(i, cb + cb2, bytesBuf[..bytesLen]); + // Do not alter i; now that the WTF-8 has been dealt with, apply other filters. + break; + } + + case -2: + case -1 or 0xFFFE or 0xFFFF when sanitizeInvalidCharacters: + case >= 0xD800 and <= 0xDFFF when sanitizeInvalidCharacters: + case > char.MaxValue when sanitizeNonUcs2Characters: + { + buf.ReplaceRange(i, cb, invalidChar); + i += invalidChar.Length; + break; + } + + // See String.Manipulation.cs: IndexOfNewlineChar. + // CR; Carriage Return + // LF; Line Feed + // FF; Form Feed + // NEL; Next Line + // LS; Line Separator + // PS; Paragraph Separator + case '\r' or '\n' or '\f' or '\u0085' or '\u2028' or '\u2029' when normalizeLineEndings: + { + buf.ReplaceRange(i, cb, lineEnding); + i += lineEnding.Length; + break; + } + + default: + i += cb; + break; + } + } + } +} diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index d41ee0094..51524efc4 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -13,7 +13,7 @@ namespace Dalamud.Interface.Utility; /// /// Utility methods for . /// -public static class ImVectorWrapper +public static partial class ImVectorWrapper { /// /// Creates a new instance of the struct, initialized with @@ -394,7 +394,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi } /// - public void AddRange(Span items) + public void AddRange(ReadOnlySpan items) { this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); foreach (var item in items) @@ -466,7 +466,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// The minimum capacity to ensure. /// Whether the capacity has been changed. public bool EnsureCapacityExponential(int capacity) - => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe))); + => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)capacity))); /// /// Resizes the underlying array and fills with zeroes if grown. @@ -545,8 +545,8 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi } } - /// - public void InsertRange(int index, Span items) + /// + public void InsertRange(int index, ReadOnlySpan items) { this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); var num = this.LengthUnsafe - index; @@ -561,16 +561,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// /// The index. /// Whether to skip calling the destroyer function. - public void RemoveAt(int index, bool skipDestroyer = false) - { - this.EnsureIndex(index); - var num = this.LengthUnsafe - index - 1; - if (!skipDestroyer) - this.destroyer?.Invoke(&this.DataUnsafe[index]); - - Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T)); - this.LengthUnsafe -= 1; - } + public void RemoveAt(int index, bool skipDestroyer = false) => this.RemoveRange(index, 1, skipDestroyer); /// void IList.RemoveAt(int index) => this.RemoveAt(index); @@ -578,6 +569,73 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// void IList.RemoveAt(int index) => this.RemoveAt(index); + /// + /// Removes elements at the given index. + /// + /// The index of the first item to remove. + /// Number of items to remove. + /// Whether to skip calling the destroyer function. + public void RemoveRange(int index, int count, bool skipDestroyer = false) + { + this.EnsureIndex(index); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), count, "Must be positive."); + if (count == 0) + return; + + if (!skipDestroyer && this.destroyer is { } d) + { + for (var i = 0; i < count; i++) + d(this.DataUnsafe + index + i); + } + + var numItemsToMove = this.LengthUnsafe - index - count; + var numBytesToMove = numItemsToMove * sizeof(T); + Buffer.MemoryCopy(this.DataUnsafe + index + count, this.DataUnsafe + index, numBytesToMove, numBytesToMove); + this.LengthUnsafe -= count; + } + + /// + /// Replaces a sequence at given offset of items with + /// . + /// + /// The index of the first item to be replaced. + /// The number of items to be replaced. + /// The replacement. + /// Whether to skip calling the destroyer function. + public void ReplaceRange(int index, int count, ReadOnlySpan replacement, bool skipDestroyer = false) + { + this.EnsureIndex(index); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), count, "Must be positive."); + if (count == 0) + return; + + // Ensure the capacity first, so that we can safely destroy the items first. + this.EnsureCapacityExponential((this.LengthUnsafe + replacement.Length) - count); + + if (!skipDestroyer && this.destroyer is { } d) + { + for (var i = 0; i < count; i++) + d(this.DataUnsafe + index + i); + } + + if (count == replacement.Length) + { + replacement.CopyTo(this.DataSpan[index..]); + } + else if (count > replacement.Length) + { + replacement.CopyTo(this.DataSpan[index..]); + this.RemoveRange(index + replacement.Length, count - replacement.Length); + } + else + { + replacement[..count].CopyTo(this.DataSpan[index..]); + this.InsertRange(index + count, replacement[count..]); + } + } + /// /// Sets the capacity exactly as requested. /// @@ -615,9 +673,6 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi if (!oldSpan.IsEmpty && !newSpan.IsEmpty) oldSpan[..this.LengthUnsafe].CopyTo(newSpan); -// #if DEBUG -// new Span(newAlloc + this.LengthUnsafe, sizeof(T) * (capacity - this.LengthUnsafe)).Fill(0xCC); -// #endif if (oldAlloc != null) ImGuiNative.igMemFree(oldAlloc); From f6d16d5624f7a922de7d497fa18fb4f0f8458b6a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 02:04:04 +0900 Subject: [PATCH 43/51] Do operations that may throw first --- .../Interface/Internal/ImGuiClipboardFunctionProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 265bc5812..76e8e73e6 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -42,12 +42,12 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); var io = ImGui.GetIO(); + this.clipboardUserDataOriginal = io.ClipboardUserData; this.setTextOriginal = (delegate* unmanaged)io.SetClipboardTextFn; this.getTextOriginal = (delegate* unmanaged)io.GetClipboardTextFn; - this.clipboardUserDataOriginal = io.ClipboardUserData; - io.SetClipboardTextFn = (nint)(delegate* unmanaged)(&StaticSetClipboardTextImpl); - io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this)); + io.SetClipboardTextFn = (nint)(delegate* unmanaged)&StaticSetClipboardTextImpl; + io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; return; [UnmanagedCallersOnly] From 8f243762cc29d6a24ac82331ca49769af0a77880 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 9 Dec 2023 01:23:04 +0100 Subject: [PATCH 44/51] ChatGui: fix for new message sounds and interactable links (#1568) * Change PrintMessage parameters type to byte * Use Utf8String.AsSpan * Fix InteractableLinkClickedDetour using the wrong variable --- Dalamud/Game/Gui/ChatGui.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 1214850b0..02b52ee56 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -22,6 +22,7 @@ namespace Dalamud.Game.Gui; // TODO(api10): Update IChatGui, ChatGui and XivChatEntry to use correct types and names: // "uint SenderId" should be "int Timestamp". // "IntPtr Parameters" should be something like "bool Silent". It suppresses new message sounds in certain channels. +// This has to be a 1 byte boolean, so only change it to bool if marshalling is disabled. /// /// This class handles interacting with the native chat UI. @@ -62,7 +63,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, bool silent); + private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr); @@ -157,7 +158,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui var sender = Utf8String.FromSequence(chat.Name.Encode()); var message = Utf8String.FromSequence(chat.Message.Encode()); - this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, chat.Parameters != 0); + this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, (byte)(chat.Parameters != 0 ? 1 : 0)); sender->Dtor(true); message->Dtor(true); @@ -278,14 +279,14 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui } } - private uint HandlePrintMessageDetour(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, bool silent) + private uint HandlePrintMessageDetour(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent) { var messageId = 0u; try { - var originalSenderData = sender->Span.ToArray(); - var originalMessageData = message->Span.ToArray(); + var originalSenderData = sender->AsSpan().ToArray(); + var originalMessageData = message->AsSpan().ToArray(); var parsedSender = SeString.Parse(originalSenderData); var parsedMessage = SeString.Parse(originalMessageData); @@ -374,7 +375,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10); - var seStr = MemoryHelper.ReadSeStringNullTerminated(messagePtr); + var seStr = MemoryHelper.ReadSeStringNullTerminated(payloadPtr); var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator); var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads; if (payloads.Count == 0) return; From 06938509e79f90f15077f9180a74432e3e799687 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 15:25:50 +0900 Subject: [PATCH 45/51] Just use win32 APIs --- Dalamud/Dalamud.csproj | 1 + .../ImGuiClipboardFunctionProvider.cs | 144 ++++++++++-- .../ImVectorWrapper.ZeroTerminatedSequence.cs | 207 ------------------ Dalamud/Interface/Utility/ImVectorWrapper.cs | 4 +- 4 files changed, 125 insertions(+), 231 deletions(-) delete mode 100644 Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 3a6a0257d..d31f79e0c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -90,6 +90,7 @@ + diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 76e8e73e6..fd07d824f 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -1,11 +1,19 @@ using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using System.Text; +using CheapLoc; + +using Dalamud.Game.Gui.Toast; using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; using ImGuiNET; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + namespace Dalamud.Interface.Internal; /// @@ -29,9 +37,15 @@ namespace Dalamud.Interface.Internal; [ServiceManager.EarlyLoadedService] internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable { + private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider)); private readonly nint clipboardUserDataOriginal; - private readonly delegate* unmanaged setTextOriginal; - private readonly delegate* unmanaged getTextOriginal; + private readonly nint setTextOriginal; + private readonly nint getTextOriginal; + + [ServiceManager.ServiceDependency] + private readonly ToastGui toastGui = Service.Get(); + + private ImVectorWrapper clipboardData; private GCHandle clipboardUserData; [ServiceManager.ServiceConstructor] @@ -43,11 +57,13 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis var io = ImGui.GetIO(); this.clipboardUserDataOriginal = io.ClipboardUserData; - this.setTextOriginal = (delegate* unmanaged)io.SetClipboardTextFn; - this.getTextOriginal = (delegate* unmanaged)io.GetClipboardTextFn; + this.setTextOriginal = io.SetClipboardTextFn; + this.getTextOriginal = io.GetClipboardTextFn; io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this)); io.SetClipboardTextFn = (nint)(delegate* unmanaged)&StaticSetClipboardTextImpl; io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; + + this.clipboardData = new(0); return; [UnmanagedCallersOnly] @@ -59,10 +75,6 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis ((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); } - [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] - private static ImVectorWrapper ImGuiCurrentContextClipboardHandlerData => - new((ImVector*)(ImGui.GetCurrentContext() + 0x5520)); - /// public void Dispose() { @@ -70,30 +82,118 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis return; var io = ImGui.GetIO(); - io.SetClipboardTextFn = (nint)this.setTextOriginal; - io.GetClipboardTextFn = (nint)this.getTextOriginal; + io.SetClipboardTextFn = this.setTextOriginal; + io.GetClipboardTextFn = this.getTextOriginal; io.ClipboardUserData = this.clipboardUserDataOriginal; this.clipboardUserData.Free(); + this.clipboardData.Dispose(); + } + + private bool OpenClipboardOrShowError() + { + if (!OpenClipboard(default)) + { + this.toastGui.ShowError( + Loc.Localize( + "ImGuiClipboardFunctionProviderClipboardInUse", + "Some other application is using the clipboard. Try again later.")); + return false; + } + + return true; } private void SetClipboardTextImpl(byte* text) { - var buffer = ImGuiCurrentContextClipboardHandlerData; - buffer.SetFromZeroTerminatedSequence(text); - buffer.Utf8Normalize(); - buffer.AddZeroTerminatorIfMissing(); - this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data); + if (!this.OpenClipboardOrShowError()) + return; + + try + { + var len = 0; + while (text[len] != 0) + len++; + var str = Encoding.UTF8.GetString(text, len); + str = str.ReplaceLineEndings("\r\n"); + var hMem = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)((str.Length + 1) * 2)); + if (hMem == 0) + throw new OutOfMemoryException(); + + var ptr = (char*)GlobalLock(hMem); + if (ptr == null) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) + ?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed."); + } + + str.AsSpan().CopyTo(new(ptr, str.Length)); + ptr[str.Length] = default; + GlobalUnlock(hMem); + + SetClipboardData(CF.CF_UNICODETEXT, hMem); + } + catch (Exception e) + { + Log.Error(e, $"Error in {nameof(this.SetClipboardTextImpl)}"); + this.toastGui.ShowError( + Loc.Localize( + "ImGuiClipboardFunctionProviderErrorCopy", + "Failed to copy. See logs for details.")); + } + finally + { + CloseClipboard(); + } } private byte* GetClipboardTextImpl() { - _ = this.getTextOriginal(this.clipboardUserDataOriginal); + this.clipboardData.Clear(); + + var formats = stackalloc uint[] { CF.CF_UNICODETEXT, CF.CF_TEXT }; + if (GetPriorityClipboardFormat(formats, 2) < 1 || !this.OpenClipboardOrShowError()) + { + this.clipboardData.Add(0); + return this.clipboardData.Data; + } - var buffer = ImGuiCurrentContextClipboardHandlerData; - buffer.TrimZeroTerminator(); - buffer.Utf8Normalize(); - buffer.AddZeroTerminatorIfMissing(); - return buffer.Data; + try + { + var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT); + if (hMem != default) + { + var ptr = (char*)GlobalLock(hMem); + if (ptr == null) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) + ?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed."); + } + + var str = new string(ptr); + str = str.ReplaceLineEndings("\r\n"); + this.clipboardData.Resize(Encoding.UTF8.GetByteCount(str) + 1); + Encoding.UTF8.GetBytes(str, this.clipboardData.DataSpan); + this.clipboardData[^1] = 0; + } + else + { + this.clipboardData.Add(0); + } + } + catch (Exception e) + { + Log.Error(e, $"Error in {nameof(this.GetClipboardTextImpl)}"); + this.toastGui.ShowError( + Loc.Localize( + "ImGuiClipboardFunctionProviderErrorPaste", + "Failed to paste. See logs for details.")); + } + finally + { + CloseClipboard(); + } + + return this.clipboardData.Data; } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs b/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs deleted file mode 100644 index 507bdce20..000000000 --- a/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System.Numerics; -using System.Text; - -namespace Dalamud.Interface.Utility; - -/// -/// Utility methods for . -/// -public static partial class ImVectorWrapper -{ - /// - /// Appends from , a zero terminated sequence. - /// - /// The element type. - /// The target buffer. - /// The pointer to the zero-terminated sequence. - public static unsafe void AppendZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) - where T : unmanaged, INumber - { - var len = 0; - while (psz[len] != default) - len++; - - buf.AddRange(new Span(psz, len)); - } - - /// - /// Sets from , a zero terminated sequence. - /// - /// The element type. - /// The target buffer. - /// The pointer to the zero-terminated sequence. - public static unsafe void SetFromZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) - where T : unmanaged, INumber - { - buf.Clear(); - buf.AppendZeroTerminatedSequence(psz); - } - - /// - /// Trims zero terminator(s). - /// - /// The element type. - /// The buffer. - public static void TrimZeroTerminator(this ref ImVectorWrapper buf) - where T : unmanaged, INumber - { - ref var len = ref buf.LengthUnsafe; - while (len > 0 && buf[len - 1] == default) - len--; - } - - /// - /// Adds a zero terminator to the buffer, if missing. - /// - /// The element type. - /// The buffer. - public static void AddZeroTerminatorIfMissing(this ref ImVectorWrapper buf) - where T : unmanaged, INumber - { - if (buf.Length > 0 && buf[^1] == default) - return; - buf.Add(default); - } - - /// - /// Gets the codepoint at the given offset. - /// - /// The buffer containing bytes in UTF-8. - /// The offset in bytes. - /// Number of bytes occupied by the character, invalid or not. - /// The fallback character, if no valid UTF-8 character could be found. - /// The parsed codepoint, or if it could not be parsed correctly. - public static unsafe int Utf8GetCodepoint( - this in ImVectorWrapper buf, - int offset, - out int numBytes, - int invalid = 0xFFFD) - { - var cb = buf.LengthUnsafe - offset; - if (cb <= 0) - { - numBytes = 0; - return invalid; - } - - numBytes = 1; - - var b = buf.DataUnsafe + offset; - if ((b[0] & 0x80) == 0) - return b[0]; - - if (cb < 2 || (b[1] & 0xC0) != 0x80) - return invalid; - if ((b[0] & 0xE0) == 0xC0) - { - numBytes = 2; - return ((b[0] & 0x1F) << 6) | (b[1] & 0x3F); - } - - if (cb < 3 || (b[2] & 0xC0) != 0x80) - return invalid; - if ((b[0] & 0xF0) == 0xE0) - { - numBytes = 3; - return ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F); - } - - if (cb < 4 || (b[3] & 0xC0) != 0x80) - return invalid; - if ((b[0] & 0xF8) == 0xF0) - { - numBytes = 4; - return ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F); - } - - return invalid; - } - - /// - /// Normalizes the given UTF-8 string.
- /// Using the default values will ensure the best interop between the game, ImGui, and Windows. - ///
- /// The buffer containing bytes in UTF-8. - /// The replacement line ending. If empty, CR LF will be used. - /// The replacement invalid character. If empty, U+FFFD REPLACEMENT CHARACTER will be used. - /// Specify whether to normalize the line endings. - /// Specify whether to replace invalid characters. - /// Specify whether to replace characters that requires the use of surrogate, when encoded in UTF-16. - /// Specify whether to make sense out of WTF-8. - public static unsafe void Utf8Normalize( - this ref ImVectorWrapper buf, - ReadOnlySpan lineEnding = default, - ReadOnlySpan invalidChar = default, - bool normalizeLineEndings = true, - bool sanitizeInvalidCharacters = true, - bool sanitizeNonUcs2Characters = true, - bool sanitizeSurrogates = true) - { - if (lineEnding.IsEmpty) - lineEnding = "\r\n"u8; - if (invalidChar.IsEmpty) - invalidChar = "\uFFFD"u8; - - // Ensure an implicit null after the end of the string. - buf.EnsureCapacity(buf.Length + 1); - buf.StorageSpan[buf.Length] = 0; - - Span charsBuf = stackalloc char[2]; - Span bytesBuf = stackalloc byte[4]; - for (var i = 0; i < buf.Length;) - { - var c1 = buf.Utf8GetCodepoint(i, out var cb, -1); - switch (c1) - { - // Note that buf.Data[i + 1] is always defined. See the beginning of the function. - case '\r' when buf.Data[i + 1] == '\n': - // If it's already CR LF, it passes all filters. - i += 2; - break; - - case >= 0xD800 and <= 0xDFFF when sanitizeSurrogates: - { - var c2 = buf.Utf8GetCodepoint(i + cb, out var cb2); - if (c1 is < 0xD800 or >= 0xDC00) - goto case -2; - if (c2 is < 0xDC00 or >= 0xE000) - goto case -2; - charsBuf[0] = unchecked((char)c1); - charsBuf[1] = unchecked((char)c2); - var bytesLen = Encoding.UTF8.GetBytes(charsBuf, bytesBuf); - buf.ReplaceRange(i, cb + cb2, bytesBuf[..bytesLen]); - // Do not alter i; now that the WTF-8 has been dealt with, apply other filters. - break; - } - - case -2: - case -1 or 0xFFFE or 0xFFFF when sanitizeInvalidCharacters: - case >= 0xD800 and <= 0xDFFF when sanitizeInvalidCharacters: - case > char.MaxValue when sanitizeNonUcs2Characters: - { - buf.ReplaceRange(i, cb, invalidChar); - i += invalidChar.Length; - break; - } - - // See String.Manipulation.cs: IndexOfNewlineChar. - // CR; Carriage Return - // LF; Line Feed - // FF; Form Feed - // NEL; Next Line - // LS; Line Separator - // PS; Paragraph Separator - case '\r' or '\n' or '\f' or '\u0085' or '\u2028' or '\u2029' when normalizeLineEndings: - { - buf.ReplaceRange(i, cb, lineEnding); - i += lineEnding.Length; - break; - } - - default: - i += cb; - break; - } - } - } -} diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index 51524efc4..5ba1aec2f 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -13,7 +13,7 @@ namespace Dalamud.Interface.Utility; /// /// Utility methods for . /// -public static partial class ImVectorWrapper +public static class ImVectorWrapper { /// /// Creates a new instance of the struct, initialized with @@ -208,7 +208,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// /// The initial capacity. /// The destroyer function to call on item removal. - public ImVectorWrapper(int initialCapacity = 0, ImGuiNativeDestroyDelegate? destroyer = null) + public ImVectorWrapper(int initialCapacity, ImGuiNativeDestroyDelegate? destroyer = null) { if (initialCapacity < 0) { From 4d0cce134fa25ae03533b01a38d999ef42151f8b Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 10 Dec 2023 12:35:40 +0900 Subject: [PATCH 46/51] Fix AddonLifecycle ABI; deprecate arg class public ctors (#1570) --- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 28 +++++++++++++------ .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 10 ++++++- .../AddonArgTypes/AddonFinalizeArgs.cs | 10 ++++++- .../AddonArgTypes/AddonReceiveEventArgs.cs | 18 ++++++++---- .../AddonArgTypes/AddonRefreshArgs.cs | 16 ++++++++--- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 14 ++++++++-- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 16 ++++++++--- .../AddonArgTypes/AddonUpdateArgs.cs | 23 +++++++++++++-- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 17 ++++++----- .../AddonLifecycleReceiveEventListener.cs | 5 +++- 10 files changed, 119 insertions(+), 38 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 077ca7c93..d82bf29a9 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -1,4 +1,5 @@ using Dalamud.Memory; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -12,7 +13,7 @@ public abstract unsafe class AddonArgs /// Constant string representing the name of an addon that is invalid. ///
public const string InvalidAddon = "NullAddon"; - + private string? addonName; private IntPtr addon; @@ -26,8 +27,22 @@ public abstract unsafe class AddonArgs ///
public nint Addon { - get => this.addon; - internal set + get => this.AddonInternal; + init => this.AddonInternal = value; + } + + /// + /// Gets the type of these args. + /// + public abstract AddonArgsType Type { get; } + + /// + /// Gets or sets the pointer to the addons AtkUnitBase. + /// + internal nint AddonInternal + { + get => this.Addon; + set { if (this.addon == value) return; @@ -37,11 +52,6 @@ public abstract unsafe class AddonArgs } } - /// - /// Gets the type of these args. - /// - public abstract AddonArgsType Type { get; } - /// /// Checks if addon name matches the given span of char. /// @@ -55,7 +65,7 @@ public abstract unsafe class AddonArgs var addonPointer = (AtkUnitBase*)this.Addon; if (addonPointer->Name is null) return false; - + return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 1e1013dd5..989e11912 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -5,12 +5,20 @@ ///
public class AddonDrawArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonDrawArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.Draw; /// public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index fc26a6c33..d9401b414 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -5,12 +5,20 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; ///
public class AddonFinalizeArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonFinalizeArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.Finalize; /// public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 8f9003b4c..a557b0cb3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -5,24 +5,32 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// public class AddonReceiveEventArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonReceiveEventArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.ReceiveEvent; - + /// /// Gets or sets the AtkEventType for this event message. /// public byte AtkEventType { get; set; } - + /// /// Gets or sets the event id for this event message. /// public int EventParam { get; set; } - + /// /// Gets or sets the pointer to an AtkEvent for this event message. /// public nint AtkEvent { get; set; } - + /// /// Gets or sets the pointer to a block of data for this event message. /// @@ -30,7 +38,7 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable /// public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index bfcf02544..6e1b11ead 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -7,19 +7,27 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// public class AddonRefreshArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonRefreshArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.Refresh; - + /// /// Gets or sets the number of AtkValues. /// public uint AtkValueCount { get; set; } - + /// /// Gets or sets the address of the AtkValue array. /// public nint AtkValues { get; set; } - + /// /// Gets the AtkValues in the form of a span. /// @@ -27,7 +35,7 @@ public class AddonRefreshArgs : AddonArgs, ICloneable /// public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index 219288ccf..26357abb0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -5,14 +5,22 @@ /// public class AddonRequestedUpdateArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonRequestedUpdateArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.RequestedUpdate; - + /// /// Gets or sets the NumberArrayData** for this event. /// public nint NumberArrayData { get; set; } - + /// /// Gets or sets the StringArrayData** for this event. /// @@ -20,7 +28,7 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable /// public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index bd60879b8..19c93ce25 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -7,19 +7,27 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// public class AddonSetupArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonSetupArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.Setup; - + /// /// Gets or sets the number of AtkValues. /// public uint AtkValueCount { get; set; } - + /// /// Gets or sets the address of the AtkValue array. /// public nint AtkValues { get; set; } - + /// /// Gets the AtkValues in the form of a span. /// @@ -27,7 +35,7 @@ public class AddonSetupArgs : AddonArgs, ICloneable /// public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index b087ac15a..cc34a7531 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -5,17 +5,34 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// public class AddonUpdateArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonUpdateArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.Update; - + /// /// Gets the time since the last update. /// - public float TimeDelta { get; internal set; } + public float TimeDelta + { + get => this.TimeDeltaInternal; + init => this.TimeDeltaInternal = value; + } + + /// + /// Gets or sets the time since the last update. + /// + internal float TimeDeltaInternal { get; set; } /// public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index decb7a9f4..6288cd2cd 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -43,12 +43,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet // package, and these events are always called from the main thread, this is fine. +#pragma warning disable CS0618 // Type or member is obsolete + // TODO: turn constructors of these internal private readonly AddonSetupArgs recyclingSetupArgs = new(); private readonly AddonFinalizeArgs recyclingFinalizeArgs = new(); private readonly AddonDrawArgs recyclingDrawArgs = new(); private readonly AddonUpdateArgs recyclingUpdateArgs = new(); private readonly AddonRefreshArgs recyclingRefreshArgs = new(); private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new(); +#pragma warning restore CS0618 // Type or member is obsolete [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) @@ -275,7 +278,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); } - this.recyclingSetupArgs.Addon = (nint)addon; + this.recyclingSetupArgs.AddonInternal = (nint)addon; this.recyclingSetupArgs.AtkValueCount = valueCount; this.recyclingSetupArgs.AtkValues = (nint)values; this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs); @@ -306,7 +309,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); } - this.recyclingFinalizeArgs.Addon = (nint)atkUnitBase[0]; + this.recyclingFinalizeArgs.AddonInternal = (nint)atkUnitBase[0]; this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs); try @@ -321,7 +324,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonDraw(AtkUnitBase* addon) { - this.recyclingDrawArgs.Addon = (nint)addon; + this.recyclingDrawArgs.AddonInternal = (nint)addon; this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs); try @@ -338,8 +341,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.recyclingUpdateArgs.Addon = (nint)addon; - this.recyclingUpdateArgs.TimeDelta = delta; + this.recyclingUpdateArgs.AddonInternal = (nint)addon; + this.recyclingUpdateArgs.TimeDeltaInternal = delta; this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs); try @@ -358,7 +361,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { byte result = 0; - this.recyclingRefreshArgs.Addon = (nint)addon; + this.recyclingRefreshArgs.AddonInternal = (nint)addon; this.recyclingRefreshArgs.AtkValueCount = valueCount; this.recyclingRefreshArgs.AtkValues = (nint)values; this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs); @@ -380,7 +383,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.recyclingRequestedUpdateArgs.Addon = (nint)addon; + this.recyclingRequestedUpdateArgs.AddonInternal = (nint)addon; this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData; this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData; this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs index 1c138e447..43aa71661 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -18,7 +18,10 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet // package, and these events are always called from the main thread, this is fine. +#pragma warning disable CS0618 // Type or member is obsolete + // TODO: turn constructors of these internal private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new(); +#pragma warning restore CS0618 // Type or member is obsolete /// /// Initializes a new instance of the class. @@ -79,7 +82,7 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable return; } - this.recyclingReceiveEventArgs.Addon = (nint)addon; + this.recyclingReceiveEventArgs.AddonInternal = (nint)addon; this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType; this.recyclingReceiveEventArgs.EventParam = eventParam; this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent; From 5a5cc5701ab9dbf268b57e31b857bbe7a2513695 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 9 Dec 2023 20:30:58 -0800 Subject: [PATCH 47/51] Hotfix for AddonArgs infinite loop (#1571) --- Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index d82bf29a9..4ab3de5ca 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -41,7 +41,7 @@ public abstract unsafe class AddonArgs /// internal nint AddonInternal { - get => this.Addon; + get => this.addon; set { if (this.addon == value) From fb864dd56d9dbc868496f579138f50399b7acec8 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 15 Dec 2023 18:15:24 +0100 Subject: [PATCH 48/51] Update ClientStructs (#1565) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index dcc913975..3364dfea7 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit dcc9139758bf5e2ff5c0b53d73a3566eb0eec4f0 +Subproject commit 3364dfea769b79e43aebaa955b6b98ec1d6eb458 From df1cdff1a53919d879f9bc8dbcf8425219cb0a16 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 16 Dec 2023 12:01:40 -0800 Subject: [PATCH 49/51] AddonEventManager fix thread safety (#1576) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Game/Addon/Events/AddonEventManager.cs | 33 +++++-- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 89 +++++++------------ 2 files changed, 56 insertions(+), 66 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 23f3b1a6d..af713a771 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -9,6 +9,8 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -31,6 +33,9 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycle = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly AddonLifecycleEventListener finalizeEventListener; private readonly AddonEventManagerAddressResolver address; @@ -87,6 +92,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// IAddonEventHandle used to remove the event. internal IAddonEventHandle? AddEvent(string pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { + if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported."); + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) { return eventController.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler); @@ -103,6 +110,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// The Unique Id for this event. internal void RemoveEvent(string pluginId, IAddonEventHandle eventHandle) { + if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported."); + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) { eventController.RemoveEvent(eventHandle); @@ -130,11 +139,14 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// Unique ID for this plugin. internal void AddPluginEventController(string pluginId) { - if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId)) + this.framework.RunOnFrameworkThread(() => { - Log.Verbose($"Creating new PluginEventController for: {pluginId}"); - this.pluginEventControllers.Add(new PluginEventController(pluginId)); - } + if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId)) + { + Log.Verbose($"Creating new PluginEventController for: {pluginId}"); + this.pluginEventControllers.Add(new PluginEventController(pluginId)); + } + }); } /// @@ -143,12 +155,15 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// Unique ID for this plugin. internal void RemovePluginEventController(string pluginId) { - if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller) + this.framework.RunOnFrameworkThread(() => { - Log.Verbose($"Removing PluginEventController for: {pluginId}"); - this.pluginEventControllers.Remove(controller); - controller.Dispose(); - } + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller) + { + Log.Verbose($"Removing PluginEventController for: {pluginId}"); + this.pluginEventControllers.Remove(controller); + controller.Dispose(); + } + }); } /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 6288cd2cd..beaab7fcd 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,9 +38,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Hook onAddonRefreshHook; private readonly CallHook onAddonRequestedUpdateHook; - private readonly ConcurrentBag newEventListeners = new(); - private readonly ConcurrentBag removeEventListeners = new(); - // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet // package, and these events are always called from the main thread, this is fine. #pragma warning disable CS0618 // Type or member is obsolete @@ -61,8 +58,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType // We want value of the function pointer at vFunc[2] this.disallowedReceiveEventAddress = ((nint*)this.address.AtkEventListener)![2]; - - this.framework.Update += this.OnFrameworkUpdate; this.onAddonSetupHook = new CallHook(this.address.AddonSetup, this.OnAddonSetup); this.onAddonSetup2Hook = new CallHook(this.address.AddonSetup2, this.OnAddonSetup); @@ -106,8 +101,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// public void Dispose() { - this.framework.Update -= this.OnFrameworkUpdate; - this.onAddonSetupHook.Dispose(); this.onAddonSetup2Hook.Dispose(); this.onAddonFinalizeHook.Dispose(); @@ -128,7 +121,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.newEventListeners.Add(listener); + this.framework.RunOnTick(() => + { + this.EventListeners.Add(listener); + + // If we want receive event messages have an already active addon, enable the receive event hook. + // If the addon isn't active yet, we'll grab the hook when it sets up. + if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) + { + if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) + { + receiveEventListener.Hook?.Enable(); + } + } + }); } /// @@ -137,7 +143,24 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - this.removeEventListeners.Add(listener); + this.framework.RunOnTick(() => + { + this.EventListeners.Remove(listener); + + // If we are disabling an ReceiveEvent listener, check if we should disable the hook. + if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) + { + // Get the ReceiveEvent Listener for this addon + if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) + { + // If there are no other listeners listening for this event, disable the hook. + if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent)) + { + receiveEventListener.Hook?.Disable(); + } + } + } + }); } /// @@ -169,54 +192,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - // Used to prevent concurrency issues if plugins try to register during iteration of listeners. - private void OnFrameworkUpdate(IFramework unused) - { - if (this.newEventListeners.Any()) - { - foreach (var toAddListener in this.newEventListeners) - { - this.EventListeners.Add(toAddListener); - - // If we want receive event messages have an already active addon, enable the receive event hook. - // If the addon isn't active yet, we'll grab the hook when it sets up. - if (toAddListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toAddListener.AddonName)) is { } receiveEventListener) - { - receiveEventListener.Hook?.Enable(); - } - } - } - - this.newEventListeners.Clear(); - } - - if (this.removeEventListeners.Any()) - { - foreach (var toRemoveListener in this.removeEventListeners) - { - this.EventListeners.Remove(toRemoveListener); - - // If we are disabling an ReceiveEvent listener, check if we should disable the hook. - if (toRemoveListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - // Get the ReceiveEvent Listener for this addon - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toRemoveListener.AddonName)) is { } receiveEventListener) - { - // If there are no other listeners listening for this event, disable the hook. - if (!this.EventListeners.Any(listener => listener.AddonName.Contains(toRemoveListener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent)) - { - receiveEventListener.Hook?.Disable(); - } - } - } - } - - this.removeEventListeners.Clear(); - } - } - private void RegisterReceiveEventHook(AtkUnitBase* addon) { // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. From 5998fc687f00d75379df77011266e76600324f40 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 17 Dec 2023 05:05:13 +0900 Subject: [PATCH 50/51] Fix DataShare race condition, and add debug features (#1573) --- .../Windows/Data/Widgets/DataShareWidget.cs | 311 +++++++++++++++++- Dalamud/Plugin/Ipc/Internal/CallGate.cs | 50 ++- .../Plugin/Ipc/Internal/CallGateChannel.cs | 71 +++- Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs | 20 +- .../Plugin/Ipc/Internal/CallGatePubSubBase.cs | 8 +- Dalamud/Plugin/Ipc/Internal/DataCache.cs | 84 ++++- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 128 +++---- 7 files changed, 545 insertions(+), 127 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 570b63332..92f340a7b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -1,19 +1,44 @@ -using Dalamud.Interface.Utility; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; + +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Ipc.Internal; + using ImGuiNET; +using Newtonsoft.Json; + +using Formatting = Newtonsoft.Json.Formatting; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying plugin data share modules. /// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] internal class DataShareWidget : IDataWindowWidget { + private const ImGuiTabItemFlags NoCloseButton = (ImGuiTabItemFlags)(1 << 20); + + private readonly List<(string Name, byte[]? Data)> dataView = new(); + private int nextTab = -1; + private IReadOnlyDictionary? gates; + private List? gatesSorted; + /// public string[]? CommandShortcuts { get; init; } = { "datashare" }; - + /// - public string DisplayName { get; init; } = "Data Share"; + public string DisplayName { get; init; } = "Data Share & Call Gate"; /// public bool Ready { get; set; } @@ -25,28 +50,290 @@ internal class DataShareWidget : IDataWindowWidget } /// - public void Draw() + public unsafe void Draw() { - if (!ImGui.BeginTable("###DataShareTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) + using var tabbar = ImRaii.TabBar("##tabbar"); + if (!tabbar.Success) + return; + + var d = true; + using (var tabitem = ImRaii.TabItem( + "Data Share##tabbar-datashare", + ref d, + NoCloseButton | (this.nextTab == 0 ? ImGuiTabItemFlags.SetSelected : 0))) + { + if (tabitem.Success) + this.DrawDataShare(); + } + + using (var tabitem = ImRaii.TabItem( + "Call Gate##tabbar-callgate", + ref d, + NoCloseButton | (this.nextTab == 1 ? ImGuiTabItemFlags.SetSelected : 0))) + { + if (tabitem.Success) + this.DrawCallGate(); + } + + for (var i = 0; i < this.dataView.Count; i++) + { + using var idpush = ImRaii.PushId($"##tabbar-data-{i}"); + var (name, data) = this.dataView[i]; + d = true; + using var tabitem = ImRaii.TabItem( + name, + ref d, + this.nextTab == 2 + i ? ImGuiTabItemFlags.SetSelected : 0); + if (!d) + this.dataView.RemoveAt(i--); + if (!tabitem.Success) + continue; + + if (ImGui.Button("Refresh")) + data = null; + + if (data is null) + { + try + { + var dataShare = Service.Get(); + var data2 = dataShare.GetData(name); + try + { + data = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + data2, + Formatting.Indented, + new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All })); + } + finally + { + dataShare.RelinquishData(name); + } + } + catch (Exception e) + { + data = Encoding.UTF8.GetBytes(e.ToString()); + } + + this.dataView[i] = (name, data); + } + + ImGui.SameLine(); + if (ImGui.Button("Copy")) + { + fixed (byte* pData = data) + ImGuiNative.igSetClipboardText(pData); + } + + fixed (byte* pLabel = "text"u8) + fixed (byte* pData = data) + { + ImGuiNative.igInputTextMultiline( + pLabel, + pData, + (uint)data.Length, + ImGui.GetContentRegionAvail(), + ImGuiInputTextFlags.ReadOnly, + null, + null); + } + } + + this.nextTab = -1; + } + + private static string ReprMethod(MethodInfo? mi, bool withParams) + { + if (mi is null) + return "-"; + + var sb = new StringBuilder(); + sb.Append(ReprType(mi.DeclaringType)) + .Append("::") + .Append(mi.Name); + if (!withParams) + return sb.ToString(); + sb.Append('('); + var parfirst = true; + foreach (var par in mi.GetParameters()) + { + if (!parfirst) + sb.Append(", "); + else + parfirst = false; + sb.AppendLine() + .Append('\t') + .Append(ReprType(par.ParameterType)) + .Append(' ') + .Append(par.Name); + } + + if (!parfirst) + sb.AppendLine(); + sb.Append(')'); + if (mi.ReturnType != typeof(void)) + sb.Append(" -> ").Append(ReprType(mi.ReturnType)); + return sb.ToString(); + + static string WithoutGeneric(string s) + { + var i = s.IndexOf('`'); + return i != -1 ? s[..i] : s; + } + + static string ReprType(Type? t) => + t switch + { + null => "null", + _ when t == typeof(string) => "string", + _ when t == typeof(object) => "object", + _ when t == typeof(void) => "void", + _ when t == typeof(decimal) => "decimal", + _ when t == typeof(bool) => "bool", + _ when t == typeof(double) => "double", + _ when t == typeof(float) => "float", + _ when t == typeof(char) => "char", + _ when t == typeof(ulong) => "ulong", + _ when t == typeof(long) => "long", + _ when t == typeof(uint) => "uint", + _ when t == typeof(int) => "int", + _ when t == typeof(ushort) => "ushort", + _ when t == typeof(short) => "short", + _ when t == typeof(byte) => "byte", + _ when t == typeof(sbyte) => "sbyte", + _ when t == typeof(nint) => "nint", + _ when t == typeof(nuint) => "nuint", + _ when t.IsArray && t.HasElementType => ReprType(t.GetElementType()) + "[]", + _ when t.IsPointer && t.HasElementType => ReprType(t.GetElementType()) + "*", + _ when t.IsGenericTypeDefinition => + t.Assembly == typeof(object).Assembly + ? t.Name + "<>" + : (t.FullName ?? t.Name) + "<>", + _ when t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>) => + ReprType(t.GetGenericArguments()[0]) + "?", + _ when t.IsGenericType => + WithoutGeneric(ReprType(t.GetGenericTypeDefinition())) + + "<" + string.Join(", ", t.GetGenericArguments().Select(ReprType)) + ">", + _ => t.Assembly == typeof(object).Assembly ? t.Name : t.FullName ?? t.Name, + }; + } + + private void DrawTextCell(string s, Func? tooltip = null, bool framepad = false) + { + ImGui.TableNextColumn(); + var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); + if (framepad) + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(s); + if (ImGui.IsItemHovered()) + { + ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); + var vp = ImGui.GetWindowViewport(); + var wrx = (vp.WorkPos.X + vp.WorkSize.X) - offset.X; + ImGui.SetNextWindowSizeConstraints(Vector2.One, new(wrx, float.MaxValue)); + using (ImRaii.Tooltip()) + { + ImGui.PushTextWrapPos(wrx); + ImGui.TextWrapped((tooltip?.Invoke() ?? s).Replace("%", "%%")); + ImGui.PopTextWrapPos(); + } + } + + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText(tooltip?.Invoke() ?? s); + Service.Get().AddNotification( + $"Copied {ImGui.TableGetColumnName()} to clipboard.", + this.DisplayName, + NotificationType.Success); + } + } + + private void DrawCallGate() + { + var callGate = Service.Get(); + if (ImGui.Button("Purge empty call gates")) + callGate.PurgeEmptyGates(); + + using var table = ImRaii.Table("##callgate-table", 5); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.DefaultSort); + ImGui.TableSetupColumn("Action"); + ImGui.TableSetupColumn("Func"); + ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Subscriber"); + ImGui.TableHeadersRow(); + + var gates2 = callGate.Gates; + if (!ReferenceEquals(gates2, this.gates) || this.gatesSorted is null) + { + this.gatesSorted = (this.gates = gates2).Values.ToList(); + this.gatesSorted.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + } + + foreach (var item in this.gatesSorted) + { + var subs = item.Subscriptions; + for (var i = 0; i < subs.Count || i == 0; i++) + { + ImGui.TableNextRow(); + this.DrawTextCell(item.Name); + this.DrawTextCell( + ReprMethod(item.Action?.Method, false), + () => ReprMethod(item.Action?.Method, true)); + this.DrawTextCell( + ReprMethod(item.Func?.Method, false), + () => ReprMethod(item.Func?.Method, true)); + if (subs.Count == 0) + { + this.DrawTextCell("0"); + continue; + } + + this.DrawTextCell($"{i + 1}/{subs.Count}"); + this.DrawTextCell($"{subs[i].Method.DeclaringType}::{subs[i].Method.Name}"); + } + } + } + + private void DrawDataShare() + { + if (!ImGui.BeginTable("###DataShareTable", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) return; try { ImGui.TableSetupColumn("Shared Tag"); + ImGui.TableSetupColumn("Show"); ImGui.TableSetupColumn("Creator Assembly"); ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Consumers"); ImGui.TableHeadersRow(); foreach (var share in Service.Get().GetAllShares()) { + ImGui.TableNextRow(); + this.DrawTextCell(share.Tag, null, true); + ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.Tag); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.CreatorAssembly); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.Users.Length.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(string.Join(", ", share.Users)); + if (ImGui.Button($"Show##datasharetable-show-{share.Tag}")) + { + var index = 0; + for (; index < this.dataView.Count; index++) + { + if (this.dataView[index].Name == share.Tag) + break; + } + + if (index == this.dataView.Count) + this.dataView.Add((share.Tag, null)); + else + this.dataView[index] = (share.Tag, null); + this.nextTab = 2 + index; + } + + this.DrawTextCell(share.CreatorAssembly, null, true); + this.DrawTextCell(share.Users.Length.ToString(), null, true); + this.DrawTextCell(string.Join(", ", share.Users), null, true); } } finally diff --git a/Dalamud/Plugin/Ipc/Internal/CallGate.cs b/Dalamud/Plugin/Ipc/Internal/CallGate.cs index 7d0f90cb6..fef4b97d0 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGate.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGate.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; namespace Dalamud.Plugin.Ipc.Internal; @@ -10,11 +11,28 @@ internal class CallGate : IServiceType { private readonly Dictionary gates = new(); + private ImmutableDictionary? gatesCopy; + [ServiceManager.ServiceConstructor] private CallGate() { } + /// + /// Gets the thread-safe view of the registered gates. + /// + public IReadOnlyDictionary Gates + { + get + { + var copy = this.gatesCopy; + if (copy is not null) + return copy; + lock (this.gates) + return this.gatesCopy ??= this.gates.ToImmutableDictionary(x => x.Key, x => x.Value); + } + } + /// /// Gets the provider associated with the specified name. /// @@ -22,8 +40,34 @@ internal class CallGate : IServiceType /// A CallGate registered under the given name. public CallGateChannel GetOrCreateChannel(string name) { - if (!this.gates.TryGetValue(name, out var gate)) - gate = this.gates[name] = new CallGateChannel(name); - return gate; + lock (this.gates) + { + if (!this.gates.TryGetValue(name, out var gate)) + { + gate = this.gates[name] = new(name); + this.gatesCopy = null; + } + + return gate; + } + } + + /// + /// Remove empty gates from . + /// + public void PurgeEmptyGates() + { + lock (this.gates) + { + var changed = false; + foreach (var (k, v) in this.Gates) + { + if (v.IsEmpty) + changed |= this.gates.Remove(k); + } + + if (changed) + this.gatesCopy = null; + } } } diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index 2e2c7249e..54adf2163 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; @@ -14,6 +14,17 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal class CallGateChannel { + /// + /// The actual storage. + /// + private readonly HashSet subscriptions = new(); + + /// + /// A copy of the actual storage, that will be cleared and populated depending on changes made to + /// . + /// + private ImmutableList? subscriptionsCopy; + /// /// Initializes a new instance of the class. /// @@ -31,17 +42,52 @@ internal class CallGateChannel /// /// Gets a list of delegate subscriptions for when SendMessage is called. /// - public List Subscriptions { get; } = new(); + public IReadOnlyList Subscriptions + { + get + { + var copy = this.subscriptionsCopy; + if (copy is not null) + return copy; + lock (this.subscriptions) + return this.subscriptionsCopy ??= this.subscriptions.ToImmutableList(); + } + } /// /// Gets or sets an action for when InvokeAction is called. /// - public Delegate Action { get; set; } + public Delegate? Action { get; set; } /// /// Gets or sets a func for when InvokeFunc is called. /// - public Delegate Func { get; set; } + public Delegate? Func { get; set; } + + /// + /// Gets a value indicating whether this is not being used. + /// + public bool IsEmpty => this.Action is null && this.Func is null && this.Subscriptions.Count == 0; + + /// + internal void Subscribe(Delegate action) + { + lock (this.subscriptions) + { + this.subscriptionsCopy = null; + this.subscriptions.Add(action); + } + } + + /// + internal void Unsubscribe(Delegate action) + { + lock (this.subscriptions) + { + this.subscriptionsCopy = null; + this.subscriptions.Remove(action); + } + } /// /// Invoke all actions that have subscribed to this IPC. @@ -49,9 +95,6 @@ internal class CallGateChannel /// Message arguments. internal void SendMessage(object?[]? args) { - if (this.Subscriptions.Count == 0) - return; - foreach (var subscription in this.Subscriptions) { var methodInfo = subscription.GetMethodInfo(); @@ -105,7 +148,14 @@ internal class CallGateChannel var paramTypes = methodInfo.GetParameters() .Select(pi => pi.ParameterType).ToArray(); - if (args?.Length != paramTypes.Length) + if (args is null) + { + if (paramTypes.Length == 0) + return; + throw new IpcLengthMismatchError(this.Name, 0, paramTypes.Length); + } + + if (args.Length != paramTypes.Length) throw new IpcLengthMismatchError(this.Name, args.Length, paramTypes.Length); for (var i = 0; i < args.Length; i++) @@ -137,7 +187,7 @@ internal class CallGateChannel } } - private IEnumerable GenerateTypes(Type type) + private IEnumerable GenerateTypes(Type? type) { while (type != null && type != typeof(object)) { @@ -148,6 +198,9 @@ internal class CallGateChannel private object? ConvertObject(object? obj, Type type) { + if (obj is null) + return null; + var json = JsonConvert.SerializeObject(obj); try diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs index 39d5b9f4d..cc54a563b 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs @@ -1,5 +1,3 @@ -using System; - #pragma warning disable SA1402 // File may only contain a single type namespace Dalamud.Plugin.Ipc.Internal; @@ -37,7 +35,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider base.InvokeAction(); - /// + /// public TRet InvokeFunc() => this.InvokeFunc(); } @@ -75,7 +73,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider< public void InvokeAction(T1 arg1) => base.InvokeAction(arg1); - /// + /// public TRet InvokeFunc(T1 arg1) => this.InvokeFunc(arg1); } @@ -113,7 +111,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvi public void InvokeAction(T1 arg1, T2 arg2) => base.InvokeAction(arg1, arg2); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2) => this.InvokeFunc(arg1, arg2); } @@ -151,7 +149,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateP public void InvokeAction(T1 arg1, T2 arg2, T3 arg3) => base.InvokeAction(arg1, arg2, arg3); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3) => this.InvokeFunc(arg1, arg2, arg3); } @@ -189,7 +187,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallG public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => base.InvokeAction(arg1, arg2, arg3, arg4); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => this.InvokeFunc(arg1, arg2, arg3, arg4); } @@ -227,7 +225,7 @@ internal class CallGatePubSub : CallGatePubSubBase, IC public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5); } @@ -265,7 +263,7 @@ internal class CallGatePubSub : CallGatePubSubBase public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6); } @@ -303,7 +301,7 @@ internal class CallGatePubSub : CallGatePubSub public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6, arg7); } @@ -341,7 +339,7 @@ internal class CallGatePubSub : CallGatePu public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); } diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs index 40c0c4a59..b6a4e8a61 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Plugin.Ipc.Exceptions; namespace Dalamud.Plugin.Ipc.Internal; @@ -13,7 +11,7 @@ internal abstract class CallGatePubSubBase /// Initializes a new instance of the class. /// /// The name of the IPC registration. - public CallGatePubSubBase(string name) + protected CallGatePubSubBase(string name) { this.Channel = Service.Get().GetOrCreateChannel(name); } @@ -54,14 +52,14 @@ internal abstract class CallGatePubSubBase /// /// Action to subscribe. private protected void Subscribe(Delegate action) - => this.Channel.Subscriptions.Add(action); + => this.Channel.Subscribe(action); /// /// Unsubscribe an expression from this registration. /// /// Action to unsubscribe. private protected void Unsubscribe(Delegate action) - => this.Channel.Subscriptions.Remove(action); + => this.Channel.Unsubscribe(action); /// /// Invoke an action registered for inter-plugin communication. diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs index c357f77c2..38cea4866 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataCache.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -1,5 +1,10 @@ -using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; + +using Dalamud.Plugin.Ipc.Exceptions; + +using Serilog; namespace Dalamud.Plugin.Ipc.Internal; @@ -8,10 +13,14 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal readonly struct DataCache { + /// Name of the data. + internal readonly string Tag; + /// The assembly name of the initial creator. internal readonly string CreatorAssemblyName; /// A not-necessarily distinct list of current users. + /// Also used as a reference count tracker. internal readonly List UserAssemblyNames; /// The type the data was registered as. @@ -23,14 +32,83 @@ internal readonly struct DataCache /// /// Initializes a new instance of the struct. /// + /// Name of the data. /// The assembly name of the initial creator. /// A reference to data. /// The type of the data. - public DataCache(string creatorAssemblyName, object? data, Type type) + public DataCache(string tag, string creatorAssemblyName, object? data, Type type) { + this.Tag = tag; this.CreatorAssemblyName = creatorAssemblyName; - this.UserAssemblyNames = new List { creatorAssemblyName }; + this.UserAssemblyNames = new(); this.Data = data; this.Type = type; } + + /// + /// Creates a new instance of the struct, using the given data generator function. + /// + /// The name for the data cache. + /// The assembly name of the initial creator. + /// The function that generates the data if it does not already exist. + /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. + /// The new instance of . + public static DataCache From(string tag, string creatorAssemblyName, Func dataGenerator) + where T : class + { + try + { + var result = new DataCache(tag, creatorAssemblyName, dataGenerator.Invoke(), typeof(T)); + Log.Verbose( + "[{who}] Created new data for [{Tag:l}] for creator {Creator:l}.", + nameof(DataShare), + tag, + creatorAssemblyName); + return result; + } + catch (Exception e) + { + throw ExceptionDispatchInfo.SetCurrentStackTrace( + new DataCacheCreationError(tag, creatorAssemblyName, typeof(T), e)); + } + } + + /// + /// Attempts to fetch the data. + /// + /// The name of the caller assembly. + /// The value, if succeeded. + /// The exception, if failed. + /// Desired type of the data. + /// true on success. + public bool TryGetData( + string callerName, + [NotNullWhen(true)] out T? value, + [NotNullWhen(false)] out Exception? ex) + where T : class + { + switch (this.Data) + { + case null: + value = null; + ex = ExceptionDispatchInfo.SetCurrentStackTrace(new DataCacheValueNullError(this.Tag, this.Type)); + return false; + + case T data: + value = data; + ex = null; + + // Register the access history + lock (this.UserAssemblyNames) + this.UserAssemblyNames.Add(callerName); + + return true; + + default: + value = null; + ex = ExceptionDispatchInfo.SetCurrentStackTrace( + new DataCacheTypeMismatchError(this.Tag, this.CreatorAssemblyName, typeof(T), this.Type)); + return false; + } + } } diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index a3e314b80..b122f481d 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -1,9 +1,7 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection; using Dalamud.Plugin.Ipc.Exceptions; using Serilog; @@ -16,7 +14,11 @@ namespace Dalamud.Plugin.Ipc.Internal; [ServiceManager.BlockingEarlyLoadedService] internal class DataShare : IServiceType { - private readonly Dictionary caches = new(); + /// + /// Dictionary of cached values. Note that is being used, as it does its own locking, + /// effectively preventing calling the data generator multiple times concurrently. + /// + private readonly Dictionary> caches = new(); [ServiceManager.ServiceConstructor] private DataShare() @@ -39,38 +41,15 @@ internal class DataShare : IServiceType where T : class { var callerName = GetCallerName(); + + Lazy cacheLazy; lock (this.caches) { - if (this.caches.TryGetValue(tag, out var cache)) - { - if (!cache.Type.IsAssignableTo(typeof(T))) - { - throw new DataCacheTypeMismatchError(tag, cache.CreatorAssemblyName, typeof(T), cache.Type); - } - - cache.UserAssemblyNames.Add(callerName); - return cache.Data as T ?? throw new DataCacheValueNullError(tag, cache.Type); - } - - try - { - var obj = dataGenerator.Invoke(); - if (obj == null) - { - throw new Exception("Returned data was null."); - } - - cache = new DataCache(callerName, obj, typeof(T)); - this.caches[tag] = cache; - - Log.Verbose("[DataShare] Created new data for [{Tag:l}] for creator {Creator:l}.", tag, callerName); - return obj; - } - catch (Exception e) - { - throw new DataCacheCreationError(tag, callerName, typeof(T), e); - } + if (!this.caches.TryGetValue(tag, out cacheLazy)) + this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callerName, dataGenerator)); } + + return cacheLazy.Value.TryGetData(callerName, out var value, out var ex) ? value : throw ex; } /// @@ -80,34 +59,36 @@ internal class DataShare : IServiceType /// The name for the data cache. public void RelinquishData(string tag) { + DataCache cache; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache)) - { + if (!this.caches.TryGetValue(tag, out var cacheLazy)) return; - } var callerName = GetCallerName(); - lock (this.caches) - { - if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) - { - return; - } - if (this.caches.Remove(tag)) - { - if (cache.Data is IDisposable disposable) - { - disposable.Dispose(); - Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag); - } - else - { - Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag); - } - } + cache = cacheLazy.Value; + if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) + return; + if (!this.caches.Remove(tag)) + return; + } + + if (cache.Data is IDisposable disposable) + { + try + { + disposable.Dispose(); + Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag); } + catch (Exception e) + { + Log.Error(e, "[DataShare] Failed to dispose [{Tag:l}] after it was removed from all shares.", tag); + } + } + else + { + Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag); } } @@ -123,23 +104,14 @@ internal class DataShare : IServiceType where T : class { data = null; + Lazy cacheLazy; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache) || !cache.Type.IsAssignableTo(typeof(T))) - { + if (!this.caches.TryGetValue(tag, out cacheLazy)) return false; - } - - var callerName = GetCallerName(); - data = cache.Data as T; - if (data == null) - { - return false; - } - - cache.UserAssemblyNames.Add(callerName); - return true; } + + return cacheLazy.Value.TryGetData(GetCallerName(), out data, out _); } /// @@ -155,27 +127,14 @@ internal class DataShare : IServiceType public T GetData(string tag) where T : class { + Lazy cacheLazy; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache)) - { + if (!this.caches.TryGetValue(tag, out cacheLazy)) throw new KeyNotFoundException($"The data cache [{tag}] is not registered."); - } - - var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; - if (!cache.Type.IsAssignableTo(typeof(T))) - { - throw new DataCacheTypeMismatchError(tag, callerName, typeof(T), cache.Type); - } - - if (cache.Data is not T data) - { - throw new DataCacheValueNullError(tag, typeof(T)); - } - - cache.UserAssemblyNames.Add(callerName); - return data; } + + return cacheLazy.Value.TryGetData(GetCallerName(), out var value, out var ex) ? value : throw ex; } /// @@ -186,7 +145,8 @@ internal class DataShare : IServiceType { lock (this.caches) { - return this.caches.Select(kvp => (kvp.Key, kvp.Value.CreatorAssemblyName, kvp.Value.UserAssemblyNames.ToArray())); + return this.caches.Select( + kvp => (kvp.Key, kvp.Value.Value.CreatorAssemblyName, kvp.Value.Value.UserAssemblyNames.ToArray())); } } From 280a9d6b05102cc90c55a3cd6f65ba0d3d69ac41 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 16 Dec 2023 21:05:49 +0100 Subject: [PATCH 51/51] build: 9.0.0.14 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index d31f79e0c..a870bee17 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.13 + 9.0.0.14 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion)