diff --git a/MosswartMassacre/CharacterStats.cs b/MosswartMassacre/CharacterStats.cs index 30918de..4a7248c 100644 --- a/MosswartMassacre/CharacterStats.cs +++ b/MosswartMassacre/CharacterStats.cs @@ -26,8 +26,6 @@ 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; @@ -46,9 +44,8 @@ namespace MosswartMassacre /// /// Reset all cached data. Call on plugin init. /// - internal static void Init(IPluginLogger logger = null) + internal static void Init() { - _logger = logger; allegianceName = null; allegianceSize = 0; followers = 0; @@ -115,7 +112,7 @@ namespace MosswartMassacre } catch (Exception ex) { - _logger?.Log($"[CharStats] Allegiance processing error: {ex.Message}"); + PluginCore.WriteToChat($"[CharStats] Allegiance processing error: {ex.Message}"); } } @@ -137,15 +134,15 @@ namespace MosswartMassacre long key = tmpStruct.Value("key"); long value = tmpStruct.Value("value"); - if (key == Constants.AvailableLuminanceKey) + if (key == 6) // AvailableLuminance luminanceEarned = value; - else if (key == Constants.MaximumLuminanceKey) + else if (key == 7) // MaximumLuminance luminanceTotal = value; } } catch (Exception ex) { - _logger?.Log($"[CharStats] Property processing error: {ex.Message}"); + PluginCore.WriteToChat($"[CharStats] Property processing error: {ex.Message}"); } } @@ -165,14 +162,14 @@ namespace MosswartMassacre int key = BitConverter.ToInt32(raw, 5); long value = BitConverter.ToInt64(raw, 9); - if (key == Constants.AvailableLuminanceKey) + if (key == 6) // AvailableLuminance luminanceEarned = value; - else if (key == Constants.MaximumLuminanceKey) + else if (key == 7) // MaximumLuminance luminanceTotal = value; } catch (Exception ex) { - _logger?.Log($"[CharStats] Int64 property update error: {ex.Message}"); + PluginCore.WriteToChat($"[CharStats] Int64 property update error: {ex.Message}"); } } @@ -189,7 +186,7 @@ namespace MosswartMassacre } catch (Exception ex) { - _logger?.Log($"[CharStats] Title processing error: {ex.Message}"); + PluginCore.WriteToChat($"[CharStats] Title processing error: {ex.Message}"); } } @@ -204,7 +201,7 @@ namespace MosswartMassacre } catch (Exception ex) { - _logger?.Log($"[CharStats] Set title error: {ex.Message}"); + PluginCore.WriteToChat($"[CharStats] Set title error: {ex.Message}"); } } @@ -332,7 +329,7 @@ namespace MosswartMassacre } catch (Exception ex) { - _logger?.Log($"[CharStats] Error collecting stats: {ex.Message}"); + PluginCore.WriteToChat($"[CharStats] Error collecting stats: {ex.Message}"); } } } diff --git a/MosswartMassacre/ChatEventRouter.cs b/MosswartMassacre/ChatEventRouter.cs deleted file mode 100644 index 9729ffa..0000000 --- a/MosswartMassacre/ChatEventRouter.cs +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100644 index edba553..0000000 --- a/MosswartMassacre/CommandRouter.cs +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index 8433dc2..0000000 --- a/MosswartMassacre/Constants.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 0bcdf23..5a6e740 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 (Exception ex) + catch { - AddDebugLog($"ApplyDecalPatches failed: {ex.Message}"); + // Only log if completely unable to apply any patches } } @@ -92,15 +92,13 @@ namespace MosswartMassacre { ApplySinglePatch(method, prefixMethodName); } - catch (Exception ex) + catch { - AddDebugLog($"PatchHooksWrapper single patch failed ({prefixMethodName}): {ex.Message}"); } } } - catch (Exception ex) + catch { - AddDebugLog($"PatchHooksWrapper failed: {ex.Message}"); } } @@ -141,18 +139,16 @@ namespace MosswartMassacre { ApplySinglePatch(method, prefixMethodName); } - catch (Exception ex) + catch { - AddDebugLog($"PatchHostActions single patch failed ({prefixMethodName}): {ex.Message}"); } } - + // PATHWAY 3: Try to patch at PluginHost level PatchPluginHost(); } - catch (Exception ex) + catch { - AddDebugLog($"PatchHostActions failed: {ex.Message}"); } } @@ -163,31 +159,36 @@ namespace MosswartMassacre { try { - var coreActions = CoreManager.Current?.Actions; - if (coreActions != null && coreActions != PluginCore.MyHost?.Actions) + // Try to patch CoreManager.Current.Actions if it's different + try { - 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 coreActions = CoreManager.Current?.Actions; + if (coreActions != null && coreActions != PluginCore.MyHost?.Actions) { - var parameters = method.GetParameters(); - - try + 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) { - string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length; - ApplySinglePatch(method, prefixMethodName); - } - catch (Exception ex) - { - AddDebugLog($"PatchPluginHost single patch failed: {ex.Message}"); + var parameters = method.GetParameters(); + + try + { + string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length; + ApplySinglePatch(method, prefixMethodName); + } + catch + { + } } } } + catch + { + } } - catch (Exception ex) + catch { - AddDebugLog($"PatchPluginHost failed: {ex.Message}"); } } @@ -199,9 +200,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 @@ -209,9 +210,8 @@ namespace MosswartMassacre patchesApplied = true; } } - catch (Exception ex) + catch { - AddDebugLog($"ApplySinglePatch failed ({prefixMethodName}): {ex.Message}"); } } @@ -244,9 +244,8 @@ namespace MosswartMassacre } patchesApplied = false; } - catch (Exception ex) + catch { - AddDebugLog($"Cleanup failed: {ex.Message}"); } } @@ -391,9 +390,8 @@ namespace MosswartMassacre Task.Run(() => WebSocket.SendChatTextAsync(color, text)); } } - catch (Exception ex) + catch { - DecalHarmonyClean.AddDebugLog($"ProcessInterceptedMessage failed: {ex.Message}"); } } diff --git a/MosswartMassacre/GameEventRouter.cs b/MosswartMassacre/GameEventRouter.cs deleted file mode 100644 index 6bda3f9..0000000 --- a/MosswartMassacre/GameEventRouter.cs +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index ba0cb6f..0000000 --- a/MosswartMassacre/IGameStats.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index c9d4157..0000000 --- a/MosswartMassacre/IPluginLogger.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 1f60313..0000000 --- a/MosswartMassacre/InventoryMonitor.cs +++ /dev/null @@ -1,184 +0,0 @@ -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 deleted file mode 100644 index 0541f51..0000000 --- a/MosswartMassacre/KillTracker.cs +++ /dev/null @@ -1,176 +0,0 @@ -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 745cfa9..2e812f9 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -304,16 +304,6 @@ Shared\VCS_Connector.cs - - - - - - - - - - diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 31e42a8..522111f 100644 --- a/MosswartMassacre/PluginCore.cs +++ b/MosswartMassacre/PluginCore.cs @@ -1,9 +1,13 @@ 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; @@ -14,7 +18,7 @@ using Mag.Shared.Constants; namespace MosswartMassacre { [FriendlyName("Mosswart Massacre")] - public class PluginCore : PluginBase, IPluginLogger, IGameStats + public class PluginCore : PluginBase { // Hot Reload Support Properties private static string _assemblyDirectory = null; @@ -43,36 +47,26 @@ namespace MosswartMassacre public static bool IsHotReload { get; set; } internal static PluginHost MyHost; - // 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; - + 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; private static Timer vitalsTimer; private static System.Windows.Forms.Timer commandTimer; private static Timer characterStatsTimer; private static readonly Queue pendingCommands = new Queue(); - private static RareTracker _staticRareTracker; - public static bool RareMetaEnabled - { - get => _staticRareTracker?.RareMetaEnabled ?? true; - set { if (_staticRareTracker != null) _staticRareTracker.RareMetaEnabled = value; } - } + public static bool RareMetaEnabled { get; set; } = true; // VVS View Management private static class ViewManager @@ -127,18 +121,12 @@ 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 @@ -178,52 +166,36 @@ 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 events - CoreManager.Current.ChatBoxMessage += new EventHandler(_chatEventRouter.OnChatText); - CoreManager.Current.ChatBoxMessage += new EventHandler(ChatEventRouter.AllChatText); + // Subscribe to chat message event + CoreManager.Current.ChatBoxMessage += new EventHandler(OnChatText); + CoreManager.Current.ChatBoxMessage += new EventHandler(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; - CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate; - CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease; - CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange; - + // Subscribe to inventory change events for taper tracking + CoreManager.Current.WorldFilter.CreateObject += OnInventoryCreate; + CoreManager.Current.WorldFilter.ReleaseObject += OnInventoryRelease; + CoreManager.Current.WorldFilter.ChangeObject += OnInventoryChange; // Initialize VVS view after character login ViewManager.ViewInit(); + // Initialize the timer + updateTimer = new Timer(1000); // Update every second + updateTimer.Elapsed += UpdateStats; + updateTimer.Start(); + // Initialize vitals streaming timer - vitalsTimer = new Timer(Constants.VitalsUpdateIntervalMs); + vitalsTimer = new Timer(5000); // Send vitals every 5 seconds vitalsTimer.Elapsed += SendVitalsUpdate; vitalsTimer.Start(); // Initialize command processing timer (Windows Forms timer for main thread) commandTimer = new System.Windows.Forms.Timer(); - commandTimer.Interval = Constants.CommandProcessIntervalMs; + commandTimer.Interval = 10; // Process commands every 10ms commandTimer.Tick += ProcessPendingCommands; commandTimer.Start(); @@ -232,16 +204,13 @@ 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(this); - CoreManager.Current.EchoFilter.ServerDispatch += _gameEventRouter.OnServerDispatch; + CharacterStats.Init(); + CoreManager.Current.EchoFilter.ServerDispatch += EchoFilter_ServerDispatch; // 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 @@ -251,10 +220,6 @@ 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 @@ -277,25 +242,27 @@ namespace MosswartMassacre // Unsubscribe from chat message event - CoreManager.Current.ChatBoxMessage -= new EventHandler(_chatEventRouter.OnChatText); + CoreManager.Current.ChatBoxMessage -= new EventHandler(OnChatText); CoreManager.Current.CommandLineText -= OnChatCommand; - CoreManager.Current.ChatBoxMessage -= new EventHandler(ChatEventRouter.AllChatText); + CoreManager.Current.ChatBoxMessage -= new EventHandler(AllChatText); CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath; CoreManager.Current.WorldFilter.CreateObject -= OnSpawn; CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected; CoreManager.Current.WorldFilter.ReleaseObject -= OnDespawn; - // Unsubscribe inventory monitor - if (_inventoryMonitor != null) - { - CoreManager.Current.WorldFilter.CreateObject -= _inventoryMonitor.OnInventoryCreate; - CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease; - CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange; - } + // 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 -= _gameEventRouter.OnServerDispatch; + CoreManager.Current.EchoFilter.ServerDispatch -= EchoFilter_ServerDispatch; - // Stop kill tracker - _killTracker?.Stop(); + // Stop and dispose of the timers + if (updateTimer != null) + { + updateTimer.Stop(); + updateTimer.Dispose(); + updateTimer = null; + } if (vitalsTimer != null) { @@ -311,9 +278,14 @@ namespace MosswartMassacre commandTimer = null; } - // Stop quest streaming service - _questStreamingService?.Stop(); - _questStreamingService = null; + // Stop and dispose quest streaming timer + if (questStreamingTimer != null) + { + questStreamingTimer.Stop(); + questStreamingTimer.Elapsed -= OnQuestStreamingUpdate; + questStreamingTimer.Dispose(); + questStreamingTimer = null; + } // Stop and dispose character stats timer if (characterStatsTimer != null) @@ -349,7 +321,8 @@ namespace MosswartMassacre } // Clean up taper tracking - _inventoryMonitor?.Cleanup(); + trackedTaperContainers.Clear(); + lastKnownStackSizes.Clear(); // Clean up Harmony patches DecalHarmonyClean.Cleanup(); @@ -386,13 +359,8 @@ 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 - _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; + RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled; CharTag = PluginSettings.Instance.CharTag; ViewManager.SetRareMetaToggleState(RareMetaEnabled); @@ -415,10 +383,11 @@ namespace MosswartMassacre } // Initialize death tracking - _killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths)); + totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); + sessionDeaths = 0; // Initialize cached Prismatic Taper count - _inventoryMonitor.Initialize(); + InitializePrismaticTaperCount(); // Initialize quest manager for always-on quest streaming try @@ -428,10 +397,12 @@ namespace MosswartMassacre // Trigger full quest data refresh (same as clicking refresh button) Views.FlagTrackerView.RefreshQuestData(); - // Initialize quest streaming service (30 seconds) - _questStreamingService = new QuestStreamingService(this); - _questStreamingService.Start(); - + // Initialize quest streaming timer (30 seconds) + questStreamingTimer = new Timer(30000); + questStreamingTimer.Elapsed += OnQuestStreamingUpdate; + questStreamingTimer.AutoReset = true; + questStreamingTimer.Start(); + WriteToChat("[OK] Quest streaming initialized with full data refresh"); } catch (Exception ex) @@ -445,13 +416,13 @@ namespace MosswartMassacre try { // Start 10-minute character stats timer - characterStatsTimer = new Timer(Constants.CharacterStatsIntervalMs); + characterStatsTimer = new Timer(600000); // 10 minutes characterStatsTimer.Elapsed += OnCharacterStatsUpdate; characterStatsTimer.AutoReset = true; characterStatsTimer.Start(); // Send initial stats after 5-second delay (let CharacterFilter populate) - var initialDelay = new Timer(Constants.LoginDelayMs); + var initialDelay = new Timer(5000); initialDelay.AutoReset = false; initialDelay.Elapsed += (s, args) => { @@ -469,6 +440,102 @@ 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() { @@ -496,7 +563,7 @@ namespace MosswartMassacre } // 2. Apply the values from settings - if (_rareTracker != null) _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; + RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled; CharTag = PluginSettings.Instance.CharTag; @@ -530,10 +597,11 @@ namespace MosswartMassacre } // 6. Reinitialize death tracking - _killTracker?.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths)); + totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); + // Don't reset sessionDeaths - keep the current session count // 7. Reinitialize cached Prismatic Taper count - _inventoryMonitor?.Initialize(); + InitializePrismaticTaperCount(); // 8. Reinitialize quest manager for hot reload try @@ -557,23 +625,201 @@ namespace MosswartMassacre WriteToChat($"[ERROR] Quest manager hot reload failed: {ex.Message}"); } - // 9. Reinitialize quest streaming service for hot reload + // 9. Reinitialize quest streaming timer for hot reload try { - _questStreamingService?.Stop(); - _questStreamingService = new QuestStreamingService(this); - _questStreamingService.Start(); - - WriteToChat("[OK] Quest streaming service reinitialized (30s interval)"); + // 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)"); } catch (Exception ex) { - WriteToChat($"[ERROR] Quest streaming service hot reload failed: {ex.Message}"); + WriteToChat($"[ERROR] Quest streaming timer 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) { @@ -706,11 +952,44 @@ 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) { - _killTracker.OnDeath(); - _killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths)); + sessionDeaths++; + totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); } private void HandleServerCommand(CommandEnvelope env) @@ -747,8 +1026,55 @@ 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 @@ -765,6 +1091,24 @@ 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) { @@ -824,7 +1168,119 @@ 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 @@ -853,19 +1309,23 @@ namespace MosswartMassacre } } } - - void IPluginLogger.Log(string message) => WriteToChat(message); - public static void RestartStats() { - _staticKillTracker?.RestartStats(); - if (_staticRareTracker != null) - _staticRareTracker.RareCount = 0; - ViewManager.UpdateRareCount(0); + 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); } public static void ToggleRareMeta() { - _staticRareTracker?.ToggleRareMeta(); + PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled; + RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; ViewManager.SetRareMetaToggleState(RareMetaEnabled); } @@ -893,531 +1353,568 @@ namespace MosswartMassacre } private void HandleMmCommand(string text) { - _commandRouter.Dispatch(text); - } + // Remove the /mm prefix and trim extra whitespace test + string[] args = text.Substring(3).Trim().Split(' '); - private void RegisterCommands() - { - _commandRouter.Register("ws", args => + if (args.Length == 0 || string.IsNullOrEmpty(args[0])) { - if (args.Length > 1) - { - if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase)) + WriteToChat("Usage: /mm . Try /mm help"); + return; + } + + string subCommand = args[0].ToLower(); + + switch (subCommand) + { + case "ws": + if (args.Length > 1) { - 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."); + 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 "); + } } + else { WriteToChat("Usage: /mm ws "); } - } - else - { - WriteToChat("Usage: /mm ws "); - } - }, "Websocket streaming enable|disable"); + 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; - _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"); - - _commandRouter.Register("getmetastate", args => - { - string metaState = VtankControl.VtGetMetaState(); - WriteToChat(metaState); - }, "Gets the current metastate"); - - _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"); - - _commandRouter.Register("reset", args => - { - RestartStats(); - }, "Reset all counters"); - - _commandRouter.Register("meta", args => - { - RareMetaEnabled = !RareMetaEnabled; - WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}"); - ViewManager.SetRareMetaToggleState(RareMetaEnabled); - }, "Toggle rare meta state"); - - _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) + 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) { - PluginSettings.Instance.ChestLooterSettings.ChestName = chestName; - PluginSettings.Save(); + WriteToChat("Advanced VTank to next waypoint."); } - 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) + else { - PluginSettings.Instance.ChestLooterSettings.KeyName = keyName; - PluginSettings.Save(); + WriteToChat("Failed to advance VTank waypoint. Is VTank running?"); } - Views.VVSTabbedMainView.RefreshChestLooterUI(); - } - }, "Set key name for looter"); + break; - _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"); + 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("stoploot", args => - { - if (chestLooter != null) - chestLooter.Stop(); - else - WriteToChat("[ChestLooter] Chest looter not initialized"); - }, "Stop chest looting"); + 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("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"); + 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("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")}"); + case "stoploot": + if (chestLooter != null) + { + chestLooter.Stop(); + } + else + { + WriteToChat("[ChestLooter] Chest looter not initialized"); + } + break; - WriteToChat("=== Harmony Version Status ==="); + case "vtanktest": try { - 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}"); + 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 harmonyEx) + catch (Exception ex) { - WriteToChat($"[FAIL] Harmony Test Failed: {harmonyEx.Message}"); + WriteToChat($"VTank test error: {ex.Message}"); } - } - catch (Exception ex) - { - WriteToChat($"Status check error: {ex.Message}"); - } - }, "Check Harmony patch status"); + break; - _commandRouter.Register("decaldebug", args => - { - if (args.Length > 1) - { - if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase)) + case "decalstatus": + try { - AggressiveChatStreamingEnabled = true; - WriteToChat("[OK] DECAL debug streaming ENABLED - will show captured messages + stream via WebSocket"); + 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}"); + } } - else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase)) + catch (Exception ex) { - AggressiveChatStreamingEnabled = false; - WriteToChat("[FAIL] DECAL debug streaming DISABLED - WebSocket streaming also disabled"); + 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 "); + } } else { WriteToChat("Usage: /mm decaldebug "); } - } - else - { - WriteToChat("Usage: /mm decaldebug "); - } - }, "Enable/disable plugin message debug output"); + break; - _commandRouter.Register("harmonyraw", args => { }, ""); - _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 "harmonyraw": + // Debug functionality removed + break; - _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)) - { - 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")) - { - WriteToChat($" *** POSSIBLE MATCH: '{item.Name}' ***"); - } - } - } - - WriteToChat($"=== Total items listed: {itemNum - 1} ==="); - - 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}"); - } - }, "Test Prismatic Taper detection and icon lookup"); - - _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) - { - WriteToChat($"[WARNING] Death count sync issue detected!"); - WriteToChat($"Updating totalDeaths from {_killTracker.TotalDeaths} to {currentCharDeaths}"); - _killTracker.SetTotalDeaths(currentCharDeaths); - } - - 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) - { - WriteToChat("[OK] Cached count matches Utils count"); - } - else - { - WriteToChat($"[WARNING] Count mismatch! Cached: {_inventoryMonitor.CachedPrismaticCount}, Utils: {utilsCount}"); - WriteToChat("Refreshing cached count..."); - _inventoryMonitor.Initialize(); - } - - 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 {_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) - { - 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."); - } - } - else - { - WriteToChat("Usage: /mm finditem \"Item Name\""); - WriteToChat("Example: /mm finditem \"Prismatic Taper\""); - } - }, "Find item in inventory by name"); - - _commandRouter.Register("checkforupdate", args => - { - Task.Run(async () => - { - await UpdateManager.CheckForUpdateAsync(); + case "initgui": + case "gui": try { - ViewManager.RefreshUpdateStatus(); + 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($"Error refreshing UI: {ex.Message}"); + WriteToChat($"GUI initialization error: {ex.Message}"); } - }); - }, "Check for plugin updates"); + break; - _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) + case "testprismatic": + try { - 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) + 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)) { - string questName = questManager.GetFriendlyQuestName(quest.Id); - WriteToChat($" - {questName} ({quest.Id})"); + 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++; + + // 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($"=== 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; - 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"); + 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; - _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"); + case "testdeath": + try + { + 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."); + } + } + else + { + WriteToChat("Usage: /mm finditem \"Item Name\""); + WriteToChat("Example: /mm finditem \"Prismatic Taper\""); + } + break; + + case "checkforupdate": + // Run the update check asynchronously + Task.Run(async () => + { + await UpdateManager.CheckForUpdateAsync(); + // Update UI if available + try + { + ViewManager.RefreshUpdateStatus(); + } + catch (Exception ex) + { + WriteToChat($"Error refreshing UI: {ex.Message}"); + } + }); + break; + + case "update": + // Run the update installation asynchronously + Task.Run(async () => + { + await UpdateManager.DownloadAndInstallUpdateAsync(); + }); + break; + + case "debugupdate": + Views.VVSTabbedMainView.DebugUpdateControls(); + break; + + case "sendinventory": + // Force inventory upload with ID requests + if (_inventoryLogger != null) + { + _inventoryLogger.ForceInventoryUpload(); + } + else + { + WriteToChat("[INV] Inventory system not initialized"); + } + 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; + + case "queststatus": + // Show quest streaming status + try + { + 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) + { + 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})"); + } + } + + 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) + { + PluginSettings.Instance.VerboseLogging = !PluginSettings.Instance.VerboseLogging; + WriteToChat($"Verbose logging: {(PluginSettings.Instance.VerboseLogging ? "ENABLED" : "DISABLED")}"); + } + else + { + WriteToChat("Settings not initialized"); + } + break; + + default: + WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help"); + break; + } } diff --git a/MosswartMassacre/QuestStreamingService.cs b/MosswartMassacre/QuestStreamingService.cs deleted file mode 100644 index 5409d35..0000000 --- a/MosswartMassacre/QuestStreamingService.cs +++ /dev/null @@ -1,133 +0,0 @@ -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 deleted file mode 100644 index 5bd7c9d..0000000 --- a/MosswartMassacre/RareTracker.cs +++ /dev/null @@ -1,71 +0,0 @@ -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 4bee425..7b5e4d6 100644 --- a/MosswartMassacre/VtankControl.cs +++ b/MosswartMassacre/VtankControl.cs @@ -72,9 +72,9 @@ namespace MosswartMassacre return 0; } } - catch (Exception ex) + catch { - PluginCore.WriteToChat($"[VTank] SetSetting error ({setting}): {ex.Message}"); + // Swallow any errors and signal failure return 0; } diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs index e89cbe3..288b74d 100644 --- a/MosswartMassacre/WebSocket.cs +++ b/MosswartMassacre/WebSocket.cs @@ -35,8 +35,6 @@ 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) @@ -53,16 +51,13 @@ 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(); - _logger?.Log("[WebSocket] connecting…"); + PluginCore.WriteToChat("[WebSocket] connecting…"); _ = Task.Run(ConnectAndLoopAsync); } @@ -77,7 +72,7 @@ namespace MosswartMassacre _ws?.Dispose(); _ws = null; - _logger?.Log("[WebSocket] DISABLED"); + PluginCore.WriteToChat("[WebSocket] DISABLED"); } // ─── connect / receive / telemetry loop ────────────────────── @@ -92,7 +87,7 @@ namespace MosswartMassacre _ws = new ClientWebSocket(); _ws.Options.SetRequestHeader("X-Plugin-Secret", SharedSecret); await _ws.ConnectAsync(WsEndpoint, _cts.Token); - _logger?.Log("[WebSocket] CONNECTED"); + PluginCore.WriteToChat("[WebSocket] CONNECTED"); SessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; // ─── Register this socket under our character name ─── @@ -103,7 +98,7 @@ namespace MosswartMassacre }; var regJson = JsonConvert.SerializeObject(registerEnvelope); await SendEncodedAsync(regJson, _cts.Token); - _logger?.Log("[WebSocket] REGISTERED"); + PluginCore.WriteToChat("[WebSocket] REGISTERED"); var buffer = new byte[4096]; @@ -123,7 +118,7 @@ namespace MosswartMassacre } catch (Exception ex) { - _logger?.Log($"[WebSocket] receive error: {ex.Message}"); + PluginCore.WriteToChat($"[WebSocket] receive error: {ex.Message}"); break; } @@ -156,7 +151,7 @@ namespace MosswartMassacre }); // 5) Inline telemetry loop - _logger?.Log("[WebSocket] Starting telemetry loop"); + PluginCore.WriteToChat("[WebSocket] Starting telemetry loop"); while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested) { try @@ -166,7 +161,7 @@ namespace MosswartMassacre } catch (Exception ex) { - _logger?.Log($"[WebSocket] Telemetry failed: {ex.Message}"); + PluginCore.WriteToChat($"[WebSocket] Telemetry failed: {ex.Message}"); break; // Exit telemetry loop on failure } @@ -176,30 +171,30 @@ namespace MosswartMassacre } catch (OperationCanceledException) { - _logger?.Log("[WebSocket] Telemetry loop cancelled"); + PluginCore.WriteToChat("[WebSocket] Telemetry loop cancelled"); break; } } // Log why telemetry loop exited - _logger?.Log($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}"); + PluginCore.WriteToChat($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}"); // Wait for receive loop to finish await receiveTask; } catch (OperationCanceledException) { - _logger?.Log("[WebSocket] Connection cancelled"); + PluginCore.WriteToChat("[WebSocket] Connection cancelled"); break; } catch (Exception ex) { - _logger?.Log($"[WebSocket] Connection error: {ex.Message}"); + PluginCore.WriteToChat($"[WebSocket] Connection error: {ex.Message}"); } finally { var finalState = _ws?.State.ToString() ?? "null"; - _logger?.Log($"[WebSocket] Cleaning up connection - Final state: {finalState}"); + PluginCore.WriteToChat($"[WebSocket] Cleaning up connection - Final state: {finalState}"); _ws?.Abort(); _ws?.Dispose(); _ws = null; @@ -208,7 +203,7 @@ namespace MosswartMassacre // Pause before reconnecting if (_enabled) { - _logger?.Log("[WebSocket] Reconnecting in 2 seconds..."); + PluginCore.WriteToChat("[WebSocket] Reconnecting in 2 seconds..."); try { await Task.Delay(2000, CancellationToken.None); } catch { } } } @@ -339,7 +334,7 @@ namespace MosswartMassacre } catch (Exception ex) { - _logger?.Log($"[WebSocket] Send error: {ex.Message}"); + PluginCore.WriteToChat($"[WebSocket] Send error: {ex.Message}"); _ws?.Abort(); _ws?.Dispose(); _ws = null; @@ -352,31 +347,33 @@ 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 = stats?.CharTag ?? "", + char_tag = PluginCore.CharTag, session_id = SessionInfo.GuidString, timestamp = DateTime.UtcNow.ToString("o"), ew = coords.EW, ns = coords.NS, z = coords.Z, - 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(), + 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(), 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 334302d..b2867f3 100644 Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ