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