diff --git a/MosswartMassacre/DelayedCommandManager.cs b/MosswartMassacre/DelayedCommandManager.cs index 4976e4b..bf7a400 100644 --- a/MosswartMassacre/DelayedCommandManager.cs +++ b/MosswartMassacre/DelayedCommandManager.cs @@ -28,8 +28,8 @@ namespace MosswartMassacre { while (delayedCommands.Count > 0 && delayedCommands[0].RunAt <= DateTime.UtcNow) { - PluginCore.WriteToChat($"[Debug] Executing delayed: {delayedCommands[0].Command}"); - CoreManager.Current.Actions.InvokeChatParser(delayedCommands[0].Command); + // Use Decal_DispatchOnChatCommand to ensure other plugins can intercept + PluginCore.DispatchChatToBoxWithPluginIntercept(delayedCommands[0].Command); delayedCommands.RemoveAt(0); } diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 93f2343..8a56b54 100644 --- a/MosswartMassacre/PluginCore.cs +++ b/MosswartMassacre/PluginCore.cs @@ -25,12 +25,20 @@ namespace MosswartMassacre internal static int rareCount = 0; internal static int sessionDeaths = 0; // Deaths this session internal static int totalDeaths = 0; // Total deaths from character + internal static int cachedPrismaticCount = 0; // Cached Prismatic Taper count + internal static int lastPrismaticCount = 0; // For delta calculation + + // Track taper items and their containers for accurate release detection + private static readonly Dictionary trackedTaperContainers = new Dictionary(); + private static readonly Dictionary lastKnownStackSizes = new Dictionary(); internal static DateTime lastKillTime = DateTime.Now; internal static double killsPer5Min = 0; internal static double killsPerHour = 0; internal static DateTime statsStartTime = DateTime.Now; internal static Timer updateTimer; private static Timer vitalsTimer; + private static System.Windows.Forms.Timer commandTimer; + private static readonly Queue pendingCommands = new Queue(); public static bool RareMetaEnabled { get; set; } = true; // VVS View Management @@ -101,6 +109,10 @@ namespace MosswartMassacre CoreManager.Current.CharacterFilter.Death += OnCharacterDeath; CoreManager.Current.WorldFilter.CreateObject += OnSpawn; CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn; + // Subscribe to inventory change events for taper tracking + CoreManager.Current.WorldFilter.CreateObject += OnInventoryCreate; + CoreManager.Current.WorldFilter.ReleaseObject += OnInventoryRelease; + CoreManager.Current.WorldFilter.ChangeObject += OnInventoryChange; // Initialize VVS view after character login ViewManager.ViewInit(); @@ -114,6 +126,12 @@ namespace MosswartMassacre vitalsTimer.Elapsed += SendVitalsUpdate; vitalsTimer.Start(); + // Initialize command processing timer (Windows Forms timer for main thread) + commandTimer = new System.Windows.Forms.Timer(); + commandTimer.Interval = 10; // Process commands every 10ms + commandTimer.Tick += ProcessPendingCommands; + commandTimer.Start(); + // Note: View initialization moved to LoginComplete for VVS compatibility // Enable TLS1.2 @@ -155,6 +173,10 @@ namespace MosswartMassacre CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath; CoreManager.Current.WorldFilter.CreateObject -= OnSpawn; CoreManager.Current.WorldFilter.ReleaseObject -= OnDespawn; + // Unsubscribe from inventory change events + CoreManager.Current.WorldFilter.CreateObject -= OnInventoryCreate; + CoreManager.Current.WorldFilter.ReleaseObject -= OnInventoryRelease; + CoreManager.Current.WorldFilter.ChangeObject -= OnInventoryChange; // Stop and dispose of the timers @@ -172,6 +194,13 @@ namespace MosswartMassacre vitalsTimer = null; } + if (commandTimer != null) + { + commandTimer.Stop(); + commandTimer.Dispose(); + commandTimer = null; + } + // Clean up the view ViewManager.ViewDestroy(); //Disable vtank interface @@ -189,6 +218,10 @@ namespace MosswartMassacre navVisualization = null; } + // Clean up taper tracking + trackedTaperContainers.Clear(); + lastKnownStackSizes.Clear(); + // Clean up Harmony patches DecalHarmonyClean.Cleanup(); @@ -241,8 +274,178 @@ namespace MosswartMassacre totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); sessionDeaths = 0; + // Initialize cached Prismatic Taper count + InitializePrismaticTaperCount(); + } + private void InitializePrismaticTaperCount() + { + try + { + lastPrismaticCount = cachedPrismaticCount; + cachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper"); + + // Initialize tracking for existing tapers + trackedTaperContainers.Clear(); + lastKnownStackSizes.Clear(); + + foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory()) + { + if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase) && + IsPlayerOwnedContainer(wo.Container)) + { + int stackCount = wo.Values(LongValueKey.StackCount, 1); + trackedTaperContainers[wo.Id] = wo.Container; + lastKnownStackSizes[wo.Id] = stackCount; + } + } + + } + catch (Exception ex) + { + WriteToChat($"[TAPER] Error initializing count: {ex.Message}"); + cachedPrismaticCount = 0; + lastPrismaticCount = 0; + trackedTaperContainers.Clear(); + lastKnownStackSizes.Clear(); + } + } + + private bool IsPlayerOwnedContainer(int containerId) + { + try + { + // Check if it's the player's main inventory + if (containerId == CoreManager.Current.CharacterFilter.Id) + return true; + + // Check if it's one of the player's containers (side packs) + // Get the container object to verify it belongs to the player + WorldObject container = CoreManager.Current.WorldFilter[containerId]; + if (container != null && + container.ObjectClass == ObjectClass.Container && + container.Container == CoreManager.Current.CharacterFilter.Id) + { + return true; + } + + return false; + } + catch + { + return false; + } + } + + private void OnInventoryCreate(object sender, CreateObjectEventArgs e) + { + try + { + var item = e.New; + if (IsPlayerOwnedContainer(item.Container) && + item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase)) + { + lastPrismaticCount = cachedPrismaticCount; + int stackCount = item.Values(LongValueKey.StackCount, 1); + cachedPrismaticCount += stackCount; + int delta = cachedPrismaticCount - lastPrismaticCount; + + // Initialize tracking for this new taper + trackedTaperContainers[item.Id] = item.Container; + lastKnownStackSizes[item.Id] = stackCount; + + } + } + catch (Exception ex) + { + WriteToChat($"[TAPER] Error in OnInventoryCreate: {ex.Message}"); + } + } + + private void OnInventoryRelease(object sender, ReleaseObjectEventArgs e) + { + try + { + var item = e.Released; + if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase)) + { + // Check where this taper WAS before being released (not where it's going) + if (trackedTaperContainers.TryGetValue(item.Id, out int previousContainer)) + { + if (IsPlayerOwnedContainer(previousContainer)) + { + // This taper was in our inventory and is now being released + lastPrismaticCount = cachedPrismaticCount; + int stackCount = item.Values(LongValueKey.StackCount, 1); + cachedPrismaticCount -= stackCount; + } + + // Clean up tracking + trackedTaperContainers.Remove(item.Id); + lastKnownStackSizes.Remove(item.Id); + } + else + { + // Fallback: recalculate total count when untracked taper is released + lastPrismaticCount = cachedPrismaticCount; + cachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper"); + } + } + } + catch (Exception ex) + { + WriteToChat($"[TAPER] Error in OnInventoryRelease: {ex.Message}"); + } + } + + private void OnInventoryChange(object sender, ChangeObjectEventArgs e) + { + try + { + var item = e.Changed; + if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase)) + { + bool isInPlayerContainer = IsPlayerOwnedContainer(item.Container); + + // Track container location for release detection + if (isInPlayerContainer) + { + bool wasAlreadyTracked = trackedTaperContainers.ContainsKey(item.Id); + trackedTaperContainers[item.Id] = item.Container; + + // Handle stack size changes with pure delta math + int currentStack = item.Values(LongValueKey.StackCount, 1); + + // Check if this is a pickup from ground (item not previously tracked) + if (!wasAlreadyTracked) + { + // This is likely a pickup from ground - increment count + lastPrismaticCount = cachedPrismaticCount; + cachedPrismaticCount += currentStack; + } + else if (lastKnownStackSizes.TryGetValue(item.Id, out int previousStack)) + { + int stackDelta = currentStack - previousStack; + if (stackDelta != 0) + { + lastPrismaticCount = cachedPrismaticCount; + cachedPrismaticCount += stackDelta; + } + } + + lastKnownStackSizes[item.Id] = currentStack; + } + // Item is no longer in player containers + // DON'T clean up tracking here - let OnInventoryRelease handle cleanup + // This ensures tracking data is available for the Release event + } + } + catch (Exception ex) + { + WriteToChat($"[TAPER] Error in OnInventoryChange: {ex.Message}"); + } + } private async void OnSpawn(object sender, CreateObjectEventArgs e) { @@ -318,10 +521,38 @@ namespace MosswartMassacre private void HandleServerCommand(CommandEnvelope env) { - // Skicka commands - DispatchChatToBoxWithPluginIntercept(env.Command); - CoreManager.Current.Actions.InvokeChatParser($"/a Executed '{env.Command}' from Mosswart Overlord"); + // This is called from WebSocket thread - queue for main thread execution + lock (pendingCommands) + { + pendingCommands.Enqueue(env.Command); + } } + + private void ProcessPendingCommands(object sender, EventArgs e) + { + // This runs on the main UI thread via Windows Forms timer + string command = null; + + lock (pendingCommands) + { + if (pendingCommands.Count > 0) + command = pendingCommands.Dequeue(); + } + + if (command != null) + { + try + { + // Execute ALL WebSocket commands on main thread - fast and reliable + DispatchChatToBoxWithPluginIntercept(command); + } + catch (Exception ex) + { + WriteToChat($"[WS] Command execution error: {ex.Message}"); + } + } + } + private void OnChatText(object sender, ChatTextInterceptEventArgs e) { try @@ -688,6 +919,8 @@ namespace MosswartMassacre WriteToChat("/mm testprismatic - Test Prismatic Taper detection and icon lookup"); WriteToChat("/mm deathstats - Show current death tracking statistics"); WriteToChat("/mm testdeath - Manual death tracking test and diagnostics"); + WriteToChat("/mm testtaper - Test cached Prismatic Taper tracking"); + WriteToChat("/mm debugtaper - Show detailed taper tracking debug info"); WriteToChat("/mm gui - Manually initialize/reinitialize GUI"); break; case "report": @@ -713,7 +946,6 @@ namespace MosswartMassacre WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}"); ViewManager.SetRareMetaToggleState(RareMetaEnabled); // <-- sync the UI break; - case "http": if (args.Length > 1) { @@ -1000,6 +1232,111 @@ namespace MosswartMassacre } break; + case "testtaper": + try + { + WriteToChat("=== Cached Taper Tracking Test ==="); + WriteToChat($"Cached Count: {cachedPrismaticCount}"); + WriteToChat($"Last Count: {lastPrismaticCount}"); + + // Compare with Utils function + int utilsCount = Utils.GetItemStackSize("Prismatic Taper"); + WriteToChat($"Utils Count: {utilsCount}"); + + if (cachedPrismaticCount == utilsCount) + { + WriteToChat("[OK] Cached count matches Utils count"); + } + else + { + WriteToChat($"[WARNING] Count mismatch! Cached: {cachedPrismaticCount}, Utils: {utilsCount}"); + WriteToChat("Refreshing cached count..."); + InitializePrismaticTaperCount(); + } + + WriteToChat("=== Container Analysis ==="); + int mainPackCount = 0; + int sidePackCount = 0; + int playerId = CoreManager.Current.CharacterFilter.Id; + + foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory()) + { + if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase)) + { + int stackCount = wo.Values(LongValueKey.StackCount, 1); + if (wo.Container == playerId) + { + mainPackCount += stackCount; + } + else + { + sidePackCount += stackCount; + } + } + } + + WriteToChat($"Main Pack Tapers: {mainPackCount}"); + WriteToChat($"Side Pack Tapers: {sidePackCount}"); + WriteToChat($"Total: {mainPackCount + sidePackCount}"); + + WriteToChat("=== Event System Status ==="); + WriteToChat($"Tracking {trackedTaperContainers.Count} taper stacks for delta detection"); + WriteToChat($"Known stack sizes: {lastKnownStackSizes.Count} items"); + WriteToChat("Pure delta tracking - NO expensive inventory scans during events!"); + WriteToChat("Now tracks: consumption, drops, trades, container moves"); + WriteToChat("Try moving tapers between containers and casting spells!"); + } + catch (Exception ex) + { + WriteToChat($"Taper test error: {ex.Message}"); + } + break; + + case "debugtaper": + try + { + WriteToChat("=== Taper Tracking Debug Info ==="); + WriteToChat($"Cached Count: {cachedPrismaticCount}"); + WriteToChat($"Last Count: {lastPrismaticCount}"); + WriteToChat($"Tracked Containers: {trackedTaperContainers.Count}"); + WriteToChat($"Known Stack Sizes: {lastKnownStackSizes.Count}"); + + if (trackedTaperContainers.Count > 0) + { + WriteToChat("=== Tracked Taper Details ==="); + foreach (var kvp in trackedTaperContainers) + { + int itemId = kvp.Key; + int containerId = kvp.Value; + int stackSize = lastKnownStackSizes.TryGetValue(itemId, out int size) ? size : -1; + string containerType = containerId == CoreManager.Current.CharacterFilter.Id ? "main pack" : "side pack"; + WriteToChat($" Item {itemId}: {containerType} (container {containerId}), stack: {stackSize}"); + } + } + else + { + WriteToChat("No tapers currently tracked!"); + } + + // Cross-check with actual inventory + WriteToChat("=== Cross-Check with Actual Inventory ==="); + int actualCount = Utils.GetItemStackSize("Prismatic Taper"); + WriteToChat($"Utils.GetItemStackSize: {actualCount}"); + if (cachedPrismaticCount != actualCount) + { + WriteToChat($"[WARNING] Count mismatch! Cached: {cachedPrismaticCount}, Actual: {actualCount}"); + } + else + { + WriteToChat("[OK] Cached count matches actual count"); + } + } + catch (Exception ex) + { + WriteToChat($"Debug taper error: {ex.Message}"); + } + break; + case "finditem": if (args.Length > 1) { diff --git a/MosswartMassacre/Properties/AssemblyInfo.cs b/MosswartMassacre/Properties/AssemblyInfo.cs index d23b09e..cad7b23 100644 --- a/MosswartMassacre/Properties/AssemblyInfo.cs +++ b/MosswartMassacre/Properties/AssemblyInfo.cs @@ -26,5 +26,5 @@ using System.Runtime.InteropServices; // Minor Version // Build Number // Revision -[assembly: AssemblyVersion("4.0.0.0")] -[assembly: AssemblyFileVersion("4.0.0.0")] \ No newline at end of file +[assembly: AssemblyVersion("4.0.0.2")] +[assembly: AssemblyFileVersion("4.0.0.2")] \ No newline at end of file diff --git a/MosswartMassacre/Telemetry.cs b/MosswartMassacre/Telemetry.cs index 964a8f7..b9f8e14 100644 --- a/MosswartMassacre/Telemetry.cs +++ b/MosswartMassacre/Telemetry.cs @@ -88,7 +88,7 @@ namespace MosswartMassacre deaths = PluginCore.sessionDeaths.ToString(), total_deaths = PluginCore.totalDeaths.ToString(), rares_found = PluginCore.rareCount, - prismatic_taper_count = Utils.GetItemStackSize("Prismatic Taper").ToString(), + prismatic_taper_count = PluginCore.cachedPrismaticCount.ToString(), vt_state = VtankControl.VtGetMetaState(), }; diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs index 37f7dc5..cbad25d 100644 --- a/MosswartMassacre/WebSocket.cs +++ b/MosswartMassacre/WebSocket.cs @@ -36,17 +36,13 @@ namespace MosswartMassacre private const int IntervalSec = 5; private static string SessionId = ""; - // ─── cached prismatic taper count ─────────── - private static int _cachedPrismaticTaperCount = 0; - private static DateTime _lastPrismaticTaperUpdate = DateTime.MinValue; - private static readonly TimeSpan PrismaticTaperCacheInterval = TimeSpan.FromMinutes(10); + // ─── cached prismatic taper count ─── (now handled by PluginCore event system) // ─── runtime state ────────────────────────── private static ClientWebSocket _ws; private static CancellationTokenSource _cts; private static bool _enabled; private static readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); - private static readonly SynchronizationContext _uiCtx = SynchronizationContext.Current; /// /// Fires when a valid CommandEnvelope arrives for this character. @@ -148,17 +144,8 @@ namespace MosswartMassacre CoreManager.Current.CharacterFilter.Name, StringComparison.OrdinalIgnoreCase)) { - _uiCtx.Post(_ => - { - try - { - OnServerCommand?.Invoke(env); // now on the correct thread - } - catch (Exception ex) - { - PluginCore.WriteToChat($"[CMD] {ex.Message}"); - } - }, null); + // Fire event immediately - let PluginCore handle threading + OnServerCommand?.Invoke(env); } } }); @@ -324,29 +311,7 @@ namespace MosswartMassacre // ─── payload builder ────────────────────────────── - private static void UpdatePrismaticTaperCache() - { - try - { - _cachedPrismaticTaperCount = Utils.GetItemStackSize("Prismatic Taper"); - _lastPrismaticTaperUpdate = DateTime.Now; - PluginCore.WriteToChat($"[WebSocket] Updated prismatic taper cache: {_cachedPrismaticTaperCount}"); - } - catch (Exception ex) - { - PluginCore.WriteToChat($"[WebSocket] Failed to update prismatic taper cache: {ex.Message}"); - } - } - - private static int GetCachedPrismaticTaperCount() - { - // Update cache if it's stale (older than 2 minutes) - if (DateTime.Now - _lastPrismaticTaperUpdate > PrismaticTaperCacheInterval) - { - UpdatePrismaticTaperCache(); - } - return _cachedPrismaticTaperCount; - } + // Removed old cache system - now using PluginCore.cachedPrismaticCount private static string BuildPayloadJson() { @@ -367,7 +332,7 @@ namespace MosswartMassacre onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"), deaths = PluginCore.sessionDeaths.ToString(), total_deaths = PluginCore.totalDeaths.ToString(), - prismatic_taper_count = GetCachedPrismaticTaperCount().ToString(), + prismatic_taper_count = PluginCore.cachedPrismaticCount.ToString(), vt_state = VtankControl.VtGetMetaState(), mem_mb = tele.MemoryBytes, cpu_pct = tele.GetCpuUsage(), diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll index a395bec..9a81c05 100644 Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ