diff --git a/MosswartMassacre/LiveInventoryTracker.cs b/MosswartMassacre/LiveInventoryTracker.cs index 4b3546b..8712463 100644 --- a/MosswartMassacre/LiveInventoryTracker.cs +++ b/MosswartMassacre/LiveInventoryTracker.cs @@ -15,9 +15,23 @@ namespace MosswartMassacre private readonly IPluginLogger _logger; private readonly HashSet _trackedItemIds = new HashSet(); + // Items whose state changed since the last flush. DECAL fires + // ChangeObject constantly for tracked items (mana-burn ticks, ID data + // arriving, stack tweaks) — previously each one sent a full payload, a + // firehose across all characters that saturated the backend. We now + // coalesce those updates and flush the LATEST state of each dirty item + // on a WinForms timer (game main thread → STA-safe for DECAL access), + // so the backend sees ~1 update per item per flush instead of many per + // second. Adds and removes stay immediate (they're rare and meaningful). + private readonly HashSet _dirtyItemIds = new HashSet(); + private const int FlushIntervalMs = 60000; // 60s — tune here if needed + private readonly System.Windows.Forms.Timer _flushTimer; + internal LiveInventoryTracker(IPluginLogger logger) { _logger = logger; + _flushTimer = new System.Windows.Forms.Timer { Interval = FlushIntervalMs }; + _flushTimer.Tick += FlushDirtyItems; } /// @@ -27,6 +41,7 @@ namespace MosswartMassacre internal void Initialize() { _trackedItemIds.Clear(); + _dirtyItemIds.Clear(); try { foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory()) @@ -38,6 +53,7 @@ namespace MosswartMassacre { _logger?.Log($"[LiveInv] Error initializing: {ex.Message}"); } + _flushTimer.Start(); } internal void OnCreateObject(object sender, CreateObjectEventArgs e) @@ -111,9 +127,11 @@ namespace MosswartMassacre } else { - // Existing item changed (equip/unequip, stack change, container move) - var mwo = MyWorldObjectCreator.Create(item); - _ = WebSocket.SendInventoryDeltaAsync("update", mwo); + // Existing item changed (equip/unequip, stack change, mana + // burn, ID data, container move). Coalesce: mark the item + // dirty and let the flush timer send its latest state once, + // instead of sending a full payload on every change. + _dirtyItemIds.Add(item.Id); } } catch (Exception ex) @@ -122,8 +140,48 @@ namespace MosswartMassacre } } + /// + /// Flush coalesced item updates: send the CURRENT state of each item + /// that changed since the last tick, one "update" per item. Runs on the + /// WinForms timer, i.e. the game's main thread, so the DECAL world-object + /// access here is STA-safe. + /// + private void FlushDirtyItems(object sender, EventArgs e) + { + if (_dirtyItemIds.Count == 0) return; + + // Snapshot then clear, so changes arriving during the flush are + // captured for the next tick rather than dropped. + var ids = new List(_dirtyItemIds); + _dirtyItemIds.Clear(); + + foreach (int id in ids) + { + try + { + // Skip items that left inventory since being marked dirty — + // the release/change-out path already sent a remove. + if (!_trackedItemIds.Contains(id)) continue; + + WorldObject item = CoreManager.Current.WorldFilter[id]; + if (item == null || !IsPlayerInventory(item)) continue; + + var mwo = MyWorldObjectCreator.Create(item); + _ = WebSocket.SendInventoryDeltaAsync("update", mwo); + } + catch (Exception ex) + { + _logger?.Log($"[LiveInv] Error flushing item {id}: {ex.Message}"); + } + } + } + internal void Cleanup() { + // Stop the flush timer so the disposed/old instance can't keep + // firing (and can be GC'd) across logout / hot reload. + _flushTimer.Stop(); + _dirtyItemIds.Clear(); _trackedItemIds.Clear(); } diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll index c8ae994..0bbea51 100644 Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ