feat: add live inventory delta tracking via WebSocket

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-28 15:38:05 +00:00
parent ce0fae7d10
commit afa85ef80d
5 changed files with 202 additions and 0 deletions

View file

@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using Mag.Shared;
namespace MosswartMassacre
{
/// <summary>
/// Sends inventory delta events (add/remove/update) via WebSocket
/// whenever items change in the player's inventory.
/// </summary>
internal class LiveInventoryTracker
{
private readonly IPluginLogger _logger;
private readonly HashSet<int> _trackedItemIds = new HashSet<int>();
internal LiveInventoryTracker(IPluginLogger logger)
{
_logger = logger;
}
/// <summary>
/// Initialize tracking for all current inventory items.
/// Called after login or hot reload, after the full inventory dump.
/// </summary>
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;
}
}
}
}

View file

@ -312,6 +312,7 @@
<Compile Include="IPluginLogger.cs" /> <Compile Include="IPluginLogger.cs" />
<Compile Include="QuestStreamingService.cs" /> <Compile Include="QuestStreamingService.cs" />
<Compile Include="InventoryMonitor.cs" /> <Compile Include="InventoryMonitor.cs" />
<Compile Include="LiveInventoryTracker.cs" />
<Compile Include="KillTracker.cs" /> <Compile Include="KillTracker.cs" />
<Compile Include="RareTracker.cs" /> <Compile Include="RareTracker.cs" />
<Compile Include="ClientTelemetry.cs" /> <Compile Include="ClientTelemetry.cs" />

View file

@ -144,6 +144,7 @@ namespace MosswartMassacre
private GameEventRouter _gameEventRouter; private GameEventRouter _gameEventRouter;
private QuestStreamingService _questStreamingService; private QuestStreamingService _questStreamingService;
private CommandRouter _commandRouter; private CommandRouter _commandRouter;
private LiveInventoryTracker _liveInventoryTracker;
protected override void Startup() protected override void Startup()
{ {
@ -181,6 +182,12 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease; CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange; 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) if (_gameEventRouter != null)
CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch; CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
WebSocket.OnServerCommand -= HandleServerCommand; WebSocket.OnServerCommand -= HandleServerCommand;
@ -218,6 +225,9 @@ namespace MosswartMassacre
_inventoryMonitor = new InventoryMonitor(this); _inventoryMonitor = new InventoryMonitor(this);
_staticInventoryMonitor = _inventoryMonitor; _staticInventoryMonitor = _inventoryMonitor;
// Initialize live inventory tracker (delta WebSocket messages)
_liveInventoryTracker = new LiveInventoryTracker(this);
// Initialize chat event router (rareTracker set later in LoginComplete) // Initialize chat event router (rareTracker set later in LoginComplete)
_chatEventRouter = new ChatEventRouter( _chatEventRouter = new ChatEventRouter(
this, _killTracker, null, this, _killTracker, null,
@ -240,6 +250,9 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate; CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease; CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange; 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 // Initialize VVS view after character login
ViewManager.ViewInit(); ViewManager.ViewInit();
@ -413,6 +426,15 @@ namespace MosswartMassacre
// Clean up taper tracking // Clean up taper tracking
_inventoryMonitor?.Cleanup(); _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 // Clean up Harmony patches
DecalHarmonyClean.Cleanup(); DecalHarmonyClean.Cleanup();
@ -482,6 +504,9 @@ namespace MosswartMassacre
// Initialize cached Prismatic Taper count // Initialize cached Prismatic Taper count
_inventoryMonitor.Initialize(); _inventoryMonitor.Initialize();
// Initialize live inventory tracking (after full inventory dump)
_liveInventoryTracker?.Initialize();
// Initialize quest manager for always-on quest streaming // Initialize quest manager for always-on quest streaming
try try
{ {
@ -606,6 +631,9 @@ namespace MosswartMassacre
// 7. Reinitialize cached Prismatic Taper count // 7. Reinitialize cached Prismatic Taper count
_inventoryMonitor?.Initialize(); _inventoryMonitor?.Initialize();
// 7b. Reinitialize live inventory tracking
_liveInventoryTracker?.Initialize();
// 8. Reinitialize quest manager for hot reload // 8. Reinitialize quest manager for hot reload
try try
{ {

View file

@ -295,6 +295,34 @@ namespace MosswartMassacre
await SendEncodedAsync(json, CancellationToken.None); 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) public static async Task SendVitalsAsync(object vitalsData)
{ {
var json = JsonConvert.SerializeObject(vitalsData); var json = JsonConvert.SerializeObject(vitalsData);