diff --git a/MosswartMassacre/PluginCore.backup.cs b/MosswartMassacre/PluginCore.backup.cs new file mode 100644 index 0000000..543fa88 --- /dev/null +++ b/MosswartMassacre/PluginCore.backup.cs @@ -0,0 +1,1654 @@ +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; +using Decal.Adapter.Wrappers; +using MosswartMassacre.Views; +using Mag.Shared.Constants; + +namespace MosswartMassacre +{ + [FriendlyName("Mosswart Massacre")] + public class PluginCore : PluginBase + { + // Hot Reload Support Properties + public static string AssemblyDirectory { get; set; } + public static bool IsHotReload { get; set; } + + internal static PluginHost MyHost; + internal static int totalKills = 0; + internal static int rareCount = 0; + internal static int sessionDeaths = 0; // Deaths this session + internal static int totalDeaths = 0; // Total deaths from character + internal static int cachedPrismaticCount = 0; // Cached Prismatic Taper count + internal static int lastPrismaticCount = 0; // For delta calculation + + // Track taper items and their containers for accurate release detection + private static readonly Dictionary trackedTaperContainers = new Dictionary(); + private static readonly Dictionary lastKnownStackSizes = new Dictionary(); + internal static DateTime lastKillTime = DateTime.Now; + internal static double killsPer5Min = 0; + internal static double killsPerHour = 0; + internal static DateTime statsStartTime = DateTime.Now; + internal static Timer updateTimer; + private static Timer vitalsTimer; + private static System.Windows.Forms.Timer commandTimer; + private static readonly Queue pendingCommands = new Queue(); + public static bool RareMetaEnabled { get; set; } = true; + + // VVS View Management + private static class ViewManager + { + public static void ViewInit() + { + Views.VVSTabbedMainView.ViewInit(); + } + + public static void ViewDestroy() + { + Views.VVSTabbedMainView.ViewDestroy(); + } + + public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour) + { + Views.VVSTabbedMainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour); + } + + public static void UpdateElapsedTime(TimeSpan elapsed) + { + Views.VVSTabbedMainView.UpdateElapsedTime(elapsed); + } + + public static void UpdateRareCount(int rareCount) + { + Views.VVSTabbedMainView.UpdateRareCount(rareCount); + } + + public static void SetRareMetaToggleState(bool enabled) + { + Views.VVSTabbedMainView.SetRareMetaToggleState(enabled); + } + + public static void RefreshSettingsFromConfig() + { + Views.VVSTabbedMainView.RefreshSettingsFromConfig(); + } + } + public static bool RemoteCommandsEnabled { get; set; } = false; + public static bool HttpServerEnabled { get; set; } = false; + public static string CharTag { get; set; } = ""; + public static bool TelemetryEnabled { get; set; } = false; + public static bool WebSocketEnabled { get; set; } = false; + public bool InventoryLogEnabled { get; set; } = false; + public static bool AggressiveChatStreamingEnabled { get; set; } = true; + private MossyInventory _inventoryLogger; + public static NavVisualization navVisualization; + + // 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(); + + protected override void Startup() + { + try + { + // DEBUG: Add startup debug message + WriteToChat("[DEBUG] PluginCore.Startup() called"); + + // Set MyHost - for hot reload scenarios, Host might be null + if (Host != null) + { + MyHost = Host; + } + else if (MyHost == null) + { + // Hot reload fallback - this is okay, WriteToChat will handle it + MyHost = null; + } + + // Check if this is a hot reload + var isCharacterLoaded = CoreManager.Current.CharacterFilter.LoginStatus == 3; + if (IsHotReload || isCharacterLoaded) + { + // Hot reload detected - reinitialize connections and state + WriteToChat("[INFO] Hot reload detected - reinitializing plugin"); + + // Reload settings if character is already logged in + if (isCharacterLoaded) + { + try + { + WriteToChat("Hot reload - reinitializing character-dependent systems"); + // Don't call LoginComplete - create hot reload specific initialization + InitializeForHotReload(); + WriteToChat("[INFO] Hot reload initialization complete"); + } + catch (Exception ex) + { + WriteToChat($"[ERROR] Hot reload initialization failed: {ex.Message}"); + } + } + } + + // Note: Startup messages will appear after character login + // Subscribe to chat message event + WriteToChat("[DEBUG] Subscribing to events..."); + CoreManager.Current.ChatBoxMessage += new EventHandler(OnChatText); + CoreManager.Current.ChatBoxMessage += new EventHandler(AllChatText); + CoreManager.Current.CommandLineText += OnChatCommand; + WriteToChat("[DEBUG] About to subscribe to LoginComplete event"); + CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete; + WriteToChat("[DEBUG] LoginComplete event subscription successful"); + CoreManager.Current.CharacterFilter.Death += OnCharacterDeath; + CoreManager.Current.WorldFilter.CreateObject += OnSpawn; + CoreManager.Current.WorldFilter.CreateObject += OnPortalDetected; + CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn; + // Subscribe to inventory change events for taper tracking + CoreManager.Current.WorldFilter.CreateObject += OnInventoryCreate; + CoreManager.Current.WorldFilter.ReleaseObject += OnInventoryRelease; + CoreManager.Current.WorldFilter.ChangeObject += OnInventoryChange; + // Initialize VVS view after character login + ViewManager.ViewInit(); + + // Initialize the timer + updateTimer = new Timer(1000); // Update every second + updateTimer.Elapsed += UpdateStats; + updateTimer.Start(); + + // Initialize vitals streaming timer + vitalsTimer = new Timer(5000); // Send vitals every 5 seconds + vitalsTimer.Elapsed += SendVitalsUpdate; + vitalsTimer.Start(); + + // Initialize command processing timer (Windows Forms timer for main thread) + commandTimer = new System.Windows.Forms.Timer(); + commandTimer.Interval = 10; // Process commands every 10ms + commandTimer.Tick += ProcessPendingCommands; + commandTimer.Start(); + + // Note: View initialization moved to LoginComplete for VVS compatibility + + // Enable TLS1.2 + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + //Enable vTank interface + vTank.Enable(); + //lyssna på commands + WebSocket.OnServerCommand += HandleServerCommand; + //starta inventory. Hanterar subscriptions i den med + + _inventoryLogger = new MossyInventory(); + + // Initialize navigation visualization system + navVisualization = new NavVisualization(); + + // Note: DECAL Harmony patches will be initialized in LoginComplete event + // where the chat system is available for error messages + + } + catch (Exception ex) + { + WriteToChat("Error during startup: " + ex.Message); + } + } + + protected override void Shutdown() + { + try + { + PluginSettings.Save(); + if (TelemetryEnabled) + Telemetry.Stop(); // ensure no dangling timer / HttpClient + WriteToChat("Mosswart Massacre is shutting down..."); + + // Unsubscribe from chat message event + CoreManager.Current.ChatBoxMessage -= new EventHandler(OnChatText); + CoreManager.Current.CommandLineText -= OnChatCommand; + 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 from inventory change events + CoreManager.Current.WorldFilter.CreateObject -= OnInventoryCreate; + CoreManager.Current.WorldFilter.ReleaseObject -= OnInventoryRelease; + CoreManager.Current.WorldFilter.ChangeObject -= OnInventoryChange; + + + // Stop and dispose of the timers + if (updateTimer != null) + { + updateTimer.Stop(); + updateTimer.Dispose(); + updateTimer = null; + } + + if (vitalsTimer != null) + { + vitalsTimer.Stop(); + vitalsTimer.Dispose(); + vitalsTimer = null; + } + + if (commandTimer != null) + { + commandTimer.Stop(); + commandTimer.Dispose(); + commandTimer = null; + } + + // Stop and dispose quest streaming timer + if (questStreamingTimer != null) + { + questStreamingTimer.Stop(); + questStreamingTimer.Elapsed -= OnQuestStreamingUpdate; + questStreamingTimer.Dispose(); + questStreamingTimer = null; + } + + // Dispose quest manager + if (questManager != null) + { + questManager.Dispose(); + questManager = null; + } + + // Clean up the view + ViewManager.ViewDestroy(); + //Disable vtank interface + vTank.Disable(); + // sluta lyssna på commands + WebSocket.OnServerCommand -= HandleServerCommand; + WebSocket.Stop(); + //shutdown inv + _inventoryLogger.Dispose(); + + // Clean up navigation visualization + if (navVisualization != null) + { + navVisualization.Dispose(); + navVisualization = null; + } + + // Clean up taper tracking + trackedTaperContainers.Clear(); + lastKnownStackSizes.Clear(); + + // Clean up Harmony patches + DecalHarmonyClean.Cleanup(); + + MyHost = null; + } + catch (Exception ex) + { + WriteToChat("Error during shutdown: " + ex.Message); + } + } + private void CharacterFilter_LoginComplete(object sender, EventArgs e) + { + WriteToChat("[DEBUG] CharacterFilter_LoginComplete event fired!"); + CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete; + + WriteToChat("Mosswart Massacre has started!"); + + + + PluginSettings.Initialize(); // Safe to call now + + // Apply the values + RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; + WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled; + RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled; + HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled; + TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled; + CharTag = PluginSettings.Instance.CharTag; + ViewManager.SetRareMetaToggleState(RareMetaEnabled); + ViewManager.RefreshSettingsFromConfig(); // Refresh all UI settings after loading + if (TelemetryEnabled) + Telemetry.Start(); + if (WebSocketEnabled) + WebSocket.Start(); + + // Initialize Harmony patches using UtilityBelt's loaded DLL + try + { + bool success = DecalHarmonyClean.Initialize(); + if (success) + WriteToChat("[OK] Plugin message interception active"); + else + WriteToChat("[FAIL] Could not initialize message interception"); + } + catch (Exception ex) + { + WriteToChat($"[ERROR] Harmony initialization failed: {ex.Message}"); + } + + // Initialize death tracking + totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); + sessionDeaths = 0; + + // Initialize cached Prismatic Taper count + InitializePrismaticTaperCount(); + + // Initialize quest manager for always-on quest streaming + try + { + questManager = new QuestManager(); + questManager.RefreshQuests(); + + // Initialize quest streaming timer (30 seconds) + questStreamingTimer = new Timer(30000); + questStreamingTimer.Elapsed += OnQuestStreamingUpdate; + questStreamingTimer.AutoReset = true; + questStreamingTimer.Start(); + + WriteToChat("[OK] Quest streaming initialized"); + } + catch (Exception ex) + { + WriteToChat($"[ERROR] Quest streaming initialization failed: {ex.Message}"); + } + + } + + #region Quest Streaming Methods + private static void OnQuestStreamingUpdate(object sender, ElapsedEventArgs e) + { + try + { + // Stream high priority quest data via WebSocket + if (WebSocketEnabled && questManager?.QuestList != null && questManager.QuestList.Count > 0) + { + 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(); + + foreach (var quest in priorityQuests) + { + try + { + string questName = questManager.GetFriendlyQuestName(quest.Id); + long timeRemaining = quest.ExpireTime - currentTime; + string countdown = FormatCountdown(timeRemaining); + + // Stream quest data + System.Threading.Tasks.Task.Run(() => WebSocket.SendQuestDataAsync(questName, countdown)); + } + catch (Exception) + { + // Silently handle individual quest streaming errors + } + } + } + } + catch (Exception) + { + // Silently handle quest streaming errors to avoid spam + } + } + + 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() + { + // This method handles initialization that depends on character being logged in + // Similar to LoginComplete but designed for hot reload scenarios + + WriteToChat("Mosswart Massacre hot reload initialization started!"); + + // 1. Initialize settings - CRITICAL first step + PluginSettings.Initialize(); + + // 2. Apply the values from settings + RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; + WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled; + RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled; + HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled; + TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled; + CharTag = PluginSettings.Instance.CharTag; + + // 3. Update UI with current settings + ViewManager.SetRareMetaToggleState(RareMetaEnabled); + ViewManager.RefreshSettingsFromConfig(); + + // 4. Restart services if they were enabled (stop first, then start) + if (TelemetryEnabled) + { + Telemetry.Stop(); // Stop existing + Telemetry.Start(); // Restart + } + + if (WebSocketEnabled) + { + WebSocket.Stop(); // Stop existing + WebSocket.Start(); // Restart + } + + if (HttpServerEnabled) + { + HttpCommandServer.Stop(); // Stop existing + HttpCommandServer.Start(); // Restart + } + + // 5. Initialize Harmony patches (only if not already done) + // Note: Harmony patches are global and don't need reinitialization + if (!DecalHarmonyClean.IsActive()) + { + try + { + bool success = DecalHarmonyClean.Initialize(); + if (success) + WriteToChat("[OK] Plugin message interception active"); + else + WriteToChat("[FAIL] Could not initialize message interception"); + } + catch (Exception ex) + { + WriteToChat($"[ERROR] Harmony initialization failed: {ex.Message}"); + } + } + + // 6. Reinitialize death tracking + totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); + // Don't reset sessionDeaths - keep the current session count + + // 7. Reinitialize cached Prismatic Taper count + InitializePrismaticTaperCount(); + + 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) + { + var mob = e.New; + if (mob.ObjectClass != ObjectClass.Monster) return; + + try + { + // Get DECAL coordinates + var decalCoords = mob.Coordinates(); + if (decalCoords == null) return; + + const string fmt = "F7"; + string ns = decalCoords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture); + string ew = decalCoords.EastWest.ToString(fmt, CultureInfo.InvariantCulture); + + // Get Z coordinate using RawCoordinates() for accurate world Z position + string zCoord = "0"; + try + { + var rawCoords = mob.RawCoordinates(); + if (rawCoords != null) + { + zCoord = rawCoords.Z.ToString("F2", CultureInfo.InvariantCulture); + } + else + { + // Fallback to player Z approximation if RawCoordinates fails + var playerCoords = Coordinates.Me; + if (Math.Abs(playerCoords.Z) > 0.1) + { + zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture); + } + } + } + catch + { + // Fallback to player Z approximation on error + try + { + var playerCoords = Coordinates.Me; + if (Math.Abs(playerCoords.Z) > 0.1) + { + zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture); + } + } + catch + { + zCoord = "0"; + } + } + + await WebSocket.SendSpawnAsync(ns, ew, zCoord, mob.Name); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[WS] Spawn send failed: {ex}"); + } + } + + private async void OnPortalDetected(object sender, CreateObjectEventArgs e) + { + var portal = e.New; + if (portal.ObjectClass != ObjectClass.Portal) return; + + try + { + // Get portal coordinates from DECAL + var decalCoords = portal.Coordinates(); + if (decalCoords == null) return; + + const string fmt = "F7"; + string ns = decalCoords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture); + string ew = decalCoords.EastWest.ToString(fmt, CultureInfo.InvariantCulture); + + // Get Z coordinate using RawCoordinates() for accurate world Z position + string zCoord = "0"; + try + { + var rawCoords = portal.RawCoordinates(); + if (rawCoords != null) + { + zCoord = rawCoords.Z.ToString("F2", CultureInfo.InvariantCulture); + } + else + { + // Fallback to player Z approximation if RawCoordinates fails + var playerCoords = Coordinates.Me; + if (Math.Abs(playerCoords.Z) > 0.1) + { + zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture); + } + } + } + catch + { + // Fallback to player Z approximation on error + try + { + var playerCoords = Coordinates.Me; + if (Math.Abs(playerCoords.Z) > 0.1) + { + zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture); + } + } + catch + { + zCoord = "0"; + } + } + + await WebSocket.SendPortalAsync(ns, ew, zCoord, portal.Name); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[PORTAL ERROR] {ex.Message}"); + PluginCore.WriteToChat($"[WS] Portal send failed: {ex}"); + } + } + + + private void OnDespawn(object sender, ReleaseObjectEventArgs e) + { + var mob = e.Released; + if (mob.ObjectClass != ObjectClass.Monster) return; + + + // var c = mob.Coordinates(); + // PluginCore.WriteToChat( + // $"[Despawn] {mob.Name} @ (NS={c.NorthSouth:F1}, EW={c.EastWest:F1})"); + } + + private async void AllChatText(object sender, ChatTextInterceptEventArgs e) + { + try + { + string cleaned = NormalizeChatLine(e.Text); + + // Send to WebSocket + await WebSocket.SendChatTextAsync(e.Color, cleaned); + + // Note: Plugin message analysis is now handled by Harmony patches + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[WS] Chat send failed: {ex}"); + } + } + + private static string NormalizeChatLine(string raw) + { + if (string.IsNullOrEmpty(raw)) + return raw; + + // 1) Remove all <…> tags + var noTags = Regex.Replace(raw, "<[^>]+>", ""); + + // 2) Trim trailing newline or carriage-return + var trimmed = noTags.TrimEnd('\r', '\n'); + + // 3) Collapse multiple spaces into one + var collapsed = Regex.Replace(trimmed, @"[ ]{2,}", " "); + + return collapsed; + } + + private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e) + { + sessionDeaths++; + totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); + } + + private void HandleServerCommand(CommandEnvelope env) + { + // This is called from WebSocket thread - queue for main thread execution + lock (pendingCommands) + { + pendingCommands.Enqueue(env.Command); + } + } + + private void ProcessPendingCommands(object sender, EventArgs e) + { + // This runs on the main UI thread via Windows Forms timer + string command = null; + + lock (pendingCommands) + { + if (pendingCommands.Count > 0) + command = pendingCommands.Dequeue(); + } + + if (command != null) + { + try + { + // Execute ALL WebSocket commands on main thread - fast and reliable + DispatchChatToBoxWithPluginIntercept(command); + } + catch (Exception ex) + { + WriteToChat($"[WS] Command execution error: {ex.Message}"); + } + } + } + + 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}"); + } + if (RemoteCommandsEnabled && e.Color == 18) + { + string characterName = Regex.Escape(CoreManager.Current.CharacterFilter.Name); + string pattern = $@"^\[Allegiance\].*Dunking Rares.*say[s]?, \""!do {characterName} (?.+)\""$"; + string tag = Regex.Escape(PluginCore.CharTag); + string patterntag = $@"^\[Allegiance\].*Dunking Rares.*say[s]?, \""!dot {tag} (?.+)\""$"; + + + var match = Regex.Match(e.Text, pattern); + var matchtag = Regex.Match(e.Text, patterntag); + + if (match.Success) + { + string command = match.Groups["command"].Value; + DispatchChatToBoxWithPluginIntercept(command); + DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000); + } + else if (matchtag.Success) + { + string command = matchtag.Groups["command"].Value; + DispatchChatToBoxWithPluginIntercept(command); + DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000); + } + + } + + + + + + + + + } + catch (Exception ex) + { + WriteToChat("Error processing chat message: " + ex.Message); + } + } + private void OnChatCommand(object sender, ChatParserInterceptEventArgs e) + { + try + { + if (e.Text.StartsWith("/mm", StringComparison.OrdinalIgnoreCase)) + { + e.Eat = true; // Prevent the message from showing in chat + HandleMmCommand(e.Text); + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[Error] Failed to process /mm command: {ex.Message}"); + } + } + + 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) + { + try + { + // Only send if WebSocket is enabled + if (!WebSocketEnabled) + return; + + // Collect vitals data + int currentHealth = CoreManager.Current.Actions.Vital[VitalType.CurrentHealth]; + int currentStamina = CoreManager.Current.Actions.Vital[VitalType.CurrentStamina]; + int currentMana = CoreManager.Current.Actions.Vital[VitalType.CurrentMana]; + + int maxHealth = CoreManager.Current.Actions.Vital[VitalType.MaximumHealth]; + int maxStamina = CoreManager.Current.Actions.Vital[VitalType.MaximumStamina]; + int maxMana = CoreManager.Current.Actions.Vital[VitalType.MaximumMana]; + + int vitae = CoreManager.Current.CharacterFilter.Vitae; + + // Create vitals data structure + var vitalsData = new + { + type = "vitals", + timestamp = DateTime.UtcNow.ToString("o"), + character_name = CoreManager.Current.CharacterFilter.Name, + health_current = currentHealth, + health_max = maxHealth, + health_percentage = maxHealth > 0 ? Math.Round((double)currentHealth / maxHealth * 100, 1) : 0, + stamina_current = currentStamina, + stamina_max = maxStamina, + stamina_percentage = maxStamina > 0 ? Math.Round((double)currentStamina / maxStamina * 100, 1) : 0, + mana_current = currentMana, + mana_max = maxMana, + mana_percentage = maxMana > 0 ? Math.Round((double)currentMana / maxMana * 100, 1) : 0, + vitae = vitae + }; + + // Send via WebSocket + _ = WebSocket.SendVitalsAsync(vitalsData); + } + catch (Exception ex) + { + WriteToChat($"Error sending vitals: {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 + { + // For hot reload scenarios where MyHost might be null, use CoreManager directly + if (MyHost != null) + { + MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1); + } + else + { + // Hot reload fallback - use CoreManager directly like the original template + CoreManager.Current.Actions.AddChatText("[Mosswart Massacre] " + message, 1); + } + } + catch (Exception ex) + { + // Last resort fallback - try CoreManager even if MyHost was supposed to work + try + { + CoreManager.Current.Actions.AddChatText($"[Mosswart Massacre] {message} (WriteToChat error: {ex.Message})", 1); + } + catch + { + // Give up - can't write to chat at all + } + } + } + public static void RestartStats() + { + totalKills = 0; + rareCount = 0; + sessionDeaths = 0; // Reset session deaths only + statsStartTime = DateTime.Now; + killsPer5Min = 0; + killsPerHour = 0; + + WriteToChat($"Stats have been reset. Session deaths: {sessionDeaths}, Total deaths: {totalDeaths}"); + ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour); + ViewManager.UpdateRareCount(rareCount); + } + public static void ToggleRareMeta() + { + PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled; + RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; + ViewManager.SetRareMetaToggleState(RareMetaEnabled); + } + + [DllImport("Decal.dll")] + private static extern int DispatchOnChatCommand(ref IntPtr str, [MarshalAs(UnmanagedType.U4)] int target); + + public static bool Decal_DispatchOnChatCommand(string cmd) + { + IntPtr bstr = Marshal.StringToBSTR(cmd); + + try + { + bool eaten = (DispatchOnChatCommand(ref bstr, 1) & 0x1) > 0; + return eaten; + } + finally + { + Marshal.FreeBSTR(bstr); + } + } + public static void DispatchChatToBoxWithPluginIntercept(string cmd) + { + if (!Decal_DispatchOnChatCommand(cmd)) + CoreManager.Current.Actions.InvokeChatParser(cmd); + } + private void HandleMmCommand(string text) + { + // Remove the /mm prefix and trim extra whitespace + string[] args = text.Substring(3).Trim().Split(' '); + + if (args.Length == 0 || string.IsNullOrEmpty(args[0])) + { + WriteToChat("Usage: /mm . Try /mm help"); + return; + } + + string subCommand = args[0].ToLower(); + + switch (subCommand) + { + case "telemetry": + if (args.Length > 1) + { + if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase)) + { + TelemetryEnabled = true; + Telemetry.Start(); + PluginSettings.Instance.TelemetryEnabled = true; + WriteToChat("Telemetry streaming ENABLED."); + } + else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase)) + { + TelemetryEnabled = false; + Telemetry.Stop(); + PluginSettings.Instance.TelemetryEnabled = false; + WriteToChat("Telemetry streaming DISABLED."); + } + else + { + WriteToChat("Usage: /mm telemetry "); + } + } + else + { + WriteToChat("Usage: /mm telemetry "); + } + break; + case "ws": + if (args.Length > 1) + { + 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 "); + } + break; + case "help": + WriteToChat("Mosswart Massacre Commands:"); + WriteToChat("/mm report - Show current stats"); + WriteToChat("/mm loc - Show current location"); + WriteToChat("/mm telemetry - Telemetry streaming enable|disable"); + WriteToChat("/mm ws - Websocket streaming enable|disable"); + WriteToChat("/mm reset - Reset all counters"); + WriteToChat("/mm meta - Toggle rare meta state"); + WriteToChat("/mm http - Local http-command server enable|disable"); + WriteToChat("/mm remotecommand - Listen to allegiance !do/!dot enable|disable"); + WriteToChat("/mm getmetastate - Gets the current metastate"); + 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!!!"); + 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; + + 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 "http": + if (args.Length > 1) + { + if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase)) + { + PluginSettings.Instance.HttpServerEnabled = true; + HttpServerEnabled = true; + HttpCommandServer.Start(); + } + else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase)) + { + PluginSettings.Instance.HttpServerEnabled = false; + HttpServerEnabled = false; + HttpCommandServer.Stop(); + } + else + { + WriteToChat("Usage: /mm http "); + } + } + else + { + WriteToChat("Usage: /mm http "); + } + break; + + case "remotecommands": + if (args.Length > 1) + { + if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase)) + { + PluginSettings.Instance.RemoteCommandsEnabled = true; + RemoteCommandsEnabled = true; + WriteToChat("Remote command listening is now ENABLED."); + } + else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase)) + { + PluginSettings.Instance.RemoteCommandsEnabled = false; + RemoteCommandsEnabled = false; + WriteToChat("Remote command listening is now DISABLED."); + } + else + { + WriteToChat("Invalid remotecommands argument. Use 'enable' or 'disable'."); + } + } + else + { + WriteToChat("Usage: /mm remotecommands "); + } + break; + + case "nextwp": + double result = VtankControl.VtAdvanceWaypoint(); + if (result == 1) + { + WriteToChat("Advanced VTank to next waypoint."); + } + else + { + WriteToChat("Failed to advance VTank waypoint. Is VTank running?"); + } + break; + + case "vtanktest": + 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}"); + } + break; + + case "decalstatus": + try + { + 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}"); + } + } + catch (Exception ex) + { + 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 "); + } + break; + + + case "harmonyraw": + // Debug functionality removed + break; + + case "initgui": + case "gui": + try + { + WriteToChat("Attempting to manually initialize GUI..."); + ViewManager.ViewDestroy(); // Clean up any existing view + ViewManager.ViewInit(); // Reinitialize + WriteToChat("GUI initialization attempt completed."); + } + catch (Exception ex) + { + WriteToChat($"GUI initialization error: {ex.Message}"); + } + break; + + case "testprismatic": + try + { + WriteToChat("=== FULL INVENTORY DUMP ==="); + var worldFilter = CoreManager.Current.WorldFilter; + var playerInv = CoreManager.Current.CharacterFilter.Id; + + WriteToChat("Listing ALL items in your main inventory:"); + int itemNum = 1; + + foreach (WorldObject item in worldFilter.GetByContainer(playerInv)) + { + if (!string.IsNullOrEmpty(item.Name)) + { + 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; + + 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; + + 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; + + + default: + WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help"); + break; + } + } + + + } +} + + diff --git a/MosswartMassacre/UpdateManager.cs b/MosswartMassacre/UpdateManager.cs index af14e60..7f85741 100644 --- a/MosswartMassacre/UpdateManager.cs +++ b/MosswartMassacre/UpdateManager.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Net.Http; +using System.Security.Cryptography; using System.Threading.Tasks; namespace MosswartMassacre @@ -10,20 +11,47 @@ namespace MosswartMassacre private const string UPDATE_URL = "https://git.snakedesert.se/SawatoMosswartsEnjoyersClub/MosswartMassacre/raw/branch/spawn-detection/MosswartMassacre/bin/Release/MosswartMassacre.dll"; private static bool updateAvailable = false; - private static long remoteFileSize = 0; - private static long localFileSize = 0; + private static string remoteFileHash = string.Empty; + private static string localFileHash = string.Empty; private static DateTime lastCheckTime = DateTime.MinValue; public static bool IsUpdateAvailable => updateAvailable; public static DateTime LastCheckTime => lastCheckTime; + /// + /// Calculate SHA256 hash of a file + /// + private static string CalculateFileHash(string filePath) + { + using (var sha256 = SHA256.Create()) + { + using (var stream = File.OpenRead(filePath)) + { + byte[] hashBytes = sha256.ComputeHash(stream); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + } + } + + /// + /// Calculate SHA256 hash of byte array + /// + private static string CalculateHash(byte[] data) + { + using (var sha256 = SHA256.Create()) + { + byte[] hashBytes = sha256.ComputeHash(data); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + } + public static async Task CheckForUpdateAsync() { try { PluginCore.WriteToChat("[Update] Checking for updates..."); - // Get local file size + // Get local file hash string localPath = GetLocalDllPath(); if (!File.Exists(localPath)) { @@ -31,36 +59,40 @@ namespace MosswartMassacre return false; } - localFileSize = new FileInfo(localPath).Length; + PluginCore.WriteToChat("[Update] Calculating local file hash..."); + localFileHash = CalculateFileHash(localPath); - // Check remote file size + // Download remote file and calculate hash using (var client = new HttpClient()) { - client.Timeout = TimeSpan.FromSeconds(10); + client.Timeout = TimeSpan.FromSeconds(30); - var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, UPDATE_URL)); - response.EnsureSuccessStatusCode(); + PluginCore.WriteToChat("[Update] Downloading remote file for comparison..."); + var remoteData = await client.GetByteArrayAsync(UPDATE_URL); - remoteFileSize = response.Content.Headers.ContentLength ?? 0; - - if (remoteFileSize == 0) + if (remoteData == null || remoteData.Length == 0) { - PluginCore.WriteToChat("[Update] Error: Could not determine remote file size"); + PluginCore.WriteToChat("[Update] Error: Could not download remote file"); return false; } + + PluginCore.WriteToChat("[Update] Calculating remote file hash..."); + remoteFileHash = CalculateHash(remoteData); } - // Compare sizes - updateAvailable = (remoteFileSize != localFileSize); + // Compare hashes + updateAvailable = !string.Equals(localFileHash, remoteFileHash, StringComparison.OrdinalIgnoreCase); lastCheckTime = DateTime.Now; if (updateAvailable) { - PluginCore.WriteToChat($"[Update] Update available! Local: {localFileSize} bytes, Remote: {remoteFileSize} bytes"); + PluginCore.WriteToChat($"[Update] Update available!"); + PluginCore.WriteToChat($"[Update] Local hash: {localFileHash}"); + PluginCore.WriteToChat($"[Update] Remote hash: {remoteFileHash}"); } else { - PluginCore.WriteToChat("[Update] Up to date"); + PluginCore.WriteToChat("[Update] Up to date - hashes match"); } return true; @@ -112,12 +144,15 @@ namespace MosswartMassacre } } - // Validate downloaded file - var downloadedSize = new FileInfo(tempPath).Length; - if (downloadedSize != remoteFileSize) + // Validate downloaded file by hash + PluginCore.WriteToChat("[Update] Validating downloaded file..."); + var downloadedHash = CalculateFileHash(tempPath); + if (!string.Equals(downloadedHash, remoteFileHash, StringComparison.OrdinalIgnoreCase)) { File.Delete(tempPath); - PluginCore.WriteToChat($"[Update] Download validation failed. Expected {remoteFileSize} bytes, got {downloadedSize} bytes"); + PluginCore.WriteToChat($"[Update] Download validation failed. Hash mismatch!"); + PluginCore.WriteToChat($"[Update] Expected: {remoteFileHash}"); + PluginCore.WriteToChat($"[Update] Got: {downloadedHash}"); return false; } diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll index db2d14d..f39aa62 100644 Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ