Added hot reload

This commit is contained in:
erik 2025-06-22 12:10:15 +02:00
parent bb493febb4
commit 73ba7082d8
16 changed files with 1203 additions and 398 deletions

View file

@ -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; }
/// <summary>
/// Assembly directory (contains both loader and plugin dlls)
/// </summary>
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 { }
}
}
}

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<OutputType>Library</OutputType>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<PlatformTarget>x86</PlatformTarget>
<Version>1.0.0</Version>
<LangVersion>8</LangVersion>
<ProjectGuid>{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}</ProjectGuid>
<RootNamespace>MosswartMassacre.Loader</RootNamespace>
<AssemblyName>MosswartMassacre.Loader</AssemblyName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath>..\MosswartMassacre\bin\Debug\</OutputPath>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>..\MosswartMassacre\bin\Release\</OutputPath>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<Reference Include="Decal.Adapter">
<HintPath>..\MosswartMassacre\lib\Decal.Adapter.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="Decal.Interop.Core, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Core.DLL</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View file

@ -62,10 +62,9 @@ namespace MosswartMassacre
// PATHWAY 2: Target Host.Actions.AddChatText (what our plugin uses) // PATHWAY 2: Target Host.Actions.AddChatText (what our plugin uses)
PatchHostActions(); PatchHostActions();
} }
catch (Exception ex) catch
{ {
// Only log if completely unable to apply any patches // 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); 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); 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 // PATHWAY 3: Try to patch at PluginHost level
PatchPluginHost(); 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; string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length;
ApplySinglePatch(method, prefixMethodName); 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; 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(); var parameters = method.GetParameters();
string paramInfo = string.Join(", ", parameters.Select(p => p.ParameterType.Name)); string paramInfo = string.Join(", ", parameters.Select(p => p.ParameterType.Name));
System.Diagnostics.Debug.WriteLine($"AddChatText({paramInfo})");
} }
} }
@ -254,9 +244,8 @@ namespace MosswartMassacre
} }
patchesApplied = false; 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 // Always increment to verify patch is working
DecalHarmonyClean.messagesIntercepted++; DecalHarmonyClean.messagesIntercepted++;
// DEBUG: Log ALL intercepted messages for troubleshooting
if (PluginCore.AggressiveChatStreamingEnabled) if (PluginCore.AggressiveChatStreamingEnabled)
{ {
DecalHarmonyClean.AddDebugLog($"[DEBUG] HOOKS-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color})");
} }
// Process ALL messages (including our own) for streaming // Process ALL messages (including our own) for streaming
@ -326,10 +313,9 @@ namespace MosswartMassacre
// Always return true to let the original AddChatText continue // Always return true to let the original AddChatText continue
return true; return true;
} }
catch (Exception ex) catch
{ {
// Never let our interception break other plugins // Never let our interception break other plugins
System.Diagnostics.Debug.WriteLine($"Harmony interception error: {ex.Message}");
return true; return true;
} }
} }
@ -352,9 +338,8 @@ namespace MosswartMassacre
return true; return true;
} }
catch (Exception ex) catch
{ {
System.Diagnostics.Debug.WriteLine($"Harmony interception3 error: {ex.Message}");
return true; return true;
} }
} }
@ -377,9 +362,8 @@ namespace MosswartMassacre
return true; return true;
} }
catch (Exception ex) catch
{ {
System.Diagnostics.Debug.WriteLine($"Harmony generic interception error: {ex.Message}");
return true; return true;
} }
} }
@ -399,7 +383,6 @@ namespace MosswartMassacre
var fullMessage = $"{timestamp} [{sourcePlugin}] {text}"; var fullMessage = $"{timestamp} [{sourcePlugin}] {text}";
// Debug logging // Debug logging
System.Diagnostics.Debug.WriteLine($"[TARGET] HARMONY CAPTURED ({source}): {fullMessage}");
// Stream to WebSocket if both debug streaming AND WebSocket are enabled // Stream to WebSocket if both debug streaming AND WebSocket are enabled
if (PluginCore.AggressiveChatStreamingEnabled && PluginCore.WebSocketEnabled) if (PluginCore.AggressiveChatStreamingEnabled && PluginCore.WebSocketEnabled)
@ -407,9 +390,8 @@ namespace MosswartMassacre
Task.Run(() => WebSocket.SendChatTextAsync(color, text)); 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++; DecalHarmonyClean.messagesIntercepted++;
// DEBUG: Log ALL intercepted messages for troubleshooting
if (PluginCore.AggressiveChatStreamingEnabled) if (PluginCore.AggressiveChatStreamingEnabled)
{ {
DecalHarmonyClean.AddDebugLog($"[DEBUG] HOST-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color}, target={target})");
} }
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]")) if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
@ -462,9 +442,8 @@ namespace MosswartMassacre
return true; return true;
} }
catch (Exception ex) catch
{ {
System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions interception error: {ex.Message}");
return true; return true;
} }
} }
@ -487,9 +466,8 @@ namespace MosswartMassacre
return true; return true;
} }
catch (Exception ex) catch
{ {
System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions 4param interception error: {ex.Message}");
return true; return true;
} }
} }
@ -512,9 +490,8 @@ namespace MosswartMassacre
return true; return true;
} }
catch (Exception ex) catch
{ {
System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions generic interception error: {ex.Message}");
return true; return true;
} }
} }
@ -532,7 +509,6 @@ namespace MosswartMassacre
if (PluginCore.AggressiveChatStreamingEnabled) if (PluginCore.AggressiveChatStreamingEnabled)
{ {
DecalHarmonyClean.AddDebugLog($"[DEBUG] CORE2-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color})");
} }
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]")) if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
@ -544,9 +520,8 @@ namespace MosswartMassacre
return true; return true;
} }
catch (Exception ex) catch
{ {
System.Diagnostics.Debug.WriteLine($"Harmony Core2 interception error: {ex.Message}");
return true; return true;
} }
} }
@ -562,7 +537,6 @@ namespace MosswartMassacre
if (PluginCore.AggressiveChatStreamingEnabled) if (PluginCore.AggressiveChatStreamingEnabled)
{ {
DecalHarmonyClean.AddDebugLog($"[DEBUG] CORE3-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color}, target={target})");
} }
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]")) if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
@ -574,9 +548,8 @@ namespace MosswartMassacre
return true; return true;
} }
catch (Exception ex) catch
{ {
System.Diagnostics.Debug.WriteLine($"Harmony Core3 interception error: {ex.Message}");
return true; return true;
} }
} }

View file

@ -391,9 +391,8 @@ namespace MosswartMassacre
{ {
// DECAL API uses CharacterFilter.GetCharProperty for character properties // DECAL API uses CharacterFilter.GetCharProperty for character properties
aug.CurrentValue = characterFilter.GetCharProperty(aug.IntId.Value); 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 alternative access using reflection
try try
@ -402,37 +401,31 @@ namespace MosswartMassacre
if (valuesMethod != null) if (valuesMethod != null)
{ {
aug.CurrentValue = (int)valuesMethod.Invoke(playerObject, new object[] { aug.IntId.Value }); 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 else
{ {
aug.CurrentValue = 0; aug.CurrentValue = 0;
PluginCore.WriteToChat($"Debug: {aug.Name} - Values method not found");
} }
} }
catch (Exception ex2) catch
{ {
aug.CurrentValue = 0; aug.CurrentValue = 0;
PluginCore.WriteToChat($"Debug: {aug.Name} - Failed: {ex.Message}, Reflection: {ex2.Message}");
} }
} }
} }
else else
{ {
aug.CurrentValue = 0; aug.CurrentValue = 0;
PluginCore.WriteToChat($"Debug: {aug.Name} - Player object is null");
} }
} }
else else
{ {
aug.CurrentValue = 0; aug.CurrentValue = 0;
PluginCore.WriteToChat($"Debug: {aug.Name} - CharacterFilter is null");
} }
} }
catch (Exception ex) catch
{ {
aug.CurrentValue = 0; aug.CurrentValue = 0;
PluginCore.WriteToChat($"Debug: {aug.Name} - Exception: {ex.Message}");
} }
} }
else else
@ -512,12 +505,10 @@ namespace MosswartMassacre
{ {
// Use CharacterFilter.GetCharProperty for luminance auras // Use CharacterFilter.GetCharProperty for luminance auras
aura.CurrentValue = characterFilter.GetCharProperty(aura.IntId); 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; aura.CurrentValue = 0;
PluginCore.WriteToChat($"Debug: {aura.Name} - Failed to read: {ex.Message}");
} }
} }
} }
@ -552,14 +543,12 @@ namespace MosswartMassacre
recall.IconId = GetSpellIcon(recall.SpellId); recall.IconId = GetSpellIcon(recall.SpellId);
} }
} }
catch (Exception ex) catch
{ {
recall.IsKnown = false; 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) 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 // 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 // Add offset for spell icons to display correctly in VVS
// Based on MagTools pattern, spell icons need the offset for display // Based on MagTools pattern, spell icons need the offset for display
int finalIconId = iconId + 0x6000000; int finalIconId = iconId + 0x6000000;
PluginCore.WriteToChat($"Debug: Found cantrip spell {spellId} -> raw icon {iconId} -> final icon 0x{finalIconId:X}");
return finalIconId; return finalIconId;
} }
@ -900,11 +887,9 @@ namespace MosswartMassacre
{ {
try try
{ {
PluginCore.WriteToChat("Debug: RefreshCantrips() starting");
if (CoreManager.Current?.CharacterFilter?.Name == null) if (CoreManager.Current?.CharacterFilter?.Name == null)
{ {
PluginCore.WriteToChat("Debug: No character filter available");
return; return;
} }
@ -913,11 +898,9 @@ namespace MosswartMassacre
if (playerObject == null) if (playerObject == null)
{ {
PluginCore.WriteToChat("Debug: No player object found");
return; return;
} }
PluginCore.WriteToChat($"Debug: Character {characterFilter.Name} found, {playerObject.ActiveSpellCount} active spells");
// Clear dynamic skill lists // Clear dynamic skill lists
Cantrips["Specialized Skills"].Clear(); Cantrips["Specialized Skills"].Clear();
@ -940,24 +923,20 @@ namespace MosswartMassacre
var enchantments = characterFilter.Enchantments; var enchantments = characterFilter.Enchantments;
if (enchantments != null) if (enchantments != null)
{ {
PluginCore.WriteToChat($"Debug: Found {enchantments.Count} active enchantments");
for (int i = 0; i < enchantments.Count; i++) for (int i = 0; i < enchantments.Count; i++)
{ {
var ench = enchantments[i]; var ench = enchantments[i];
var spell = SpellManager.GetSpell(ench.SpellId); var spell = SpellManager.GetSpell(ench.SpellId);
if (spell != null && spell.CantripLevel != Mag.Shared.Spells.Spell.CantripLevels.None) 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); DetectCantrip(ench.SpellId);
} }
} }
} }
else 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 // Compute final icon IDs for all cantrips after refresh
foreach (var category in Cantrips) foreach (var category in Cantrips)
{ {
@ -1057,7 +1036,6 @@ namespace MosswartMassacre
if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Specialized) if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Specialized)
{ {
PluginCore.WriteToChat($"Debug: Adding specialized skill: {skillName} (ID {skillId})");
Cantrips["Specialized Skills"][skillName] = new CantripInfo Cantrips["Specialized Skills"][skillName] = new CantripInfo
{ {
Name = skillName, Name = skillName,
@ -1068,7 +1046,6 @@ namespace MosswartMassacre
} }
else if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Trained) else if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Trained)
{ {
PluginCore.WriteToChat($"Debug: Adding trained skill: {skillName} (ID {skillId})");
Cantrips["Trained Skills"][skillName] = new CantripInfo Cantrips["Trained Skills"][skillName] = new CantripInfo
{ {
Name = skillName, Name = skillName,
@ -1097,14 +1074,12 @@ namespace MosswartMassacre
var characterFilter = CoreManager.Current.CharacterFilter; var characterFilter = CoreManager.Current.CharacterFilter;
if (characterFilter == null) if (characterFilter == null)
{ {
PluginCore.WriteToChat($"Debug: CharacterFilter is null for skill {skillId}");
return GetFallbackSkillIcon(skillId); return GetFallbackSkillIcon(skillId);
} }
// Validate skillId range for DECAL API // Validate skillId range for DECAL API
if (skillId < 1 || skillId > 54) if (skillId < 1 || skillId > 54)
{ {
PluginCore.WriteToChat($"Debug: Invalid skill ID {skillId} (must be 1-54)");
return GetFallbackSkillIcon(skillId); return GetFallbackSkillIcon(skillId);
} }
@ -1113,21 +1088,17 @@ namespace MosswartMassacre
var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId]; var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId];
if (skillInfo == null) if (skillInfo == null)
{ {
PluginCore.WriteToChat($"Debug: No skill info found for skill {skillId}");
return GetFallbackSkillIcon(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) // Try to access skill icon via reflection (DECAL's SkillInfoWrapper.Dat property)
var skillType = skillInfo.GetType(); var skillType = skillInfo.GetType();
PluginCore.WriteToChat($"Debug: SkillInfo type: {skillType.Name}");
// Method 1: Try FileService SkillTable approach (most reliable) // Method 1: Try FileService SkillTable approach (most reliable)
int realIconId = GetRealSkillIconFromDat(skillId); int realIconId = GetRealSkillIconFromDat(skillId);
if (realIconId > 0) if (realIconId > 0)
{ {
PluginCore.WriteToChat($"Debug: Found real skill icon {realIconId} for skill {skillId}, applying offset");
return realIconId + 0x6000000; return realIconId + 0x6000000;
} }
@ -1139,7 +1110,6 @@ namespace MosswartMassacre
if (datObject != null) if (datObject != null)
{ {
var datType = datObject.GetType(); var datType = datObject.GetType();
PluginCore.WriteToChat($"Debug: Dat object type: {datType.Name}");
// Try the exact property names from AC system // Try the exact property names from AC system
string[] iconPropertyNames = { "IconID", "Icon", "IconId", "uiGraphic", "GraphicID" }; string[] iconPropertyNames = { "IconID", "Icon", "IconId", "uiGraphic", "GraphicID" };
@ -1152,15 +1122,12 @@ namespace MosswartMassacre
var iconValue = iconProperty.GetValue(datObject, null); var iconValue = iconProperty.GetValue(datObject, null);
if (iconValue != null) if (iconValue != null)
{ {
PluginCore.WriteToChat($"Debug: Found {propName} property with value {iconValue} (type: {iconValue.GetType().Name})");
if (iconValue is int iconId && iconId > 0) if (iconValue is int iconId && iconId > 0)
{ {
PluginCore.WriteToChat($"Debug: Using icon {iconId} from {propName} for skill {skillId}");
return iconId + 0x6000000; return iconId + 0x6000000;
} }
else if (iconValue is uint uiconId && uiconId > 0) else if (iconValue is uint uiconId && uiconId > 0)
{ {
PluginCore.WriteToChat($"Debug: Using uint icon {uiconId} from {propName} for skill {skillId}");
return (int)uiconId + 0x6000000; return (int)uiconId + 0x6000000;
} }
} }
@ -1174,15 +1141,12 @@ namespace MosswartMassacre
var iconValue = iconField.GetValue(datObject); var iconValue = iconField.GetValue(datObject);
if (iconValue != null) if (iconValue != null)
{ {
PluginCore.WriteToChat($"Debug: Found {propName} field with value {iconValue} (type: {iconValue.GetType().Name})");
if (iconValue is int iconId && iconId > 0) if (iconValue is int iconId && iconId > 0)
{ {
PluginCore.WriteToChat($"Debug: Using icon {iconId} from field {propName} for skill {skillId}");
return iconId + 0x6000000; return iconId + 0x6000000;
} }
else if (iconValue is uint uiconId && uiconId > 0) 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; 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)) foreach (var prop in datType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{ {
try try
@ -1207,12 +1169,10 @@ namespace MosswartMassacre
} }
else else
{ {
PluginCore.WriteToChat($"Debug: Dat property exists but returns null for skill {skillId}");
} }
} }
else else
{ {
PluginCore.WriteToChat($"Debug: No Dat property found on SkillInfoWrapper for skill {skillId}");
} }
// Method 3: Try direct properties on SkillInfoWrapper // Method 3: Try direct properties on SkillInfoWrapper
@ -1225,24 +1185,20 @@ namespace MosswartMassacre
var iconValue = iconProperty.GetValue(skillInfo, null); var iconValue = iconProperty.GetValue(skillInfo, null);
if (iconValue is int iconId && iconId > 0) if (iconValue is int iconId && iconId > 0)
{ {
PluginCore.WriteToChat($"Debug: Using direct icon {iconId} from {propName} for skill {skillId}");
return iconId + 0x6000000; return iconId + 0x6000000;
} }
} }
} }
} }
catch (Exception ex) catch
{ {
PluginCore.WriteToChat($"Debug: Skill access failed for skill {skillId}: {ex.Message}");
} }
// Fallback to predefined mapping // Fallback to predefined mapping
PluginCore.WriteToChat($"Debug: Using fallback icon for skill {skillId}");
return GetFallbackSkillIcon(skillId); return GetFallbackSkillIcon(skillId);
} }
catch (Exception ex) catch
{ {
PluginCore.WriteToChat($"Debug: Error in GetSkillIconId for skill {skillId}: {ex.Message}");
return GetFallbackSkillIcon(skillId); return GetFallbackSkillIcon(skillId);
} }
} }
@ -1280,7 +1236,6 @@ namespace MosswartMassacre
{ {
// Access SkillTable via reflection to get skill data // Access SkillTable via reflection to get skill data
var skillTableType = fileService.SkillTable.GetType(); var skillTableType = fileService.SkillTable.GetType();
PluginCore.WriteToChat($"Debug: SkillTable type: {skillTableType.Name}");
// Look for methods that can get skill by ID // Look for methods that can get skill by ID
var methods = skillTableType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 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 }); var skillData = method.Invoke(fileService.SkillTable, new object[] { skillId });
if (skillData != null) if (skillData != null)
{ {
PluginCore.WriteToChat($"Debug: Found skill data via {method.Name}: {skillData.GetType().Name}");
// Look for icon properties on the skill data // Look for icon properties on the skill data
var skillDataType = skillData.GetType(); var skillDataType = skillData.GetType();
@ -1310,42 +1264,36 @@ namespace MosswartMassacre
var iconValue = iconProp.GetValue(skillData, null); var iconValue = iconProp.GetValue(skillData, null);
if (iconValue is int iconInt && iconInt > 0) if (iconValue is int iconInt && iconInt > 0)
{ {
PluginCore.WriteToChat($"Debug: Found skill icon {iconInt} via FileService.{method.Name}.{propName}");
return iconInt; return iconInt;
} }
else if (iconValue is uint iconUint && iconUint > 0) else if (iconValue is uint iconUint && iconUint > 0)
{ {
PluginCore.WriteToChat($"Debug: Found skill icon {iconUint} via FileService.{method.Name}.{propName}");
return (int)iconUint; return (int)iconUint;
} }
} }
} }
} }
} }
catch (Exception ex) catch
{ {
// Method call failed, try next one // 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 else
{ {
PluginCore.WriteToChat($"Debug: FileService or SkillTable is null");
} }
return 0; // No icon found return 0; // No icon found
} }
catch (Exception ex) catch
{ {
PluginCore.WriteToChat($"Debug: GetRealSkillIconFromDat failed: {ex.Message}");
return 0; return 0;
} }
} }
@ -1420,12 +1368,10 @@ namespace MosswartMassacre
if (skillIconMap.ContainsKey(skillId)) if (skillIconMap.ContainsKey(skillId))
{ {
PluginCore.WriteToChat($"Debug: Using fallback icon 0x{skillIconMap[skillId]:X} for skill {skillId} ({GetSkillName(skillId)})");
return skillIconMap[skillId]; return skillIconMap[skillId];
} }
// Final fallback to proven working icon from recalls system // 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 return 0x6002D14; // Portal icon - confirmed working in recalls
} }
@ -1457,12 +1403,10 @@ namespace MosswartMassacre
string spellName = GetSpellName(spellId); string spellName = GetSpellName(spellId);
if (string.IsNullOrEmpty(spellName)) if (string.IsNullOrEmpty(spellName))
{ {
PluginCore.WriteToChat($"Debug: FAILED to get spell name for spell ID {spellId}");
return; return;
} }
// Debug output to see what spells we're processing // Debug output to see what spells we're processing
PluginCore.WriteToChat($"Debug: Processing spell ID {spellId}: '{spellName}'");
// Define cantrip levels and their patterns // Define cantrip levels and their patterns
var cantripPatterns = new Dictionary<string, (string level, System.Drawing.Color color)> var cantripPatterns = new Dictionary<string, (string level, System.Drawing.Color color)>
@ -1484,42 +1428,35 @@ namespace MosswartMassacre
// Remove the level prefix to get the skill/attribute name // Remove the level prefix to get the skill/attribute name
string skillPart = spellName.Substring(pattern.Length + 1); string skillPart = spellName.Substring(pattern.Length + 1);
PluginCore.WriteToChat($"Debug: Found {level} cantrip, skillPart='{skillPart}'");
// Get the spell icon for this cantrip spell // Get the spell icon for this cantrip spell
int spellIconId = GetRealSpellIcon(spellId); int spellIconId = GetRealSpellIcon(spellId);
if (spellIconId == 0) if (spellIconId == 0)
{ {
spellIconId = 0x6002D14; // Default fallback icon spellIconId = 0x6002D14; // Default fallback icon
PluginCore.WriteToChat($"Debug: No real icon found for spell {spellId}, using default");
} }
else 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") // Try to match Protection Auras first (exact format: "Minor Armor", "Epic Bludgeoning Ward")
if (MatchProtectionAura(skillPart, level, color, spellIconId)) if (MatchProtectionAura(skillPart, level, color, spellIconId))
{ {
PluginCore.WriteToChat($"Debug: Matched as Protection Aura: '{skillPart}' with spell icon {spellIconId}");
return; return;
} }
// Try to match Attributes (exact format: "Minor Strength", "Epic Focus") // Try to match Attributes (exact format: "Minor Strength", "Epic Focus")
if (MatchAttribute(skillPart, level, color, spellIconId)) if (MatchAttribute(skillPart, level, color, spellIconId))
{ {
PluginCore.WriteToChat($"Debug: Matched as Attribute: '{skillPart}' with spell icon {spellIconId}");
return; return;
} }
// Try to match Skills using the replacement mappings // Try to match Skills using the replacement mappings
if (MatchSkill(skillPart, level, color, spellIconId)) if (MatchSkill(skillPart, level, color, spellIconId))
{ {
PluginCore.WriteToChat($"Debug: Matched as Skill: '{skillPart}' with spell icon {spellIconId}");
return; return;
} }
PluginCore.WriteToChat($"Debug: No match found for: '{skillPart}' (level: {level}) - Full spell: '{spellName}'");
} }
} }
catch (Exception ex) catch (Exception ex)
@ -1610,13 +1547,11 @@ namespace MosswartMassacre
["Willpower"] = "Willpower" // "Epic Willpower" -> Willpower ["Willpower"] = "Willpower" // "Epic Willpower" -> Willpower
}; };
PluginCore.WriteToChat($"Debug: MatchAttribute checking '{cleanedSkillPart}' for {level}");
foreach (var mapping in attributeMappings) foreach (var mapping in attributeMappings)
{ {
if (cleanedSkillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase)) 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 // Create the cantrip entry if it doesn't exist
if (!Cantrips["Attributes"].ContainsKey(mapping.Value)) if (!Cantrips["Attributes"].ContainsKey(mapping.Value))
@ -1632,7 +1567,6 @@ namespace MosswartMassacre
var cantrip = Cantrips["Attributes"][mapping.Value]; var cantrip = Cantrips["Attributes"][mapping.Value];
if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value)) if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value))
{ {
PluginCore.WriteToChat($"Debug: Setting {mapping.Value} to {level}");
cantrip.Value = level; cantrip.Value = level;
cantrip.Color = color; cantrip.Color = color;
cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip 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) 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 // Create the cantrip entry if it doesn't exist
if (!Cantrips["Attributes"].ContainsKey(mapping.Value)) if (!Cantrips["Attributes"].ContainsKey(mapping.Value))
@ -1662,7 +1595,6 @@ namespace MosswartMassacre
var cantrip = Cantrips["Attributes"][mapping.Value]; var cantrip = Cantrips["Attributes"][mapping.Value];
if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.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.Value = level;
cantrip.Color = color; cantrip.Color = color;
cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip
@ -1764,9 +1696,8 @@ namespace MosswartMassacre
} }
return ""; return "";
} }
catch (Exception ex) catch
{ {
PluginCore.WriteToChat($"Debug: Error getting spell {spellId}: {ex.Message}");
return ""; return "";
} }
} }
@ -1812,7 +1743,6 @@ namespace MosswartMassacre
foreach (var spellName in testSpells) foreach (var spellName in testSpells)
{ {
PluginCore.WriteToChat($"Debug: Testing detection for: {spellName}");
TestDetectCantripByName(spellName); TestDetectCantripByName(spellName);
} }
} }
@ -1827,7 +1757,6 @@ namespace MosswartMassacre
try try
{ {
// Simulate the detection logic with a fake spell name // Simulate the detection logic with a fake spell name
PluginCore.WriteToChat($"Debug: Processing spell: {spellName}");
// Define cantrip levels and their patterns // Define cantrip levels and their patterns
var cantripPatterns = new Dictionary<string, (string level, System.Drawing.Color color)> var cantripPatterns = new Dictionary<string, (string level, System.Drawing.Color color)>
@ -1850,7 +1779,6 @@ namespace MosswartMassacre
// Remove the level prefix to get the skill/attribute name // Remove the level prefix to get the skill/attribute name
string skillPart = spellName.Substring(pattern.Length + 1); string skillPart = spellName.Substring(pattern.Length + 1);
PluginCore.WriteToChat($"Debug: Detected {level} cantrip for {skillPart}");
// Get a test spell icon (use default for testing) // Get a test spell icon (use default for testing)
int testSpellIconId = 0x6002D14; int testSpellIconId = 0x6002D14;
@ -1858,25 +1786,21 @@ namespace MosswartMassacre
// Try to match Protection Auras first // Try to match Protection Auras first
if (MatchProtectionAura(skillPart, level, color, testSpellIconId)) if (MatchProtectionAura(skillPart, level, color, testSpellIconId))
{ {
PluginCore.WriteToChat($"Debug: Matched protection aura: {skillPart}");
return; return;
} }
// Try to match Attributes // Try to match Attributes
if (MatchAttribute(skillPart, level, color, testSpellIconId)) if (MatchAttribute(skillPart, level, color, testSpellIconId))
{ {
PluginCore.WriteToChat($"Debug: Matched attribute: {skillPart}");
return; return;
} }
// Try to match Skills // Try to match Skills
if (MatchSkill(skillPart, level, color, testSpellIconId)) if (MatchSkill(skillPart, level, color, testSpellIconId))
{ {
PluginCore.WriteToChat($"Debug: Matched skill: {skillPart}");
return; return;
} }
PluginCore.WriteToChat($"Debug: No match found for: {skillPart}");
} }
} }
catch (Exception ex) catch (Exception ex)

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura>
<IncludeAssemblies>
YamlDotNet
Newtonsoft.Json
</IncludeAssemblies>
</Costura>
</Weavers>

View file

@ -178,6 +178,7 @@
<Compile Include="HttpCommandServer.cs" /> <Compile Include="HttpCommandServer.cs" />
<Compile Include="DelayedCommandManager.cs" /> <Compile Include="DelayedCommandManager.cs" />
<Compile Include="PluginCore.cs" /> <Compile Include="PluginCore.cs" />
<Compile Include="QuestNames.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs"> <Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
@ -229,4 +230,13 @@
</COMReference> </COMReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\Fody.6.8.0\build\Fody.targets" Condition="Exists('..\packages\Fody.6.8.0\build\Fody.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Fody.6.8.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Fody.6.8.0\build\Fody.targets'))" />
<Error Condition="!Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets'))" />
</Target>
<Import Project="..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets" Condition="Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" />
</Project> </Project>

View file

@ -22,12 +22,20 @@ namespace MosswartMassacre
// 1) Character name // 1) Character name
var characterName = CoreManager.Current.CharacterFilter.Name; var characterName = CoreManager.Current.CharacterFilter.Name;
// 2) Plugin folder // 2) Plugin folder - handle hot reload scenarios
var pluginFolder = Path.GetDirectoryName( string pluginFolder;
System.Reflection.Assembly if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory))
.GetExecutingAssembly() {
.Location pluginFolder = PluginCore.AssemblyDirectory;
); }
else
{
pluginFolder = Path.GetDirectoryName(
System.Reflection.Assembly
.GetExecutingAssembly()
.Location
);
}
// 3) Character-specific folder path // 3) Character-specific folder path
var characterFolder = Path.Combine(pluginFolder, characterName); var characterFolder = Path.Combine(pluginFolder, characterName);
@ -86,7 +94,20 @@ namespace MosswartMassacre
try try
{ {
loginComplete = true; 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; return;
if (!File.Exists(InventoryFileName)) if (!File.Exists(InventoryFileName))
@ -112,7 +133,16 @@ namespace MosswartMassacre
private void WorldFilter_CreateObject(object sender, CreateObjectEventArgs e) 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) if (!e.New.HasIdData && ObjectClassNeedsIdent(e.New.ObjectClass, e.New.Name)
&& !requestedIds.Contains(e.New.Id) && !requestedIds.Contains(e.New.Id)
@ -125,7 +155,16 @@ namespace MosswartMassacre
private void WorldFilter_ChangeObject(object sender, ChangeObjectEventArgs e) 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) if (loggedInAndWaitingForIdData)
{ {
@ -162,7 +201,14 @@ namespace MosswartMassacre
{ {
try try
{ {
if (!PluginSettings.Instance.InventoryLog) return; try
{
if (!PluginSettings.Instance.InventoryLog) return;
}
catch (InvalidOperationException)
{
return; // Settings not ready, skip silently
}
DumpInventoryToFile(); DumpInventoryToFile();
} }
catch (Exception ex) catch (Exception ex)

View file

@ -20,6 +20,32 @@ namespace MosswartMassacre
[FriendlyName("Mosswart Massacre")] [FriendlyName("Mosswart Massacre")]
public class PluginCore : PluginBase 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 PluginHost MyHost;
internal static int totalKills = 0; internal static int totalKills = 0;
internal static int rareCount = 0; internal static int rareCount = 0;
@ -88,6 +114,10 @@ namespace MosswartMassacre
public static bool AggressiveChatStreamingEnabled { get; set; } = true; public static bool AggressiveChatStreamingEnabled { get; set; } = true;
private MossyInventory _inventoryLogger; private MossyInventory _inventoryLogger;
public static NavVisualization navVisualization; public static NavVisualization navVisualization;
// Quest Management for always-on quest streaming
public static QuestManager questManager;
private static Timer questStreamingTimer;
private static Queue<string> rareMessageQueue = new Queue<string>(); private static Queue<string> rareMessageQueue = new Queue<string>();
private static DateTime _lastSent = DateTime.MinValue; private static DateTime _lastSent = DateTime.MinValue;
@ -97,9 +127,41 @@ namespace MosswartMassacre
{ {
try 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 // Note: Startup messages will appear after character login
// Subscribe to chat message event // Subscribe to chat message event
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText); CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
@ -165,7 +227,7 @@ namespace MosswartMassacre
PluginSettings.Save(); PluginSettings.Save();
if (TelemetryEnabled) if (TelemetryEnabled)
Telemetry.Stop(); // ensure no dangling timer / HttpClient Telemetry.Stop(); // ensure no dangling timer / HttpClient
WriteToChat("Mosswart Massacre is shutting down..."); WriteToChat("Mosswart Massacre is shutting down!!!!!");
// Unsubscribe from chat message event // Unsubscribe from chat message event
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText); CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
@ -203,6 +265,22 @@ namespace MosswartMassacre
commandTimer = null; 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 // Clean up the view
ViewManager.ViewDestroy(); ViewManager.ViewDestroy();
//Disable vtank interface //Disable vtank interface
@ -279,6 +357,160 @@ namespace MosswartMassacre
// Initialize cached Prismatic Taper count // Initialize cached Prismatic Taper count
InitializePrismaticTaperCount(); 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() private void InitializePrismaticTaperCount()
@ -658,7 +890,6 @@ namespace MosswartMassacre
{ {
try try
{ {
// WriteToChat($"[Debug] Chat Color: {e.Color}, Message: {e.Text}");
if (IsKilledByMeMessage(e.Text)) if (IsKilledByMeMessage(e.Text))
{ {
@ -889,7 +1120,31 @@ namespace MosswartMassacre
} }
public static void WriteToChat(string message) 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() public static void RestartStats()
{ {
@ -997,6 +1252,7 @@ namespace MosswartMassacre
WriteToChat("Usage: /mm ws <enable|disable>"); WriteToChat("Usage: /mm ws <enable|disable>");
} }
} }
else else
{ {
WriteToChat("Usage: /mm ws <enable|disable>"); WriteToChat("Usage: /mm ws <enable|disable>");
@ -1019,10 +1275,9 @@ namespace MosswartMassacre
WriteToChat("/mm harmonyraw - Show raw intercepted messages (debug output)"); WriteToChat("/mm harmonyraw - Show raw intercepted messages (debug output)");
WriteToChat("/mm testprismatic - Test Prismatic Taper detection and icon lookup"); WriteToChat("/mm testprismatic - Test Prismatic Taper detection and icon lookup");
WriteToChat("/mm deathstats - Show current death tracking statistics"); 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 testtaper - Test cached Prismatic Taper tracking");
WriteToChat("/mm debugtaper - Show detailed taper tracking debug info"); WriteToChat("/mm debugtaper - Show detailed taper tracking debug info");
WriteToChat("/mm gui - Manually initialize/reinitialize GUI"); WriteToChat("/mm gui - Manually initialize/reinitialize GUI!!!");
break; break;
case "report": case "report":
TimeSpan elapsed = DateTime.Now - statsStartTime; TimeSpan elapsed = DateTime.Now - statsStartTime;
@ -1134,7 +1389,6 @@ namespace MosswartMassacre
WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ==="); WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ===");
WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}"); WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}");
WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}"); WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}");
WriteToChat($"Debug Streaming: {AggressiveChatStreamingEnabled}");
WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}"); WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}");
// Test Harmony availability // Test Harmony availability
@ -1186,31 +1440,7 @@ namespace MosswartMassacre
case "harmonyraw": case "harmonyraw":
try // Debug functionality removed
{
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}");
}
break; break;
case "initgui": case "initgui":
@ -1394,48 +1624,7 @@ namespace MosswartMassacre
break; break;
case "debugtaper": case "debugtaper":
try // Debug functionality removed
{
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}");
}
break; break;
case "finditem": case "finditem":

View file

@ -32,7 +32,18 @@ namespace MosswartMassacre
{ {
// determine plugin folder and character-specific folder // determine plugin folder and character-specific folder
string characterName = CoreManager.Current.CharacterFilter.Name; 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 // Path for character-specific folder
string characterFolder = Path.Combine(pluginFolder, characterName); string characterFolder = Path.Combine(pluginFolder, characterName);
@ -40,7 +51,14 @@ namespace MosswartMassacre
// Create the character folder if it doesn't exist // Create the character folder if it doesn't exist
if (!Directory.Exists(characterFolder)) 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 // YAML file is now in the character-specific folder

View file

@ -62,6 +62,23 @@ namespace MosswartMassacre
} }
#endregion #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 #region Quest Parsing
private void OnChatBoxMessage(object sender, ChatTextInterceptEventArgs e) private void OnChatBoxMessage(object sender, ChatTextInterceptEventArgs e)
{ {

View file

@ -0,0 +1,228 @@
using System.Collections.Generic;
namespace MosswartMassacre
{
/// <summary>
/// Static quest name mappings from quest stamp to friendly display name
/// Based on questtracker repository data
/// </summary>
public static class QuestNames
{
/// <summary>
/// Dictionary mapping quest stamps to friendly quest names
/// </summary>
public static readonly Dictionary<string, string> QuestStampToName = new Dictionary<string, string>
{
// 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"
};
/// <summary>
/// Get friendly name for a quest stamp, with fallback to original stamp
/// </summary>
/// <param name="questStamp">The quest stamp to lookup</param>
/// <returns>Friendly name if found, otherwise the original quest stamp</returns>
public static string GetFriendlyName(string questStamp)
{
if (string.IsNullOrEmpty(questStamp))
return questStamp;
return QuestStampToName.TryGetValue(questStamp.ToLower(), out string friendlyName)
? friendlyName
: questStamp;
}
/// <summary>
/// Get display name with friendly name and original stamp in parentheses
/// </summary>
/// <param name="questStamp">The quest stamp to format</param>
/// <returns>Formatted display name</returns>
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;
}
}
}

View file

@ -34,10 +34,6 @@ namespace MosswartMassacre.Views
private HudList lstCantrips; private HudList lstCantrips;
private HudButton btnRefreshCantrips; private HudButton btnRefreshCantrips;
// Weapons Tab
private HudList lstWeapons;
private HudButton btnRefreshWeapons;
// Quests Tab // Quests Tab
private HudList lstQuests; private HudList lstQuests;
private HudButton btnRefreshQuests; private HudButton btnRefreshQuests;
@ -46,14 +42,25 @@ namespace MosswartMassacre.Views
#region Data Management #region Data Management
private FlagTrackerData data; private FlagTrackerData data;
private QuestManager questManager; private System.Timers.Timer questUpdateTimer;
#endregion #endregion
public FlagTrackerView(PluginCore core) : base(core) public FlagTrackerView(PluginCore core) : base(core)
{ {
instance = this; try
data = new FlagTrackerData(); {
questManager = new QuestManager(); 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 #region Static Interface
@ -108,17 +115,18 @@ namespace MosswartMassacre.Views
{ {
try try
{ {
// Create view from XML layout
CreateFromXMLResource("MosswartMassacre.ViewXML.flagTracker.xml"); CreateFromXMLResource("MosswartMassacre.ViewXML.flagTracker.xml");
if (view == null)
{
PluginCore.WriteToChat("[MossyTracker] Failed to create view");
return;
}
// Initialize all tab controls
InitializeTabControls(); InitializeTabControls();
InitializeEventHandlers(); InitializeEventHandlers();
// Initialize the base view
Initialize(); Initialize();
// Make the view visible
if (view != null) if (view != null)
{ {
view.Visible = true; view.Visible = true;
@ -126,12 +134,19 @@ namespace MosswartMassacre.Views
view.Title = "Mossy Tracker v3.0.1.1"; view.Title = "Mossy Tracker v3.0.1.1";
} }
// Initial data refresh
RefreshAllData(); RefreshAllData();
// Start quest update timer
if (questUpdateTimer != null)
{
questUpdateTimer.Start();
}
PluginCore.WriteToChat("[MossyTracker] Initialized successfully");
} }
catch (Exception ex) 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 try
{ {
// Get main tab view
mainTabView = GetControl<HudTabView>("mainTabView"); mainTabView = GetControl<HudTabView>("mainTabView");
// Augmentations Tab
lstAugmentations = GetControl<HudList>("lstAugmentations"); lstAugmentations = GetControl<HudList>("lstAugmentations");
btnRefreshAugs = GetControl<HudButton>("btnRefreshAugs"); btnRefreshAugs = GetControl<HudButton>("btnRefreshAugs");
// Luminance Tab
lstLuminanceAuras = GetControl<HudList>("lstLuminanceAuras"); lstLuminanceAuras = GetControl<HudList>("lstLuminanceAuras");
btnRefreshLum = GetControl<HudButton>("btnRefreshLum"); btnRefreshLum = GetControl<HudButton>("btnRefreshLum");
// Recalls Tab
lstRecallSpells = GetControl<HudList>("lstRecallSpells"); lstRecallSpells = GetControl<HudList>("lstRecallSpells");
btnRefreshRecalls = GetControl<HudButton>("btnRefreshRecalls"); btnRefreshRecalls = GetControl<HudButton>("btnRefreshRecalls");
// Cantrips Tab
lstCantrips = GetControl<HudList>("lstCantrips"); lstCantrips = GetControl<HudList>("lstCantrips");
btnRefreshCantrips = GetControl<HudButton>("btnRefreshCantrips"); btnRefreshCantrips = GetControl<HudButton>("btnRefreshCantrips");
// Weapons Tab
lstWeapons = GetControl<HudList>("lstWeapons");
btnRefreshWeapons = GetControl<HudButton>("btnRefreshWeapons");
// Quests Tab
lstQuests = GetControl<HudList>("lstQuests"); lstQuests = GetControl<HudList>("lstQuests");
btnRefreshQuests = GetControl<HudButton>("btnRefreshQuests"); btnRefreshQuests = GetControl<HudButton>("btnRefreshQuests");
} }
catch (Exception ex) 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 (btnRefreshLum != null) btnRefreshLum.Hit += OnRefreshLuminance;
if (btnRefreshRecalls != null) btnRefreshRecalls.Hit += OnRefreshRecalls; if (btnRefreshRecalls != null) btnRefreshRecalls.Hit += OnRefreshRecalls;
if (btnRefreshCantrips != null) btnRefreshCantrips.Hit += OnRefreshCantrips; if (btnRefreshCantrips != null) btnRefreshCantrips.Hit += OnRefreshCantrips;
if (btnRefreshWeapons != null) btnRefreshWeapons.Hit += OnRefreshWeapons;
if (btnRefreshQuests != null) btnRefreshQuests.Hit += OnRefreshQuests; if (btnRefreshQuests != null) btnRefreshQuests.Hit += OnRefreshQuests;
} }
@ -193,6 +191,68 @@ namespace MosswartMassacre.Views
} }
#endregion #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 #region Event Handlers
private void OnRefreshAugmentations(object sender, EventArgs e) 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) private void OnRefreshQuests(object sender, EventArgs e)
{ {
try try
{ {
questManager.RefreshQuests(); if (PluginCore.questManager != null)
PopulateQuestsList(); {
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) 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 ?? ""; ((HudStaticText)control).Text = text ?? "";
} }
// Column control is null - ignore silently
} }
catch (IndexOutOfRangeException) catch (IndexOutOfRangeException)
{ {
// Column doesn't exist - ignore silently // 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 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 #endregion
#region Data Population Methods #region Data Population Methods
@ -369,8 +461,22 @@ namespace MosswartMassacre.Views
{ {
try try
{ {
questManager.RefreshQuests(); if (PluginCore.questManager != null)
data.RefreshAll(); {
PluginCore.questManager.RefreshQuests();
}
if (data != null)
{
try
{
data.RefreshAll();
}
catch (Exception dataEx)
{
PluginCore.WriteToChat($"[MossyTracker] Data refresh failed: {dataEx.Message}");
}
}
PopulateAugmentationsList(); PopulateAugmentationsList();
PopulateLuminanceList(); PopulateLuminanceList();
@ -380,7 +486,7 @@ namespace MosswartMassacre.Views
} }
catch (Exception ex) 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 try
{ {
if (lstAugmentations == null || data?.AugmentationCategories == null) return; if (lstAugmentations == null) return;
lstAugmentations.ClearRows(); 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) foreach (var category in data.AugmentationCategories)
{ {
// Add category header // Add category header
@ -495,10 +611,19 @@ namespace MosswartMassacre.Views
{ {
try try
{ {
if (lstRecallSpells == null || data?.RecallSpells == null) return; if (lstRecallSpells == null) return;
lstRecallSpells.ClearRows(); 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) foreach (var recall in data.RecallSpells)
{ {
var row = lstRecallSpells.AddRow(); 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() private void PopulateQuestsList()
{ {
try try
{ {
if (lstQuests == null) if (lstQuests == null) return;
{
PluginCore.WriteToChat("Quest list control is null");
return;
}
lstQuests.ClearRows(); lstQuests.ClearRows();
// Always show debug info for now // Add column headers - New order: Quest Name, Countdown, Last Solved, Cooldown, Solves
var row = lstQuests.AddRow(); var headerRow = lstQuests.AddRow();
SafeSetListText(row, 0, $"Quest Manager: {(questManager != null ? "OK" : "NULL")}"); SafeSetListText(headerRow, 0, "--- Quest Name ---");
SafeSetListText(row, 1, $"Quest Count: {questManager?.QuestList?.Count ?? 0}"); SafeSetListText(headerRow, 1, "Countdown");
SafeSetListText(row, 2, "Click Refresh to load quest data"); SafeSetListText(headerRow, 2, "Last Solved");
SafeSetListText(row, 3, ""); SafeSetListText(headerRow, 3, "Cooldown");
SafeSetListText(row, 4, ""); SafeSetListText(headerRow, 4, "Solves");
SafeSetListText(row, 5, "");
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(); var questRow = lstQuests.AddRow();
// Column 0: Quest Name // Column 0: Quest Name (friendly name only, wider)
SafeSetListText(questRow, 0, quest.Id); string questName = PluginCore.questManager.GetFriendlyQuestName(quest.Id);
SafeSetListText(questRow, 0, questName);
// Column 1: Solves // Column 1: Countdown Timer
SafeSetListText(questRow, 1, quest.Solves.ToString()); long timeRemaining = quest.ExpireTime - currentTime;
string countdownText = FormatCountdown(timeRemaining);
SafeSetListText(questRow, 1, countdownText);
// Column 2: Completed date // Column 2: Last Solved (date) - moved from Column 3
SafeSetListText(questRow, 2, questManager.FormatTimeStamp(quest.Timestamp)); SafeSetListText(questRow, 2, PluginCore.questManager.FormatTimeStamp(quest.Timestamp));
// Column 3: Max solves // Column 3: Cooldown (formatted duration) - moved from Column 4
string maxText = quest.MaxSolves < 0 ? "∞" : quest.MaxSolves.ToString(); SafeSetListText(questRow, 3, PluginCore.questManager.FormatSeconds(quest.Delta));
SafeSetListText(questRow, 3, maxText);
// Column 4: Delta (cooldown in seconds) // Column 4: Solves (white text) - moved from Column 5
SafeSetListText(questRow, 4, questManager.FormatSeconds(quest.Delta)); SafeSetListText(questRow, 4, quest.Solves.ToString());
SafeSetListColor(questRow, 4, System.Drawing.Color.White);
// Column 5: Expire time // Color code the countdown based on availability
string expireText = questManager.GetTimeUntilExpire(quest); if (quest.ExpireTime <= currentTime)
SafeSetListText(questRow, 5, expireText);
// Color coding based on availability
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (quest.MaxSolves > 0 && quest.Solves >= quest.MaxSolves)
{ {
// Quest is maxed out - red SafeSetListColor(questRow, 1, System.Drawing.Color.Green); // Ready - green countdown
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);
} }
else else
{ {
// Quest is on cooldown - yellow SafeSetListColor(questRow, 1, System.Drawing.Color.Yellow); // On cooldown - yellow countdown
SafeSetListColor(questRow, 5, System.Drawing.Color.Yellow);
} }
} }
} }
@ -702,23 +762,47 @@ namespace MosswartMassacre.Views
#region Cleanup #region Cleanup
private bool _disposed = false;
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (_disposed) return; // Prevent double disposal
if (disposing) if (disposing)
{ {
try 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) if (data != null)
{ {
data.Dispose(); data.Dispose();
data = null; data = null;
} }
if (questManager != null) // Stop and dispose quest update timer
if (questUpdateTimer != null)
{ {
questManager.Dispose(); questUpdateTimer.Stop();
questManager = null; questUpdateTimer.Elapsed -= OnQuestTimerUpdate;
questUpdateTimer.Dispose();
questUpdateTimer = null;
} }
_disposed = true;
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -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 try
{ {

View file

@ -56,7 +56,7 @@ namespace MosswartMassacre.Views
#region Flag Tracker Tab Controls #region Flag Tracker Tab Controls
private HudButton btnOpenFlagTracker; private HudButton btnOpenFlagTracker;
private HudStaticText lblFlagTrackerStatus; // lblFlagTrackerStatus removed - not present in XML
#endregion #endregion
#region Statistics Tracking #region Statistics Tracking
@ -280,15 +280,10 @@ namespace MosswartMassacre.Views
{ {
// Flag Tracker tab controls // Flag Tracker tab controls
btnOpenFlagTracker = GetControl<HudButton>("btnOpenFlagTracker"); btnOpenFlagTracker = GetControl<HudButton>("btnOpenFlagTracker");
lblFlagTrackerStatus = GetControl<HudStaticText>("lblFlagTrackerStatus");
// Hook up Flag Tracker events // Hook up Flag Tracker events
if (btnOpenFlagTracker != null) if (btnOpenFlagTracker != null)
btnOpenFlagTracker.Hit += OnOpenFlagTrackerClick; btnOpenFlagTracker.Hit += OnOpenFlagTrackerClick;
// Update initial status
if (lblFlagTrackerStatus != null)
lblFlagTrackerStatus.Text = "Status: Click to open the Flag Tracker window";
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -463,22 +458,13 @@ namespace MosswartMassacre.Views
{ {
try try
{ {
// Update status to show opening
if (lblFlagTrackerStatus != null)
lblFlagTrackerStatus.Text = "Status: Opening Flag Tracker window...";
// Open the Flag Tracker window // Open the Flag Tracker window
FlagTrackerView.OpenFlagTracker(); FlagTrackerView.OpenFlagTracker();
PluginCore.WriteToChat("Flag Tracker window opened");
// Update status
if (lblFlagTrackerStatus != null)
lblFlagTrackerStatus.Text = "Status: Flag Tracker window is open";
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"Error opening Flag Tracker: {ex.Message}"); PluginCore.WriteToChat($"Error opening Flag Tracker: {ex.Message}");
if (lblFlagTrackerStatus != null)
lblFlagTrackerStatus.Text = "Status: Error opening Flag Tracker";
} }
} }
#endregion #endregion

View file

@ -296,6 +296,20 @@ namespace MosswartMassacre
await SendEncodedAsync(json, CancellationToken.None); 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 ─────────────── // ─── shared send helper with locking ───────────────
private static async Task SendEncodedAsync(string text, CancellationToken token) private static async Task SendEncodedAsync(string text, CancellationToken token)

View file

@ -5,6 +5,8 @@ VisualStudioVersion = 17.13.35919.96
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MosswartMassacre", "MosswartMassacre\MosswartMassacre.csproj", "{8C97E839-4D05-4A5F-B0C8-E8E778654322}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MosswartMassacre", "MosswartMassacre\MosswartMassacre.csproj", "{8C97E839-4D05-4A5F-B0C8-E8E778654322}"
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
EndProject EndProject
Global Global
@ -17,6 +19,10 @@ Global
{8C97E839-4D05-4A5F-B0C8-E8E778654322}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{8C97E839-4D05-4A5F-B0C8-E8E778654322}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE