feat: add live inventory delta tracking via WebSocket
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ce0fae7d10
commit
afa85ef80d
5 changed files with 202 additions and 0 deletions
145
MosswartMassacre/LiveInventoryTracker.cs
Normal file
145
MosswartMassacre/LiveInventoryTracker.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue