From 2172ff0a512a42daca1eb796df151786aa365e9e Mon Sep 17 00:00:00 2001 From: Zeffuro Date: Sun, 11 Jan 2026 19:33:14 +0100 Subject: [PATCH] InventoryMonitor, LootedItemsTracker Fix --- AetherBags/Addons/AddonInventoryWindow.cs | 2 +- AetherBags/Addons/InventoryAddonBase.cs | 47 ++------- .../InventoryMonitor.cs} | 70 ++++++++------ .../LootedItemsTracker.cs | 96 +++++++++++-------- AetherBags/Plugin.cs | 8 +- AetherBags/System.cs | 1 + 6 files changed, 113 insertions(+), 111 deletions(-) rename AetherBags/{AddonLifecycles/InventoryLifecycles.cs => Monitoring/InventoryMonitor.cs} (81%) rename AetherBags/{Inventory => Monitoring}/LootedItemsTracker.cs (66%) diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index 5053139..af9eff0 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -139,6 +139,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase private void OnClearAllLootedItems() { System.LootedItemsTracker.Clear(); + System.LootedItemsTracker.FlushPendingChanges(); } public void ManualCurrencyRefresh() @@ -155,7 +156,6 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase }, delayTicks: 3); } - protected override void OnFinalize(AtkUnitBase* addon) { System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged; diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs index 3299817..1c756f8 100644 --- a/AetherBags/Addons/InventoryAddonBase.cs +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Numerics; -using AetherBags.AddonLifecycles; using AetherBags.Configuration; using AetherBags.Helpers; using AetherBags.Inventory; @@ -10,6 +9,7 @@ using AetherBags.Inventory.Context; using AetherBags.Inventory.Items; using AetherBags.Inventory.Scanning; using AetherBags.Inventory.State; +using AetherBags.Monitoring; using AetherBags.Nodes.Input; using AetherBags.Nodes.Inventory; using AetherBags.Nodes.Layout; @@ -66,25 +66,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow private int _refreshFromLifecycleCount; private long _lastLogTick; - public void ManualRefresh() - { - if (!IsOpen) return; - if (!Services.ClientState.IsLoggedIn) return; - if (_isRefreshing) return; - if (!IsSetupComplete) return; - - try - { - _isRefreshing = true; - InventoryState.RefreshFromGame(); - RefreshCategoriesCore(autosize: true); - } - finally - { - _isRefreshing = false; - } - } - + public void ManualRefresh() => ExecuteRefresh(true); public string GetSearchText() => SearchInputNode?.SearchString.ExtractText() ?? string.Empty; @@ -99,21 +81,16 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow }, delayTicks: 3); } - public void RefreshFromLifecycle() + private void ExecuteRefresh(bool autosize) { - if (!IsSetupComplete) return; - if (!IsOpen) return; - if (_isRefreshing) return; + if (!IsSetupComplete || !IsOpen || _isRefreshing) return; try { _isRefreshing = true; - - _refreshFromLifecycleCount++; - LogRefreshStats(); - InventoryState.RefreshFromGame(); - RefreshCategoriesCore(autosize: true); + System.LootedItemsTracker.FlushPendingChanges(); + RefreshCategoriesCore(autosize); } finally { @@ -121,6 +98,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow } } + public void RefreshFromLifecycle() => ExecuteRefresh(autosize: true); + protected virtual void RefreshCategoriesCore(bool autosize) { if (!IsSetupComplete) @@ -446,16 +425,10 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - _requestedUpdateCount++; - LogRefreshStats(); - base.OnRequestedUpdate(addon, numberArrayData, stringArrayData); - if (DragDropState.IsDragging) - return; - - InventoryState.RefreshFromGame(); - RefreshCategoriesCore(autosize: true); + if (DragDropState.IsDragging) return; + ExecuteRefresh(autosize: true); } diff --git a/AetherBags/AddonLifecycles/InventoryLifecycles.cs b/AetherBags/Monitoring/InventoryMonitor.cs similarity index 81% rename from AetherBags/AddonLifecycles/InventoryLifecycles.cs rename to AetherBags/Monitoring/InventoryMonitor.cs index 4e35a9b..fb8523b 100644 --- a/AetherBags/AddonLifecycles/InventoryLifecycles.cs +++ b/AetherBags/Monitoring/InventoryMonitor.cs @@ -1,18 +1,22 @@ using System; +using System.Collections.Generic; using System.Linq; using AetherBags.Configuration; using AetherBags.Inventory.Context; +using AetherBags.Inventory.Scanning; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.Inventory.InventoryEventArgTypes; using Dalamud.Game.NativeWrapper; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.ReadOnly; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; -namespace AetherBags.AddonLifecycles; +namespace AetherBags.Monitoring; public static unsafe class DragDropState { @@ -22,10 +26,10 @@ public static unsafe class DragDropState public static bool IsDragging => AtkStage.Instance()->DragDropManager.IsDragging; } -public class InventoryLifecycles : IDisposable +public class InventoryMonitor : IDisposable { - public InventoryLifecycles() + public InventoryMonitor() { var bags = new[] { "Inventory", "InventoryLarge", "InventoryExpansion" }; var saddle = new[] { "InventoryBuddy" }; @@ -45,8 +49,8 @@ public class InventoryLifecycles : IDisposable Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "InventoryBuddy", OnSaddleBagUpdate); Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, retainer, OnRetainerInventoryUpdate); - // PreShow - Services.AddonLifecycle.RegisterListener(AddonEvent.PreOpen, "InventoryBuddy", OnSaddleBagOpen); + // Dalamud raw event for raw inventory changes (scans once per frame) + Services.GameInventory.InventoryChangedRaw += OnInventoryChangedRaw; Services.Logger.Verbose("InventoryLifecycles initialized"); } @@ -122,6 +126,32 @@ public class InventoryLifecycles : IDisposable values[7] = can use Saddlebags (Agent InventoryBuddy IsActivatable) */ + private void OnInventoryChangedRaw(IReadOnlyCollection events) + { + bool needsRefresh = false; + foreach (var inventoryEventArgs in events) + { + if (InventoryScanner.StandardInventories.Contains((InventoryType)inventoryEventArgs.Item.ContainerType)) + { + needsRefresh = true; + break; + } + } + + if (needsRefresh) + { + Services.Framework.RunOnTick(() => + { + if (IsInUnsafeState() || DragDropState.IsDragging) return; + + System.LootedItemsTracker.FlushPendingChanges(); + System.AddonInventoryWindow?.RefreshFromLifecycle(); + System.AddonSaddleBagWindow?.RefreshFromLifecycle(); + System.AddonRetainerWindow?.RefreshFromLifecycle(); + }); + } + } + private unsafe void InventoryPreRefreshHandler(AddonEvent type, AddonArgs args) { if (args is not AddonRefreshArgs refreshArgs) @@ -165,25 +195,6 @@ public class InventoryLifecycles : IDisposable } } - // TODO: Inventory/Retainers are not perma open, need some way to close it too. - private void InventoryBuddyPreRefreshHandler(AddonEvent type, AddonArgs args) - { - if (args is not AddonRefreshArgs refreshArgs) - return; - - if (IsInUnsafeState()) - return; - - GeneralSettings config = System.Config.General; - - if (config.HideGameSaddleBags) refreshArgs.AtkValueCount = 0; - if (config.OpenSaddleBagsWithGameInventory) - { - System.AddonSaddleBagWindow.Toggle(); - } - } - - private void OnInventoryUpdate(AddonEvent type, AddonArgs args) { if (IsInUnsafeState()) @@ -204,6 +215,7 @@ public class InventoryLifecycles : IDisposable if (DragDropState.IsDragging) return; + System.LootedItemsTracker.FlushPendingChanges(); System.AddonSaddleBagWindow?.RefreshFromLifecycle(); } @@ -215,17 +227,13 @@ public class InventoryLifecycles : IDisposable if (DragDropState.IsDragging) return; + System.LootedItemsTracker.FlushPendingChanges(); System.AddonRetainerWindow?.RefreshFromLifecycle(); } - private void OnSaddleBagOpen(AddonEvent type, AddonArgs args) - { - if (args is not AddonShowArgs showArgs) - return; - } - public void Dispose() { - Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate, OnSaddleBagOpen); + Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw; + Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate); } } \ No newline at end of file diff --git a/AetherBags/Inventory/LootedItemsTracker.cs b/AetherBags/Monitoring/LootedItemsTracker.cs similarity index 66% rename from AetherBags/Inventory/LootedItemsTracker.cs rename to AetherBags/Monitoring/LootedItemsTracker.cs index 5463bc5..a7603cd 100644 --- a/AetherBags/Inventory/LootedItemsTracker.cs +++ b/AetherBags/Monitoring/LootedItemsTracker.cs @@ -9,7 +9,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Lumina.Excel.Sheets; -namespace AetherBags.Inventory; +namespace AetherBags.Monitoring; public sealed unsafe class LootedItemsTracker : IDisposable { @@ -32,6 +32,8 @@ public sealed unsafe class LootedItemsTracker : IDisposable public bool HasPendingChanges => _pendingChanges.Count > 0 || _hasPendingRemoval; + private int GetNextIndex() => _lootedItems.Count > 0 ? _lootedItems.Max(x => x.Index) + 1 : 0; + public void Enable() { if (_isEnabled) return; @@ -81,6 +83,8 @@ public sealed unsafe class LootedItemsTracker : IDisposable { if (_pendingChanges.Count == 0 && !_hasPendingRemoval) return; + ProcessPendingChanges(); + _hasPendingRemoval = false; OnLootedItemsChanged?.Invoke(_lootedItems); } @@ -90,12 +94,40 @@ public sealed unsafe class LootedItemsTracker : IDisposable Disable(); } + private void ProcessPendingChanges() + { + if (_pendingChanges.Count == 0) return; + + foreach (var ((itemId, isHq), (item, delta)) in _pendingChanges) + { + int existingIndex = _lootedItems.FindIndex(x => + x.Item.ItemId == itemId && + x.Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality) == isHq); + + if (existingIndex >= 0) + { + var current = _lootedItems[existingIndex]; + int newQty = current.Quantity + delta; + + if (newQty <= 0) + _lootedItems.RemoveAt(existingIndex); + else + _lootedItems[existingIndex] = current with { Quantity = newQty }; + } + else if (delta > 0) + { + _lootedItems.Add(new LootedItemInfo(GetNextIndex(), item, delta)); + } + } + + _pendingChanges.Clear(); + } + private void OnInventoryChangedRaw(IReadOnlyCollection events) { - if (!_isEnabled) return; - if (!Services.ClientState.IsLoggedIn) return; + if (!_isEnabled || !Services.ClientState.IsLoggedIn) return; - bool anyAdded = false; + bool anyChanged = false; foreach (var eventData in events) { @@ -105,38 +137,42 @@ public sealed unsafe class LootedItemsTracker : IDisposable if (eventData.Item.ContainerType == GameInventoryType.DamagedGear) continue; - if (eventData is not (InventoryItemAddedArgs or InventoryItemChangedArgs)) - continue; - - if (eventData is InventoryItemChangedArgs changedArgs && - changedArgs.OldItemState.Quantity >= changedArgs.Item.Quantity) + int changeAmount = eventData switch { - continue; - } + InventoryItemAddedArgs added => added.Item.Quantity, + InventoryItemRemovedArgs removed => -removed.Item.Quantity, + InventoryItemChangedArgs changed => changed.Item.Quantity - changed.OldItemState.Quantity, + _ => 0 + }; + + if (changeAmount == 0) continue; if (ShouldFilterItem(eventData.Item.ItemId)) continue; - var inventoryItem = *(InventoryItem*)eventData.Item.Address; - var changeAmount = eventData is InventoryItemChangedArgs changed - ? changed.Item.Quantity - changed.OldItemState.Quantity - : eventData.Item.Quantity; - - var key = (inventoryItem.ItemId, IsHq: inventoryItem.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality)); + uint itemId = eventData.Item.ItemId; + bool isHq = eventData.Item.IsHq; + var key = (itemId, isHq); if (_pendingChanges.TryGetValue(key, out var existing)) { - _pendingChanges[key] = (inventoryItem, existing.Quantity + changeAmount); + _pendingChanges[key] = (existing.Item, existing.Quantity + changeAmount); } else { - _pendingChanges[key] = (inventoryItem, changeAmount); + InventoryItem itemStruct = default; + if (changeAmount > 0) + { + itemStruct = *(InventoryItem*)eventData.Item.Address; + } + + _pendingChanges[key] = (itemStruct, changeAmount); } - anyAdded = true; + anyChanged = true; } - if (anyAdded && _batchStartTick == 0) + if (anyChanged && _batchStartTick == 0) { _batchStartTick = Environment.TickCount64; } @@ -152,23 +188,7 @@ public sealed unsafe class LootedItemsTracker : IDisposable _batchStartTick = 0; - if (_pendingChanges.Count == 0) - return; - - foreach (var ((itemId, isHq), (item, quantity)) in _pendingChanges) - { - if (quantity <= 0) - continue; - - _lootedItems.Add(new LootedItemInfo( - _lootedItems.Count, - item, - quantity)); - } - - _pendingChanges.Clear(); - - OnLootedItemsChanged?.Invoke(_lootedItems); + FlushPendingChanges(); } private static bool ShouldFilterItem(uint itemId) diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs index b10ce2e..cac7b99 100644 --- a/AetherBags/Plugin.cs +++ b/AetherBags/Plugin.cs @@ -1,5 +1,4 @@ using System.Numerics; -using AetherBags.AddonLifecycles; using AetherBags.Addons; using AetherBags.Commands; using AetherBags.Helpers; @@ -7,6 +6,7 @@ using AetherBags.Hooks; using AetherBags.Inventory; using AetherBags.Inventory.Context; using AetherBags.IPC; +using AetherBags.Monitoring; using Dalamud.Plugin; using KamiToolKit; @@ -16,7 +16,7 @@ public class Plugin : IDalamudPlugin { private readonly CommandHandler _commandHandler; private readonly InventoryHooks _inventoryHooks; - private readonly InventoryLifecycles _inventoryLifecycles; + private readonly InventoryMonitor inventoryMonitor; public Plugin(IDalamudPluginInterface pluginInterface) { @@ -72,14 +72,14 @@ public class Plugin : IDalamudPlugin } _inventoryHooks = new InventoryHooks(); - _inventoryLifecycles = new InventoryLifecycles(); + inventoryMonitor = new InventoryMonitor(); } public void Dispose() { InventoryAddonContextMenu.Close(); _inventoryHooks.Dispose(); - _inventoryLifecycles.Dispose(); + inventoryMonitor.Dispose(); System.LootedItemsTracker.Dispose(); System.IPC.Dispose(); diff --git a/AetherBags/System.cs b/AetherBags/System.cs index a25528a..d86af5f 100644 --- a/AetherBags/System.cs +++ b/AetherBags/System.cs @@ -2,6 +2,7 @@ using AetherBags.Addons; using AetherBags.Configuration; using AetherBags.Inventory; using AetherBags.IPC; +using AetherBags.Monitoring; namespace AetherBags;