MosswartMassacre/MosswartMassacre/PluginCore.cs
erik 361c2012da Fix hot reload init ordering: move after core objects are created
InitializeForHotReload() was called at the top of Startup() before
_killTracker, _chatEventRouter, and _inventoryMonitor were created,
causing NullReferenceException. Move the hot reload block to after
all core objects are initialized.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:55:26 +00:00

1411 lines
57 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Timers;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using MosswartMassacre.Views;
using Mag.Shared.Constants;
namespace MosswartMassacre
{
[FriendlyName("Mosswart Massacre")]
public class PluginCore : PluginBase, IPluginLogger, IGameStats
{
// 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;
// Static bridge properties for VVSTabbedMainView (reads from manager instances)
private static InventoryMonitor _staticInventoryMonitor;
internal static int cachedPrismaticCount => _staticInventoryMonitor?.CachedPrismaticCount ?? 0;
private static KillTracker _staticKillTracker;
internal static int totalKills => _staticKillTracker?.TotalKills ?? 0;
internal static double killsPerHour => _staticKillTracker?.KillsPerHour ?? 0;
internal static int sessionDeaths => _staticKillTracker?.SessionDeaths ?? 0;
internal static int totalDeaths => _staticKillTracker?.TotalDeaths ?? 0;
internal static DateTime statsStartTime => _staticKillTracker?.StatsStartTime ?? DateTime.Now;
internal static DateTime lastKillTime => _staticKillTracker?.LastKillTime ?? DateTime.Now;
// IGameStats explicit implementation (for WebSocket telemetry)
int IGameStats.TotalKills => _staticKillTracker?.TotalKills ?? 0;
double IGameStats.KillsPerHour => _staticKillTracker?.KillsPerHour ?? 0;
int IGameStats.SessionDeaths => _staticKillTracker?.SessionDeaths ?? 0;
int IGameStats.TotalDeaths => _staticKillTracker?.TotalDeaths ?? 0;
int IGameStats.CachedPrismaticCount => _staticInventoryMonitor?.CachedPrismaticCount ?? 0;
string IGameStats.CharTag => CharTag;
DateTime IGameStats.StatsStartTime => _staticKillTracker?.StatsStartTime ?? DateTime.Now;
private static Timer vitalsTimer;
private static System.Windows.Forms.Timer commandTimer;
private static Timer characterStatsTimer;
private static readonly Queue<string> pendingCommands = new Queue<string>();
private static RareTracker _staticRareTracker;
public static bool RareMetaEnabled
{
get => _staticRareTracker?.RareMetaEnabled ?? true;
set { if (_staticRareTracker != null) _staticRareTracker.RareMetaEnabled = value; }
}
// VVS View Management
private static class ViewManager
{
public static void ViewInit()
{
Views.VVSTabbedMainView.ViewInit();
}
public static void ViewDestroy()
{
Views.VVSTabbedMainView.ViewDestroy();
}
public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour)
{
Views.VVSTabbedMainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
public static void UpdateElapsedTime(TimeSpan elapsed)
{
Views.VVSTabbedMainView.UpdateElapsedTime(elapsed);
}
public static void UpdateRareCount(int rareCount)
{
Views.VVSTabbedMainView.UpdateRareCount(rareCount);
}
public static void SetRareMetaToggleState(bool enabled)
{
Views.VVSTabbedMainView.SetRareMetaToggleState(enabled);
}
public static void RefreshSettingsFromConfig()
{
Views.VVSTabbedMainView.RefreshSettingsFromConfig();
}
public static void RefreshUpdateStatus()
{
Views.VVSTabbedMainView.RefreshUpdateStatus();
}
}
public static string CharTag { get; set; } = "";
public static bool WebSocketEnabled { get; set; } = false;
public bool InventoryLogEnabled { get; set; } = false;
public static bool AggressiveChatStreamingEnabled { get; set; } = true;
private MossyInventory _inventoryLogger;
public static NavVisualization navVisualization;
public static ChestLooter chestLooter;
// Quest Management for always-on quest streaming
public static QuestManager questManager;
private static readonly Queue<string> _chatQueue = new Queue<string>();
// Managers
private KillTracker _killTracker;
private RareTracker _rareTracker;
private InventoryMonitor _inventoryMonitor;
private ChatEventRouter _chatEventRouter;
private GameEventRouter _gameEventRouter;
private QuestStreamingService _questStreamingService;
private CommandRouter _commandRouter;
protected override void Startup()
{
try
{
// 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 (flag for post-init handling)
var isCharacterLoaded = CoreManager.Current.CharacterFilter.LoginStatus == 3;
var needsHotReload = IsHotReload || isCharacterLoaded;
// Initialize kill tracker (owns the 1-sec stats timer)
_killTracker = new KillTracker(
this,
(kills, per5, perHr) => ViewManager.UpdateKillStats(kills, per5, perHr),
elapsed => ViewManager.UpdateElapsedTime(elapsed));
_staticKillTracker = _killTracker;
_killTracker.Start();
// Initialize inventory monitor (taper tracking)
_inventoryMonitor = new InventoryMonitor(this);
_staticInventoryMonitor = _inventoryMonitor;
// Initialize chat event router (rareTracker set later in LoginComplete)
_chatEventRouter = new ChatEventRouter(
this, _killTracker, null,
count => ViewManager.UpdateRareCount(count),
msg => MyHost?.Actions.InvokeChatParser($"/a {msg}"));
// Initialize game event router
_gameEventRouter = new GameEventRouter(this);
// Note: Startup messages will appear after character login
// Subscribe to events
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(_chatEventRouter.OnChatText);
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(ChatEventRouter.AllChatText);
CoreManager.Current.CommandLineText += OnChatCommand;
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
CoreManager.Current.CharacterFilter.Death += OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject += OnSpawn;
CoreManager.Current.WorldFilter.CreateObject += OnPortalDetected;
CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn;
CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange;
// Initialize VVS view after character login
ViewManager.ViewInit();
// Initialize vitals streaming timer
vitalsTimer = new Timer(Constants.VitalsUpdateIntervalMs);
vitalsTimer.Elapsed += SendVitalsUpdate;
vitalsTimer.Start();
// Initialize command processing timer (Windows Forms timer for main thread)
commandTimer = new System.Windows.Forms.Timer();
commandTimer.Interval = Constants.CommandProcessIntervalMs;
commandTimer.Tick += ProcessPendingCommands;
commandTimer.Start();
// Note: View initialization moved to LoginComplete for VVS compatibility
// Initialize character stats and hook ServerDispatch early
// 0x0013 (character properties with luminance) fires DURING login,
// BEFORE LoginComplete — must hook here to catch it
CharacterStats.Init(this);
CoreManager.Current.EchoFilter.ServerDispatch += _gameEventRouter.OnServerDispatch;
// Enable TLS1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
//Enable vTank interface
vTank.Enable();
// Set logger for WebSocket
WebSocket.SetLogger(this);
WebSocket.SetGameStats(this);
//lyssna på commands
WebSocket.OnServerCommand += HandleServerCommand;
//starta inventory. Hanterar subscriptions i den med
_inventoryLogger = new MossyInventory();
// Initialize navigation visualization system
navVisualization = new NavVisualization();
// Initialize command router
_commandRouter = new CommandRouter();
RegisterCommands();
// Note: ChestLooter is initialized in LoginComplete after PluginSettings.Initialize()
// Note: DECAL Harmony patches will be initialized in LoginComplete event
// where the chat system is available for error messages
// Hot reload: run after all core objects are initialized
if (needsHotReload && isCharacterLoaded)
{
try
{
WriteToChat("[INFO] Hot reload detected - reinitializing plugin");
InitializeForHotReload();
WriteToChat("[INFO] Hot reload initialization complete");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Hot reload initialization failed: {ex.Message}");
}
}
}
catch (Exception ex)
{
WriteToChat("Error during startup: " + ex.Message);
}
}
protected override void Shutdown()
{
try
{
PluginSettings.Save();
WriteToChat("Mosswart Massacre is shutting down. Bye!");
// Unsubscribe from chat message event
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(_chatEventRouter.OnChatText);
CoreManager.Current.CommandLineText -= OnChatCommand;
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(ChatEventRouter.AllChatText);
CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject -= OnSpawn;
CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected;
CoreManager.Current.WorldFilter.ReleaseObject -= OnDespawn;
// Unsubscribe inventory monitor
if (_inventoryMonitor != null)
{
CoreManager.Current.WorldFilter.CreateObject -= _inventoryMonitor.OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange;
}
// Unsubscribe from server dispatch
CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
// Stop kill tracker
_killTracker?.Stop();
if (vitalsTimer != null)
{
vitalsTimer.Stop();
vitalsTimer.Dispose();
vitalsTimer = null;
}
if (commandTimer != null)
{
commandTimer.Stop();
commandTimer.Dispose();
commandTimer = null;
}
// Stop quest streaming service
_questStreamingService?.Stop();
_questStreamingService = null;
// Stop and dispose character stats timer
if (characterStatsTimer != null)
{
characterStatsTimer.Stop();
characterStatsTimer.Elapsed -= OnCharacterStatsUpdate;
characterStatsTimer.Dispose();
characterStatsTimer = null;
}
// Dispose quest manager
if (questManager != null)
{
questManager.Dispose();
questManager = null;
}
// Clean up the view
ViewManager.ViewDestroy();
//Disable vtank interface
vTank.Disable();
// sluta lyssna på commands
WebSocket.OnServerCommand -= HandleServerCommand;
WebSocket.Stop();
//shutdown inv
_inventoryLogger.Dispose();
// Clean up navigation visualization
if (navVisualization != null)
{
navVisualization.Dispose();
navVisualization = null;
}
// Clean up taper tracking
_inventoryMonitor?.Cleanup();
// Clean up Harmony patches
DecalHarmonyClean.Cleanup();
MyHost = null;
}
catch (Exception ex)
{
WriteToChat("Error during shutdown: " + ex.Message);
}
}
private void CharacterFilter_LoginComplete(object sender, EventArgs e)
{
CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
WriteToChat("Mosswart Massacre has started!");
PluginSettings.Initialize(); // Safe to call now
// Initialize chest looter system (needs PluginSettings to be ready)
try
{
chestLooter = new ChestLooter(CoreManager.Current, PluginSettings.Instance.ChestLooterSettings);
chestLooter.Initialize();
chestLooter.StatusChanged += (sender, status) =>
{
VVSTabbedMainView.UpdateChestLooterStatus(status);
};
}
catch (Exception ex)
{
WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}");
}
// Initialize rare tracker and wire to chat router
_rareTracker = new RareTracker(this);
_staticRareTracker = _rareTracker;
_chatEventRouter.SetRareTracker(_rareTracker);
// Apply the values
_rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
CharTag = PluginSettings.Instance.CharTag;
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
ViewManager.RefreshSettingsFromConfig(); // Refresh all UI settings after loading
if (WebSocketEnabled)
WebSocket.Start();
// Initialize Harmony patches using UtilityBelt's loaded DLL
try
{
bool success = DecalHarmonyClean.Initialize();
if (success)
WriteToChat("[OK] Plugin message interception active");
else
WriteToChat("[FAIL] Could not initialize message interception");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Harmony initialization failed: {ex.Message}");
}
// Initialize death tracking
_killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
// Initialize cached Prismatic Taper count
_inventoryMonitor.Initialize();
// Initialize quest manager for always-on quest streaming
try
{
questManager = new QuestManager();
// Trigger full quest data refresh (same as clicking refresh button)
Views.FlagTrackerView.RefreshQuestData();
// Initialize quest streaming service (30 seconds)
_questStreamingService = new QuestStreamingService(this);
_questStreamingService.Start();
WriteToChat("[OK] Quest streaming initialized with full data refresh");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Quest streaming initialization failed: {ex.Message}");
}
// Start character stats streaming
// Note: Init() and ServerDispatch hook are in Startup() so we catch
// 0x0013 (luminance/properties) which fires BEFORE LoginComplete
try
{
// Start 10-minute character stats timer
characterStatsTimer = new Timer(Constants.CharacterStatsIntervalMs);
characterStatsTimer.Elapsed += OnCharacterStatsUpdate;
characterStatsTimer.AutoReset = true;
characterStatsTimer.Start();
// Send initial stats after 5-second delay (let CharacterFilter populate)
var initialDelay = new Timer(Constants.LoginDelayMs);
initialDelay.AutoReset = false;
initialDelay.Elapsed += (s, args) =>
{
CharacterStats.CollectAndSend();
((Timer)s).Dispose();
};
initialDelay.Start();
WriteToChat("[OK] Character stats streaming initialized (10-min interval)");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Character stats initialization failed: {ex.Message}");
}
}
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();
// 1b. Initialize chest looter system (needs PluginSettings to be ready)
try
{
chestLooter = new ChestLooter(CoreManager.Current, PluginSettings.Instance.ChestLooterSettings);
chestLooter.Initialize();
chestLooter.StatusChanged += (sender, status) =>
{
VVSTabbedMainView.UpdateChestLooterStatus(status);
};
}
catch (Exception ex)
{
WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}");
}
// 2. Initialize rare tracker if not already set (missed when LoginComplete doesn't fire)
if (_rareTracker == null)
{
_rareTracker = new RareTracker(this);
_staticRareTracker = _rareTracker;
_chatEventRouter.SetRareTracker(_rareTracker);
}
// Apply the values from settings
_rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
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 (WebSocketEnabled)
{
WebSocket.Stop(); // Stop existing
WebSocket.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
_killTracker?.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
// 7. Reinitialize cached Prismatic Taper count
_inventoryMonitor?.Initialize();
// 8. Reinitialize quest manager for hot reload
try
{
if (questManager == null)
{
questManager = new QuestManager();
WriteToChat("[OK] Quest manager reinitialized");
}
else
{
WriteToChat("[INFO] Quest manager already active");
}
// Trigger full quest data refresh (same as clicking refresh button)
Views.FlagTrackerView.RefreshQuestData();
WriteToChat("[INFO] Quest data refresh triggered for hot reload");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Quest manager hot reload failed: {ex.Message}");
}
// 9. Reinitialize quest streaming service for hot reload
try
{
_questStreamingService?.Stop();
_questStreamingService = new QuestStreamingService(this);
_questStreamingService.Start();
WriteToChat("[OK] Quest streaming service reinitialized (30s interval)");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Quest streaming service hot reload failed: {ex.Message}");
}
WriteToChat("Hot reload initialization completed!");
}
private async void OnSpawn(object sender, CreateObjectEventArgs e)
{
var mob = e.New;
if (mob.ObjectClass != ObjectClass.Monster) return;
try
{
// Get DECAL coordinates
var decalCoords = mob.Coordinates();
if (decalCoords == null) return;
const string fmt = "F7";
string ns = decalCoords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture);
string ew = decalCoords.EastWest.ToString(fmt, CultureInfo.InvariantCulture);
// Get Z coordinate using RawCoordinates() for accurate world Z position
string zCoord = "0";
try
{
var rawCoords = mob.RawCoordinates();
if (rawCoords != null)
{
zCoord = rawCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
else
{
// Fallback to player Z approximation if RawCoordinates fails
var playerCoords = Coordinates.Me;
if (Math.Abs(playerCoords.Z) > 0.1)
{
zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
}
}
catch
{
// Fallback to player Z approximation on error
try
{
var playerCoords = Coordinates.Me;
if (Math.Abs(playerCoords.Z) > 0.1)
{
zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
}
catch
{
zCoord = "0";
}
}
await WebSocket.SendSpawnAsync(ns, ew, zCoord, mob.Name);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WS] Spawn send failed: {ex}");
}
}
private async void OnPortalDetected(object sender, CreateObjectEventArgs e)
{
var portal = e.New;
if (portal.ObjectClass != ObjectClass.Portal) return;
try
{
// Get portal coordinates from DECAL
var decalCoords = portal.Coordinates();
if (decalCoords == null) return;
const string fmt = "F7";
string ns = decalCoords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture);
string ew = decalCoords.EastWest.ToString(fmt, CultureInfo.InvariantCulture);
// Get Z coordinate using RawCoordinates() for accurate world Z position
string zCoord = "0";
try
{
var rawCoords = portal.RawCoordinates();
if (rawCoords != null)
{
zCoord = rawCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
else
{
// Fallback to player Z approximation if RawCoordinates fails
var playerCoords = Coordinates.Me;
if (Math.Abs(playerCoords.Z) > 0.1)
{
zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
}
}
catch
{
// Fallback to player Z approximation on error
try
{
var playerCoords = Coordinates.Me;
if (Math.Abs(playerCoords.Z) > 0.1)
{
zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
}
catch
{
zCoord = "0";
}
}
await WebSocket.SendPortalAsync(ns, ew, zCoord, portal.Name);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[PORTAL ERROR] {ex.Message}");
PluginCore.WriteToChat($"[WS] Portal send failed: {ex}");
}
}
private void OnDespawn(object sender, ReleaseObjectEventArgs e)
{
var mob = e.Released;
if (mob.ObjectClass != ObjectClass.Monster) return;
// var c = mob.Coordinates();
// PluginCore.WriteToChat(
// $"[Despawn] {mob.Name} @ (NS={c.NorthSouth:F1}, EW={c.EastWest:F1})");
}
private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e)
{
_killTracker.OnDeath();
_killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
}
private void HandleServerCommand(CommandEnvelope env)
{
// This is called from WebSocket thread - queue for main thread execution
lock (pendingCommands)
{
pendingCommands.Enqueue(env.Command);
}
}
private void ProcessPendingCommands(object sender, EventArgs e)
{
// This runs on the main UI thread via Windows Forms timer
string command = null;
lock (pendingCommands)
{
if (pendingCommands.Count > 0)
command = pendingCommands.Dequeue();
}
if (command != null)
{
try
{
// Execute ALL WebSocket commands on main thread - fast and reliable
DispatchChatToBoxWithPluginIntercept(command);
}
catch (Exception ex)
{
WriteToChat($"[WS] Command execution error: {ex.Message}");
}
}
}
private void OnChatCommand(object sender, ChatParserInterceptEventArgs e)
{
try
{
if (e.Text.StartsWith("/mm", StringComparison.OrdinalIgnoreCase))
{
e.Eat = true; // Prevent the message from showing in chat
HandleMmCommand(e.Text);
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[Error] Failed to process /mm command: {ex.Message}");
}
}
private static void SendVitalsUpdate(object sender, ElapsedEventArgs e)
{
try
{
// Only send if WebSocket is enabled
if (!WebSocketEnabled)
return;
// Collect vitals data
int currentHealth = CoreManager.Current.Actions.Vital[VitalType.CurrentHealth];
int currentStamina = CoreManager.Current.Actions.Vital[VitalType.CurrentStamina];
int currentMana = CoreManager.Current.Actions.Vital[VitalType.CurrentMana];
int maxHealth = CoreManager.Current.Actions.Vital[VitalType.MaximumHealth];
int maxStamina = CoreManager.Current.Actions.Vital[VitalType.MaximumStamina];
int maxMana = CoreManager.Current.Actions.Vital[VitalType.MaximumMana];
int vitae = CoreManager.Current.CharacterFilter.Vitae;
// Create vitals data structure
var vitalsData = new
{
type = "vitals",
timestamp = DateTime.UtcNow.ToString("o"),
character_name = CoreManager.Current.CharacterFilter.Name,
health_current = currentHealth,
health_max = maxHealth,
health_percentage = maxHealth > 0 ? Math.Round((double)currentHealth / maxHealth * 100, 1) : 0,
stamina_current = currentStamina,
stamina_max = maxStamina,
stamina_percentage = maxStamina > 0 ? Math.Round((double)currentStamina / maxStamina * 100, 1) : 0,
mana_current = currentMana,
mana_max = maxMana,
mana_percentage = maxMana > 0 ? Math.Round((double)currentMana / maxMana * 100, 1) : 0,
vitae = vitae
};
// Send via WebSocket
_ = WebSocket.SendVitalsAsync(vitalsData);
}
catch (Exception ex)
{
WriteToChat($"Error sending vitals: {ex.Message}");
}
}
private static void OnCharacterStatsUpdate(object sender, ElapsedEventArgs e)
{
try
{
CharacterStats.CollectAndSend();
}
catch (Exception ex)
{
WriteToChat($"[CharStats] Timer error: {ex.Message}");
}
}
public static void WriteToChat(string message)
{
try
{
// For hot reload scenarios where MyHost might be null, use CoreManager directly
if (MyHost != null)
{
MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1);
}
else
{
// Hot reload fallback1 - 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
}
}
}
void IPluginLogger.Log(string message) => WriteToChat(message);
public static void RestartStats()
{
_staticKillTracker?.RestartStats();
if (_staticRareTracker != null)
_staticRareTracker.RareCount = 0;
ViewManager.UpdateRareCount(0);
}
public static void ToggleRareMeta()
{
_staticRareTracker?.ToggleRareMeta();
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
}
[DllImport("Decal.dll")]
private static extern int DispatchOnChatCommand(ref IntPtr str, [MarshalAs(UnmanagedType.U4)] int target);
public static bool Decal_DispatchOnChatCommand(string cmd)
{
IntPtr bstr = Marshal.StringToBSTR(cmd);
try
{
bool eaten = (DispatchOnChatCommand(ref bstr, 1) & 0x1) > 0;
return eaten;
}
finally
{
Marshal.FreeBSTR(bstr);
}
}
public static void DispatchChatToBoxWithPluginIntercept(string cmd)
{
if (!Decal_DispatchOnChatCommand(cmd))
CoreManager.Current.Actions.InvokeChatParser(cmd);
}
private void HandleMmCommand(string text)
{
_commandRouter.Dispatch(text);
}
private void RegisterCommands()
{
_commandRouter.Register("ws", args =>
{
if (args.Length > 1)
{
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
WebSocketEnabled = true;
WebSocket.Start();
PluginSettings.Instance.WebSocketEnabled = true;
WriteToChat("WS streaming ENABLED.");
}
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
WebSocketEnabled = false;
WebSocket.Stop();
PluginSettings.Instance.WebSocketEnabled = false;
WriteToChat("WS streaming DISABLED.");
}
else
{
WriteToChat("Usage: /mm ws <enable|disable>");
}
}
else
{
WriteToChat("Usage: /mm ws <enable|disable>");
}
}, "Enable/disable WebSocket streaming");
_commandRouter.Register("report", args =>
{
TimeSpan elapsed = DateTime.Now - _killTracker.StatsStartTime;
string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_rareTracker?.RareCount ?? 0}, Session Deaths: {_killTracker.SessionDeaths}, Total Deaths: {_killTracker.TotalDeaths}";
WriteToChat(reportMessage);
}, "Show kill/death/rare stats");
_commandRouter.Register("getmetastate", args =>
{
string metaState = VtankControl.VtGetMetaState();
WriteToChat(metaState);
}, "Show current VTank meta state");
_commandRouter.Register("loc", args =>
{
Coordinates here = Coordinates.Me;
var pos = Utils.GetPlayerPosition();
WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
}, "Show current location");
_commandRouter.Register("reset", args =>
{
RestartStats();
}, "Reset kill/rare counters");
_commandRouter.Register("meta", args =>
{
RareMetaEnabled = !RareMetaEnabled;
WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
}, "Toggle rare meta state");
_commandRouter.Register("nextwp", args =>
{
double result = VtankControl.VtAdvanceWaypoint();
if (result == 1)
WriteToChat("Advanced VTank to next waypoint.");
else
WriteToChat("Failed to advance VTank waypoint. Is VTank running?");
}, "Advance VTank to next waypoint");
_commandRouter.Register("setchest", args =>
{
if (args.Length < 2)
{
WriteToChat("[ChestLooter] Usage: /mm setchest <chest name>");
return;
}
string chestName = string.Join(" ", args.Skip(1));
if (chestLooter != null)
{
chestLooter.SetChestName(chestName);
if (PluginSettings.Instance?.ChestLooterSettings != null)
{
PluginSettings.Instance.ChestLooterSettings.ChestName = chestName;
PluginSettings.Save();
}
Views.VVSTabbedMainView.RefreshChestLooterUI();
}
}, "Set chest name for looter");
_commandRouter.Register("setkey", args =>
{
if (args.Length < 2)
{
WriteToChat("[ChestLooter] Usage: /mm setkey <key name>");
return;
}
string keyName = string.Join(" ", args.Skip(1));
if (chestLooter != null)
{
chestLooter.SetKeyName(keyName);
if (PluginSettings.Instance?.ChestLooterSettings != null)
{
PluginSettings.Instance.ChestLooterSettings.KeyName = keyName;
PluginSettings.Save();
}
Views.VVSTabbedMainView.RefreshChestLooterUI();
}
}, "Set key name for looter");
_commandRouter.Register("lootchest", args =>
{
if (chestLooter != null)
{
if (!chestLooter.StartByName())
WriteToChat("[ChestLooter] Failed to start. Check chest/key names are set.");
}
else
{
WriteToChat("[ChestLooter] Chest looter not initialized");
}
}, "Start chest looting");
_commandRouter.Register("stoploot", args =>
{
if (chestLooter != null)
chestLooter.Stop();
else
WriteToChat("[ChestLooter] Chest looter not initialized");
}, "Stop chest looting");
_commandRouter.Register("vtanktest", args =>
{
try
{
WriteToChat("Testing VTank interface...");
WriteToChat($"VTank Instance: {(vTank.Instance != null ? "Found" : "NULL")}");
WriteToChat($"VTank Type: {vTank.Instance?.GetType()?.Name ?? "NULL"}");
WriteToChat($"NavCurrent: {vTank.Instance?.NavCurrent ?? -1}");
WriteToChat($"NavNumPoints: {vTank.Instance?.NavNumPoints ?? -1}");
WriteToChat($"NavType: {vTank.Instance?.NavType}");
WriteToChat($"MacroEnabled: {vTank.Instance?.MacroEnabled}");
}
catch (Exception ex)
{
WriteToChat($"VTank test error: {ex.Message}");
}
}, "");
_commandRouter.Register("decalstatus", args =>
{
try
{
WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ===");
WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}");
WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}");
WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}");
WriteToChat("=== Harmony Version Status ===");
try
{
var harmonyTest = Harmony.HarmonyInstance.Create("test.version.check");
WriteToChat($"[OK] Harmony Available (ID: {harmonyTest.Id})");
var harmonyAssembly = typeof(Harmony.HarmonyInstance).Assembly;
WriteToChat($"[OK] Harmony Version: {harmonyAssembly.GetName().Version}");
WriteToChat($"[OK] Harmony Location: {harmonyAssembly.Location}");
}
catch (Exception harmonyEx)
{
WriteToChat($"[FAIL] Harmony Test Failed: {harmonyEx.Message}");
}
}
catch (Exception ex)
{
WriteToChat($"Status check error: {ex.Message}");
}
}, "");
_commandRouter.Register("decaldebug", args =>
{
if (args.Length > 1)
{
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
AggressiveChatStreamingEnabled = true;
WriteToChat("[OK] DECAL debug streaming ENABLED - will show captured messages + stream via WebSocket");
}
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
AggressiveChatStreamingEnabled = false;
WriteToChat("[FAIL] DECAL debug streaming DISABLED - WebSocket streaming also disabled");
}
else
{
WriteToChat("Usage: /mm decaldebug <enable|disable>");
}
}
else
{
WriteToChat("Usage: /mm decaldebug <enable|disable>");
}
}, "");
_commandRouter.Register("gui", args =>
{
try
{
WriteToChat("Attempting to manually initialize GUI...");
ViewManager.ViewDestroy();
ViewManager.ViewInit();
WriteToChat("GUI initialization attempt completed.");
}
catch (Exception ex)
{
WriteToChat($"GUI initialization error: {ex.Message}");
}
}, "Reinitialize GUI");
_commandRouter.Register("testprismatic", args =>
{
try
{
WriteToChat("=== FULL INVENTORY DUMP ===");
var worldFilter = CoreManager.Current.WorldFilter;
var playerInv = CoreManager.Current.CharacterFilter.Id;
WriteToChat("Listing ALL items in your main inventory:");
int itemNum = 1;
foreach (WorldObject item in worldFilter.GetByContainer(playerInv))
{
if (!string.IsNullOrEmpty(item.Name))
{
int stackCount = item.Values(LongValueKey.StackCount, 0);
WriteToChat($"{itemNum:D2}: '{item.Name}' (count: {stackCount}, icon: 0x{item.Icon:X}, class: {item.ObjectClass})");
itemNum++;
string nameLower = item.Name.ToLower();
if (nameLower.Contains("taper") || nameLower.Contains("prismatic") ||
nameLower.Contains("prism") || nameLower.Contains("component"))
{
WriteToChat($" *** POSSIBLE MATCH: '{item.Name}' ***");
}
}
}
WriteToChat($"=== Total items listed: {itemNum - 1} ===");
WriteToChat("=== Testing Utility Functions on Prismatic Taper ===");
var foundItem = Utils.FindItemByName("Prismatic Taper");
if (foundItem != null)
{
WriteToChat($"SUCCESS! Found: '{foundItem.Name}'");
WriteToChat($"Utils.GetItemStackSize: {Utils.GetItemStackSize("Prismatic Taper")}");
WriteToChat($"Utils.GetItemIcon: 0x{Utils.GetItemIcon("Prismatic Taper"):X}");
WriteToChat($"Utils.GetItemDisplayIcon: 0x{Utils.GetItemDisplayIcon("Prismatic Taper"):X}");
WriteToChat("=== TELEMETRY WILL NOW WORK! ===");
}
else
{
WriteToChat("ERROR: Still can't find Prismatic Taper with utility functions!");
}
}
catch (Exception ex)
{
WriteToChat($"Search error: {ex.Message}");
}
}, "");
_commandRouter.Register("deathstats", args =>
{
try
{
WriteToChat("=== Death Tracking Statistics ===");
WriteToChat($"Session Deaths: {_killTracker.SessionDeaths}");
WriteToChat($"Total Deaths: {_killTracker.TotalDeaths}");
int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
WriteToChat($"Character Property NumDeaths: {currentCharDeaths}");
if (currentCharDeaths != _killTracker.TotalDeaths)
{
WriteToChat($"[WARNING] Death count sync issue detected!");
WriteToChat($"Updating totalDeaths from {_killTracker.TotalDeaths} to {currentCharDeaths}");
_killTracker.SetTotalDeaths(currentCharDeaths);
}
WriteToChat("Death tracking is active and will increment on character death.");
}
catch (Exception ex)
{
WriteToChat($"Death stats error: {ex.Message}");
}
}, "Show death tracking stats");
_commandRouter.Register("testdeath", args =>
{
try
{
WriteToChat("=== Manual Death Test ===");
WriteToChat($"Current sessionDeaths variable: {_killTracker.SessionDeaths}");
WriteToChat($"Current totalDeaths variable: {_killTracker.TotalDeaths}");
int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
WriteToChat($"Character Property NumDeaths (43): {currentCharDeaths}");
_killTracker.OnDeath();
WriteToChat($"Manually incremented sessionDeaths to: {_killTracker.SessionDeaths}");
WriteToChat("Note: This doesn't simulate a real death, just tests the tracking variables.");
WriteToChat($"Death event subscription check:");
var deathEvent = typeof(Decal.Adapter.Wrappers.CharacterFilter).GetEvent("Death");
WriteToChat($"Death event exists: {deathEvent != null}");
}
catch (Exception ex)
{
WriteToChat($"Test death error: {ex.Message}");
}
}, "");
_commandRouter.Register("testtaper", args =>
{
try
{
WriteToChat("=== Cached Taper Tracking Test ===");
WriteToChat($"Cached Count: {_inventoryMonitor.CachedPrismaticCount}");
WriteToChat($"Last Count: {_inventoryMonitor.LastPrismaticCount}");
int utilsCount = Utils.GetItemStackSize("Prismatic Taper");
WriteToChat($"Utils Count: {utilsCount}");
if (_inventoryMonitor.CachedPrismaticCount == utilsCount)
{
WriteToChat("[OK] Cached count matches Utils count");
}
else
{
WriteToChat($"[WARNING] Count mismatch! Cached: {_inventoryMonitor.CachedPrismaticCount}, Utils: {utilsCount}");
WriteToChat("Refreshing cached count...");
_inventoryMonitor.Initialize();
}
WriteToChat("=== Container Analysis ===");
int mainPackCount = 0;
int sidePackCount = 0;
int playerId = CoreManager.Current.CharacterFilter.Id;
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
{
int stackCount = wo.Values(LongValueKey.StackCount, 1);
if (wo.Container == playerId)
mainPackCount += stackCount;
else
sidePackCount += stackCount;
}
}
WriteToChat($"Main Pack Tapers: {mainPackCount}");
WriteToChat($"Side Pack Tapers: {sidePackCount}");
WriteToChat($"Total: {mainPackCount + sidePackCount}");
WriteToChat("=== Event System Status ===");
WriteToChat($"Tracking {_inventoryMonitor.TrackedTaperCount} taper stacks for delta detection");
WriteToChat($"Known stack sizes: {_inventoryMonitor.KnownStackSizesCount} items");
WriteToChat("Pure delta tracking - NO expensive inventory scans during events!");
WriteToChat("Now tracks: consumption, drops, trades, container moves");
WriteToChat("Try moving tapers between containers and casting spells!");
}
catch (Exception ex)
{
WriteToChat($"Taper test error: {ex.Message}");
}
}, "");
_commandRouter.Register("finditem", args =>
{
if (args.Length > 1)
{
string itemName = string.Join(" ", args, 1, args.Length - 1).Trim('"');
WriteToChat($"=== Searching for: '{itemName}' ===");
var foundItem = Utils.FindItemByName(itemName);
if (foundItem != null)
{
WriteToChat($"FOUND: '{foundItem.Name}'");
WriteToChat($"Count: {foundItem.Values(LongValueKey.StackCount, 0)}");
WriteToChat($"Icon: 0x{foundItem.Icon:X}");
WriteToChat($"Display Icon: 0x{(foundItem.Icon + 0x6000000):X}");
WriteToChat($"Object Class: {foundItem.ObjectClass}");
}
else
{
WriteToChat($"NOT FOUND: '{itemName}'");
WriteToChat("Make sure the name is exactly as it appears in-game.");
}
}
else
{
WriteToChat("Usage: /mm finditem \"Item Name\"");
WriteToChat("Example: /mm finditem \"Prismatic Taper\"");
}
}, "Find item in inventory");
_commandRouter.Register("checkforupdate", args =>
{
Task.Run(async () =>
{
await UpdateManager.CheckForUpdateAsync();
try
{
ViewManager.RefreshUpdateStatus();
}
catch (Exception ex)
{
WriteToChat($"Error refreshing UI: {ex.Message}");
}
});
}, "Check for plugin updates");
_commandRouter.Register("update", args =>
{
Task.Run(async () =>
{
await UpdateManager.DownloadAndInstallUpdateAsync();
});
}, "Download and install latest update");
_commandRouter.Register("debugupdate", args =>
{
Views.VVSTabbedMainView.DebugUpdateControls();
}, "");
_commandRouter.Register("sendinventory", args =>
{
if (_inventoryLogger != null)
_inventoryLogger.ForceInventoryUpload();
else
WriteToChat("[INV] Inventory system not initialized");
}, "Force full inventory upload");
_commandRouter.Register("refreshquests", args =>
{
try
{
WriteToChat("[QUEST] Refreshing quest data...");
Views.FlagTrackerView.RefreshQuestData();
}
catch (Exception ex)
{
WriteToChat($"[QUEST] Refresh failed: {ex.Message}");
}
}, "Refresh quest data");
_commandRouter.Register("queststatus", args =>
{
try
{
WriteToChat("=== Quest Streaming Status ===");
WriteToChat($"Timer Active: {_questStreamingService?.IsRunning ?? false}");
WriteToChat($"WebSocket Enabled: {WebSocketEnabled}");
WriteToChat($"Quest Manager: {(questManager != null ? "Active" : "Not Active")}");
WriteToChat($"Quest Count: {questManager?.QuestList?.Count ?? 0}");
if (questManager?.QuestList != null)
{
var priorityQuests = questManager.QuestList
.Where(q => QuestStreamingService.IsHighPriorityQuest(q.Id))
.GroupBy(q => q.Id)
.Select(g => g.First())
.ToList();
WriteToChat($"Priority Quests Found: {priorityQuests.Count}");
foreach (var quest in priorityQuests)
{
string questName = questManager.GetFriendlyQuestName(quest.Id);
WriteToChat($" - {questName} ({quest.Id})");
}
}
WriteToChat($"Verbose Logging: {PluginSettings.Instance?.VerboseLogging ?? false}");
WriteToChat("Use '/mm verbose' to toggle debug logging");
}
catch (Exception ex)
{
WriteToChat($"[QUEST] Status check failed: {ex.Message}");
}
}, "Show quest streaming status");
_commandRouter.Register("verbose", args =>
{
if (PluginSettings.Instance != null)
{
PluginSettings.Instance.VerboseLogging = !PluginSettings.Instance.VerboseLogging;
WriteToChat($"Verbose logging: {(PluginSettings.Instance.VerboseLogging ? "ENABLED" : "DISABLED")}");
}
else
{
WriteToChat("Settings not initialized");
}
}, "Toggle verbose debug logging");
}
}
}