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/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index 360d3e0..745cfa9 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -308,7 +308,9 @@
+
+
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index 4b876cf..a7d971e 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -18,7 +18,7 @@ using Mag.Shared.Constants;
namespace MosswartMassacre
{
[FriendlyName("Mosswart Massacre")]
- public class PluginCore : PluginBase, IPluginLogger
+ public class PluginCore : PluginBase, IPluginLogger, IGameStats
{
// Hot Reload Support Properties
private static string _assemblyDirectory = null;
@@ -47,10 +47,9 @@ namespace MosswartMassacre
public static bool IsHotReload { get; set; }
internal static PluginHost MyHost;
- // Bridge properties for WebSocket telemetry until IGameStats migration (Phase 5)
+ // Static bridge properties for VVSTabbedMainView (reads from manager instances)
private static InventoryMonitor _staticInventoryMonitor;
internal static int cachedPrismaticCount => _staticInventoryMonitor?.CachedPrismaticCount ?? 0;
- // Bridge properties for WebSocket telemetry until IGameStats migration (Phase 5)
private static KillTracker _staticKillTracker;
internal static int totalKills => _staticKillTracker?.TotalKills ?? 0;
internal static double killsPerHour => _staticKillTracker?.KillsPerHour ?? 0;
@@ -58,6 +57,16 @@ namespace MosswartMassacre
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;
@@ -122,7 +131,6 @@ namespace MosswartMassacre
// Quest Management for always-on quest streaming
public static QuestManager questManager;
- private static Timer questStreamingTimer;
private static readonly Queue _chatQueue = new Queue();
@@ -132,6 +140,7 @@ namespace MosswartMassacre
private InventoryMonitor _inventoryMonitor;
private ChatEventRouter _chatEventRouter;
private GameEventRouter _gameEventRouter;
+ private QuestStreamingService _questStreamingService;
private CommandRouter _commandRouter;
protected override void Startup()
@@ -236,6 +245,7 @@ namespace MosswartMassacre
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
@@ -305,14 +315,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)
@@ -427,11 +432,9 @@ namespace MosswartMassacre
// Trigger full quest data refresh (same as clicking refresh button)
Views.FlagTrackerView.RefreshQuestData();
- // Initialize quest streaming timer (30 seconds)
- questStreamingTimer = new Timer(Constants.QuestStreamingIntervalMs);
- 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");
}
@@ -470,102 +473,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()
{
@@ -654,29 +561,18 @@ 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(Constants.QuestStreamingIntervalMs);
- questStreamingTimer.Elapsed += OnQuestStreamingUpdate;
- questStreamingTimer.AutoReset = true;
- questStreamingTimer.Start();
+ _questStreamingService?.Stop();
+ _questStreamingService = new QuestStreamingService(this);
+ _questStreamingService.Start();
- WriteToChat("[OK] Quest streaming timer reinitialized (30s interval)");
+ 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!");
@@ -1485,7 +1381,7 @@ namespace MosswartMassacre
try
{
WriteToChat("=== Quest Streaming Status ===");
- WriteToChat($"Timer Active: {questStreamingTimer != null && questStreamingTimer.Enabled}");
+ 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}");
@@ -1493,7 +1389,7 @@ namespace MosswartMassacre
if (questManager?.QuestList != null)
{
var priorityQuests = questManager.QuestList
- .Where(q => IsHighPriorityQuest(q.Id))
+ .Where(q => QuestStreamingService.IsHighPriorityQuest(q.Id))
.GroupBy(q => q.Id)
.Select(g => g.First())
.ToList();
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/WebSocket.cs b/MosswartMassacre/WebSocket.cs
index e9c96e0..e89cbe3 100644
--- a/MosswartMassacre/WebSocket.cs
+++ b/MosswartMassacre/WebSocket.cs
@@ -36,6 +36,7 @@ namespace MosswartMassacre
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)
@@ -53,6 +54,7 @@ namespace MosswartMassacre
// ─── public API ─────────────────────────────
public static void SetLogger(IPluginLogger logger) => _logger = logger;
+ public static void SetGameStats(IGameStats gameStats) => _gameStats = gameStats;
public static void Start()
{
@@ -350,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..e789944 100644
Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ