diff --git a/MosswartMassacre/CharacterStats.cs b/MosswartMassacre/CharacterStats.cs
index 4a7248c..30918de 100644
--- a/MosswartMassacre/CharacterStats.cs
+++ b/MosswartMassacre/CharacterStats.cs
@@ -26,6 +26,8 @@ namespace MosswartMassacre
public static class CharacterStats
{
+ private static IPluginLogger _logger;
+
// Cached allegiance data (populated from network messages)
private static string allegianceName;
private static int allegianceSize;
@@ -44,8 +46,9 @@ namespace MosswartMassacre
///
/// Reset all cached data. Call on plugin init.
///
- internal static void Init()
+ internal static void Init(IPluginLogger logger = null)
{
+ _logger = logger;
allegianceName = null;
allegianceSize = 0;
followers = 0;
@@ -112,7 +115,7 @@ namespace MosswartMassacre
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[CharStats] Allegiance processing error: {ex.Message}");
+ _logger?.Log($"[CharStats] Allegiance processing error: {ex.Message}");
}
}
@@ -134,15 +137,15 @@ namespace MosswartMassacre
long key = tmpStruct.Value("key");
long value = tmpStruct.Value("value");
- if (key == 6) // AvailableLuminance
+ if (key == Constants.AvailableLuminanceKey)
luminanceEarned = value;
- else if (key == 7) // MaximumLuminance
+ else if (key == Constants.MaximumLuminanceKey)
luminanceTotal = value;
}
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[CharStats] Property processing error: {ex.Message}");
+ _logger?.Log($"[CharStats] Property processing error: {ex.Message}");
}
}
@@ -162,14 +165,14 @@ namespace MosswartMassacre
int key = BitConverter.ToInt32(raw, 5);
long value = BitConverter.ToInt64(raw, 9);
- if (key == 6) // AvailableLuminance
+ if (key == Constants.AvailableLuminanceKey)
luminanceEarned = value;
- else if (key == 7) // MaximumLuminance
+ else if (key == Constants.MaximumLuminanceKey)
luminanceTotal = value;
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[CharStats] Int64 property update error: {ex.Message}");
+ _logger?.Log($"[CharStats] Int64 property update error: {ex.Message}");
}
}
@@ -186,7 +189,7 @@ namespace MosswartMassacre
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[CharStats] Title processing error: {ex.Message}");
+ _logger?.Log($"[CharStats] Title processing error: {ex.Message}");
}
}
@@ -201,7 +204,7 @@ namespace MosswartMassacre
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[CharStats] Set title error: {ex.Message}");
+ _logger?.Log($"[CharStats] Set title error: {ex.Message}");
}
}
@@ -329,7 +332,7 @@ namespace MosswartMassacre
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[CharStats] Error collecting stats: {ex.Message}");
+ _logger?.Log($"[CharStats] Error collecting stats: {ex.Message}");
}
}
}
diff --git a/MosswartMassacre/ChatEventRouter.cs b/MosswartMassacre/ChatEventRouter.cs
new file mode 100644
index 0000000..9729ffa
--- /dev/null
+++ b/MosswartMassacre/ChatEventRouter.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Text.RegularExpressions;
+using Decal.Adapter;
+using Decal.Adapter.Wrappers;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Routes chat events to the appropriate handler (KillTracker, RareTracker, etc.)
+ /// Replaces the big if/else chain in PluginCore.OnChatText.
+ ///
+ internal class ChatEventRouter
+ {
+ private readonly IPluginLogger _logger;
+ private readonly KillTracker _killTracker;
+ private RareTracker _rareTracker;
+ private readonly Action _onRareCountChanged;
+ private readonly Action _onAllegianceReport;
+
+ internal void SetRareTracker(RareTracker rareTracker) => _rareTracker = rareTracker;
+
+ internal ChatEventRouter(
+ IPluginLogger logger,
+ KillTracker killTracker,
+ RareTracker rareTracker,
+ Action onRareCountChanged,
+ Action onAllegianceReport)
+ {
+ _logger = logger;
+ _killTracker = killTracker;
+ _rareTracker = rareTracker;
+ _onRareCountChanged = onRareCountChanged;
+ _onAllegianceReport = onAllegianceReport;
+ }
+
+ internal void OnChatText(object sender, ChatTextInterceptEventArgs e)
+ {
+ try
+ {
+ _killTracker.CheckForKill(e.Text);
+
+ if (_rareTracker != null && _rareTracker.CheckForRare(e.Text, out string rareText))
+ {
+ _killTracker.RareCount = _rareTracker.RareCount;
+ _onRareCountChanged?.Invoke(_rareTracker.RareCount);
+ }
+
+ if (e.Color == 18 && e.Text.EndsWith("!report\""))
+ {
+ TimeSpan elapsed = DateTime.Now - _killTracker.StatsStartTime;
+ string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_rareTracker?.RareCount ?? 0}";
+ _logger?.Log($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
+ _onAllegianceReport?.Invoke(reportMessage);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log("Error processing chat message: " + ex.Message);
+ }
+ }
+
+ ///
+ /// Streams all chat text to WebSocket (separate handler from the filtered one above).
+ ///
+ internal static async void AllChatText(object sender, ChatTextInterceptEventArgs e)
+ {
+ try
+ {
+ string cleaned = NormalizeChatLine(e.Text);
+ await WebSocket.SendChatTextAsync(e.Color, cleaned);
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[WS] Chat send failed: {ex}");
+ }
+ }
+
+ private static string NormalizeChatLine(string raw)
+ {
+ if (string.IsNullOrEmpty(raw))
+ return raw;
+
+ var noTags = Regex.Replace(raw, "<[^>]+>", "");
+ var trimmed = noTags.TrimEnd('\r', '\n');
+ var collapsed = Regex.Replace(trimmed, @"[ ]{2,}", " ");
+
+ return collapsed;
+ }
+ }
+}
diff --git a/MosswartMassacre/CommandRouter.cs b/MosswartMassacre/CommandRouter.cs
new file mode 100644
index 0000000..edba553
--- /dev/null
+++ b/MosswartMassacre/CommandRouter.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Dictionary-based /mm command dispatcher. Commands are registered with descriptions
+ /// and routed by name lookup instead of a giant switch statement.
+ ///
+ internal class CommandRouter
+ {
+ private readonly Dictionary handler, string description)> _commands
+ = new Dictionary, string)>(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Register a command with its handler and help description.
+ ///
+ internal void Register(string name, Action handler, string description)
+ {
+ _commands[name] = (handler, description);
+ }
+
+ ///
+ /// Dispatch a raw /mm command string. Returns false if the command was not found.
+ ///
+ internal bool Dispatch(string rawText)
+ {
+ string[] args = rawText.Substring(3).Trim().Split(' ');
+
+ if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
+ {
+ PluginCore.WriteToChat("Usage: /mm . Try /mm help");
+ return true;
+ }
+
+ string subCommand = args[0].ToLower();
+
+ if (subCommand == "help")
+ {
+ PrintHelp();
+ return true;
+ }
+
+ if (_commands.TryGetValue(subCommand, out var entry))
+ {
+ entry.handler(args);
+ return true;
+ }
+
+ PluginCore.WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help");
+ return false;
+ }
+
+ private void PrintHelp()
+ {
+ PluginCore.WriteToChat("Mosswart Massacre Commands:");
+ foreach (var kvp in _commands)
+ {
+ if (!string.IsNullOrEmpty(kvp.Value.description))
+ {
+ PluginCore.WriteToChat($"/mm {kvp.Key,-18} - {kvp.Value.description}");
+ }
+ }
+ }
+ }
+}
diff --git a/MosswartMassacre/Constants.cs b/MosswartMassacre/Constants.cs
new file mode 100644
index 0000000..8433dc2
--- /dev/null
+++ b/MosswartMassacre/Constants.cs
@@ -0,0 +1,30 @@
+namespace MosswartMassacre
+{
+ ///
+ /// Centralized constants for timer intervals, message type IDs, and property keys.
+ ///
+ internal static class Constants
+ {
+ // Timer intervals (milliseconds)
+ internal const int StatsUpdateIntervalMs = 1000;
+ internal const int VitalsUpdateIntervalMs = 5000;
+ internal const int CommandProcessIntervalMs = 10;
+ internal const int QuestStreamingIntervalMs = 30000;
+ internal const int CharacterStatsIntervalMs = 600000; // 10 minutes
+ internal const int LoginDelayMs = 5000;
+
+ // Network message types
+ internal const int GameEventMessageType = 0xF7B0;
+ internal const int PrivateUpdatePropertyInt64 = 0x02CF;
+
+ // Game event IDs (sub-events within 0xF7B0)
+ internal const int AllegianceInfoEvent = 0x0020;
+ internal const int LoginCharacterEvent = 0x0013;
+ internal const int TitlesListEvent = 0x0029;
+ internal const int SetTitleEvent = 0x002b;
+
+ // Int64 property keys
+ internal const int AvailableLuminanceKey = 6;
+ internal const int MaximumLuminanceKey = 7;
+ }
+}
diff --git a/MosswartMassacre/DecalHarmonyClean.cs b/MosswartMassacre/DecalHarmonyClean.cs
index 5a6e740..0bcdf23 100644
--- a/MosswartMassacre/DecalHarmonyClean.cs
+++ b/MosswartMassacre/DecalHarmonyClean.cs
@@ -62,9 +62,9 @@ namespace MosswartMassacre
// PATHWAY 2: Target Host.Actions.AddChatText (what our plugin uses)
PatchHostActions();
}
- catch
+ catch (Exception ex)
{
- // Only log if completely unable to apply any patches
+ AddDebugLog($"ApplyDecalPatches failed: {ex.Message}");
}
}
@@ -92,13 +92,15 @@ namespace MosswartMassacre
{
ApplySinglePatch(method, prefixMethodName);
}
- catch
+ catch (Exception ex)
{
+ AddDebugLog($"PatchHooksWrapper single patch failed ({prefixMethodName}): {ex.Message}");
}
}
}
- catch
+ catch (Exception ex)
{
+ AddDebugLog($"PatchHooksWrapper failed: {ex.Message}");
}
}
@@ -139,16 +141,18 @@ namespace MosswartMassacre
{
ApplySinglePatch(method, prefixMethodName);
}
- catch
+ catch (Exception ex)
{
+ AddDebugLog($"PatchHostActions single patch failed ({prefixMethodName}): {ex.Message}");
}
}
-
+
// PATHWAY 3: Try to patch at PluginHost level
PatchPluginHost();
}
- catch
+ catch (Exception ex)
{
+ AddDebugLog($"PatchHostActions failed: {ex.Message}");
}
}
@@ -159,36 +163,31 @@ namespace MosswartMassacre
{
try
{
- // Try to patch CoreManager.Current.Actions if it's different
- try
+ var coreActions = CoreManager.Current?.Actions;
+ if (coreActions != null && coreActions != PluginCore.MyHost?.Actions)
{
- var coreActions = CoreManager.Current?.Actions;
- if (coreActions != null && coreActions != PluginCore.MyHost?.Actions)
+ var coreActionsMethods = coreActions.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
+ .Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw" || m.Name == "AddStatusText").ToArray();
+
+ foreach (var method in coreActionsMethods)
{
- var coreActionsMethods = coreActions.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
- .Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw" || m.Name == "AddStatusText").ToArray();
-
- foreach (var method in coreActionsMethods)
+ var parameters = method.GetParameters();
+
+ try
{
- var parameters = method.GetParameters();
-
- try
- {
- string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length;
- ApplySinglePatch(method, prefixMethodName);
- }
- catch
- {
- }
+ string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length;
+ ApplySinglePatch(method, prefixMethodName);
+ }
+ catch (Exception ex)
+ {
+ AddDebugLog($"PatchPluginHost single patch failed: {ex.Message}");
}
}
}
- catch
- {
- }
}
- catch
+ catch (Exception ex)
{
+ AddDebugLog($"PatchPluginHost failed: {ex.Message}");
}
}
@@ -200,9 +199,9 @@ namespace MosswartMassacre
try
{
// Get our prefix method
- var prefixMethod = typeof(DecalPatchMethods).GetMethod(prefixMethodName,
+ var prefixMethod = typeof(DecalPatchMethods).GetMethod(prefixMethodName,
BindingFlags.Static | BindingFlags.Public);
-
+
if (prefixMethod != null)
{
// Use UtilityBelt's exact approach
@@ -210,8 +209,9 @@ namespace MosswartMassacre
patchesApplied = true;
}
}
- catch
+ catch (Exception ex)
{
+ AddDebugLog($"ApplySinglePatch failed ({prefixMethodName}): {ex.Message}");
}
}
@@ -244,8 +244,9 @@ namespace MosswartMassacre
}
patchesApplied = false;
}
- catch
+ catch (Exception ex)
{
+ AddDebugLog($"Cleanup failed: {ex.Message}");
}
}
@@ -390,8 +391,9 @@ namespace MosswartMassacre
Task.Run(() => WebSocket.SendChatTextAsync(color, text));
}
}
- catch
+ catch (Exception ex)
{
+ DecalHarmonyClean.AddDebugLog($"ProcessInterceptedMessage failed: {ex.Message}");
}
}
diff --git a/MosswartMassacre/GameEventRouter.cs b/MosswartMassacre/GameEventRouter.cs
new file mode 100644
index 0000000..6bda3f9
--- /dev/null
+++ b/MosswartMassacre/GameEventRouter.cs
@@ -0,0 +1,55 @@
+using System;
+using Decal.Adapter;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Routes EchoFilter.ServerDispatch network messages to the appropriate handlers.
+ /// Owns the routing of 0xF7B0 sub-events and 0x02CF to CharacterStats.
+ ///
+ internal class GameEventRouter
+ {
+ private readonly IPluginLogger _logger;
+
+ internal GameEventRouter(IPluginLogger logger)
+ {
+ _logger = logger;
+ }
+
+ internal void OnServerDispatch(object sender, NetworkMessageEventArgs e)
+ {
+ try
+ {
+ if (e.Message.Type == Constants.GameEventMessageType)
+ {
+ int eventId = (int)e.Message["event"];
+
+ if (eventId == Constants.AllegianceInfoEvent)
+ {
+ CharacterStats.ProcessAllegianceInfoMessage(e);
+ }
+ else if (eventId == Constants.LoginCharacterEvent)
+ {
+ CharacterStats.ProcessCharacterPropertyData(e);
+ }
+ else if (eventId == Constants.TitlesListEvent)
+ {
+ CharacterStats.ProcessTitlesMessage(e);
+ }
+ else if (eventId == Constants.SetTitleEvent)
+ {
+ CharacterStats.ProcessSetTitleMessage(e);
+ }
+ }
+ else if (e.Message.Type == Constants.PrivateUpdatePropertyInt64)
+ {
+ CharacterStats.ProcessPropertyInt64Update(e);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[CharStats] ServerDispatch error: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/MosswartMassacre/IGameStats.cs b/MosswartMassacre/IGameStats.cs
new file mode 100644
index 0000000..ba0cb6f
--- /dev/null
+++ b/MosswartMassacre/IGameStats.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Provides game statistics for WebSocket telemetry payloads.
+ /// Replaces direct static field access on PluginCore.
+ ///
+ public interface IGameStats
+ {
+ int TotalKills { get; }
+ double KillsPerHour { get; }
+ int SessionDeaths { get; }
+ int TotalDeaths { get; }
+ int CachedPrismaticCount { get; }
+ string CharTag { get; }
+ DateTime StatsStartTime { get; }
+ }
+}
diff --git a/MosswartMassacre/IPluginLogger.cs b/MosswartMassacre/IPluginLogger.cs
new file mode 100644
index 0000000..c9d4157
--- /dev/null
+++ b/MosswartMassacre/IPluginLogger.cs
@@ -0,0 +1,11 @@
+namespace MosswartMassacre
+{
+ ///
+ /// Interface for writing messages to the game chat window.
+ /// Eliminates direct PluginCore.WriteToChat() dependencies from manager classes.
+ ///
+ public interface IPluginLogger
+ {
+ void Log(string message);
+ }
+}
diff --git a/MosswartMassacre/InventoryMonitor.cs b/MosswartMassacre/InventoryMonitor.cs
new file mode 100644
index 0000000..1f60313
--- /dev/null
+++ b/MosswartMassacre/InventoryMonitor.cs
@@ -0,0 +1,184 @@
+using System;
+using System.Collections.Generic;
+using Decal.Adapter;
+using Decal.Adapter.Wrappers;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Tracks Prismatic Taper inventory counts using event-driven delta math.
+ /// Avoids expensive inventory scans during gameplay.
+ ///
+ internal class InventoryMonitor
+ {
+ private readonly IPluginLogger _logger;
+ private readonly Dictionary _trackedTaperContainers = new Dictionary();
+ private readonly Dictionary _lastKnownStackSizes = new Dictionary();
+
+ internal int CachedPrismaticCount { get; private set; }
+ internal int LastPrismaticCount { get; private set; }
+
+ internal InventoryMonitor(IPluginLogger logger)
+ {
+ _logger = logger;
+ }
+
+ internal void Initialize()
+ {
+ try
+ {
+ LastPrismaticCount = CachedPrismaticCount;
+ CachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
+
+ _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)
+ {
+ _logger?.Log($"[TAPER] Error initializing count: {ex.Message}");
+ CachedPrismaticCount = 0;
+ LastPrismaticCount = 0;
+ _trackedTaperContainers.Clear();
+ _lastKnownStackSizes.Clear();
+ }
+ }
+
+ internal 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;
+
+ _trackedTaperContainers[item.Id] = item.Container;
+ _lastKnownStackSizes[item.Id] = stackCount;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[TAPER] Error in OnInventoryCreate: {ex.Message}");
+ }
+ }
+
+ internal void OnInventoryRelease(object sender, ReleaseObjectEventArgs e)
+ {
+ try
+ {
+ var item = e.Released;
+ if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
+ {
+ if (_trackedTaperContainers.TryGetValue(item.Id, out int previousContainer))
+ {
+ if (IsPlayerOwnedContainer(previousContainer))
+ {
+ LastPrismaticCount = CachedPrismaticCount;
+ int stackCount = item.Values(LongValueKey.StackCount, 1);
+ CachedPrismaticCount -= stackCount;
+ }
+
+ _trackedTaperContainers.Remove(item.Id);
+ _lastKnownStackSizes.Remove(item.Id);
+ }
+ else
+ {
+ LastPrismaticCount = CachedPrismaticCount;
+ CachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[TAPER] Error in OnInventoryRelease: {ex.Message}");
+ }
+ }
+
+ internal void OnInventoryChange(object sender, ChangeObjectEventArgs e)
+ {
+ try
+ {
+ var item = e.Changed;
+ if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
+ {
+ bool isInPlayerContainer = IsPlayerOwnedContainer(item.Container);
+
+ if (isInPlayerContainer)
+ {
+ bool wasAlreadyTracked = _trackedTaperContainers.ContainsKey(item.Id);
+ _trackedTaperContainers[item.Id] = item.Container;
+
+ int currentStack = item.Values(LongValueKey.StackCount, 1);
+
+ if (!wasAlreadyTracked)
+ {
+ 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;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[TAPER] Error in OnInventoryChange: {ex.Message}");
+ }
+ }
+
+ internal void Cleanup()
+ {
+ _trackedTaperContainers.Clear();
+ _lastKnownStackSizes.Clear();
+ }
+
+ internal int TrackedTaperCount => _trackedTaperContainers.Count;
+ internal int KnownStackSizesCount => _lastKnownStackSizes.Count;
+
+ private static bool IsPlayerOwnedContainer(int containerId)
+ {
+ try
+ {
+ if (containerId == CoreManager.Current.CharacterFilter.Id)
+ return true;
+
+ 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;
+ }
+ }
+ }
+}
diff --git a/MosswartMassacre/KillTracker.cs b/MosswartMassacre/KillTracker.cs
new file mode 100644
index 0000000..0541f51
--- /dev/null
+++ b/MosswartMassacre/KillTracker.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Text.RegularExpressions;
+using System.Timers;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Tracks kills, deaths, and kill rate calculations.
+ /// Owns the 1-second stats update timer.
+ ///
+ internal class KillTracker
+ {
+ private readonly IPluginLogger _logger;
+ private readonly Action _onStatsUpdated;
+ private readonly Action _onElapsedUpdated;
+
+ private int _totalKills;
+ private int _sessionDeaths;
+ private int _totalDeaths;
+ private double _killsPer5Min;
+ private double _killsPerHour;
+ private DateTime _lastKillTime = DateTime.Now;
+ private DateTime _statsStartTime = DateTime.Now;
+ private Timer _updateTimer;
+
+ // Kill message patterns — all 35+ patterns preserved exactly
+ private static readonly string[] KillPatterns = new string[]
+ {
+ @"^You flatten (?.+)'s body with the force of your assault!$",
+ @"^You bring (?.+) to a fiery end!$",
+ @"^You beat (?.+) to a lifeless pulp!$",
+ @"^You smite (?.+) mightily!$",
+ @"^You obliterate (?.+)!$",
+ @"^You run (?.+) through!$",
+ @"^You reduce (?.+) to a sizzling, oozing mass!$",
+ @"^You knock (?.+) into next Morningthaw!$",
+ @"^You split (?.+) apart!$",
+ @"^You cleave (?.+) in twain!$",
+ @"^You slay (?.+) viciously enough to impart death several times over!$",
+ @"^You reduce (?.+) to a drained, twisted corpse!$",
+ @"^Your killing blow nearly turns (?.+) inside-out!$",
+ @"^Your attack stops (?.+) cold!$",
+ @"^Your lightning coruscates over (?.+)'s mortal remains!$",
+ @"^Your assault sends (?.+) to an icy death!$",
+ @"^You killed (?.+)!$",
+ @"^The thunder of crushing (?.+) is followed by the deafening silence of death!$",
+ @"^The deadly force of your attack is so strong that (?.+)'s ancestors feel it!$",
+ @"^(?.+)'s seared corpse smolders before you!$",
+ @"^(?.+) is reduced to cinders!$",
+ @"^(?.+) is shattered by your assault!$",
+ @"^(?.+) catches your attack, with dire consequences!$",
+ @"^(?.+) is utterly destroyed by your attack!$",
+ @"^(?.+) suffers a frozen fate!$",
+ @"^(?.+)'s perforated corpse falls before you!$",
+ @"^(?.+) is fatally punctured!$",
+ @"^(?.+)'s death is preceded by a sharp, stabbing pain!$",
+ @"^(?.+) is torn to ribbons by your assault!$",
+ @"^(?.+) is liquified by your attack!$",
+ @"^(?.+)'s last strength dissolves before you!$",
+ @"^Electricity tears (?.+) apart!$",
+ @"^Blistered by lightning, (?.+) falls!$",
+ @"^(?.+)'s last strength withers before you!$",
+ @"^(?.+) is dessicated by your attack!$",
+ @"^(?.+) is incinerated by your assault!$"
+ };
+
+ internal int TotalKills => _totalKills;
+ internal double KillsPerHour => _killsPerHour;
+ internal double KillsPer5Min => _killsPer5Min;
+ internal int SessionDeaths => _sessionDeaths;
+ internal int TotalDeaths => _totalDeaths;
+ internal DateTime StatsStartTime => _statsStartTime;
+ internal DateTime LastKillTime => _lastKillTime;
+ internal int RareCount { get; set; }
+
+ /// Logger for chat output
+ /// Callback(totalKills, killsPer5Min, killsPerHour) for UI updates
+ /// Callback(elapsed) for UI elapsed time updates
+ internal KillTracker(IPluginLogger logger, Action onStatsUpdated, Action onElapsedUpdated)
+ {
+ _logger = logger;
+ _onStatsUpdated = onStatsUpdated;
+ _onElapsedUpdated = onElapsedUpdated;
+ }
+
+ internal void Start()
+ {
+ _updateTimer = new Timer(Constants.StatsUpdateIntervalMs);
+ _updateTimer.Elapsed += UpdateStats;
+ _updateTimer.Start();
+ }
+
+ internal void Stop()
+ {
+ if (_updateTimer != null)
+ {
+ _updateTimer.Stop();
+ _updateTimer.Dispose();
+ _updateTimer = null;
+ }
+ }
+
+ internal bool CheckForKill(string text)
+ {
+ if (IsKilledByMeMessage(text))
+ {
+ _totalKills++;
+ _lastKillTime = DateTime.Now;
+ CalculateKillsPerInterval();
+ _onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
+ return true;
+ }
+ return false;
+ }
+
+ internal void OnDeath()
+ {
+ _sessionDeaths++;
+ }
+
+ internal void SetTotalDeaths(int totalDeaths)
+ {
+ _totalDeaths = totalDeaths;
+ }
+
+ internal void RestartStats()
+ {
+ _totalKills = 0;
+ RareCount = 0;
+ _sessionDeaths = 0;
+ _statsStartTime = DateTime.Now;
+ _killsPer5Min = 0;
+ _killsPerHour = 0;
+
+ _logger?.Log($"Stats have been reset. Session deaths: {_sessionDeaths}, Total deaths: {_totalDeaths}");
+ _onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
+ }
+
+ private void UpdateStats(object sender, ElapsedEventArgs e)
+ {
+ try
+ {
+ TimeSpan elapsed = DateTime.Now - _statsStartTime;
+ _onElapsedUpdated?.Invoke(elapsed);
+
+ CalculateKillsPerInterval();
+ _onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log("Error updating stats: " + ex.Message);
+ }
+ }
+
+ private void CalculateKillsPerInterval()
+ {
+ double minutesElapsed = (DateTime.Now - _statsStartTime).TotalMinutes;
+
+ if (minutesElapsed > 0)
+ {
+ _killsPer5Min = (_totalKills / minutesElapsed) * 5;
+ _killsPerHour = (_totalKills / minutesElapsed) * 60;
+ }
+ }
+
+ private bool IsKilledByMeMessage(string text)
+ {
+ foreach (string pattern in KillPatterns)
+ {
+ if (Regex.IsMatch(text, pattern))
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index 2e812f9..745cfa9 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -304,6 +304,16 @@
Shared\VCS_Connector.cs
+
+
+
+
+
+
+
+
+
+
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index 522111f..31e42a8 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -1,13 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
-using System.Text;
-using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Timers;
using Decal.Adapter;
@@ -18,7 +14,7 @@ using Mag.Shared.Constants;
namespace MosswartMassacre
{
[FriendlyName("Mosswart Massacre")]
- public class PluginCore : PluginBase
+ public class PluginCore : PluginBase, IPluginLogger, IGameStats
{
// Hot Reload Support Properties
private static string _assemblyDirectory = null;
@@ -47,26 +43,36 @@ namespace MosswartMassacre
public static bool IsHotReload { get; set; }
internal static PluginHost MyHost;
- internal static int totalKills = 0;
- 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;
+ // Static bridge properties for VVSTabbedMainView (reads from manager instances)
+ private static InventoryMonitor _staticInventoryMonitor;
+ internal static int cachedPrismaticCount => _staticInventoryMonitor?.CachedPrismaticCount ?? 0;
+ private static KillTracker _staticKillTracker;
+ internal static int totalKills => _staticKillTracker?.TotalKills ?? 0;
+ internal static double killsPerHour => _staticKillTracker?.KillsPerHour ?? 0;
+ internal static int sessionDeaths => _staticKillTracker?.SessionDeaths ?? 0;
+ internal static int totalDeaths => _staticKillTracker?.TotalDeaths ?? 0;
+ internal static DateTime statsStartTime => _staticKillTracker?.StatsStartTime ?? DateTime.Now;
+ internal static DateTime lastKillTime => _staticKillTracker?.LastKillTime ?? DateTime.Now;
+
+ // IGameStats explicit implementation (for WebSocket telemetry)
+ int IGameStats.TotalKills => _staticKillTracker?.TotalKills ?? 0;
+ double IGameStats.KillsPerHour => _staticKillTracker?.KillsPerHour ?? 0;
+ int IGameStats.SessionDeaths => _staticKillTracker?.SessionDeaths ?? 0;
+ int IGameStats.TotalDeaths => _staticKillTracker?.TotalDeaths ?? 0;
+ int IGameStats.CachedPrismaticCount => _staticInventoryMonitor?.CachedPrismaticCount ?? 0;
+ string IGameStats.CharTag => CharTag;
+ DateTime IGameStats.StatsStartTime => _staticKillTracker?.StatsStartTime ?? DateTime.Now;
+
private static Timer vitalsTimer;
private static System.Windows.Forms.Timer commandTimer;
private static Timer characterStatsTimer;
private static readonly Queue pendingCommands = new Queue();
- public static bool RareMetaEnabled { get; set; } = true;
+ private static RareTracker _staticRareTracker;
+ public static bool RareMetaEnabled
+ {
+ get => _staticRareTracker?.RareMetaEnabled ?? true;
+ set { if (_staticRareTracker != null) _staticRareTracker.RareMetaEnabled = value; }
+ }
// VVS View Management
private static class ViewManager
@@ -121,12 +127,18 @@ namespace MosswartMassacre
// Quest Management for always-on quest streaming
public static QuestManager questManager;
- private static Timer questStreamingTimer;
- private static Queue rareMessageQueue = new Queue();
- private static DateTime _lastSent = DateTime.MinValue;
private static readonly Queue _chatQueue = new Queue();
+ // Managers
+ private KillTracker _killTracker;
+ private RareTracker _rareTracker;
+ private InventoryMonitor _inventoryMonitor;
+ private ChatEventRouter _chatEventRouter;
+ private GameEventRouter _gameEventRouter;
+ private QuestStreamingService _questStreamingService;
+ private CommandRouter _commandRouter;
+
protected override void Startup()
{
try
@@ -166,36 +178,52 @@ namespace MosswartMassacre
}
}
+ // Initialize kill tracker (owns the 1-sec stats timer)
+ _killTracker = new KillTracker(
+ this,
+ (kills, per5, perHr) => ViewManager.UpdateKillStats(kills, per5, perHr),
+ elapsed => ViewManager.UpdateElapsedTime(elapsed));
+ _staticKillTracker = _killTracker;
+ _killTracker.Start();
+
+ // Initialize inventory monitor (taper tracking)
+ _inventoryMonitor = new InventoryMonitor(this);
+ _staticInventoryMonitor = _inventoryMonitor;
+
+ // Initialize chat event router (rareTracker set later in LoginComplete)
+ _chatEventRouter = new ChatEventRouter(
+ this, _killTracker, null,
+ count => ViewManager.UpdateRareCount(count),
+ msg => MyHost?.Actions.InvokeChatParser($"/a {msg}"));
+
+ // Initialize game event router
+ _gameEventRouter = new GameEventRouter(this);
+
// Note: Startup messages will appear after character login
- // Subscribe to chat message event
- CoreManager.Current.ChatBoxMessage += new EventHandler(OnChatText);
- CoreManager.Current.ChatBoxMessage += new EventHandler(AllChatText);
+ // Subscribe to events
+ CoreManager.Current.ChatBoxMessage += new EventHandler(_chatEventRouter.OnChatText);
+ CoreManager.Current.ChatBoxMessage += new EventHandler(ChatEventRouter.AllChatText);
CoreManager.Current.CommandLineText += OnChatCommand;
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
CoreManager.Current.CharacterFilter.Death += OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject += OnSpawn;
CoreManager.Current.WorldFilter.CreateObject += OnPortalDetected;
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;
+ CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate;
+ CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease;
+ CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange;
+
// Initialize VVS view after character login
ViewManager.ViewInit();
- // Initialize the timer
- updateTimer = new Timer(1000); // Update every second
- updateTimer.Elapsed += UpdateStats;
- updateTimer.Start();
-
// Initialize vitals streaming timer
- vitalsTimer = new Timer(5000); // Send vitals every 5 seconds
+ vitalsTimer = new Timer(Constants.VitalsUpdateIntervalMs);
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.Interval = Constants.CommandProcessIntervalMs;
commandTimer.Tick += ProcessPendingCommands;
commandTimer.Start();
@@ -204,13 +232,16 @@ namespace MosswartMassacre
// Initialize character stats and hook ServerDispatch early
// 0x0013 (character properties with luminance) fires DURING login,
// BEFORE LoginComplete — must hook here to catch it
- CharacterStats.Init();
- CoreManager.Current.EchoFilter.ServerDispatch += EchoFilter_ServerDispatch;
+ CharacterStats.Init(this);
+ CoreManager.Current.EchoFilter.ServerDispatch += _gameEventRouter.OnServerDispatch;
// Enable TLS1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
//Enable vTank interface
vTank.Enable();
+ // Set logger for WebSocket
+ WebSocket.SetLogger(this);
+ WebSocket.SetGameStats(this);
//lyssna på commands
WebSocket.OnServerCommand += HandleServerCommand;
//starta inventory. Hanterar subscriptions i den med
@@ -220,6 +251,10 @@ namespace MosswartMassacre
// Initialize navigation visualization system
navVisualization = new NavVisualization();
+ // Initialize command router
+ _commandRouter = new CommandRouter();
+ RegisterCommands();
+
// Note: ChestLooter is initialized in LoginComplete after PluginSettings.Initialize()
// Note: DECAL Harmony patches will be initialized in LoginComplete event
@@ -242,27 +277,25 @@ namespace MosswartMassacre
// Unsubscribe from chat message event
- CoreManager.Current.ChatBoxMessage -= new EventHandler(OnChatText);
+ CoreManager.Current.ChatBoxMessage -= new EventHandler(_chatEventRouter.OnChatText);
CoreManager.Current.CommandLineText -= OnChatCommand;
- CoreManager.Current.ChatBoxMessage -= new EventHandler(AllChatText);
+ CoreManager.Current.ChatBoxMessage -= new EventHandler(ChatEventRouter.AllChatText);
CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject -= OnSpawn;
CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected;
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;
- // Unsubscribe from server dispatch
- CoreManager.Current.EchoFilter.ServerDispatch -= EchoFilter_ServerDispatch;
-
- // Stop and dispose of the timers
- if (updateTimer != null)
+ // Unsubscribe inventory monitor
+ if (_inventoryMonitor != null)
{
- updateTimer.Stop();
- updateTimer.Dispose();
- updateTimer = null;
+ CoreManager.Current.WorldFilter.CreateObject -= _inventoryMonitor.OnInventoryCreate;
+ CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease;
+ CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange;
}
+ // Unsubscribe from server dispatch
+ CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
+
+ // Stop kill tracker
+ _killTracker?.Stop();
if (vitalsTimer != null)
{
@@ -278,14 +311,9 @@ namespace MosswartMassacre
commandTimer = null;
}
- // Stop and dispose quest streaming timer
- if (questStreamingTimer != null)
- {
- questStreamingTimer.Stop();
- questStreamingTimer.Elapsed -= OnQuestStreamingUpdate;
- questStreamingTimer.Dispose();
- questStreamingTimer = null;
- }
+ // Stop quest streaming service
+ _questStreamingService?.Stop();
+ _questStreamingService = null;
// Stop and dispose character stats timer
if (characterStatsTimer != null)
@@ -321,8 +349,7 @@ namespace MosswartMassacre
}
// Clean up taper tracking
- trackedTaperContainers.Clear();
- lastKnownStackSizes.Clear();
+ _inventoryMonitor?.Cleanup();
// Clean up Harmony patches
DecalHarmonyClean.Cleanup();
@@ -359,8 +386,13 @@ namespace MosswartMassacre
WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}");
}
+ // Initialize rare tracker and wire to chat router
+ _rareTracker = new RareTracker(this);
+ _staticRareTracker = _rareTracker;
+ _chatEventRouter.SetRareTracker(_rareTracker);
+
// Apply the values
- RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
CharTag = PluginSettings.Instance.CharTag;
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
@@ -383,11 +415,10 @@ namespace MosswartMassacre
}
// Initialize death tracking
- totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
- sessionDeaths = 0;
+ _killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
// Initialize cached Prismatic Taper count
- InitializePrismaticTaperCount();
+ _inventoryMonitor.Initialize();
// Initialize quest manager for always-on quest streaming
try
@@ -397,12 +428,10 @@ namespace MosswartMassacre
// Trigger full quest data refresh (same as clicking refresh button)
Views.FlagTrackerView.RefreshQuestData();
- // Initialize quest streaming timer (30 seconds)
- questStreamingTimer = new Timer(30000);
- questStreamingTimer.Elapsed += OnQuestStreamingUpdate;
- questStreamingTimer.AutoReset = true;
- questStreamingTimer.Start();
-
+ // Initialize quest streaming service (30 seconds)
+ _questStreamingService = new QuestStreamingService(this);
+ _questStreamingService.Start();
+
WriteToChat("[OK] Quest streaming initialized with full data refresh");
}
catch (Exception ex)
@@ -416,13 +445,13 @@ namespace MosswartMassacre
try
{
// Start 10-minute character stats timer
- characterStatsTimer = new Timer(600000); // 10 minutes
+ characterStatsTimer = new Timer(Constants.CharacterStatsIntervalMs);
characterStatsTimer.Elapsed += OnCharacterStatsUpdate;
characterStatsTimer.AutoReset = true;
characterStatsTimer.Start();
// Send initial stats after 5-second delay (let CharacterFilter populate)
- var initialDelay = new Timer(5000);
+ var initialDelay = new Timer(Constants.LoginDelayMs);
initialDelay.AutoReset = false;
initialDelay.Elapsed += (s, args) =>
{
@@ -440,102 +469,6 @@ namespace MosswartMassacre
}
- #region Quest Streaming Methods
- private static void OnQuestStreamingUpdate(object sender, ElapsedEventArgs e)
- {
- try
- {
- // Debug: Log when timer fires
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- WriteToChat("[QUEST-STREAM] Timer fired, checking conditions...");
- }
-
- // Stream high priority quest data via WebSocket
- if (!WebSocketEnabled)
- {
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- WriteToChat("[QUEST-STREAM] WebSocket not enabled, skipping");
- }
- return;
- }
-
- if (questManager?.QuestList == null || questManager.QuestList.Count == 0)
- {
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- WriteToChat($"[QUEST-STREAM] No quest data available (null: {questManager?.QuestList == null}, count: {questManager?.QuestList?.Count ?? 0})");
- }
- return;
- }
-
- var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
-
- // Find and stream priority quests (deduplicated by quest ID)
- var priorityQuests = questManager.QuestList
- .Where(q => IsHighPriorityQuest(q.Id))
- .GroupBy(q => q.Id)
- .Select(g => g.First()) // Take first occurrence of each quest ID
- .ToList();
-
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- WriteToChat($"[QUEST-STREAM] Found {priorityQuests.Count} priority quests to stream");
- }
-
- foreach (var quest in priorityQuests)
- {
- try
- {
- string questName = questManager.GetFriendlyQuestName(quest.Id);
- long timeRemaining = quest.ExpireTime - currentTime;
- string countdown = FormatCountdown(timeRemaining);
-
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- WriteToChat($"[QUEST-STREAM] Sending: {questName} - {countdown}");
- }
-
- // Stream quest data
- System.Threading.Tasks.Task.Run(() => WebSocket.SendQuestDataAsync(questName, countdown));
- }
- catch (Exception ex)
- {
- WriteToChat($"[QUEST-STREAM] Error streaming quest {quest.Id}: {ex.Message}");
- }
- }
- }
- catch (Exception ex)
- {
- WriteToChat($"[QUEST-STREAM] Error in timer handler: {ex.Message}");
- }
- }
-
- private static bool IsHighPriorityQuest(string questId)
- {
- return questId == "stipendtimer_0812" || // Changed from stipendtimer_monthly to stipendtimer_0812
- questId == "augmentationblankgemacquired" ||
- questId == "insatiableeaterjaw";
- }
-
- private static string FormatCountdown(long seconds)
- {
- if (seconds <= 0)
- return "READY";
-
- var timeSpan = TimeSpan.FromSeconds(seconds);
-
- if (timeSpan.TotalDays >= 1)
- return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours:D2}h";
- else if (timeSpan.TotalHours >= 1)
- return $"{timeSpan.Hours}h {timeSpan.Minutes:D2}m";
- else if (timeSpan.TotalMinutes >= 1)
- return $"{timeSpan.Minutes}m {timeSpan.Seconds:D2}s";
- else
- return $"{timeSpan.Seconds}s";
- }
- #endregion
private void InitializeForHotReload()
{
@@ -563,7 +496,7 @@ namespace MosswartMassacre
}
// 2. Apply the values from settings
- RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ if (_rareTracker != null) _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
CharTag = PluginSettings.Instance.CharTag;
@@ -597,11 +530,10 @@ namespace MosswartMassacre
}
// 6. Reinitialize death tracking
- totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
- // Don't reset sessionDeaths - keep the current session count
+ _killTracker?.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
// 7. Reinitialize cached Prismatic Taper count
- InitializePrismaticTaperCount();
+ _inventoryMonitor?.Initialize();
// 8. Reinitialize quest manager for hot reload
try
@@ -625,201 +557,23 @@ namespace MosswartMassacre
WriteToChat($"[ERROR] Quest manager hot reload failed: {ex.Message}");
}
- // 9. Reinitialize quest streaming timer for hot reload
+ // 9. Reinitialize quest streaming service for hot reload
try
{
- // Stop existing timer if any
- if (questStreamingTimer != null)
- {
- questStreamingTimer.Stop();
- questStreamingTimer.Elapsed -= OnQuestStreamingUpdate;
- questStreamingTimer.Dispose();
- questStreamingTimer = null;
- }
-
- // Create new timer
- questStreamingTimer = new Timer(30000); // 30 seconds
- questStreamingTimer.Elapsed += OnQuestStreamingUpdate;
- questStreamingTimer.AutoReset = true;
- questStreamingTimer.Start();
-
- WriteToChat("[OK] Quest streaming timer reinitialized (30s interval)");
+ _questStreamingService?.Stop();
+ _questStreamingService = new QuestStreamingService(this);
+ _questStreamingService.Start();
+
+ WriteToChat("[OK] Quest streaming service reinitialized (30s interval)");
}
catch (Exception ex)
{
- WriteToChat($"[ERROR] Quest streaming timer hot reload failed: {ex.Message}");
+ WriteToChat($"[ERROR] Quest streaming service hot reload failed: {ex.Message}");
}
WriteToChat("Hot reload initialization completed!");
}
- 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)
{
@@ -952,44 +706,11 @@ namespace MosswartMassacre
// $"[Despawn] {mob.Name} @ (NS={c.NorthSouth:F1}, EW={c.EastWest:F1})");
}
- private async void AllChatText(object sender, ChatTextInterceptEventArgs e)
- {
- try
- {
- string cleaned = NormalizeChatLine(e.Text);
-
- // Send to WebSocket
- await WebSocket.SendChatTextAsync(e.Color, cleaned);
-
- // Note: Plugin message analysis is now handled by Harmony patches
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[WS] Chat send failed: {ex}");
- }
- }
-
- private static string NormalizeChatLine(string raw)
- {
- if (string.IsNullOrEmpty(raw))
- return raw;
-
- // 1) Remove all <…> tags
- var noTags = Regex.Replace(raw, "<[^>]+>", "");
-
- // 2) Trim trailing newline or carriage-return
- var trimmed = noTags.TrimEnd('\r', '\n');
-
- // 3) Collapse multiple spaces into one
- var collapsed = Regex.Replace(trimmed, @"[ ]{2,}", " ");
-
- return collapsed;
- }
private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e)
{
- sessionDeaths++;
- totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
+ _killTracker.OnDeath();
+ _killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
}
private void HandleServerCommand(CommandEnvelope env)
@@ -1026,55 +747,8 @@ namespace MosswartMassacre
}
}
- private void OnChatText(object sender, ChatTextInterceptEventArgs e)
- {
- try
- {
-
- if (IsKilledByMeMessage(e.Text))
- {
- totalKills++;
- lastKillTime = DateTime.Now;
- CalculateKillsPerInterval();
- ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
- }
-
- if (IsRareDiscoveryMessage(e.Text, out string rareText))
- {
- rareCount++;
- ViewManager.UpdateRareCount(rareCount);
-
- if (RareMetaEnabled)
- {
- Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
- }
-
- DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
- // Fire and forget: we don't await, since sending is not critical and we don't want to block.
- _ = WebSocket.SendRareAsync(rareText);
- }
-
- if (e.Color == 18 && e.Text.EndsWith("!report\""))
- {
- TimeSpan elapsed = DateTime.Now - statsStartTime;
- string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}";
- WriteToChat($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
- MyHost.Actions.InvokeChatParser($"/a {reportMessage}");
- }
-
-
-
-
-
-
- }
- catch (Exception ex)
- {
- WriteToChat("Error processing chat message: " + ex.Message);
- }
- }
private void OnChatCommand(object sender, ChatParserInterceptEventArgs e)
{
try
@@ -1091,24 +765,6 @@ namespace MosswartMassacre
}
}
- private void UpdateStats(object sender, ElapsedEventArgs e)
- {
- try
- {
- // Update the elapsed time
- TimeSpan elapsed = DateTime.Now - statsStartTime;
- ViewManager.UpdateElapsedTime(elapsed);
-
- // Recalculate kill rates
- CalculateKillsPerInterval();
- ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
-
- }
- catch (Exception ex)
- {
- WriteToChat("Error updating stats: " + ex.Message);
- }
- }
private static void SendVitalsUpdate(object sender, ElapsedEventArgs e)
{
@@ -1168,119 +824,7 @@ namespace MosswartMassacre
}
}
- private void EchoFilter_ServerDispatch(object sender, NetworkMessageEventArgs e)
- {
- try
- {
- if (e.Message.Type == 0xF7B0) // Game Event
- {
- int eventId = (int)e.Message["event"];
- if (eventId == 0x0020) // Allegiance info
- {
- CharacterStats.ProcessAllegianceInfoMessage(e);
- }
- else if (eventId == 0x0013) // Login Character (properties)
- {
- CharacterStats.ProcessCharacterPropertyData(e);
- }
- else if (eventId == 0x0029) // Titles list
- {
- CharacterStats.ProcessTitlesMessage(e);
- }
- else if (eventId == 0x002b) // Set title
- {
- CharacterStats.ProcessSetTitleMessage(e);
- }
- }
- else if (e.Message.Type == 0x02CF) // PrivateUpdatePropertyInt64 (runtime luminance changes)
- {
- CharacterStats.ProcessPropertyInt64Update(e);
- }
- }
- catch (Exception ex)
- {
- WriteToChat($"[CharStats] ServerDispatch error: {ex.Message}");
- }
- }
-
- private void CalculateKillsPerInterval()
- {
- double minutesElapsed = (DateTime.Now - statsStartTime).TotalMinutes;
-
- if (minutesElapsed > 0)
- {
- killsPer5Min = (totalKills / minutesElapsed) * 5;
- killsPerHour = (totalKills / minutesElapsed) * 60;
- }
- }
-
- private bool IsKilledByMeMessage(string text)
- {
- string[] killPatterns = new string[]
- {
- @"^You flatten (?.+)'s body with the force of your assault!$",
- @"^You bring (?.+) to a fiery end!$",
- @"^You beat (?.+) to a lifeless pulp!$",
- @"^You smite (?.+) mightily!$",
- @"^You obliterate (?.+)!$",
- @"^You run (?.+) through!$",
- @"^You reduce (?.+) to a sizzling, oozing mass!$",
- @"^You knock (?.+) into next Morningthaw!$",
- @"^You split (?.+) apart!$",
- @"^You cleave (?.+) in twain!$",
- @"^You slay (?.+) viciously enough to impart death several times over!$",
- @"^You reduce (?.+) to a drained, twisted corpse!$",
- @"^Your killing blow nearly turns (?.+) inside-out!$",
- @"^Your attack stops (?.+) cold!$",
- @"^Your lightning coruscates over (?.+)'s mortal remains!$",
- @"^Your assault sends (?.+) to an icy death!$",
- @"^You killed (?.+)!$",
- @"^The thunder of crushing (?.+) is followed by the deafening silence of death!$",
- @"^The deadly force of your attack is so strong that (?.+)'s ancestors feel it!$",
- @"^(?.+)'s seared corpse smolders before you!$",
- @"^(?.+) is reduced to cinders!$",
- @"^(?.+) is shattered by your assault!$",
- @"^(?.+) catches your attack, with dire consequences!$",
- @"^(?.+) is utterly destroyed by your attack!$",
- @"^(?.+) suffers a frozen fate!$",
- @"^(?.+)'s perforated corpse falls before you!$",
- @"^(?.+) is fatally punctured!$",
- @"^(?.+)'s death is preceded by a sharp, stabbing pain!$",
- @"^(?.+) is torn to ribbons by your assault!$",
- @"^(?.+) is liquified by your attack!$",
- @"^(?.+)'s last strength dissolves before you!$",
- @"^Electricity tears (?.+) apart!$",
- @"^Blistered by lightning, (?.+) falls!$",
- @"^(?.+)'s last strength withers before you!$",
- @"^(?.+) is dessicated by your attack!$",
- @"^(?.+) is incinerated by your assault!$"
- };
-
- foreach (string pattern in killPatterns)
- {
- if (Regex.IsMatch(text, pattern))
- return true;
- }
-
- return false;
- }
- private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
- {
- rareTextOnly = null;
-
- // Match pattern: " has discovered the !"
- string pattern = @"^(?['A-Za-z ]+)\shas discovered the (?- .*?)!$";
- Match match = Regex.Match(text, pattern);
-
- if (match.Success && match.Groups["name"].Value == CoreManager.Current.CharacterFilter.Name)
- {
- rareTextOnly = match.Groups["item"].Value; // just "Ancient Pickle"
- return true;
- }
-
- return false;
- }
public static void WriteToChat(string message)
{
try
@@ -1309,23 +853,19 @@ namespace MosswartMassacre
}
}
}
+
+ void IPluginLogger.Log(string message) => WriteToChat(message);
+
public static void RestartStats()
{
- totalKills = 0;
- rareCount = 0;
- sessionDeaths = 0; // Reset session deaths only
- statsStartTime = DateTime.Now;
- killsPer5Min = 0;
- killsPerHour = 0;
-
- WriteToChat($"Stats have been reset. Session deaths: {sessionDeaths}, Total deaths: {totalDeaths}");
- ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
- ViewManager.UpdateRareCount(rareCount);
+ _staticKillTracker?.RestartStats();
+ if (_staticRareTracker != null)
+ _staticRareTracker.RareCount = 0;
+ ViewManager.UpdateRareCount(0);
}
public static void ToggleRareMeta()
{
- PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
- RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ _staticRareTracker?.ToggleRareMeta();
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
}
@@ -1353,568 +893,531 @@ namespace MosswartMassacre
}
private void HandleMmCommand(string text)
{
- // Remove the /mm prefix and trim extra whitespace test
- string[] args = text.Substring(3).Trim().Split(' ');
+ _commandRouter.Dispatch(text);
+ }
- if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
+ private void RegisterCommands()
+ {
+ _commandRouter.Register("ws", args =>
{
- WriteToChat("Usage: /mm . Try /mm help");
- return;
- }
-
- string subCommand = args[0].ToLower();
-
- switch (subCommand)
- {
- case "ws":
- if (args.Length > 1)
+ if (args.Length > 1)
+ {
+ if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
- if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
- {
- WebSocketEnabled = true;
- WebSocket.Start();
- PluginSettings.Instance.WebSocketEnabled = true;
- WriteToChat("WS streaming ENABLED.");
- }
- else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
- {
- WebSocketEnabled = false;
- WebSocket.Stop();
- PluginSettings.Instance.WebSocketEnabled = false;
- WriteToChat("WS streaming DISABLED.");
- }
- else
- {
- WriteToChat("Usage: /mm ws ");
- }
+ WebSocketEnabled = true;
+ WebSocket.Start();
+ PluginSettings.Instance.WebSocketEnabled = true;
+ WriteToChat("WS streaming ENABLED.");
+ }
+ else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
+ {
+ WebSocketEnabled = false;
+ WebSocket.Stop();
+ PluginSettings.Instance.WebSocketEnabled = false;
+ WriteToChat("WS streaming DISABLED.");
}
-
else
{
WriteToChat("Usage: /mm ws ");
}
- break;
- case "help":
- WriteToChat("Mosswart Massacre Commands:");
- WriteToChat("/mm report - Show current stats");
- WriteToChat("/mm loc - Show current location");
- WriteToChat("/mm ws - Websocket streaming enable|disable");
- WriteToChat("/mm reset - Reset all counters");
- WriteToChat("/mm meta - Toggle rare meta state!!");
- WriteToChat("/mm getmetastate - Gets the current metastate");
- WriteToChat("/mm setchest - Set chest name for looter");
- WriteToChat("/mm setkey - Set key name for looter");
- WriteToChat("/mm lootchest - Start chest looting");
- WriteToChat("/mm stoploot - Stop chest looting");
- WriteToChat("/mm nextwp - Advance VTank to next waypoint");
- WriteToChat("/mm decalstatus - Check Harmony patch status (UtilityBelt version)");
- WriteToChat("/mm decaldebug - Enable/disable plugin message debug output + WebSocket streaming");
- WriteToChat("/mm harmonyraw - Show raw intercepted messages (debug output)");
- WriteToChat("/mm testprismatic - Test Prismatic Taper detection and icon lookup");
- WriteToChat("/mm deathstats - Show current death tracking statistics");
- WriteToChat("/mm testtaper - Test cached Prismatic Taper tracking");
- WriteToChat("/mm debugtaper - Show detailed taper tracking debug info");
- WriteToChat("/mm gui - Manually initialize/reinitialize GUI!!!");
- WriteToChat("/mm checkforupdate - Check for plugin updates");
- WriteToChat("/mm update - Download and install update (if available)");
- WriteToChat("/mm debugupdate - Debug update UI controls");
- WriteToChat("/mm sendinventory - Force inventory upload with ID requests");
- WriteToChat("/mm refreshquests - Force quest data refresh for Flag Tracker");
- WriteToChat("/mm queststatus - Show quest streaming status and diagnostics");
- WriteToChat("/mm verbose - Toggle verbose debug logging");
- break;
- case "report":
- TimeSpan elapsed = DateTime.Now - statsStartTime;
- string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}, Session Deaths: {sessionDeaths}, Total Deaths: {totalDeaths}";
- WriteToChat(reportMessage);
- break;
- case "getmetastate":
- string metaState = VtankControl.VtGetMetaState();
- WriteToChat(metaState);
- break;
+ }
+ else
+ {
+ WriteToChat("Usage: /mm ws ");
+ }
+ }, "Websocket streaming enable|disable");
- case "loc":
- Coordinates here = Coordinates.Me;
- var pos = Utils.GetPlayerPosition();
- WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
- break;
- case "reset":
- RestartStats();
- break;
- case "meta":
- RareMetaEnabled = !RareMetaEnabled;
- WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");
- ViewManager.SetRareMetaToggleState(RareMetaEnabled); // <-- sync the UI
- break;
- case "nextwp":
- double result = VtankControl.VtAdvanceWaypoint();
- if (result == 1)
- {
- WriteToChat("Advanced VTank to next waypoint.");
- }
- else
- {
- WriteToChat("Failed to advance VTank waypoint. Is VTank running?");
- }
- break;
+ _commandRouter.Register("report", args =>
+ {
+ TimeSpan elapsed = DateTime.Now - _killTracker.StatsStartTime;
+ string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_rareTracker?.RareCount ?? 0}, Session Deaths: {_killTracker.SessionDeaths}, Total Deaths: {_killTracker.TotalDeaths}";
+ WriteToChat(reportMessage);
+ }, "Show current stats");
- case "setchest":
- if (args.Length < 2)
- {
- WriteToChat("[ChestLooter] Usage: /mm setchest ");
- return;
- }
- string chestName = string.Join(" ", args.Skip(1));
- if (chestLooter != null)
- {
- chestLooter.SetChestName(chestName);
- if (PluginSettings.Instance?.ChestLooterSettings != null)
- {
- PluginSettings.Instance.ChestLooterSettings.ChestName = chestName;
- PluginSettings.Save();
- }
- Views.VVSTabbedMainView.RefreshChestLooterUI();
- }
- break;
+ _commandRouter.Register("getmetastate", args =>
+ {
+ string metaState = VtankControl.VtGetMetaState();
+ WriteToChat(metaState);
+ }, "Gets the current metastate");
- case "setkey":
- if (args.Length < 2)
- {
- WriteToChat("[ChestLooter] Usage: /mm setkey ");
- return;
- }
- string keyName = string.Join(" ", args.Skip(1));
- if (chestLooter != null)
- {
- chestLooter.SetKeyName(keyName);
- if (PluginSettings.Instance?.ChestLooterSettings != null)
- {
- PluginSettings.Instance.ChestLooterSettings.KeyName = keyName;
- PluginSettings.Save();
- }
- Views.VVSTabbedMainView.RefreshChestLooterUI();
- }
- break;
+ _commandRouter.Register("loc", args =>
+ {
+ Coordinates here = Coordinates.Me;
+ var pos = Utils.GetPlayerPosition();
+ WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
+ }, "Show current location");
- case "lootchest":
- if (chestLooter != null)
- {
- if (!chestLooter.StartByName())
- {
- WriteToChat("[ChestLooter] Failed to start. Check chest/key names are set.");
- }
- }
- else
- {
- WriteToChat("[ChestLooter] Chest looter not initialized");
- }
- break;
+ _commandRouter.Register("reset", args =>
+ {
+ RestartStats();
+ }, "Reset all counters");
- case "stoploot":
- if (chestLooter != null)
- {
- chestLooter.Stop();
- }
- else
- {
- WriteToChat("[ChestLooter] Chest looter not initialized");
- }
- break;
+ _commandRouter.Register("meta", args =>
+ {
+ RareMetaEnabled = !RareMetaEnabled;
+ WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");
+ ViewManager.SetRareMetaToggleState(RareMetaEnabled);
+ }, "Toggle rare meta state");
- case "vtanktest":
+ _commandRouter.Register("nextwp", args =>
+ {
+ double result = VtankControl.VtAdvanceWaypoint();
+ if (result == 1)
+ WriteToChat("Advanced VTank to next waypoint.");
+ else
+ WriteToChat("Failed to advance VTank waypoint. Is VTank running?");
+ }, "Advance VTank to next waypoint");
+
+ _commandRouter.Register("setchest", args =>
+ {
+ if (args.Length < 2)
+ {
+ WriteToChat("[ChestLooter] Usage: /mm setchest ");
+ return;
+ }
+ string chestName = string.Join(" ", args.Skip(1));
+ if (chestLooter != null)
+ {
+ chestLooter.SetChestName(chestName);
+ if (PluginSettings.Instance?.ChestLooterSettings != null)
+ {
+ PluginSettings.Instance.ChestLooterSettings.ChestName = chestName;
+ PluginSettings.Save();
+ }
+ Views.VVSTabbedMainView.RefreshChestLooterUI();
+ }
+ }, "Set chest name for looter");
+
+ _commandRouter.Register("setkey", args =>
+ {
+ if (args.Length < 2)
+ {
+ WriteToChat("[ChestLooter] Usage: /mm setkey ");
+ return;
+ }
+ string keyName = string.Join(" ", args.Skip(1));
+ if (chestLooter != null)
+ {
+ chestLooter.SetKeyName(keyName);
+ if (PluginSettings.Instance?.ChestLooterSettings != null)
+ {
+ PluginSettings.Instance.ChestLooterSettings.KeyName = keyName;
+ PluginSettings.Save();
+ }
+ Views.VVSTabbedMainView.RefreshChestLooterUI();
+ }
+ }, "Set key name for looter");
+
+ _commandRouter.Register("lootchest", args =>
+ {
+ if (chestLooter != null)
+ {
+ if (!chestLooter.StartByName())
+ WriteToChat("[ChestLooter] Failed to start. Check chest/key names are set.");
+ }
+ else
+ {
+ WriteToChat("[ChestLooter] Chest looter not initialized");
+ }
+ }, "Start chest looting");
+
+ _commandRouter.Register("stoploot", args =>
+ {
+ if (chestLooter != null)
+ chestLooter.Stop();
+ else
+ WriteToChat("[ChestLooter] Chest looter not initialized");
+ }, "Stop chest looting");
+
+ _commandRouter.Register("vtanktest", args =>
+ {
+ try
+ {
+ WriteToChat("Testing VTank interface...");
+ WriteToChat($"VTank Instance: {(vTank.Instance != null ? "Found" : "NULL")}");
+ WriteToChat($"VTank Type: {vTank.Instance?.GetType()?.Name ?? "NULL"}");
+ WriteToChat($"NavCurrent: {vTank.Instance?.NavCurrent ?? -1}");
+ WriteToChat($"NavNumPoints: {vTank.Instance?.NavNumPoints ?? -1}");
+ WriteToChat($"NavType: {vTank.Instance?.NavType}");
+ WriteToChat($"MacroEnabled: {vTank.Instance?.MacroEnabled}");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"VTank test error: {ex.Message}");
+ }
+ }, "Test VTank interface");
+
+ _commandRouter.Register("decalstatus", args =>
+ {
+ try
+ {
+ WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ===");
+ WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}");
+ WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}");
+ WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}");
+
+ WriteToChat("=== Harmony Version Status ===");
try
{
- WriteToChat("Testing VTank interface...");
- WriteToChat($"VTank Instance: {(vTank.Instance != null ? "Found" : "NULL")}");
- WriteToChat($"VTank Type: {vTank.Instance?.GetType()?.Name ?? "NULL"}");
- WriteToChat($"NavCurrent: {vTank.Instance?.NavCurrent ?? -1}");
- WriteToChat($"NavNumPoints: {vTank.Instance?.NavNumPoints ?? -1}");
- WriteToChat($"NavType: {vTank.Instance?.NavType}");
- WriteToChat($"MacroEnabled: {vTank.Instance?.MacroEnabled}");
+ var harmonyTest = Harmony.HarmonyInstance.Create("test.version.check");
+ WriteToChat($"[OK] Harmony Available (ID: {harmonyTest.Id})");
+ var harmonyAssembly = typeof(Harmony.HarmonyInstance).Assembly;
+ WriteToChat($"[OK] Harmony Version: {harmonyAssembly.GetName().Version}");
+ WriteToChat($"[OK] Harmony Location: {harmonyAssembly.Location}");
}
- catch (Exception ex)
+ catch (Exception harmonyEx)
{
- WriteToChat($"VTank test error: {ex.Message}");
+ WriteToChat($"[FAIL] Harmony Test Failed: {harmonyEx.Message}");
}
- break;
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"Status check error: {ex.Message}");
+ }
+ }, "Check Harmony patch status");
- case "decalstatus":
- try
+ _commandRouter.Register("decaldebug", args =>
+ {
+ if (args.Length > 1)
+ {
+ if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
- WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ===");
- WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}");
- WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}");
- WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}");
-
- // Test Harmony availability
- WriteToChat("=== Harmony Version Status ===");
- try
- {
- var harmonyTest = Harmony.HarmonyInstance.Create("test.version.check");
- WriteToChat($"[OK] Harmony Available (ID: {harmonyTest.Id})");
-
- // Check Harmony assembly version
- var harmonyAssembly = typeof(Harmony.HarmonyInstance).Assembly;
- WriteToChat($"[OK] Harmony Version: {harmonyAssembly.GetName().Version}");
- WriteToChat($"[OK] Harmony Location: {harmonyAssembly.Location}");
- }
- catch (Exception harmonyEx)
- {
- WriteToChat($"[FAIL] Harmony Test Failed: {harmonyEx.Message}");
- }
+ AggressiveChatStreamingEnabled = true;
+ WriteToChat("[OK] DECAL debug streaming ENABLED - will show captured messages + stream via WebSocket");
}
- catch (Exception ex)
+ else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
- WriteToChat($"Status check error: {ex.Message}");
- }
- break;
-
- case "decaldebug":
- if (args.Length > 1)
- {
- if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
- {
- AggressiveChatStreamingEnabled = true;
- WriteToChat("[OK] DECAL debug streaming ENABLED - will show captured messages + stream via WebSocket");
- }
- else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
- {
- AggressiveChatStreamingEnabled = false;
- WriteToChat("[FAIL] DECAL debug streaming DISABLED - WebSocket streaming also disabled");
- }
- else
- {
- WriteToChat("Usage: /mm decaldebug ");
- }
+ AggressiveChatStreamingEnabled = false;
+ WriteToChat("[FAIL] DECAL debug streaming DISABLED - WebSocket streaming also disabled");
}
else
{
WriteToChat("Usage: /mm decaldebug ");
}
- break;
+ }
+ else
+ {
+ WriteToChat("Usage: /mm decaldebug ");
+ }
+ }, "Enable/disable plugin message debug output");
+ _commandRouter.Register("harmonyraw", args => { }, "");
- case "harmonyraw":
- // Debug functionality removed
- break;
+ _commandRouter.Register("gui", args =>
+ {
+ try
+ {
+ WriteToChat("Attempting to manually initialize GUI...");
+ ViewManager.ViewDestroy();
+ ViewManager.ViewInit();
+ WriteToChat("GUI initialization attempt completed.");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"GUI initialization error: {ex.Message}");
+ }
+ }, "Manually initialize/reinitialize GUI");
- case "initgui":
- case "gui":
- try
+ _commandRouter.Register("initgui", args =>
+ {
+ try
+ {
+ WriteToChat("Attempting to manually initialize GUI...");
+ ViewManager.ViewDestroy();
+ ViewManager.ViewInit();
+ WriteToChat("GUI initialization attempt completed.");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"GUI initialization error: {ex.Message}");
+ }
+ }, "");
+
+ _commandRouter.Register("testprismatic", args =>
+ {
+ try
+ {
+ WriteToChat("=== FULL INVENTORY DUMP ===");
+ var worldFilter = CoreManager.Current.WorldFilter;
+ var playerInv = CoreManager.Current.CharacterFilter.Id;
+
+ WriteToChat("Listing ALL items in your main inventory:");
+ int itemNum = 1;
+
+ foreach (WorldObject item in worldFilter.GetByContainer(playerInv))
{
- WriteToChat("Attempting to manually initialize GUI...");
- ViewManager.ViewDestroy(); // Clean up any existing view
- ViewManager.ViewInit(); // Reinitialize
- WriteToChat("GUI initialization attempt completed.");
- }
- catch (Exception ex)
- {
- WriteToChat($"GUI initialization error: {ex.Message}");
- }
- break;
-
- case "testprismatic":
- try
- {
- WriteToChat("=== FULL INVENTORY DUMP ===");
- var worldFilter = CoreManager.Current.WorldFilter;
- var playerInv = CoreManager.Current.CharacterFilter.Id;
-
- WriteToChat("Listing ALL items in your main inventory:");
- int itemNum = 1;
-
- foreach (WorldObject item in worldFilter.GetByContainer(playerInv))
+ if (!string.IsNullOrEmpty(item.Name))
{
- if (!string.IsNullOrEmpty(item.Name))
+ int stackCount = item.Values(LongValueKey.StackCount, 0);
+ WriteToChat($"{itemNum:D2}: '{item.Name}' (count: {stackCount}, icon: 0x{item.Icon:X}, class: {item.ObjectClass})");
+ itemNum++;
+
+ string nameLower = item.Name.ToLower();
+ if (nameLower.Contains("taper") || nameLower.Contains("prismatic") ||
+ nameLower.Contains("prism") || nameLower.Contains("component"))
{
- int stackCount = item.Values(LongValueKey.StackCount, 0);
- WriteToChat($"{itemNum:D2}: '{item.Name}' (count: {stackCount}, icon: 0x{item.Icon:X}, class: {item.ObjectClass})");
- itemNum++;
-
- // Highlight anything that might be a taper
- string nameLower = item.Name.ToLower();
- if (nameLower.Contains("taper") || nameLower.Contains("prismatic") ||
- nameLower.Contains("prism") || nameLower.Contains("component"))
- {
- WriteToChat($" *** POSSIBLE MATCH: '{item.Name}' ***");
- }
+ WriteToChat($" *** POSSIBLE MATCH: '{item.Name}' ***");
}
}
-
- WriteToChat($"=== Total items listed: {itemNum - 1} ===");
-
- // Now test our utility functions on the found Prismatic Taper
- WriteToChat("=== Testing Utility Functions on Prismatic Taper ===");
- var foundItem = Utils.FindItemByName("Prismatic Taper");
- if (foundItem != null)
- {
- WriteToChat($"SUCCESS! Found: '{foundItem.Name}'");
- WriteToChat($"Utils.GetItemStackSize: {Utils.GetItemStackSize("Prismatic Taper")}");
- WriteToChat($"Utils.GetItemIcon: 0x{Utils.GetItemIcon("Prismatic Taper"):X}");
- WriteToChat($"Utils.GetItemDisplayIcon: 0x{Utils.GetItemDisplayIcon("Prismatic Taper"):X}");
- WriteToChat("=== TELEMETRY WILL NOW WORK! ===");
- }
- else
- {
- WriteToChat("ERROR: Still can't find Prismatic Taper with utility functions!");
- }
}
- catch (Exception ex)
- {
- WriteToChat($"Search error: {ex.Message}");
- }
- break;
- case "deathstats":
- try
- {
- WriteToChat("=== Death Tracking Statistics ===");
- WriteToChat($"Session Deaths: {sessionDeaths}");
- WriteToChat($"Total Deaths: {totalDeaths}");
-
- // Get current character death count to verify sync
- int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
- WriteToChat($"Character Property NumDeaths: {currentCharDeaths}");
-
- if (currentCharDeaths != totalDeaths)
- {
- WriteToChat($"[WARNING] Death count sync issue detected!");
- WriteToChat($"Updating totalDeaths from {totalDeaths} to {currentCharDeaths}");
- totalDeaths = currentCharDeaths;
- }
-
- WriteToChat("Death tracking is active and will increment on character death.");
- }
- catch (Exception ex)
- {
- WriteToChat($"Death stats error: {ex.Message}");
- }
- break;
+ WriteToChat($"=== Total items listed: {itemNum - 1} ===");
- case "testdeath":
- try
+ WriteToChat("=== Testing Utility Functions on Prismatic Taper ===");
+ var foundItem = Utils.FindItemByName("Prismatic Taper");
+ if (foundItem != null)
{
- WriteToChat("=== Manual Death Test ===");
- WriteToChat($"Current sessionDeaths variable: {sessionDeaths}");
- WriteToChat($"Current totalDeaths variable: {totalDeaths}");
-
- // Read directly from character property
- int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
- WriteToChat($"Character Property NumDeaths (43): {currentCharDeaths}");
-
- // Manually increment session deaths for testing
- sessionDeaths++;
- WriteToChat($"Manually incremented sessionDeaths to: {sessionDeaths}");
- WriteToChat("Note: This doesn't simulate a real death, just tests the tracking variables.");
-
- // Check if death event is properly subscribed
- WriteToChat($"Death event subscription check:");
- var deathEvent = typeof(Decal.Adapter.Wrappers.CharacterFilter).GetEvent("Death");
- WriteToChat($"Death event exists: {deathEvent != null}");
- }
- catch (Exception ex)
- {
- WriteToChat($"Test death error: {ex.Message}");
- }
- 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":
- // Debug functionality removed
- break;
-
- case "finditem":
- if (args.Length > 1)
- {
- string itemName = string.Join(" ", args, 1, args.Length - 1).Trim('"');
- WriteToChat($"=== Searching for: '{itemName}' ===");
-
- var foundItem = Utils.FindItemByName(itemName);
- if (foundItem != null)
- {
- WriteToChat($"FOUND: '{foundItem.Name}'");
- WriteToChat($"Count: {foundItem.Values(LongValueKey.StackCount, 0)}");
- WriteToChat($"Icon: 0x{foundItem.Icon:X}");
- WriteToChat($"Display Icon: 0x{(foundItem.Icon + 0x6000000):X}");
- WriteToChat($"Object Class: {foundItem.ObjectClass}");
- }
- else
- {
- WriteToChat($"NOT FOUND: '{itemName}'");
- WriteToChat("Make sure the name is exactly as it appears in-game.");
- }
+ WriteToChat($"SUCCESS! Found: '{foundItem.Name}'");
+ WriteToChat($"Utils.GetItemStackSize: {Utils.GetItemStackSize("Prismatic Taper")}");
+ WriteToChat($"Utils.GetItemIcon: 0x{Utils.GetItemIcon("Prismatic Taper"):X}");
+ WriteToChat($"Utils.GetItemDisplayIcon: 0x{Utils.GetItemDisplayIcon("Prismatic Taper"):X}");
+ WriteToChat("=== TELEMETRY WILL NOW WORK! ===");
}
else
{
- WriteToChat("Usage: /mm finditem \"Item Name\"");
- WriteToChat("Example: /mm finditem \"Prismatic Taper\"");
+ WriteToChat("ERROR: Still can't find Prismatic Taper with utility functions!");
}
- break;
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"Search error: {ex.Message}");
+ }
+ }, "Test Prismatic Taper detection and icon lookup");
- case "checkforupdate":
- // Run the update check asynchronously
- Task.Run(async () =>
+ _commandRouter.Register("deathstats", args =>
+ {
+ try
+ {
+ WriteToChat("=== Death Tracking Statistics ===");
+ WriteToChat($"Session Deaths: {_killTracker.SessionDeaths}");
+ WriteToChat($"Total Deaths: {_killTracker.TotalDeaths}");
+
+ int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
+ WriteToChat($"Character Property NumDeaths: {currentCharDeaths}");
+
+ if (currentCharDeaths != _killTracker.TotalDeaths)
{
- await UpdateManager.CheckForUpdateAsync();
- // Update UI if available
- try
- {
- ViewManager.RefreshUpdateStatus();
- }
- catch (Exception ex)
- {
- WriteToChat($"Error refreshing UI: {ex.Message}");
- }
- });
- break;
+ WriteToChat($"[WARNING] Death count sync issue detected!");
+ WriteToChat($"Updating totalDeaths from {_killTracker.TotalDeaths} to {currentCharDeaths}");
+ _killTracker.SetTotalDeaths(currentCharDeaths);
+ }
- case "update":
- // Run the update installation asynchronously
- Task.Run(async () =>
+ WriteToChat("Death tracking is active and will increment on character death.");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"Death stats error: {ex.Message}");
+ }
+ }, "Show current death tracking statistics");
+
+ _commandRouter.Register("testdeath", args =>
+ {
+ try
+ {
+ WriteToChat("=== Manual Death Test ===");
+ WriteToChat($"Current sessionDeaths variable: {_killTracker.SessionDeaths}");
+ WriteToChat($"Current totalDeaths variable: {_killTracker.TotalDeaths}");
+
+ int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
+ WriteToChat($"Character Property NumDeaths (43): {currentCharDeaths}");
+
+ _killTracker.OnDeath();
+ WriteToChat($"Manually incremented sessionDeaths to: {_killTracker.SessionDeaths}");
+ WriteToChat("Note: This doesn't simulate a real death, just tests the tracking variables.");
+
+ WriteToChat($"Death event subscription check:");
+ var deathEvent = typeof(Decal.Adapter.Wrappers.CharacterFilter).GetEvent("Death");
+ WriteToChat($"Death event exists: {deathEvent != null}");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"Test death error: {ex.Message}");
+ }
+ }, "Test death tracking variables");
+
+ _commandRouter.Register("testtaper", args =>
+ {
+ try
+ {
+ WriteToChat("=== Cached Taper Tracking Test ===");
+ WriteToChat($"Cached Count: {_inventoryMonitor.CachedPrismaticCount}");
+ WriteToChat($"Last Count: {_inventoryMonitor.LastPrismaticCount}");
+
+ int utilsCount = Utils.GetItemStackSize("Prismatic Taper");
+ WriteToChat($"Utils Count: {utilsCount}");
+
+ if (_inventoryMonitor.CachedPrismaticCount == utilsCount)
{
- await UpdateManager.DownloadAndInstallUpdateAsync();
- });
- break;
-
- case "debugupdate":
- Views.VVSTabbedMainView.DebugUpdateControls();
- break;
-
- case "sendinventory":
- // Force inventory upload with ID requests
- if (_inventoryLogger != null)
- {
- _inventoryLogger.ForceInventoryUpload();
+ WriteToChat("[OK] Cached count matches Utils count");
}
else
{
- WriteToChat("[INV] Inventory system not initialized");
+ WriteToChat($"[WARNING] Count mismatch! Cached: {_inventoryMonitor.CachedPrismaticCount}, Utils: {utilsCount}");
+ WriteToChat("Refreshing cached count...");
+ _inventoryMonitor.Initialize();
}
- break;
- case "refreshquests":
- // Force quest data refresh (same as clicking refresh button)
- try
- {
- WriteToChat("[QUEST] Refreshing quest data...");
- Views.FlagTrackerView.RefreshQuestData();
- }
- catch (Exception ex)
- {
- WriteToChat($"[QUEST] Refresh failed: {ex.Message}");
- }
- break;
+ WriteToChat("=== Container Analysis ===");
+ int mainPackCount = 0;
+ int sidePackCount = 0;
+ int playerId = CoreManager.Current.CharacterFilter.Id;
- case "queststatus":
- // Show quest streaming status
- try
+ foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
- WriteToChat("=== Quest Streaming Status ===");
- WriteToChat($"Timer Active: {questStreamingTimer != null && questStreamingTimer.Enabled}");
- WriteToChat($"WebSocket Enabled: {WebSocketEnabled}");
- WriteToChat($"Quest Manager: {(questManager != null ? "Active" : "Not Active")}");
- WriteToChat($"Quest Count: {questManager?.QuestList?.Count ?? 0}");
-
- if (questManager?.QuestList != null)
+ if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
{
- var priorityQuests = questManager.QuestList
- .Where(q => IsHighPriorityQuest(q.Id))
- .GroupBy(q => q.Id)
- .Select(g => g.First())
- .ToList();
- WriteToChat($"Priority Quests Found: {priorityQuests.Count}");
- foreach (var quest in priorityQuests)
- {
- string questName = questManager.GetFriendlyQuestName(quest.Id);
- WriteToChat($" - {questName} ({quest.Id})");
- }
+ int stackCount = wo.Values(LongValueKey.StackCount, 1);
+ if (wo.Container == playerId)
+ mainPackCount += stackCount;
+ else
+ sidePackCount += stackCount;
}
-
- WriteToChat($"Verbose Logging: {PluginSettings.Instance?.VerboseLogging ?? false}");
- WriteToChat("Use '/mm verbose' to toggle debug logging");
}
- catch (Exception ex)
- {
- WriteToChat($"[QUEST] Status check failed: {ex.Message}");
- }
- break;
- case "verbose":
- // Toggle verbose logging
- if (PluginSettings.Instance != null)
+ WriteToChat($"Main Pack Tapers: {mainPackCount}");
+ WriteToChat($"Side Pack Tapers: {sidePackCount}");
+ WriteToChat($"Total: {mainPackCount + sidePackCount}");
+
+ WriteToChat("=== Event System Status ===");
+ WriteToChat($"Tracking {_inventoryMonitor.TrackedTaperCount} taper stacks for delta detection");
+ WriteToChat($"Known stack sizes: {_inventoryMonitor.KnownStackSizesCount} 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}");
+ }
+ }, "Test cached Prismatic Taper tracking");
+
+ _commandRouter.Register("debugtaper", args => { }, "");
+
+ _commandRouter.Register("finditem", args =>
+ {
+ if (args.Length > 1)
+ {
+ string itemName = string.Join(" ", args, 1, args.Length - 1).Trim('"');
+ WriteToChat($"=== Searching for: '{itemName}' ===");
+
+ var foundItem = Utils.FindItemByName(itemName);
+ if (foundItem != null)
{
- PluginSettings.Instance.VerboseLogging = !PluginSettings.Instance.VerboseLogging;
- WriteToChat($"Verbose logging: {(PluginSettings.Instance.VerboseLogging ? "ENABLED" : "DISABLED")}");
+ WriteToChat($"FOUND: '{foundItem.Name}'");
+ WriteToChat($"Count: {foundItem.Values(LongValueKey.StackCount, 0)}");
+ WriteToChat($"Icon: 0x{foundItem.Icon:X}");
+ WriteToChat($"Display Icon: 0x{(foundItem.Icon + 0x6000000):X}");
+ WriteToChat($"Object Class: {foundItem.ObjectClass}");
}
else
{
- WriteToChat("Settings not initialized");
+ WriteToChat($"NOT FOUND: '{itemName}'");
+ WriteToChat("Make sure the name is exactly as it appears in-game.");
}
- break;
+ }
+ else
+ {
+ WriteToChat("Usage: /mm finditem \"Item Name\"");
+ WriteToChat("Example: /mm finditem \"Prismatic Taper\"");
+ }
+ }, "Find item in inventory by name");
- default:
- WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help");
- break;
- }
+ _commandRouter.Register("checkforupdate", args =>
+ {
+ Task.Run(async () =>
+ {
+ await UpdateManager.CheckForUpdateAsync();
+ try
+ {
+ ViewManager.RefreshUpdateStatus();
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"Error refreshing UI: {ex.Message}");
+ }
+ });
+ }, "Check for plugin updates");
+
+ _commandRouter.Register("update", args =>
+ {
+ Task.Run(async () =>
+ {
+ await UpdateManager.DownloadAndInstallUpdateAsync();
+ });
+ }, "Download and install update");
+
+ _commandRouter.Register("debugupdate", args =>
+ {
+ Views.VVSTabbedMainView.DebugUpdateControls();
+ }, "Debug update UI controls");
+
+ _commandRouter.Register("sendinventory", args =>
+ {
+ if (_inventoryLogger != null)
+ _inventoryLogger.ForceInventoryUpload();
+ else
+ WriteToChat("[INV] Inventory system not initialized");
+ }, "Force inventory upload with ID requests");
+
+ _commandRouter.Register("refreshquests", args =>
+ {
+ try
+ {
+ WriteToChat("[QUEST] Refreshing quest data...");
+ Views.FlagTrackerView.RefreshQuestData();
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[QUEST] Refresh failed: {ex.Message}");
+ }
+ }, "Force quest data refresh for Flag Tracker");
+
+ _commandRouter.Register("queststatus", args =>
+ {
+ try
+ {
+ WriteToChat("=== Quest Streaming Status ===");
+ WriteToChat($"Timer Active: {_questStreamingService?.IsRunning ?? false}");
+ WriteToChat($"WebSocket Enabled: {WebSocketEnabled}");
+ WriteToChat($"Quest Manager: {(questManager != null ? "Active" : "Not Active")}");
+ WriteToChat($"Quest Count: {questManager?.QuestList?.Count ?? 0}");
+
+ if (questManager?.QuestList != null)
+ {
+ var priorityQuests = questManager.QuestList
+ .Where(q => QuestStreamingService.IsHighPriorityQuest(q.Id))
+ .GroupBy(q => q.Id)
+ .Select(g => g.First())
+ .ToList();
+ WriteToChat($"Priority Quests Found: {priorityQuests.Count}");
+ foreach (var quest in priorityQuests)
+ {
+ string questName = questManager.GetFriendlyQuestName(quest.Id);
+ WriteToChat($" - {questName} ({quest.Id})");
+ }
+ }
+
+ WriteToChat($"Verbose Logging: {PluginSettings.Instance?.VerboseLogging ?? false}");
+ WriteToChat("Use '/mm verbose' to toggle debug logging");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[QUEST] Status check failed: {ex.Message}");
+ }
+ }, "Show quest streaming status and diagnostics");
+
+ _commandRouter.Register("verbose", args =>
+ {
+ if (PluginSettings.Instance != null)
+ {
+ PluginSettings.Instance.VerboseLogging = !PluginSettings.Instance.VerboseLogging;
+ WriteToChat($"Verbose logging: {(PluginSettings.Instance.VerboseLogging ? "ENABLED" : "DISABLED")}");
+ }
+ else
+ {
+ WriteToChat("Settings not initialized");
+ }
+ }, "Toggle verbose debug logging");
}
diff --git a/MosswartMassacre/QuestStreamingService.cs b/MosswartMassacre/QuestStreamingService.cs
new file mode 100644
index 0000000..5409d35
--- /dev/null
+++ b/MosswartMassacre/QuestStreamingService.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Linq;
+using System.Timers;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Streams high-priority quest timer data via WebSocket on a 30-second interval.
+ ///
+ internal class QuestStreamingService
+ {
+ private readonly IPluginLogger _logger;
+ private Timer _timer;
+
+ internal QuestStreamingService(IPluginLogger logger)
+ {
+ _logger = logger;
+ }
+
+ internal void Start()
+ {
+ _timer = new Timer(Constants.QuestStreamingIntervalMs);
+ _timer.Elapsed += OnTimerElapsed;
+ _timer.AutoReset = true;
+ _timer.Start();
+ }
+
+ internal void Stop()
+ {
+ if (_timer != null)
+ {
+ _timer.Stop();
+ _timer.Elapsed -= OnTimerElapsed;
+ _timer.Dispose();
+ _timer = null;
+ }
+ }
+
+ internal bool IsRunning => _timer != null && _timer.Enabled;
+
+ private void OnTimerElapsed(object sender, ElapsedEventArgs e)
+ {
+ try
+ {
+ if (PluginSettings.Instance?.VerboseLogging == true)
+ {
+ _logger?.Log("[QUEST-STREAM] Timer fired, checking conditions...");
+ }
+
+ if (!PluginCore.WebSocketEnabled)
+ {
+ if (PluginSettings.Instance?.VerboseLogging == true)
+ {
+ _logger?.Log("[QUEST-STREAM] WebSocket not enabled, skipping");
+ }
+ return;
+ }
+
+ var questManager = PluginCore.questManager;
+ if (questManager?.QuestList == null || questManager.QuestList.Count == 0)
+ {
+ if (PluginSettings.Instance?.VerboseLogging == true)
+ {
+ _logger?.Log($"[QUEST-STREAM] No quest data available (null: {questManager?.QuestList == null}, count: {questManager?.QuestList?.Count ?? 0})");
+ }
+ return;
+ }
+
+ var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+
+ var priorityQuests = questManager.QuestList
+ .Where(q => IsHighPriorityQuest(q.Id))
+ .GroupBy(q => q.Id)
+ .Select(g => g.First())
+ .ToList();
+
+ if (PluginSettings.Instance?.VerboseLogging == true)
+ {
+ _logger?.Log($"[QUEST-STREAM] Found {priorityQuests.Count} priority quests to stream");
+ }
+
+ foreach (var quest in priorityQuests)
+ {
+ try
+ {
+ string questName = questManager.GetFriendlyQuestName(quest.Id);
+ long timeRemaining = quest.ExpireTime - currentTime;
+ string countdown = FormatCountdown(timeRemaining);
+
+ if (PluginSettings.Instance?.VerboseLogging == true)
+ {
+ _logger?.Log($"[QUEST-STREAM] Sending: {questName} - {countdown}");
+ }
+
+ System.Threading.Tasks.Task.Run(() => WebSocket.SendQuestDataAsync(questName, countdown));
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[QUEST-STREAM] Error streaming quest {quest.Id}: {ex.Message}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.Log($"[QUEST-STREAM] Error in timer handler: {ex.Message}");
+ }
+ }
+
+ internal static bool IsHighPriorityQuest(string questId)
+ {
+ return questId == "stipendtimer_0812" ||
+ questId == "augmentationblankgemacquired" ||
+ questId == "insatiableeaterjaw";
+ }
+
+ internal static string FormatCountdown(long seconds)
+ {
+ if (seconds <= 0)
+ return "READY";
+
+ var timeSpan = TimeSpan.FromSeconds(seconds);
+
+ if (timeSpan.TotalDays >= 1)
+ return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours:D2}h";
+ else if (timeSpan.TotalHours >= 1)
+ return $"{timeSpan.Hours}h {timeSpan.Minutes:D2}m";
+ else if (timeSpan.TotalMinutes >= 1)
+ return $"{timeSpan.Minutes}m {timeSpan.Seconds:D2}s";
+ else
+ return $"{timeSpan.Seconds}s";
+ }
+ }
+}
diff --git a/MosswartMassacre/RareTracker.cs b/MosswartMassacre/RareTracker.cs
new file mode 100644
index 0000000..5bd7c9d
--- /dev/null
+++ b/MosswartMassacre/RareTracker.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using Decal.Adapter;
+
+namespace MosswartMassacre
+{
+ ///
+ /// Tracks rare item discoveries, handles rare meta state toggles,
+ /// and sends rare notifications via WebSocket.
+ ///
+ internal class RareTracker
+ {
+ private readonly IPluginLogger _logger;
+ private readonly string _characterName;
+
+ internal int RareCount { get; set; }
+ internal bool RareMetaEnabled { get; set; } = true;
+
+ internal RareTracker(IPluginLogger logger)
+ {
+ _logger = logger;
+ _characterName = CoreManager.Current.CharacterFilter.Name;
+ }
+
+ ///
+ /// Check if the chat text is a rare discovery by this character.
+ /// If so, increments count, triggers meta switch, allegiance announce, and WebSocket notification.
+ /// Returns true if a rare was found.
+ ///
+ internal bool CheckForRare(string text, out string rareText)
+ {
+ if (IsRareDiscoveryMessage(text, out rareText))
+ {
+ RareCount++;
+
+ if (RareMetaEnabled)
+ {
+ PluginCore.Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
+ }
+
+ DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
+ _ = WebSocket.SendRareAsync(rareText);
+ return true;
+ }
+ return false;
+ }
+
+ internal void ToggleRareMeta()
+ {
+ PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
+ RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ }
+
+ private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
+ {
+ rareTextOnly = null;
+
+ string pattern = @"^(?['A-Za-z ]+)\shas discovered the (?
- .*?)!$";
+ Match match = Regex.Match(text, pattern);
+
+ if (match.Success && match.Groups["name"].Value == _characterName)
+ {
+ rareTextOnly = match.Groups["item"].Value;
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MosswartMassacre/VtankControl.cs b/MosswartMassacre/VtankControl.cs
index 7b5e4d6..4bee425 100644
--- a/MosswartMassacre/VtankControl.cs
+++ b/MosswartMassacre/VtankControl.cs
@@ -72,9 +72,9 @@ namespace MosswartMassacre
return 0;
}
}
- catch
+ catch (Exception ex)
{
- // Swallow any errors and signal failure
+ PluginCore.WriteToChat($"[VTank] SetSetting error ({setting}): {ex.Message}");
return 0;
}
diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs
index 288b74d..e89cbe3 100644
--- a/MosswartMassacre/WebSocket.cs
+++ b/MosswartMassacre/WebSocket.cs
@@ -35,6 +35,8 @@ namespace MosswartMassacre
private const string SharedSecret = "your_shared_secret";
private const int IntervalSec = 5;
private static string SessionId = "";
+ private static IPluginLogger _logger;
+ private static IGameStats _gameStats;
// ─── cached prismatic taper count ─── (now handled by PluginCore event system)
@@ -51,13 +53,16 @@ namespace MosswartMassacre
// ─── public API ─────────────────────────────
+ public static void SetLogger(IPluginLogger logger) => _logger = logger;
+ public static void SetGameStats(IGameStats gameStats) => _gameStats = gameStats;
+
public static void Start()
{
if (_enabled) return;
_enabled = true;
_cts = new CancellationTokenSource();
- PluginCore.WriteToChat("[WebSocket] connecting…");
+ _logger?.Log("[WebSocket] connecting…");
_ = Task.Run(ConnectAndLoopAsync);
}
@@ -72,7 +77,7 @@ namespace MosswartMassacre
_ws?.Dispose();
_ws = null;
- PluginCore.WriteToChat("[WebSocket] DISABLED");
+ _logger?.Log("[WebSocket] DISABLED");
}
// ─── connect / receive / telemetry loop ──────────────────────
@@ -87,7 +92,7 @@ namespace MosswartMassacre
_ws = new ClientWebSocket();
_ws.Options.SetRequestHeader("X-Plugin-Secret", SharedSecret);
await _ws.ConnectAsync(WsEndpoint, _cts.Token);
- PluginCore.WriteToChat("[WebSocket] CONNECTED");
+ _logger?.Log("[WebSocket] CONNECTED");
SessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
// ─── Register this socket under our character name ───
@@ -98,7 +103,7 @@ namespace MosswartMassacre
};
var regJson = JsonConvert.SerializeObject(registerEnvelope);
await SendEncodedAsync(regJson, _cts.Token);
- PluginCore.WriteToChat("[WebSocket] REGISTERED");
+ _logger?.Log("[WebSocket] REGISTERED");
var buffer = new byte[4096];
@@ -118,7 +123,7 @@ namespace MosswartMassacre
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[WebSocket] receive error: {ex.Message}");
+ _logger?.Log($"[WebSocket] receive error: {ex.Message}");
break;
}
@@ -151,7 +156,7 @@ namespace MosswartMassacre
});
// 5) Inline telemetry loop
- PluginCore.WriteToChat("[WebSocket] Starting telemetry loop");
+ _logger?.Log("[WebSocket] Starting telemetry loop");
while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested)
{
try
@@ -161,7 +166,7 @@ namespace MosswartMassacre
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[WebSocket] Telemetry failed: {ex.Message}");
+ _logger?.Log($"[WebSocket] Telemetry failed: {ex.Message}");
break; // Exit telemetry loop on failure
}
@@ -171,30 +176,30 @@ namespace MosswartMassacre
}
catch (OperationCanceledException)
{
- PluginCore.WriteToChat("[WebSocket] Telemetry loop cancelled");
+ _logger?.Log("[WebSocket] Telemetry loop cancelled");
break;
}
}
// Log why telemetry loop exited
- PluginCore.WriteToChat($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}");
+ _logger?.Log($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}");
// Wait for receive loop to finish
await receiveTask;
}
catch (OperationCanceledException)
{
- PluginCore.WriteToChat("[WebSocket] Connection cancelled");
+ _logger?.Log("[WebSocket] Connection cancelled");
break;
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[WebSocket] Connection error: {ex.Message}");
+ _logger?.Log($"[WebSocket] Connection error: {ex.Message}");
}
finally
{
var finalState = _ws?.State.ToString() ?? "null";
- PluginCore.WriteToChat($"[WebSocket] Cleaning up connection - Final state: {finalState}");
+ _logger?.Log($"[WebSocket] Cleaning up connection - Final state: {finalState}");
_ws?.Abort();
_ws?.Dispose();
_ws = null;
@@ -203,7 +208,7 @@ namespace MosswartMassacre
// Pause before reconnecting
if (_enabled)
{
- PluginCore.WriteToChat("[WebSocket] Reconnecting in 2 seconds...");
+ _logger?.Log("[WebSocket] Reconnecting in 2 seconds...");
try { await Task.Delay(2000, CancellationToken.None); } catch { }
}
}
@@ -334,7 +339,7 @@ namespace MosswartMassacre
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[WebSocket] Send error: {ex.Message}");
+ _logger?.Log($"[WebSocket] Send error: {ex.Message}");
_ws?.Abort();
_ws?.Dispose();
_ws = null;
@@ -347,33 +352,31 @@ namespace MosswartMassacre
// ─── payload builder ──────────────────────────────
- // Removed old cache system - now using PluginCore.cachedPrismaticCount
-
private static string BuildPayloadJson()
{
var tele = new ClientTelemetry();
var coords = Coordinates.Me;
+ var stats = _gameStats;
var payload = new
{
type = "telemetry",
character_name = CoreManager.Current.CharacterFilter.Name,
- char_tag = PluginCore.CharTag,
+ char_tag = stats?.CharTag ?? "",
session_id = SessionInfo.GuidString,
timestamp = DateTime.UtcNow.ToString("o"),
ew = coords.EW,
ns = coords.NS,
z = coords.Z,
- kills = PluginCore.totalKills,
- kills_per_hour = PluginCore.killsPerHour.ToString("F0"),
- onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"),
- deaths = PluginCore.sessionDeaths.ToString(),
- total_deaths = PluginCore.totalDeaths.ToString(),
- prismatic_taper_count = PluginCore.cachedPrismaticCount.ToString(),
+ kills = stats?.TotalKills ?? 0,
+ kills_per_hour = (stats?.KillsPerHour ?? 0).ToString("F0"),
+ onlinetime = (DateTime.Now - (stats?.StatsStartTime ?? DateTime.Now)).ToString(@"dd\.hh\:mm\:ss"),
+ deaths = (stats?.SessionDeaths ?? 0).ToString(),
+ total_deaths = (stats?.TotalDeaths ?? 0).ToString(),
+ prismatic_taper_count = (stats?.CachedPrismaticCount ?? 0).ToString(),
vt_state = VtankControl.VtGetMetaState(),
mem_mb = tele.MemoryBytes,
cpu_pct = tele.GetCpuUsage(),
mem_handles = tele.HandleCount
-
};
return JsonConvert.SerializeObject(payload);
}
diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll
index b2867f3..334302d 100644
Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ