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