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>
1411 lines
57 KiB
C#
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");
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|