From 73ba7082d8e09ff09afb5efa9b8a7863286dd97b Mon Sep 17 00:00:00 2001 From: erik Date: Sun, 22 Jun 2025 12:10:15 +0200 Subject: [PATCH] Added hot reload --- MosswartMassacre.Loader/LoaderCore.cs | 264 ++++++++++++ .../MosswartMassacre.Loader.csproj | 37 ++ MosswartMassacre/DecalHarmonyClean.cs | 65 +-- MosswartMassacre/FlagTrackerData.cs | 100 +---- MosswartMassacre/FodyWeavers.xml | 9 + MosswartMassacre/MosswartMassacre.csproj | 10 + MosswartMassacre/MossyInventory.cs | 66 ++- MosswartMassacre/PluginCore.cs | 339 +++++++++++---- MosswartMassacre/PluginSettings.cs | 22 +- MosswartMassacre/QuestManager.cs | 17 + MosswartMassacre/QuestNames.cs | 228 ++++++++++ MosswartMassacre/Views/FlagTrackerView.cs | 404 +++++++++++------- MosswartMassacre/Views/VVSBaseView.cs | 2 +- MosswartMassacre/Views/VVSTabbedMainView.cs | 18 +- MosswartMassacre/WebSocket.cs | 14 + mossy.sln | 6 + 16 files changed, 1203 insertions(+), 398 deletions(-) create mode 100644 MosswartMassacre.Loader/LoaderCore.cs create mode 100644 MosswartMassacre.Loader/MosswartMassacre.Loader.csproj create mode 100644 MosswartMassacre/FodyWeavers.xml create mode 100644 MosswartMassacre/QuestNames.cs diff --git a/MosswartMassacre.Loader/LoaderCore.cs b/MosswartMassacre.Loader/LoaderCore.cs new file mode 100644 index 0000000..4aae64c --- /dev/null +++ b/MosswartMassacre.Loader/LoaderCore.cs @@ -0,0 +1,264 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using Decal.Adapter; + +namespace MosswartMassacre.Loader +{ + [FriendlyName("MosswartMassacre.Loader")] + public class LoaderCore : FilterBase + { + private Assembly pluginAssembly; + private Type pluginType; + private object pluginInstance; + private FileSystemWatcher pluginWatcher; + private bool isSubscribedToRenderFrame = false; + private bool needsReload; + + public static string PluginAssemblyNamespace => "MosswartMassacre"; + public static string PluginAssemblyName => $"{PluginAssemblyNamespace}.dll"; + public static string PluginAssemblyGuid => "{8C97E839-4D05-4A5F-B0C8-E8E778654322}"; + + public static bool IsPluginLoaded { get; private set; } + + /// + /// Assembly directory (contains both loader and plugin dlls) + /// + public static string AssemblyDirectory => System.IO.Path.GetDirectoryName(Assembly.GetAssembly(typeof(LoaderCore)).Location); + + public DateTime LastDllChange { get; private set; } + + #region Event Handlers + protected override void Startup() + { + try + { + Core.PluginInitComplete += Core_PluginInitComplete; + Core.PluginTermComplete += Core_PluginTermComplete; + Core.FilterInitComplete += Core_FilterInitComplete; + + // Set up assembly resolution for hot-loaded plugin dependencies + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + + // watch the AssemblyDirectory for any .dll file changes + pluginWatcher = new FileSystemWatcher(); + pluginWatcher.Path = AssemblyDirectory; + pluginWatcher.NotifyFilter = NotifyFilters.LastWrite; + pluginWatcher.Filter = "*.dll"; + pluginWatcher.Changed += PluginWatcher_Changed; + pluginWatcher.EnableRaisingEvents = true; + } + catch (Exception ex) + { + Log(ex); + } + } + + private void Core_FilterInitComplete(object sender, EventArgs e) + { + Core.EchoFilter.ClientDispatch += EchoFilter_ClientDispatch; + } + + private void EchoFilter_ClientDispatch(object sender, NetworkMessageEventArgs e) + { + try + { + // Login_SendEnterWorldRequest + if (e.Message.Type == 0xF7C8) + { + //EnsurePluginIsDisabledInRegistry(); + } + } + catch (Exception ex) + { + Log(ex); + } + } + + private void Core_PluginInitComplete(object sender, EventArgs e) + { + try + { + LoadPluginAssembly(); + } + catch (Exception ex) + { + Log(ex); + } + } + + private void Core_PluginTermComplete(object sender, EventArgs e) + { + try + { + UnloadPluginAssembly(); + } + catch (Exception ex) + { + Log(ex); + } + } + + protected override void Shutdown() + { + try + { + Core.PluginInitComplete -= Core_PluginInitComplete; + Core.PluginTermComplete -= Core_PluginTermComplete; + Core.FilterInitComplete -= Core_FilterInitComplete; + AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve; + UnloadPluginAssembly(); + } + catch (Exception ex) + { + Log(ex); + } + } + + private void Core_RenderFrame(object sender, EventArgs e) + { + try + { + if (IsPluginLoaded && needsReload && DateTime.UtcNow - LastDllChange > TimeSpan.FromSeconds(1)) + { + needsReload = false; + Core.RenderFrame -= Core_RenderFrame; + isSubscribedToRenderFrame = false; + LoadPluginAssembly(); + } + } + catch (Exception ex) + { + Log(ex); + } + } + + private void PluginWatcher_Changed(object sender, FileSystemEventArgs e) + { + try + { + // Only reload if it's the main plugin DLL + if (e.Name == PluginAssemblyName) + { + LastDllChange = DateTime.UtcNow; + needsReload = true; + + if (!isSubscribedToRenderFrame) + { + isSubscribedToRenderFrame = true; + Core.RenderFrame += Core_RenderFrame; + } + } + } + catch (Exception ex) + { + Log(ex); + } + } + + private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + try + { + // Extract just the assembly name (without version info) + string assemblyName = args.Name.Split(',')[0] + ".dll"; + string assemblyPath = System.IO.Path.Combine(AssemblyDirectory, assemblyName); + + // If the dependency exists in our plugin directory, load it + if (File.Exists(assemblyPath)) + { + return Assembly.LoadFrom(assemblyPath); + } + } + catch (Exception ex) + { + Log($"AssemblyResolve failed for {args.Name}: {ex.Message}"); + } + + // Return null to let default resolution continue + return null; + } + #endregion + + #region Plugin Loading/Unloading + internal void LoadPluginAssembly() + { + try + { + if (IsPluginLoaded) + { + UnloadPluginAssembly(); + try + { + CoreManager.Current.Actions.AddChatText($"[MosswartMassacre] Reloading {PluginAssemblyName}", 5); + } + catch { } + } + + pluginAssembly = Assembly.Load(File.ReadAllBytes(System.IO.Path.Combine(AssemblyDirectory, PluginAssemblyName))); + pluginType = pluginAssembly.GetType($"{PluginAssemblyNamespace}.PluginCore"); + pluginInstance = Activator.CreateInstance(pluginType); + + // Set the AssemblyDirectory property if it exists + var assemblyDirProperty = pluginType.GetProperty("AssemblyDirectory", BindingFlags.Public | BindingFlags.Static); + assemblyDirProperty?.SetValue(null, AssemblyDirectory); + + // Set the IsHotReload flag if it exists + var isHotReloadProperty = pluginType.GetProperty("IsHotReload", BindingFlags.Public | BindingFlags.Static); + isHotReloadProperty?.SetValue(null, true); + + // The original template doesn't set up Host - it just calls Startup + // The plugin should use CoreManager.Current.Actions instead of MyHost for hot reload scenarios + // We'll set a flag so the plugin knows it's being hot loaded + + // Call Startup method + var startupMethod = pluginType.GetMethod("Startup", BindingFlags.NonPublic | BindingFlags.Instance); + startupMethod.Invoke(pluginInstance, new object[] { }); + + IsPluginLoaded = true; + } + catch (Exception ex) + { + Log(ex); + } + } + + private void UnloadPluginAssembly() + { + try + { + if (pluginInstance != null && pluginType != null) + { + MethodInfo shutdownMethod = pluginType.GetMethod("Shutdown", BindingFlags.NonPublic | BindingFlags.Instance); + shutdownMethod.Invoke(pluginInstance, null); + pluginInstance = null; + pluginType = null; + pluginAssembly = null; + } + IsPluginLoaded = false; + } + catch (Exception ex) + { + Log(ex); + } + } + #endregion + + private void Log(Exception ex) + { + Log(ex.ToString()); + } + + private void Log(string message) + { + File.AppendAllText(System.IO.Path.Combine(AssemblyDirectory, "loader_log.txt"), $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}\n"); + try + { + CoreManager.Current.Actions.AddChatText($"[MosswartMassacre.Loader] {message}", 3); + } + catch { } + } + } +} \ No newline at end of file diff --git a/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj b/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj new file mode 100644 index 0000000..59c8e03 --- /dev/null +++ b/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj @@ -0,0 +1,37 @@ + + + + net48 + Library + true + x86 + 1.0.0 + 8 + {A1B2C3D4-E5F6-7890-1234-567890ABCDEF} + MosswartMassacre.Loader + MosswartMassacre.Loader + true + + + ..\MosswartMassacre\bin\Debug\ + true + full + + + ..\MosswartMassacre\bin\Release\ + pdbonly + true + + + + ..\MosswartMassacre\lib\Decal.Adapter.dll + False + + + False + False + ..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Core.DLL + False + + + \ No newline at end of file diff --git a/MosswartMassacre/DecalHarmonyClean.cs b/MosswartMassacre/DecalHarmonyClean.cs index 5e75f6a..5a6e740 100644 --- a/MosswartMassacre/DecalHarmonyClean.cs +++ b/MosswartMassacre/DecalHarmonyClean.cs @@ -62,10 +62,9 @@ namespace MosswartMassacre // PATHWAY 2: Target Host.Actions.AddChatText (what our plugin uses) PatchHostActions(); } - catch (Exception ex) + catch { // Only log if completely unable to apply any patches - System.Diagnostics.Debug.WriteLine($"Patch application failed: {ex.Message}"); } } @@ -93,15 +92,13 @@ namespace MosswartMassacre { ApplySinglePatch(method, prefixMethodName); } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"HooksWrapper patch failed: {ex.Message}"); } } } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"HooksWrapper patch failed: {ex.Message}"); } } @@ -142,18 +139,16 @@ namespace MosswartMassacre { ApplySinglePatch(method, prefixMethodName); } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Host.Actions patch failed: {ex.Message}"); } } // PATHWAY 3: Try to patch at PluginHost level PatchPluginHost(); } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Host.Actions patch failed: {ex.Message}"); } } @@ -182,21 +177,18 @@ namespace MosswartMassacre string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length; ApplySinglePatch(method, prefixMethodName); } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"CoreActions patch failed: {ex.Message}"); } } } } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"CoreManager patch failed: {ex.Message}"); } } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"PluginHost patch failed: {ex.Message}"); } } @@ -218,9 +210,8 @@ namespace MosswartMassacre patchesApplied = true; } } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Failed to apply patch {prefixMethodName}: {ex.Message}"); } } @@ -236,7 +227,6 @@ namespace MosswartMassacre { var parameters = method.GetParameters(); string paramInfo = string.Join(", ", parameters.Select(p => p.ParameterType.Name)); - System.Diagnostics.Debug.WriteLine($"AddChatText({paramInfo})"); } } @@ -254,9 +244,8 @@ namespace MosswartMassacre } patchesApplied = false; } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Harmony cleanup error: {ex.Message}"); } } @@ -308,10 +297,8 @@ namespace MosswartMassacre // Always increment to verify patch is working DecalHarmonyClean.messagesIntercepted++; - // DEBUG: Log ALL intercepted messages for troubleshooting if (PluginCore.AggressiveChatStreamingEnabled) { - DecalHarmonyClean.AddDebugLog($"[DEBUG] HOOKS-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color})"); } // Process ALL messages (including our own) for streaming @@ -326,10 +313,9 @@ namespace MosswartMassacre // Always return true to let the original AddChatText continue return true; } - catch (Exception ex) + catch { // Never let our interception break other plugins - System.Diagnostics.Debug.WriteLine($"Harmony interception error: {ex.Message}"); return true; } } @@ -352,9 +338,8 @@ namespace MosswartMassacre return true; } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Harmony interception3 error: {ex.Message}"); return true; } } @@ -377,9 +362,8 @@ namespace MosswartMassacre return true; } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Harmony generic interception error: {ex.Message}"); return true; } } @@ -399,7 +383,6 @@ namespace MosswartMassacre var fullMessage = $"{timestamp} [{sourcePlugin}] {text}"; // Debug logging - System.Diagnostics.Debug.WriteLine($"[TARGET] HARMONY CAPTURED ({source}): {fullMessage}"); // Stream to WebSocket if both debug streaming AND WebSocket are enabled if (PluginCore.AggressiveChatStreamingEnabled && PluginCore.WebSocketEnabled) @@ -407,9 +390,8 @@ namespace MosswartMassacre Task.Run(() => WebSocket.SendChatTextAsync(color, text)); } } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Message processing error: {ex.Message}"); } } @@ -447,10 +429,8 @@ namespace MosswartMassacre { DecalHarmonyClean.messagesIntercepted++; - // DEBUG: Log ALL intercepted messages for troubleshooting if (PluginCore.AggressiveChatStreamingEnabled) { - DecalHarmonyClean.AddDebugLog($"[DEBUG] HOST-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color}, target={target})"); } if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]")) @@ -462,9 +442,8 @@ namespace MosswartMassacre return true; } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions interception error: {ex.Message}"); return true; } } @@ -487,9 +466,8 @@ namespace MosswartMassacre return true; } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions 4param interception error: {ex.Message}"); return true; } } @@ -512,9 +490,8 @@ namespace MosswartMassacre return true; } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions generic interception error: {ex.Message}"); return true; } } @@ -532,7 +509,6 @@ namespace MosswartMassacre if (PluginCore.AggressiveChatStreamingEnabled) { - DecalHarmonyClean.AddDebugLog($"[DEBUG] CORE2-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color})"); } if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]")) @@ -544,9 +520,8 @@ namespace MosswartMassacre return true; } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Harmony Core2 interception error: {ex.Message}"); return true; } } @@ -562,7 +537,6 @@ namespace MosswartMassacre if (PluginCore.AggressiveChatStreamingEnabled) { - DecalHarmonyClean.AddDebugLog($"[DEBUG] CORE3-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color}, target={target})"); } if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]")) @@ -574,9 +548,8 @@ namespace MosswartMassacre return true; } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Harmony Core3 interception error: {ex.Message}"); return true; } } diff --git a/MosswartMassacre/FlagTrackerData.cs b/MosswartMassacre/FlagTrackerData.cs index 2db33b5..9ac261d 100644 --- a/MosswartMassacre/FlagTrackerData.cs +++ b/MosswartMassacre/FlagTrackerData.cs @@ -391,9 +391,8 @@ namespace MosswartMassacre { // DECAL API uses CharacterFilter.GetCharProperty for character properties aug.CurrentValue = characterFilter.GetCharProperty(aug.IntId.Value); - // Debug disabled: PluginCore.WriteToChat($"Debug: {aug.Name} = {aug.CurrentValue} (Property: {aug.IntId.Value})"); } - catch (Exception ex) + catch { // Try alternative access using reflection try @@ -402,37 +401,31 @@ namespace MosswartMassacre if (valuesMethod != null) { aug.CurrentValue = (int)valuesMethod.Invoke(playerObject, new object[] { aug.IntId.Value }); - PluginCore.WriteToChat($"Debug: {aug.Name} = {aug.CurrentValue} via reflection (ID: {aug.IntId.Value})"); } else { aug.CurrentValue = 0; - PluginCore.WriteToChat($"Debug: {aug.Name} - Values method not found"); } } - catch (Exception ex2) + catch { aug.CurrentValue = 0; - PluginCore.WriteToChat($"Debug: {aug.Name} - Failed: {ex.Message}, Reflection: {ex2.Message}"); } } } else { aug.CurrentValue = 0; - PluginCore.WriteToChat($"Debug: {aug.Name} - Player object is null"); } } else { aug.CurrentValue = 0; - PluginCore.WriteToChat($"Debug: {aug.Name} - CharacterFilter is null"); } } - catch (Exception ex) + catch { aug.CurrentValue = 0; - PluginCore.WriteToChat($"Debug: {aug.Name} - Exception: {ex.Message}"); } } else @@ -512,12 +505,10 @@ namespace MosswartMassacre { // Use CharacterFilter.GetCharProperty for luminance auras aura.CurrentValue = characterFilter.GetCharProperty(aura.IntId); - // Debug disabled: PluginCore.WriteToChat($"Debug: {aura.Name} = {aura.CurrentValue}/{aura.Cap} (Property: {aura.IntId})"); } - catch (Exception ex) + catch { aura.CurrentValue = 0; - PluginCore.WriteToChat($"Debug: {aura.Name} - Failed to read: {ex.Message}"); } } } @@ -552,14 +543,12 @@ namespace MosswartMassacre recall.IconId = GetSpellIcon(recall.SpellId); } } - catch (Exception ex) + catch { recall.IsKnown = false; - PluginCore.WriteToChat($"Debug: Error checking {recall.Name}: {ex.Message}"); } } - // PluginCore.WriteToChat($"Debug: Recall spells refresh completed - {knownCount}/{RecallSpells.Count} known"); // Debug disabled } catch (Exception ex) { @@ -705,9 +694,8 @@ namespace MosswartMassacre } } } - catch (Exception ex) + catch { - PluginCore.WriteToChat($"Debug: GetRealSpellIcon FileService error: {ex.Message}"); } // Method 2: Use known spell icon mappings for cantrips @@ -848,7 +836,6 @@ namespace MosswartMassacre // Add offset for spell icons to display correctly in VVS // Based on MagTools pattern, spell icons need the offset for display int finalIconId = iconId + 0x6000000; - PluginCore.WriteToChat($"Debug: Found cantrip spell {spellId} -> raw icon {iconId} -> final icon 0x{finalIconId:X}"); return finalIconId; } @@ -900,11 +887,9 @@ namespace MosswartMassacre { try { - PluginCore.WriteToChat("Debug: RefreshCantrips() starting"); if (CoreManager.Current?.CharacterFilter?.Name == null) { - PluginCore.WriteToChat("Debug: No character filter available"); return; } @@ -913,11 +898,9 @@ namespace MosswartMassacre if (playerObject == null) { - PluginCore.WriteToChat("Debug: No player object found"); return; } - PluginCore.WriteToChat($"Debug: Character {characterFilter.Name} found, {playerObject.ActiveSpellCount} active spells"); // Clear dynamic skill lists Cantrips["Specialized Skills"].Clear(); @@ -940,24 +923,20 @@ namespace MosswartMassacre var enchantments = characterFilter.Enchantments; if (enchantments != null) { - PluginCore.WriteToChat($"Debug: Found {enchantments.Count} active enchantments"); for (int i = 0; i < enchantments.Count; i++) { var ench = enchantments[i]; var spell = SpellManager.GetSpell(ench.SpellId); if (spell != null && spell.CantripLevel != Mag.Shared.Spells.Spell.CantripLevels.None) { - PluginCore.WriteToChat($"Debug: Found cantrip - Spell {ench.SpellId}: {spell.Name} (Level: {spell.CantripLevel})"); DetectCantrip(ench.SpellId); } } } else { - PluginCore.WriteToChat("Debug: No enchantments to scan"); } - // Debug: Always show spell info regardless of count // Compute final icon IDs for all cantrips after refresh foreach (var category in Cantrips) { @@ -1057,7 +1036,6 @@ namespace MosswartMassacre if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Specialized) { - PluginCore.WriteToChat($"Debug: Adding specialized skill: {skillName} (ID {skillId})"); Cantrips["Specialized Skills"][skillName] = new CantripInfo { Name = skillName, @@ -1068,7 +1046,6 @@ namespace MosswartMassacre } else if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Trained) { - PluginCore.WriteToChat($"Debug: Adding trained skill: {skillName} (ID {skillId})"); Cantrips["Trained Skills"][skillName] = new CantripInfo { Name = skillName, @@ -1097,14 +1074,12 @@ namespace MosswartMassacre var characterFilter = CoreManager.Current.CharacterFilter; if (characterFilter == null) { - PluginCore.WriteToChat($"Debug: CharacterFilter is null for skill {skillId}"); return GetFallbackSkillIcon(skillId); } // Validate skillId range for DECAL API if (skillId < 1 || skillId > 54) { - PluginCore.WriteToChat($"Debug: Invalid skill ID {skillId} (must be 1-54)"); return GetFallbackSkillIcon(skillId); } @@ -1113,21 +1088,17 @@ namespace MosswartMassacre var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId]; if (skillInfo == null) { - PluginCore.WriteToChat($"Debug: No skill info found for skill {skillId}"); return GetFallbackSkillIcon(skillId); } - PluginCore.WriteToChat($"Debug: Attempting icon access for skill {skillId} ({GetSkillName(skillId)})"); // Try to access skill icon via reflection (DECAL's SkillInfoWrapper.Dat property) var skillType = skillInfo.GetType(); - PluginCore.WriteToChat($"Debug: SkillInfo type: {skillType.Name}"); // Method 1: Try FileService SkillTable approach (most reliable) int realIconId = GetRealSkillIconFromDat(skillId); if (realIconId > 0) { - PluginCore.WriteToChat($"Debug: Found real skill icon {realIconId} for skill {skillId}, applying offset"); return realIconId + 0x6000000; } @@ -1139,7 +1110,6 @@ namespace MosswartMassacre if (datObject != null) { var datType = datObject.GetType(); - PluginCore.WriteToChat($"Debug: Dat object type: {datType.Name}"); // Try the exact property names from AC system string[] iconPropertyNames = { "IconID", "Icon", "IconId", "uiGraphic", "GraphicID" }; @@ -1152,15 +1122,12 @@ namespace MosswartMassacre var iconValue = iconProperty.GetValue(datObject, null); if (iconValue != null) { - PluginCore.WriteToChat($"Debug: Found {propName} property with value {iconValue} (type: {iconValue.GetType().Name})"); if (iconValue is int iconId && iconId > 0) { - PluginCore.WriteToChat($"Debug: Using icon {iconId} from {propName} for skill {skillId}"); return iconId + 0x6000000; } else if (iconValue is uint uiconId && uiconId > 0) { - PluginCore.WriteToChat($"Debug: Using uint icon {uiconId} from {propName} for skill {skillId}"); return (int)uiconId + 0x6000000; } } @@ -1174,15 +1141,12 @@ namespace MosswartMassacre var iconValue = iconField.GetValue(datObject); if (iconValue != null) { - PluginCore.WriteToChat($"Debug: Found {propName} field with value {iconValue} (type: {iconValue.GetType().Name})"); if (iconValue is int iconId && iconId > 0) { - PluginCore.WriteToChat($"Debug: Using icon {iconId} from field {propName} for skill {skillId}"); return iconId + 0x6000000; } else if (iconValue is uint uiconId && uiconId > 0) { - PluginCore.WriteToChat($"Debug: Using uint icon {uiconId} from field {propName} for skill {skillId}"); return (int)uiconId + 0x6000000; } } @@ -1190,8 +1154,6 @@ namespace MosswartMassacre } } - // Debug: List all available properties and fields on Dat object - PluginCore.WriteToChat($"Debug: Available properties on {datType.Name}:"); foreach (var prop in datType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { try @@ -1207,12 +1169,10 @@ namespace MosswartMassacre } else { - PluginCore.WriteToChat($"Debug: Dat property exists but returns null for skill {skillId}"); } } else { - PluginCore.WriteToChat($"Debug: No Dat property found on SkillInfoWrapper for skill {skillId}"); } // Method 3: Try direct properties on SkillInfoWrapper @@ -1225,24 +1185,20 @@ namespace MosswartMassacre var iconValue = iconProperty.GetValue(skillInfo, null); if (iconValue is int iconId && iconId > 0) { - PluginCore.WriteToChat($"Debug: Using direct icon {iconId} from {propName} for skill {skillId}"); return iconId + 0x6000000; } } } } - catch (Exception ex) + catch { - PluginCore.WriteToChat($"Debug: Skill access failed for skill {skillId}: {ex.Message}"); } // Fallback to predefined mapping - PluginCore.WriteToChat($"Debug: Using fallback icon for skill {skillId}"); return GetFallbackSkillIcon(skillId); } - catch (Exception ex) + catch { - PluginCore.WriteToChat($"Debug: Error in GetSkillIconId for skill {skillId}: {ex.Message}"); return GetFallbackSkillIcon(skillId); } } @@ -1280,7 +1236,6 @@ namespace MosswartMassacre { // Access SkillTable via reflection to get skill data var skillTableType = fileService.SkillTable.GetType(); - PluginCore.WriteToChat($"Debug: SkillTable type: {skillTableType.Name}"); // Look for methods that can get skill by ID var methods = skillTableType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); @@ -1296,7 +1251,6 @@ namespace MosswartMassacre var skillData = method.Invoke(fileService.SkillTable, new object[] { skillId }); if (skillData != null) { - PluginCore.WriteToChat($"Debug: Found skill data via {method.Name}: {skillData.GetType().Name}"); // Look for icon properties on the skill data var skillDataType = skillData.GetType(); @@ -1310,42 +1264,36 @@ namespace MosswartMassacre var iconValue = iconProp.GetValue(skillData, null); if (iconValue is int iconInt && iconInt > 0) { - PluginCore.WriteToChat($"Debug: Found skill icon {iconInt} via FileService.{method.Name}.{propName}"); return iconInt; } else if (iconValue is uint iconUint && iconUint > 0) { - PluginCore.WriteToChat($"Debug: Found skill icon {iconUint} via FileService.{method.Name}.{propName}"); return (int)iconUint; } } } } } - catch (Exception ex) + catch { // Method call failed, try next one - PluginCore.WriteToChat($"Debug: Method {method.Name} failed: {ex.Message}"); } } } } } - catch (Exception ex) + catch { - PluginCore.WriteToChat($"Debug: FileService SkillTable access failed: {ex.Message}"); } } else { - PluginCore.WriteToChat($"Debug: FileService or SkillTable is null"); } return 0; // No icon found } - catch (Exception ex) + catch { - PluginCore.WriteToChat($"Debug: GetRealSkillIconFromDat failed: {ex.Message}"); return 0; } } @@ -1420,12 +1368,10 @@ namespace MosswartMassacre if (skillIconMap.ContainsKey(skillId)) { - PluginCore.WriteToChat($"Debug: Using fallback icon 0x{skillIconMap[skillId]:X} for skill {skillId} ({GetSkillName(skillId)})"); return skillIconMap[skillId]; } // Final fallback to proven working icon from recalls system - PluginCore.WriteToChat($"Debug: Using default fallback icon 0x6002D14 for unknown skill {skillId}"); return 0x6002D14; // Portal icon - confirmed working in recalls } @@ -1457,12 +1403,10 @@ namespace MosswartMassacre string spellName = GetSpellName(spellId); if (string.IsNullOrEmpty(spellName)) { - PluginCore.WriteToChat($"Debug: FAILED to get spell name for spell ID {spellId}"); return; } // Debug output to see what spells we're processing - PluginCore.WriteToChat($"Debug: Processing spell ID {spellId}: '{spellName}'"); // Define cantrip levels and their patterns var cantripPatterns = new Dictionary @@ -1484,42 +1428,35 @@ namespace MosswartMassacre // Remove the level prefix to get the skill/attribute name string skillPart = spellName.Substring(pattern.Length + 1); - PluginCore.WriteToChat($"Debug: Found {level} cantrip, skillPart='{skillPart}'"); // Get the spell icon for this cantrip spell int spellIconId = GetRealSpellIcon(spellId); if (spellIconId == 0) { spellIconId = 0x6002D14; // Default fallback icon - PluginCore.WriteToChat($"Debug: No real icon found for spell {spellId}, using default"); } else { - PluginCore.WriteToChat($"Debug: Got spell icon 0x{spellIconId:X} for spell {spellId}"); } // Try to match Protection Auras first (exact format: "Minor Armor", "Epic Bludgeoning Ward") if (MatchProtectionAura(skillPart, level, color, spellIconId)) { - PluginCore.WriteToChat($"Debug: Matched as Protection Aura: '{skillPart}' with spell icon {spellIconId}"); return; } // Try to match Attributes (exact format: "Minor Strength", "Epic Focus") if (MatchAttribute(skillPart, level, color, spellIconId)) { - PluginCore.WriteToChat($"Debug: Matched as Attribute: '{skillPart}' with spell icon {spellIconId}"); return; } // Try to match Skills using the replacement mappings if (MatchSkill(skillPart, level, color, spellIconId)) { - PluginCore.WriteToChat($"Debug: Matched as Skill: '{skillPart}' with spell icon {spellIconId}"); return; } - PluginCore.WriteToChat($"Debug: No match found for: '{skillPart}' (level: {level}) - Full spell: '{spellName}'"); } } catch (Exception ex) @@ -1610,13 +1547,11 @@ namespace MosswartMassacre ["Willpower"] = "Willpower" // "Epic Willpower" -> Willpower }; - PluginCore.WriteToChat($"Debug: MatchAttribute checking '{cleanedSkillPart}' for {level}"); foreach (var mapping in attributeMappings) { if (cleanedSkillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase)) { - PluginCore.WriteToChat($"Debug: Found mapping match! '{mapping.Key}' -> '{mapping.Value}'"); // Create the cantrip entry if it doesn't exist if (!Cantrips["Attributes"].ContainsKey(mapping.Value)) @@ -1632,7 +1567,6 @@ namespace MosswartMassacre var cantrip = Cantrips["Attributes"][mapping.Value]; if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value)) { - PluginCore.WriteToChat($"Debug: Setting {mapping.Value} to {level}"); cantrip.Value = level; cantrip.Color = color; cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip @@ -1646,7 +1580,6 @@ namespace MosswartMassacre { if (cleanedSkillPart.IndexOf(mapping.Key, StringComparison.OrdinalIgnoreCase) >= 0) { - PluginCore.WriteToChat($"Debug: Found partial mapping match! '{cleanedSkillPart}' contains '{mapping.Key}' -> '{mapping.Value}'"); // Create the cantrip entry if it doesn't exist if (!Cantrips["Attributes"].ContainsKey(mapping.Value)) @@ -1662,7 +1595,6 @@ namespace MosswartMassacre var cantrip = Cantrips["Attributes"][mapping.Value]; if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value)) { - PluginCore.WriteToChat($"Debug: Setting {mapping.Value} to {level} via partial match"); cantrip.Value = level; cantrip.Color = color; cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip @@ -1764,9 +1696,8 @@ namespace MosswartMassacre } return ""; } - catch (Exception ex) + catch { - PluginCore.WriteToChat($"Debug: Error getting spell {spellId}: {ex.Message}"); return ""; } } @@ -1812,7 +1743,6 @@ namespace MosswartMassacre foreach (var spellName in testSpells) { - PluginCore.WriteToChat($"Debug: Testing detection for: {spellName}"); TestDetectCantripByName(spellName); } } @@ -1827,7 +1757,6 @@ namespace MosswartMassacre try { // Simulate the detection logic with a fake spell name - PluginCore.WriteToChat($"Debug: Processing spell: {spellName}"); // Define cantrip levels and their patterns var cantripPatterns = new Dictionary @@ -1850,7 +1779,6 @@ namespace MosswartMassacre // Remove the level prefix to get the skill/attribute name string skillPart = spellName.Substring(pattern.Length + 1); - PluginCore.WriteToChat($"Debug: Detected {level} cantrip for {skillPart}"); // Get a test spell icon (use default for testing) int testSpellIconId = 0x6002D14; @@ -1858,25 +1786,21 @@ namespace MosswartMassacre // Try to match Protection Auras first if (MatchProtectionAura(skillPart, level, color, testSpellIconId)) { - PluginCore.WriteToChat($"Debug: Matched protection aura: {skillPart}"); return; } // Try to match Attributes if (MatchAttribute(skillPart, level, color, testSpellIconId)) { - PluginCore.WriteToChat($"Debug: Matched attribute: {skillPart}"); return; } // Try to match Skills if (MatchSkill(skillPart, level, color, testSpellIconId)) { - PluginCore.WriteToChat($"Debug: Matched skill: {skillPart}"); return; } - PluginCore.WriteToChat($"Debug: No match found for: {skillPart}"); } } catch (Exception ex) diff --git a/MosswartMassacre/FodyWeavers.xml b/MosswartMassacre/FodyWeavers.xml new file mode 100644 index 0000000..8c6d208 --- /dev/null +++ b/MosswartMassacre/FodyWeavers.xml @@ -0,0 +1,9 @@ + + + + + YamlDotNet + Newtonsoft.Json + + + \ No newline at end of file diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index 81236e7..1c081fe 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -178,6 +178,7 @@ + True @@ -229,4 +230,13 @@ + + + + This project references NuGet package(s) that are missing on this machine. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/MosswartMassacre/MossyInventory.cs b/MosswartMassacre/MossyInventory.cs index 0f6f5bd..c99cd6b 100644 --- a/MosswartMassacre/MossyInventory.cs +++ b/MosswartMassacre/MossyInventory.cs @@ -22,12 +22,20 @@ namespace MosswartMassacre // 1) Character name var characterName = CoreManager.Current.CharacterFilter.Name; - // 2) Plugin folder - var pluginFolder = Path.GetDirectoryName( - System.Reflection.Assembly - .GetExecutingAssembly() - .Location - ); + // 2) Plugin folder - handle hot reload scenarios + string pluginFolder; + if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory)) + { + pluginFolder = PluginCore.AssemblyDirectory; + } + else + { + pluginFolder = Path.GetDirectoryName( + System.Reflection.Assembly + .GetExecutingAssembly() + .Location + ); + } // 3) Character-specific folder path var characterFolder = Path.Combine(pluginFolder, characterName); @@ -86,7 +94,20 @@ namespace MosswartMassacre try { loginComplete = true; - if (!PluginSettings.Instance.InventoryLog) + + // Defensive check - settings might not be initialized yet due to event handler order + bool inventoryLogEnabled; + try + { + inventoryLogEnabled = PluginSettings.Instance.InventoryLog; + } + catch (InvalidOperationException) + { + PluginCore.WriteToChat("[INV] Settings not ready, skipping inventory check"); + return; + } + + if (!inventoryLogEnabled) return; if (!File.Exists(InventoryFileName)) @@ -112,7 +133,16 @@ namespace MosswartMassacre private void WorldFilter_CreateObject(object sender, CreateObjectEventArgs e) { - if (!loginComplete || !PluginSettings.Instance.InventoryLog) return; + if (!loginComplete) return; + + try + { + if (!PluginSettings.Instance.InventoryLog) return; + } + catch (InvalidOperationException) + { + return; // Settings not ready, skip silently + } if (!e.New.HasIdData && ObjectClassNeedsIdent(e.New.ObjectClass, e.New.Name) && !requestedIds.Contains(e.New.Id) @@ -125,7 +155,16 @@ namespace MosswartMassacre private void WorldFilter_ChangeObject(object sender, ChangeObjectEventArgs e) { - if (!loginComplete || !PluginSettings.Instance.InventoryLog) return; + if (!loginComplete) return; + + try + { + if (!PluginSettings.Instance.InventoryLog) return; + } + catch (InvalidOperationException) + { + return; // Settings not ready, skip silently + } if (loggedInAndWaitingForIdData) { @@ -162,7 +201,14 @@ namespace MosswartMassacre { try { - if (!PluginSettings.Instance.InventoryLog) return; + try + { + if (!PluginSettings.Instance.InventoryLog) return; + } + catch (InvalidOperationException) + { + return; // Settings not ready, skip silently + } DumpInventoryToFile(); } catch (Exception ex) diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 787821b..9c3579a 100644 --- a/MosswartMassacre/PluginCore.cs +++ b/MosswartMassacre/PluginCore.cs @@ -20,6 +20,32 @@ namespace MosswartMassacre [FriendlyName("Mosswart Massacre")] public class PluginCore : PluginBase { + // Hot Reload Support Properties + private static string _assemblyDirectory = null; + public static string AssemblyDirectory + { + get + { + if (_assemblyDirectory == null) + { + try + { + _assemblyDirectory = System.IO.Path.GetDirectoryName(typeof(PluginCore).Assembly.Location); + } + catch + { + _assemblyDirectory = Environment.CurrentDirectory; + } + } + return _assemblyDirectory; + } + set + { + _assemblyDirectory = value; + } + } + public static bool IsHotReload { get; set; } + internal static PluginHost MyHost; internal static int totalKills = 0; internal static int rareCount = 0; @@ -88,6 +114,10 @@ namespace MosswartMassacre 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; @@ -97,9 +127,41 @@ namespace MosswartMassacre { try { - MyHost = Host; + // 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 CoreManager.Current.ChatBoxMessage += new EventHandler(OnChatText); @@ -165,7 +227,7 @@ namespace MosswartMassacre PluginSettings.Save(); if (TelemetryEnabled) Telemetry.Stop(); // ensure no dangling timer / HttpClient - WriteToChat("Mosswart Massacre is shutting down..."); + WriteToChat("Mosswart Massacre is shutting down!!!!!"); // Unsubscribe from chat message event CoreManager.Current.ChatBoxMessage -= new EventHandler(OnChatText); @@ -203,6 +265,22 @@ namespace MosswartMassacre 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 @@ -279,6 +357,160 @@ namespace MosswartMassacre // 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() @@ -658,7 +890,6 @@ namespace MosswartMassacre { try { - // WriteToChat($"[Debug] Chat Color: {e.Color}, Message: {e.Text}"); if (IsKilledByMeMessage(e.Text)) { @@ -889,7 +1120,31 @@ namespace MosswartMassacre } public static void WriteToChat(string message) { - MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1); + 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() { @@ -997,6 +1252,7 @@ namespace MosswartMassacre WriteToChat("Usage: /mm ws "); } } + else { WriteToChat("Usage: /mm ws "); @@ -1019,10 +1275,9 @@ namespace MosswartMassacre 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 testdeath - Manual death tracking test and diagnostics"); 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 gui - Manually initialize/reinitialize GUI!!!"); break; case "report": TimeSpan elapsed = DateTime.Now - statsStartTime; @@ -1134,7 +1389,6 @@ namespace MosswartMassacre WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ==="); WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}"); WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}"); - WriteToChat($"Debug Streaming: {AggressiveChatStreamingEnabled}"); WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}"); // Test Harmony availability @@ -1186,31 +1440,7 @@ namespace MosswartMassacre case "harmonyraw": - try - { - WriteToChat("=== Raw Harmony Interception Log ==="); - var debugEntries = DecalHarmonyClean.GetDebugLog(); - if (debugEntries.Length == 0) - { - WriteToChat("No debug entries found. Enable debug streaming first: /mm decaldebug enable"); - } - else - { - WriteToChat($"Last {debugEntries.Length} intercepted messages:"); - foreach (var entry in debugEntries.Skip(Math.Max(0, debugEntries.Length - 10))) - { - WriteToChat($" {entry}"); - } - if (debugEntries.Length > 10) - { - WriteToChat($"... ({debugEntries.Length - 10} more entries)"); - } - } - } - catch (Exception ex) - { - WriteToChat($"Debug log error: {ex.Message}"); - } + // Debug functionality removed break; case "initgui": @@ -1394,48 +1624,7 @@ namespace MosswartMassacre break; case "debugtaper": - try - { - WriteToChat("=== Taper Tracking Debug Info ==="); - WriteToChat($"Cached Count: {cachedPrismaticCount}"); - WriteToChat($"Last Count: {lastPrismaticCount}"); - WriteToChat($"Tracked Containers: {trackedTaperContainers.Count}"); - WriteToChat($"Known Stack Sizes: {lastKnownStackSizes.Count}"); - - if (trackedTaperContainers.Count > 0) - { - WriteToChat("=== Tracked Taper Details ==="); - foreach (var kvp in trackedTaperContainers) - { - int itemId = kvp.Key; - int containerId = kvp.Value; - int stackSize = lastKnownStackSizes.TryGetValue(itemId, out int size) ? size : -1; - string containerType = containerId == CoreManager.Current.CharacterFilter.Id ? "main pack" : "side pack"; - WriteToChat($" Item {itemId}: {containerType} (container {containerId}), stack: {stackSize}"); - } - } - else - { - WriteToChat("No tapers currently tracked!"); - } - - // Cross-check with actual inventory - WriteToChat("=== Cross-Check with Actual Inventory ==="); - int actualCount = Utils.GetItemStackSize("Prismatic Taper"); - WriteToChat($"Utils.GetItemStackSize: {actualCount}"); - if (cachedPrismaticCount != actualCount) - { - WriteToChat($"[WARNING] Count mismatch! Cached: {cachedPrismaticCount}, Actual: {actualCount}"); - } - else - { - WriteToChat("[OK] Cached count matches actual count"); - } - } - catch (Exception ex) - { - WriteToChat($"Debug taper error: {ex.Message}"); - } + // Debug functionality removed break; case "finditem": diff --git a/MosswartMassacre/PluginSettings.cs b/MosswartMassacre/PluginSettings.cs index e68f71e..35113d5 100644 --- a/MosswartMassacre/PluginSettings.cs +++ b/MosswartMassacre/PluginSettings.cs @@ -32,7 +32,18 @@ namespace MosswartMassacre { // determine plugin folder and character-specific folder string characterName = CoreManager.Current.CharacterFilter.Name; - string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + + // For hot reload scenarios, use the AssemblyDirectory set by the Loader + // For normal loading, fall back to the executing assembly location + string pluginFolder; + if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory)) + { + pluginFolder = PluginCore.AssemblyDirectory; + } + else + { + pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + } // Path for character-specific folder string characterFolder = Path.Combine(pluginFolder, characterName); @@ -40,7 +51,14 @@ namespace MosswartMassacre // Create the character folder if it doesn't exist if (!Directory.Exists(characterFolder)) { - Directory.CreateDirectory(characterFolder); + try + { + Directory.CreateDirectory(characterFolder); + } + catch (Exception ex) + { + PluginCore.DispatchChatToBoxWithPluginIntercept($"[Settings] Failed to create character folder: {ex.Message}"); + } } // YAML file is now in the character-specific folder diff --git a/MosswartMassacre/QuestManager.cs b/MosswartMassacre/QuestManager.cs index 5973d70..a357e51 100644 --- a/MosswartMassacre/QuestManager.cs +++ b/MosswartMassacre/QuestManager.cs @@ -62,6 +62,23 @@ namespace MosswartMassacre } #endregion + #region Quest Name Mapping + public string GetFriendlyQuestName(string questStamp) + { + return QuestNames.GetFriendlyName(questStamp); + } + + public string GetQuestDisplayName(string questStamp) + { + return QuestNames.GetDisplayName(questStamp); + } + + public int GetQuestNameMappingsCount() + { + return QuestNames.QuestStampToName.Count; + } + #endregion + #region Quest Parsing private void OnChatBoxMessage(object sender, ChatTextInterceptEventArgs e) { diff --git a/MosswartMassacre/QuestNames.cs b/MosswartMassacre/QuestNames.cs new file mode 100644 index 0000000..9c782b8 --- /dev/null +++ b/MosswartMassacre/QuestNames.cs @@ -0,0 +1,228 @@ +using System.Collections.Generic; + +namespace MosswartMassacre +{ + /// + /// Static quest name mappings from quest stamp to friendly display name + /// Based on questtracker repository data + /// + public static class QuestNames + { + /// + /// Dictionary mapping quest stamps to friendly quest names + /// + public static readonly Dictionary QuestStampToName = new Dictionary + { + // Character-specific Quest Stamps (from actual /myquests output) + ["30minattributes"] = "30 Minute Attribute Gems Timer", + ["academeyexittokengiven"] = "Academy Exit Token Received", + ["aerbaxchestkey2pickup"] = "Aerbax Chest Key #2 Pickup", + ["anekshaygemofknowledgetimer_monthly"] = "A'nekshay Gem of Knowledge Monthly Timer", + ["anekshaygemoflesserknowledgecollectedinamonth"] = "A'nekshay Gems of Lesser Knowledge Monthly Count", + ["anekshaygemoflesserknowledgetimer_monthly"] = "A'nekshay Gem of Lesser Knowledge Monthly Timer", + ["attributereset30day"] = "30-Day Attribute Reset Timer", + ["augmentationblankgemacquired"] = "Blank Augmentation Gem Pickup Timer", + ["bellowsnewbieturnedin"] = "Blacksmith's Bellows Turned In", + ["bonecrunchkeypickuptimer"] = "Bonecrunch's Key Pickup Timer", + ["callingstonegiven"] = "Calling Stone Turned Over", + ["defeatedbonecrunch"] = "Bonecrunch Defeated", + ["efmlcentermanafieldused"] = "EF Middle Level Center Mana Field Used", + ["efmleastmanafieldused"] = "EF Middle Level East Mana Field Used", + ["efmlnorthmanafieldused"] = "EF Middle Level North Mana Field Used", + ["efmlsouthmanafieldused"] = "EF Middle Level South Mana Field Used", + ["efmlwestmanafieldused"] = "EF Middle Level West Mana Field Used", + ["efulcentermanafieldused"] = "EF Upper Level Center Mana Field Used", + ["efuleastmanafieldused"] = "EF Upper Level East Mana Field Used", + ["efulnorthmanafieldused"] = "EF Upper Level North Mana Field Used", + ["efulsouthmanafieldused"] = "EF Upper Level South Mana Field Used", + ["efulwestmanafieldused"] = "EF Upper Level West Mana Field Used", + ["insatiableeaterjaw"] = "Insatiable Eater Jaw Collection", + ["pathwardencomplete"] = "Pathwarden Visit Complete", + ["pathwardenfound1111"] = "Pathwarden Greeter Encountered", + ["recallsingularitycaul"] = "Recall Singularity Bore Pickup", + ["stipendscollectedinamonth"] = "Monthly Stipends Collected Count", + ["stipendtimer_0812"] = "Stipend Collection Timer", + ["stipendtimer_monthly"] = "Monthly Stipend Timer", + ["upperinsatiablejaw"] = "Upper Insatiable Eater Jaw Collection", + ["usedattributereset"] = "Attribute Reset Used", + ["usedfreeattributereset"] = "Free Attribute Reset Used", + ["usedfreeskillreset"] = "Free Skill Reset Used", + ["usedskillreset"] = "Skill Reset Used", + ["virindiisland"] = "Singularity Island Visit", + + // Kill Tasks + ["turshscalp"] = "Tursh Scalp", + ["polarursuin"] = "Polar Ursuin Kill Task Main Flag Timer", + ["polarursuinkillcount"] = "Polar Ursuin Kill Counter", + ["polardillotask"] = "Polar Dillo Kill Task Main Flag", + ["polardillokills"] = "Polar Dillo Kill Counter", + ["repugnanteaterkilltask"] = "Repugnant Eater Kill Task", + ["repugeaterkillcount"] = "Repugnant Eater Kill Counter", + ["deathcap"] = "Deathcap Thrungus Kill Task", + ["deathcapkillcount"] = "Deathcap Thrungus Kill Counter", + ["grievverv"] = "Grievver Violator Kill Task", + ["grievvervkillcount"] = "Grievver Violator Kill Counter", + ["tuskerg"] = "Tusker Guard Kill Task Main Flag", + ["tuskergkillcount"] = "Tusker Guard Kill Counter", + + // Quest Timers and Pickups + ["blankaug"] = "Blank Aug Gem Pickup Timer", + ["greatcavepenguinegg"] = "Great Cave Penguin Egg Pickup Timer", + ["deathallurecd"] = "Death's Allure Timer Flag", + ["brewmastercover"] = "Brew Master Quest Pickup Timer Cover", + ["brewmasterback"] = "Brew Master Quest Pickup Timer Back", + ["brewmasterpages"] = "Brew Master Quest Pickup Timer Pages", + ["brewmasterspine"] = "Brew Master Quest Pickup Timer Spine", + ["eleonorasheart"] = "Elanora's Heart Quest Pickup Timer", + ["beacongemobtained"] = "Cooldown for obtaining another beacon gem", + ["beaconcomplete"] = "Beacon Quest Complete Timer", + ["sirginaziosword"] = "Pick up of Sir Ginazio's Sword", + + // Major Quests + ["maraudersjaw"] = "Marauder's Lair Quest", + ["fledgemastertusk"] = "Fledge Master's Tusk Quest", + ["crystallinekiller"] = "Crystalline Killer", + ["darkisledelivery"] = "Dark Isle Delivery", + ["defeatingvaeshok"] = "Defeating Vaeshok", + ["hollyjollyhelperquest"] = "Holly Jolly Helper Quest", + ["moarsmenjailbreak"] = "Moarsmen Jailbreak", + ["shamblingarchivistdestroyer"] = "Shambling Archivist Destroyer", + ["tracingthestone"] = "Tracing The Stone", + ["undeadjawcollection"] = "Undead Jaw Collection", + ["weedingofthederutree"] = "Weeding of the Deru Tree", + ["ironbladecommander"] = "Iron Blade Commander", + ["mumiyahhuntingneftet"] = "Mumiyah Hunting Neftet", + ["torgashstasks"] = "Torgash's Tasks", + + // Thrungus Hovels Items + ["stolenfryingpan"] = "Thrungus Hovels", + ["stolenring"] = "Thrungus Hovels", + ["stolenbrewkettle"] = "Thrungus Hovels", + ["stolenamulet"] = "Thrungus Hovels", + ["stolenewer"] = "Thrungus Hovels", + ["stolennecklace"] = "Thrungus Hovels", + ["stolenplatter"] = "Thrungus Hovels", + ["stolenbracelet"] = "Thrungus Hovels", + + // Special Items and Flags + ["ringofkarlun"] = "Knights of Karlun", + ["trainingacademycomplete"] = "Completion of Training Academy for Exit", + ["cowtipcounter"] = "Counter for Cow Tipping", + ["cowtip"] = "Main Timed Flag for Cow Tipping", + ["skillloweringgempickedup"] = "Picked up a forgetfulness gem", + + // Healing Machine Components + ["orbhealingmachine"] = "Healing Machine Orb", + ["pedestalhealingmachine"] = "Healing Machine Pedestal", + ["tihnhealingmachine"] = "Healing Machine Tihn", + ["lavushealingmachine"] = "Healing Machine Lavus", + ["hookhealingmachine"] = "Healing Machine Hook", + + // Eater Jaws + ["ravenouseaterjaw"] = "Ravenous Eater Jaw", + ["insatiableeaterjaw"] = "Insatiable Eater Jaw", + ["engorgedeaterjaw"] = "Engorged Eater Jaw", + ["voraciouseaterjaw"] = "Voracious Eater Jaw", + ["abhorrenteaterjaw"] = "Abhorrent Eater Jaw", + + // Kill Tasks (Extended) + ["altereddrudgekilltask"] = "Altered Drudge Kill Task", + ["altereddrudgekillcount"] = "Altered Drudge Kill Counter", + ["arcticmattekarkilltask"] = "Arctic Mattekar Kill Task", + ["arcticmattekarkillcount"] = "Arctic Mattekar Kill Counter", + ["armoredillohuntingneftetkilltask"] = "Armoredillo Hunting Neftet Kill Task", + ["armoredillohuntingneftetkillcount"] = "Armoredillo Hunting Neftet Kill Counter", + ["augmenteddrudgekilltask"] = "Augmented Drudge Kill Task", + ["augmenteddrudgekillcount"] = "Augmented Drudge Kill Counter", + ["banishedcreaturekilltask"] = "Banished Creature Kill Task", + ["banishedcreaturekillcount"] = "Banished Creature Kill Counter", + ["benekniffiskilltask"] = "Benek Niffis Kill Task", + ["benekniffiskillcount"] = "Benek Niffis Kill Counter", + ["blackcoralgolemkilltask"] = "Black Coral Golem Kill Task", + ["blackcoralgolemkillcount"] = "Black Coral Golem Kill Counter", + ["blessedmoarsmankilltask"] = "Blessed Moarsman Kill Task", + ["blessedmoarsmankillcount"] = "Blessed Moarsman Kill Counter", + ["blightedcoralgolemkilltask"] = "Blighted Coral Golem Kill Task", + ["blightedcoralgolemkillcount"] = "Blighted Coral Golem Kill Counter", + ["bloodshrethkilltask"] = "Blood Shreth Kill Task", + ["bloodshrethkillcount"] = "Blood Shreth Kill Counter", + ["bronzegauntlettrooperkilltask"] = "Bronze Gauntlet Trooper Kill Task", + ["bronzegauntlettrooperkillcount"] = "Bronze Gauntlet Trooper Kill Counter", + ["coppercogtrooperkilltask"] = "Copper Cog Trooper Kill Task", + ["coppercogtrooperkillcount"] = "Copper Cog Trooper Kill Counter", + ["coppergolemkingpinkilltask"] = "Copper Golem Kingpin Kill Task", + ["coppergolemkingpinkillcount"] = "Copper Golem Kingpin Kill Counter", + ["coralgolemkilltask"] = "Coral Golem Kill Task", + ["coralgolemkillcount"] = "Coral Golem Kill Counter", + ["coralgolemviceroykilltask"] = "Coral Golem Viceroy Kill Task", + ["coralgolemviceroykillcount"] = "Coral Golem Viceroy Kill Counter", + ["corruptedgravestonekilltask"] = "Corrupted Gravestone Kill Task", + ["corruptedgravestonekillcount"] = "Corrupted Gravestone Kill Counter", + ["deathcapthrunguskilltask"] = "Deathcap Thrungus Kill Task", + ["deathcapthrunguskillcount"] = "Deathcap Thrungus Kill Counter", + ["desertcactuskilltask"] = "Desert Cactus Kill Task", + ["desertcactuskillcount"] = "Desert Cactus Kill Counter", + ["devourermargulkilltask"] = "Devourer Margul Kill Task", + ["devourermargulkillcount"] = "Devourer Margul Kill Counter", + + // Society and Faction Quests + ["celestialhandintroductioncomplete"] = "Celestial Hand Introduction Complete", + ["eldrytchwebintroductioncomplete"] = "Eldrytch Web Introduction Complete", + ["radiantbloodintroductioncomplete"] = "Radiant Blood Introduction Complete", + ["celestialhandinitiatetest"] = "Celestial Hand Initiate Test", + ["eldrytchwebinitiatetest"] = "Eldrytch Web Initiate Test", + ["radiantbloodinitiatetest"] = "Radiant Blood Initiate Test", + + // Luminance Aura Related + ["aetheriaredemption"] = "Aetheria Redemption", + ["aegisofmerc"] = "Aegis of Merc", + ["lumaugtradein"] = "Luminance Augmentation Trade In", + + // Common AC Quests + ["holtburgtraderskill"] = "Holtburg Trader Skill Quest", + ["shoushitraderskill"] = "Shoushi Trader Skill Quest", + ["yaraqtraderskill"] = "Yaraq Trader Skill Quest", + ["newbiequests"] = "Newbie Academy Quests", + ["moarsmanraid"] = "Moarsman Raid", + ["virindiparadox"] = "Virindi Paradox", + ["portalspace"] = "Portal Space Exploration" + }; + + /// + /// Get friendly name for a quest stamp, with fallback to original stamp + /// + /// The quest stamp to lookup + /// Friendly name if found, otherwise the original quest stamp + public static string GetFriendlyName(string questStamp) + { + if (string.IsNullOrEmpty(questStamp)) + return questStamp; + + return QuestStampToName.TryGetValue(questStamp.ToLower(), out string friendlyName) + ? friendlyName + : questStamp; + } + + /// + /// Get display name with friendly name and original stamp in parentheses + /// + /// The quest stamp to format + /// Formatted display name + public static string GetDisplayName(string questStamp) + { + if (string.IsNullOrEmpty(questStamp)) + return questStamp; + + string friendlyName = GetFriendlyName(questStamp); + + // If we found a mapping, show friendly name with original in parentheses + if (!string.Equals(friendlyName, questStamp, System.StringComparison.OrdinalIgnoreCase)) + { + return $"{friendlyName} ({questStamp})"; + } + + // Otherwise just show the original + return questStamp; + } + } +} \ No newline at end of file diff --git a/MosswartMassacre/Views/FlagTrackerView.cs b/MosswartMassacre/Views/FlagTrackerView.cs index 22cd185..d2696f2 100644 --- a/MosswartMassacre/Views/FlagTrackerView.cs +++ b/MosswartMassacre/Views/FlagTrackerView.cs @@ -34,10 +34,6 @@ namespace MosswartMassacre.Views private HudList lstCantrips; private HudButton btnRefreshCantrips; - // Weapons Tab - private HudList lstWeapons; - private HudButton btnRefreshWeapons; - // Quests Tab private HudList lstQuests; private HudButton btnRefreshQuests; @@ -46,14 +42,25 @@ namespace MosswartMassacre.Views #region Data Management private FlagTrackerData data; - private QuestManager questManager; + private System.Timers.Timer questUpdateTimer; #endregion public FlagTrackerView(PluginCore core) : base(core) { - instance = this; - data = new FlagTrackerData(); - questManager = new QuestManager(); + try + { + instance = this; + data = new FlagTrackerData(); + + // Initialize quest update timer for real-time countdown + questUpdateTimer = new System.Timers.Timer(5000); // Update every 5 seconds + questUpdateTimer.Elapsed += OnQuestTimerUpdate; + questUpdateTimer.AutoReset = true; + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[MossyTracker] Failed to initialize: {ex.Message}"); + } } #region Static Interface @@ -108,17 +115,18 @@ namespace MosswartMassacre.Views { try { - // Create view from XML layout CreateFromXMLResource("MosswartMassacre.ViewXML.flagTracker.xml"); + + if (view == null) + { + PluginCore.WriteToChat("[MossyTracker] Failed to create view"); + return; + } - // Initialize all tab controls InitializeTabControls(); InitializeEventHandlers(); - - // Initialize the base view Initialize(); - // Make the view visible if (view != null) { view.Visible = true; @@ -126,12 +134,19 @@ namespace MosswartMassacre.Views view.Title = "Mossy Tracker v3.0.1.1"; } - // Initial data refresh RefreshAllData(); + + // Start quest update timer + if (questUpdateTimer != null) + { + questUpdateTimer.Start(); + } + + PluginCore.WriteToChat("[MossyTracker] Initialized successfully"); } catch (Exception ex) { - PluginCore.WriteToChat($"Error initializing Flag Tracker view: {ex.Message}"); + PluginCore.WriteToChat($"[MossyTracker] Failed to initialize: {ex.Message}"); } } @@ -139,37 +154,21 @@ namespace MosswartMassacre.Views { try { - // Get main tab view mainTabView = GetControl("mainTabView"); - - // Augmentations Tab lstAugmentations = GetControl("lstAugmentations"); btnRefreshAugs = GetControl("btnRefreshAugs"); - - // Luminance Tab lstLuminanceAuras = GetControl("lstLuminanceAuras"); btnRefreshLum = GetControl("btnRefreshLum"); - - // Recalls Tab lstRecallSpells = GetControl("lstRecallSpells"); btnRefreshRecalls = GetControl("btnRefreshRecalls"); - - // Cantrips Tab lstCantrips = GetControl("lstCantrips"); btnRefreshCantrips = GetControl("btnRefreshCantrips"); - - // Weapons Tab - lstWeapons = GetControl("lstWeapons"); - btnRefreshWeapons = GetControl("btnRefreshWeapons"); - - // Quests Tab lstQuests = GetControl("lstQuests"); btnRefreshQuests = GetControl("btnRefreshQuests"); - } catch (Exception ex) { - PluginCore.WriteToChat($"Error initializing tab controls: {ex.Message}"); + PluginCore.WriteToChat($"[MossyTracker] Failed to initialize controls: {ex.Message}"); } } @@ -182,7 +181,6 @@ namespace MosswartMassacre.Views if (btnRefreshLum != null) btnRefreshLum.Hit += OnRefreshLuminance; if (btnRefreshRecalls != null) btnRefreshRecalls.Hit += OnRefreshRecalls; if (btnRefreshCantrips != null) btnRefreshCantrips.Hit += OnRefreshCantrips; - if (btnRefreshWeapons != null) btnRefreshWeapons.Hit += OnRefreshWeapons; if (btnRefreshQuests != null) btnRefreshQuests.Hit += OnRefreshQuests; } @@ -193,6 +191,68 @@ namespace MosswartMassacre.Views } #endregion + #region Window Management Overrides + protected override void View_VisibleChanged(object sender, EventArgs e) + { + try + { + // Call base implementation first + base.View_VisibleChanged(sender, e); + + // If window becomes invisible and we're not already disposed, dispose it + // This handles the X button click case + if (view != null && !view.Visible && !_disposed) + { + // Use a small delay to ensure this isn't just a temporary hide + System.Threading.Timer disposeTimer = null; + disposeTimer = new System.Threading.Timer(_ => + { + try + { + // Double-check the window is still hidden and not disposed + if (view != null && !view.Visible && !_disposed) + { + Dispose(); + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error in delayed disposal: {ex.Message}"); + } + finally + { + disposeTimer?.Dispose(); + } + }, null, 100, System.Threading.Timeout.Infinite); // 100ms delay + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error in FlagTracker VisibleChanged: {ex.Message}"); + } + } + #endregion + + #region Timer Event Handlers + private void OnQuestTimerUpdate(object sender, System.Timers.ElapsedEventArgs e) + { + try + { + // Update quest list display if quests tab is visible and we have quest data + if (view != null && view.Visible && PluginCore.questManager?.QuestList != null && PluginCore.questManager.QuestList.Count > 0) + { + // Only update the quest list to refresh countdown timers + PopulateQuestsList(); + } + } + catch (Exception) + { + // Silently handle timer update errors to avoid spam + } + } + + #endregion + #region Event Handlers private void OnRefreshAugmentations(object sender, EventArgs e) { @@ -247,29 +307,41 @@ namespace MosswartMassacre.Views } } - private void OnRefreshWeapons(object sender, EventArgs e) - { - try - { - data.RefreshWeapons(); - PopulateWeaponsList(); - } - catch (Exception ex) - { - PluginCore.WriteToChat($"Error refreshing weapons: {ex.Message}"); - } - } - private void OnRefreshQuests(object sender, EventArgs e) { try { - questManager.RefreshQuests(); - PopulateQuestsList(); + if (PluginCore.questManager != null) + { + PluginCore.questManager.RefreshQuests(); + PopulateQuestsList(); + + // Schedule another refresh in a few seconds to catch any data + System.Threading.Timer refreshTimer = null; + refreshTimer = new System.Threading.Timer(_ => + { + try + { + PopulateQuestsList(); + } + catch (Exception timerEx) + { + PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {timerEx.Message}"); + } + finally + { + refreshTimer?.Dispose(); + } + }, null, 4000, System.Threading.Timeout.Infinite); + } + else + { + PluginCore.WriteToChat("[MossyTracker] Quest manager not available"); + } } catch (Exception ex) { - PluginCore.WriteToChat($"Error refreshing quests: {ex.Message}"); + PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {ex.Message}"); } } @@ -290,16 +362,18 @@ namespace MosswartMassacre.Views { ((HudStaticText)control).Text = text ?? ""; } + // Column control is null - ignore silently } catch (IndexOutOfRangeException) { // Column doesn't exist - ignore silently } } + // Invalid parameters - ignore silently } - catch (Exception ex) + catch { - PluginCore.WriteToChat($"Error setting list text at column {columnIndex}: {ex.Message}"); + // Ignore text setting errors silently } } @@ -362,6 +436,24 @@ namespace MosswartMassacre.Views return "[?]"; // Unknown category } } + + private 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 #region Data Population Methods @@ -369,8 +461,22 @@ namespace MosswartMassacre.Views { try { - questManager.RefreshQuests(); - data.RefreshAll(); + if (PluginCore.questManager != null) + { + PluginCore.questManager.RefreshQuests(); + } + + if (data != null) + { + try + { + data.RefreshAll(); + } + catch (Exception dataEx) + { + PluginCore.WriteToChat($"[MossyTracker] Data refresh failed: {dataEx.Message}"); + } + } PopulateAugmentationsList(); PopulateLuminanceList(); @@ -380,7 +486,7 @@ namespace MosswartMassacre.Views } catch (Exception ex) { - PluginCore.WriteToChat($"Error refreshing all data: {ex.Message}"); + PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {ex.Message}"); } } @@ -388,10 +494,20 @@ namespace MosswartMassacre.Views { try { - if (lstAugmentations == null || data?.AugmentationCategories == null) return; + if (lstAugmentations == null) return; lstAugmentations.ClearRows(); + if (data?.AugmentationCategories == null) + { + var row = lstAugmentations.AddRow(); + SafeSetListText(row, 0, "No augmentation data available"); + SafeSetListText(row, 1, "Click Refresh to load data"); + SafeSetListText(row, 2, ""); + SafeSetListText(row, 3, ""); + return; + } + foreach (var category in data.AugmentationCategories) { // Add category header @@ -495,10 +611,19 @@ namespace MosswartMassacre.Views { try { - if (lstRecallSpells == null || data?.RecallSpells == null) return; + if (lstRecallSpells == null) return; lstRecallSpells.ClearRows(); + if (data?.RecallSpells == null) + { + var row = lstRecallSpells.AddRow(); + SafeSetListImage(row, 0, 0x6002D14); // Default portal icon + SafeSetListText(row, 1, "No recall data available - Click Refresh"); + SafeSetListText(row, 2, "Loading..."); + return; + } + foreach (var recall in data.RecallSpells) { var row = lstRecallSpells.AddRow(); @@ -567,128 +692,63 @@ namespace MosswartMassacre.Views } } - private void PopulateWeaponsList() - { - try - { - if (lstWeapons == null || data?.WeaponCategories == null) return; - - lstWeapons.ClearRows(); - - foreach (var category in data.WeaponCategories) - { - // Add category header - var headerRow = lstWeapons.AddRow(); - SafeSetListText(headerRow, 0, $"--- {category.Key} ---"); - SafeSetListText(headerRow, 1, ""); - SafeSetListText(headerRow, 2, ""); - SafeSetListText(headerRow, 3, ""); - - // Add weapons in this category - foreach (var weapon in category.Value) - { - var row = lstWeapons.AddRow(); - - // Column 0: Category - SafeSetListText(row, 0, weapon.Category); - - // Column 1: Weapon Type - SafeSetListText(row, 1, weapon.WeaponType); - - // Column 2: Weapon Name - SafeSetListText(row, 2, weapon.Name); - - // Column 3: Status - SafeSetListText(row, 3, weapon.Status); - - // Color code based on acquisition status - System.Drawing.Color statusColor = weapon.IsAcquired ? - System.Drawing.Color.Green : System.Drawing.Color.Red; - SafeSetListColor(row, 3, statusColor); - } - } - - if (data.WeaponCategories.Count == 0) - { - var row = lstWeapons.AddRow(); - SafeSetListText(row, 0, "No weapon data - click Refresh"); - SafeSetListText(row, 1, ""); - SafeSetListText(row, 2, ""); - SafeSetListText(row, 3, ""); - SafeSetListColor(row, 0, System.Drawing.Color.Gray); - } - } - catch (Exception ex) - { - PluginCore.WriteToChat($"Error populating weapons list: {ex.Message}"); - } - } - private void PopulateQuestsList() { try { - if (lstQuests == null) - { - PluginCore.WriteToChat("Quest list control is null"); - return; - } + if (lstQuests == null) return; lstQuests.ClearRows(); - // Always show debug info for now - var row = lstQuests.AddRow(); - SafeSetListText(row, 0, $"Quest Manager: {(questManager != null ? "OK" : "NULL")}"); - SafeSetListText(row, 1, $"Quest Count: {questManager?.QuestList?.Count ?? 0}"); - SafeSetListText(row, 2, "Click Refresh to load quest data"); - SafeSetListText(row, 3, ""); - SafeSetListText(row, 4, ""); - SafeSetListText(row, 5, ""); + // Add column headers - New order: Quest Name, Countdown, Last Solved, Cooldown, Solves + var headerRow = lstQuests.AddRow(); + SafeSetListText(headerRow, 0, "--- Quest Name ---"); + SafeSetListText(headerRow, 1, "Countdown"); + SafeSetListText(headerRow, 2, "Last Solved"); + SafeSetListText(headerRow, 3, "Cooldown"); + SafeSetListText(headerRow, 4, "Solves"); - if (questManager?.QuestList != null && questManager.QuestList.Count > 0) + if (PluginCore.questManager?.QuestList != null && PluginCore.questManager.QuestList.Count > 0) { - foreach (var quest in questManager.QuestList.OrderBy(q => q.Id)) + var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + // Filter out maxed quests and sort by solve count (highest to lowest) + var visibleQuests = PluginCore.questManager.QuestList + .Where(q => !(q.MaxSolves > 0 && q.Solves >= q.MaxSolves)) // Hide maxed quests + .OrderByDescending(q => q.Solves) + .ThenBy(q => PluginCore.questManager.GetFriendlyQuestName(q.Id)); + + foreach (var quest in visibleQuests) { var questRow = lstQuests.AddRow(); - // Column 0: Quest Name - SafeSetListText(questRow, 0, quest.Id); + // Column 0: Quest Name (friendly name only, wider) + string questName = PluginCore.questManager.GetFriendlyQuestName(quest.Id); + SafeSetListText(questRow, 0, questName); - // Column 1: Solves - SafeSetListText(questRow, 1, quest.Solves.ToString()); + // Column 1: Countdown Timer + long timeRemaining = quest.ExpireTime - currentTime; + string countdownText = FormatCountdown(timeRemaining); + SafeSetListText(questRow, 1, countdownText); - // Column 2: Completed date - SafeSetListText(questRow, 2, questManager.FormatTimeStamp(quest.Timestamp)); + // Column 2: Last Solved (date) - moved from Column 3 + SafeSetListText(questRow, 2, PluginCore.questManager.FormatTimeStamp(quest.Timestamp)); - // Column 3: Max solves - string maxText = quest.MaxSolves < 0 ? "∞" : quest.MaxSolves.ToString(); - SafeSetListText(questRow, 3, maxText); + // Column 3: Cooldown (formatted duration) - moved from Column 4 + SafeSetListText(questRow, 3, PluginCore.questManager.FormatSeconds(quest.Delta)); - // Column 4: Delta (cooldown in seconds) - SafeSetListText(questRow, 4, questManager.FormatSeconds(quest.Delta)); + // Column 4: Solves (white text) - moved from Column 5 + SafeSetListText(questRow, 4, quest.Solves.ToString()); + SafeSetListColor(questRow, 4, System.Drawing.Color.White); - // Column 5: Expire time - string expireText = questManager.GetTimeUntilExpire(quest); - SafeSetListText(questRow, 5, expireText); - - // Color coding based on availability - var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - - if (quest.MaxSolves > 0 && quest.Solves >= quest.MaxSolves) + // Color code the countdown based on availability + if (quest.ExpireTime <= currentTime) { - // Quest is maxed out - red - SafeSetListColor(questRow, 1, System.Drawing.Color.Red); - SafeSetListColor(questRow, 5, System.Drawing.Color.Red); - } - else if (quest.ExpireTime <= currentTime) - { - // Quest is available - green - SafeSetListColor(questRow, 5, System.Drawing.Color.Green); + SafeSetListColor(questRow, 1, System.Drawing.Color.Green); // Ready - green countdown } else { - // Quest is on cooldown - yellow - SafeSetListColor(questRow, 5, System.Drawing.Color.Yellow); + SafeSetListColor(questRow, 1, System.Drawing.Color.Yellow); // On cooldown - yellow countdown } } } @@ -702,23 +762,47 @@ namespace MosswartMassacre.Views #region Cleanup + private bool _disposed = false; + protected override void Dispose(bool disposing) { + if (_disposed) return; // Prevent double disposal + if (disposing) { try { + // Clear static instance reference if this is the current instance + if (instance == this) + { + instance = null; + } + + // Event handlers will be cleaned up by base class + + // Remove button event handlers + if (btnRefreshAugs != null) btnRefreshAugs.Hit -= OnRefreshAugmentations; + if (btnRefreshLum != null) btnRefreshLum.Hit -= OnRefreshLuminance; + if (btnRefreshRecalls != null) btnRefreshRecalls.Hit -= OnRefreshRecalls; + if (btnRefreshCantrips != null) btnRefreshCantrips.Hit -= OnRefreshCantrips; + if (btnRefreshQuests != null) btnRefreshQuests.Hit -= OnRefreshQuests; + if (data != null) { data.Dispose(); data = null; } - if (questManager != null) + // Stop and dispose quest update timer + if (questUpdateTimer != null) { - questManager.Dispose(); - questManager = null; + questUpdateTimer.Stop(); + questUpdateTimer.Elapsed -= OnQuestTimerUpdate; + questUpdateTimer.Dispose(); + questUpdateTimer = null; } + + _disposed = true; } catch (Exception ex) { diff --git a/MosswartMassacre/Views/VVSBaseView.cs b/MosswartMassacre/Views/VVSBaseView.cs index e5adc2d..236be0d 100644 --- a/MosswartMassacre/Views/VVSBaseView.cs +++ b/MosswartMassacre/Views/VVSBaseView.cs @@ -270,7 +270,7 @@ namespace MosswartMassacre.Views } } - private void View_VisibleChanged(object sender, EventArgs e) + protected virtual void View_VisibleChanged(object sender, EventArgs e) { try { diff --git a/MosswartMassacre/Views/VVSTabbedMainView.cs b/MosswartMassacre/Views/VVSTabbedMainView.cs index 9c63728..ce2b661 100644 --- a/MosswartMassacre/Views/VVSTabbedMainView.cs +++ b/MosswartMassacre/Views/VVSTabbedMainView.cs @@ -56,7 +56,7 @@ namespace MosswartMassacre.Views #region Flag Tracker Tab Controls private HudButton btnOpenFlagTracker; - private HudStaticText lblFlagTrackerStatus; + // lblFlagTrackerStatus removed - not present in XML #endregion #region Statistics Tracking @@ -280,15 +280,10 @@ namespace MosswartMassacre.Views { // Flag Tracker tab controls btnOpenFlagTracker = GetControl("btnOpenFlagTracker"); - lblFlagTrackerStatus = GetControl("lblFlagTrackerStatus"); // Hook up Flag Tracker events if (btnOpenFlagTracker != null) btnOpenFlagTracker.Hit += OnOpenFlagTrackerClick; - - // Update initial status - if (lblFlagTrackerStatus != null) - lblFlagTrackerStatus.Text = "Status: Click to open the Flag Tracker window"; } catch (Exception ex) { @@ -463,22 +458,13 @@ namespace MosswartMassacre.Views { try { - // Update status to show opening - if (lblFlagTrackerStatus != null) - lblFlagTrackerStatus.Text = "Status: Opening Flag Tracker window..."; - // Open the Flag Tracker window FlagTrackerView.OpenFlagTracker(); - - // Update status - if (lblFlagTrackerStatus != null) - lblFlagTrackerStatus.Text = "Status: Flag Tracker window is open"; + PluginCore.WriteToChat("Flag Tracker window opened"); } catch (Exception ex) { PluginCore.WriteToChat($"Error opening Flag Tracker: {ex.Message}"); - if (lblFlagTrackerStatus != null) - lblFlagTrackerStatus.Text = "Status: Error opening Flag Tracker"; } } #endregion diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs index aceaac5..50279d5 100644 --- a/MosswartMassacre/WebSocket.cs +++ b/MosswartMassacre/WebSocket.cs @@ -296,6 +296,20 @@ namespace MosswartMassacre await SendEncodedAsync(json, CancellationToken.None); } + public static async Task SendQuestDataAsync(string questName, string countdown) + { + var envelope = new + { + type = "quest", + timestamp = DateTime.UtcNow.ToString("o"), + character_name = CoreManager.Current.CharacterFilter.Name, + quest_name = questName, + countdown = countdown + }; + var json = JsonConvert.SerializeObject(envelope); + await SendEncodedAsync(json, CancellationToken.None); + } + // ─── shared send helper with locking ─────────────── private static async Task SendEncodedAsync(string text, CancellationToken token) diff --git a/mossy.sln b/mossy.sln index fb322ce..003115a 100644 --- a/mossy.sln +++ b/mossy.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.13.35919.96 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MosswartMassacre", "MosswartMassacre\MosswartMassacre.csproj", "{8C97E839-4D05-4A5F-B0C8-E8E778654322}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MosswartMassacre.Loader", "MosswartMassacre.Loader\MosswartMassacre.Loader.csproj", "{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" EndProject Global @@ -17,6 +19,10 @@ Global {8C97E839-4D05-4A5F-B0C8-E8E778654322}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C97E839-4D05-4A5F-B0C8-E8E778654322}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C97E839-4D05-4A5F-B0C8-E8E778654322}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE