diff --git a/MosswartMassacre/LiveInventoryTracker.cs b/MosswartMassacre/LiveInventoryTracker.cs
new file mode 100644
index 0000000..9dd54a9
--- /dev/null
+++ b/MosswartMassacre/LiveInventoryTracker.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using Decal.Adapter;
+using Decal.Adapter.Wrappers;
+using Mag.Shared;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Sends inventory delta events (add/remove/update) via WebSocket
+ /// whenever items change in the player's inventory.
+ ///
+ internal class LiveInventoryTracker
+ {
+ private readonly IPluginLogger _logger;
+ private readonly HashSet _trackedItemIds = new HashSet();
+
+ internal LiveInventoryTracker(IPluginLogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// Initialize tracking for all current inventory items.
+ /// Called after login or hot reload, after the full inventory dump.
+ ///
+ internal void Initialize()
+ {
+ _trackedItemIds.Clear();
+ try
+ {
+ foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
+ {
+ _trackedItemIds.Add(wo.Id);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[LiveInv] Error initializing: {ex.Message}");
+ }
+ }
+
+ internal void OnCreateObject(object sender, CreateObjectEventArgs e)
+ {
+ try
+ {
+ var item = e.New;
+ if (!IsPlayerInventory(item)) return;
+ if (_trackedItemIds.Contains(item.Id)) return;
+
+ _trackedItemIds.Add(item.Id);
+ var mwo = MyWorldObjectCreator.Create(item);
+ _ = WebSocket.SendInventoryDeltaAsync("add", mwo);
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[LiveInv] Error in OnCreate: {ex.Message}");
+ }
+ }
+
+ internal void OnReleaseObject(object sender, ReleaseObjectEventArgs e)
+ {
+ try
+ {
+ var item = e.Released;
+ if (!_trackedItemIds.Contains(item.Id)) return;
+
+ _trackedItemIds.Remove(item.Id);
+ _ = WebSocket.SendInventoryRemoveAsync(item.Id);
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[LiveInv] Error in OnRelease: {ex.Message}");
+ }
+ }
+
+ internal void OnChangeObject(object sender, ChangeObjectEventArgs e)
+ {
+ try
+ {
+ var item = e.Changed;
+ if (!IsPlayerInventory(item))
+ {
+ // Item left our inventory
+ if (_trackedItemIds.Contains(item.Id))
+ {
+ _trackedItemIds.Remove(item.Id);
+ _ = WebSocket.SendInventoryRemoveAsync(item.Id);
+ }
+ return;
+ }
+
+ if (!_trackedItemIds.Contains(item.Id))
+ {
+ // New item appeared via ChangeObject
+ _trackedItemIds.Add(item.Id);
+ var mwo = MyWorldObjectCreator.Create(item);
+ _ = WebSocket.SendInventoryDeltaAsync("add", mwo);
+ }
+ else
+ {
+ // Existing item changed (equip/unequip, stack change, container move)
+ var mwo = MyWorldObjectCreator.Create(item);
+ _ = WebSocket.SendInventoryDeltaAsync("update", mwo);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[LiveInv] Error in OnChange: {ex.Message}");
+ }
+ }
+
+ internal void Cleanup()
+ {
+ _trackedItemIds.Clear();
+ }
+
+ private static bool IsPlayerInventory(WorldObject item)
+ {
+ try
+ {
+ int containerId = item.Container;
+ int charId = CoreManager.Current.CharacterFilter.Id;
+
+ // Directly in character's inventory
+ if (containerId == charId) return true;
+
+ // In a side pack owned by the character
+ WorldObject container = CoreManager.Current.WorldFilter[containerId];
+ if (container != null &&
+ container.ObjectClass == ObjectClass.Container &&
+ container.Container == charId)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index 93f65af..56d93d1 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -312,6 +312,7 @@
+
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index e99459c..b78ebb6 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -144,6 +144,7 @@ namespace MosswartMassacre
private GameEventRouter _gameEventRouter;
private QuestStreamingService _questStreamingService;
private CommandRouter _commandRouter;
+ private LiveInventoryTracker _liveInventoryTracker;
protected override void Startup()
{
@@ -181,6 +182,12 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange;
}
+ if (_liveInventoryTracker != null)
+ {
+ CoreManager.Current.WorldFilter.CreateObject -= _liveInventoryTracker.OnCreateObject;
+ CoreManager.Current.WorldFilter.ReleaseObject -= _liveInventoryTracker.OnReleaseObject;
+ CoreManager.Current.WorldFilter.ChangeObject -= _liveInventoryTracker.OnChangeObject;
+ }
if (_gameEventRouter != null)
CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
WebSocket.OnServerCommand -= HandleServerCommand;
@@ -218,6 +225,9 @@ namespace MosswartMassacre
_inventoryMonitor = new InventoryMonitor(this);
_staticInventoryMonitor = _inventoryMonitor;
+ // Initialize live inventory tracker (delta WebSocket messages)
+ _liveInventoryTracker = new LiveInventoryTracker(this);
+
// Initialize chat event router (rareTracker set later in LoginComplete)
_chatEventRouter = new ChatEventRouter(
this, _killTracker, null,
@@ -240,6 +250,9 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange;
+ CoreManager.Current.WorldFilter.CreateObject += _liveInventoryTracker.OnCreateObject;
+ CoreManager.Current.WorldFilter.ReleaseObject += _liveInventoryTracker.OnReleaseObject;
+ CoreManager.Current.WorldFilter.ChangeObject += _liveInventoryTracker.OnChangeObject;
// Initialize VVS view after character login
ViewManager.ViewInit();
@@ -413,6 +426,15 @@ namespace MosswartMassacre
// Clean up taper tracking
_inventoryMonitor?.Cleanup();
+ // Clean up live inventory tracker
+ if (_liveInventoryTracker != null)
+ {
+ CoreManager.Current.WorldFilter.CreateObject -= _liveInventoryTracker.OnCreateObject;
+ CoreManager.Current.WorldFilter.ReleaseObject -= _liveInventoryTracker.OnReleaseObject;
+ CoreManager.Current.WorldFilter.ChangeObject -= _liveInventoryTracker.OnChangeObject;
+ _liveInventoryTracker.Cleanup();
+ }
+
// Clean up Harmony patches
DecalHarmonyClean.Cleanup();
@@ -482,6 +504,9 @@ namespace MosswartMassacre
// Initialize cached Prismatic Taper count
_inventoryMonitor.Initialize();
+ // Initialize live inventory tracking (after full inventory dump)
+ _liveInventoryTracker?.Initialize();
+
// Initialize quest manager for always-on quest streaming
try
{
@@ -606,6 +631,9 @@ namespace MosswartMassacre
// 7. Reinitialize cached Prismatic Taper count
_inventoryMonitor?.Initialize();
+ // 7b. Reinitialize live inventory tracking
+ _liveInventoryTracker?.Initialize();
+
// 8. Reinitialize quest manager for hot reload
try
{
diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs
index e89cbe3..4a07d82 100644
--- a/MosswartMassacre/WebSocket.cs
+++ b/MosswartMassacre/WebSocket.cs
@@ -295,6 +295,34 @@ namespace MosswartMassacre
await SendEncodedAsync(json, CancellationToken.None);
}
+ public static async Task SendInventoryDeltaAsync(string action, Mag.Shared.MyWorldObject item)
+ {
+ var envelope = new
+ {
+ type = "inventory_delta",
+ timestamp = DateTime.UtcNow.ToString("o"),
+ character_name = CoreManager.Current.CharacterFilter.Name,
+ action = action,
+ item = item
+ };
+ var json = JsonConvert.SerializeObject(envelope);
+ await SendEncodedAsync(json, CancellationToken.None);
+ }
+
+ public static async Task SendInventoryRemoveAsync(int itemId)
+ {
+ var envelope = new
+ {
+ type = "inventory_delta",
+ timestamp = DateTime.UtcNow.ToString("o"),
+ character_name = CoreManager.Current.CharacterFilter.Name,
+ action = "remove",
+ item_id = itemId
+ };
+ var json = JsonConvert.SerializeObject(envelope);
+ await SendEncodedAsync(json, CancellationToken.None);
+ }
+
public static async Task SendVitalsAsync(object vitalsData)
{
var json = JsonConvert.SerializeObject(vitalsData);
diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll
index b490983..b114cc9 100644
Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ