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