MosswartMassacre/MosswartMassacre/PluginCore.backup.cs

1654 lines
71 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Timers;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using MosswartMassacre.Views;
using Mag.Shared.Constants;
namespace MosswartMassacre
{
[FriendlyName("Mosswart Massacre")]
public class PluginCore : PluginBase
{
// Hot Reload Support Properties
public static string AssemblyDirectory { get; set; }
public static bool IsHotReload { get; set; }
internal static PluginHost MyHost;
internal static int totalKills = 0;
internal static int rareCount = 0;
internal static int sessionDeaths = 0; // Deaths this session
internal static int totalDeaths = 0; // Total deaths from character
internal static int cachedPrismaticCount = 0; // Cached Prismatic Taper count
internal static int lastPrismaticCount = 0; // For delta calculation
// Track taper items and their containers for accurate release detection
private static readonly Dictionary<int, int> trackedTaperContainers = new Dictionary<int, int>();
private static readonly Dictionary<int, int> lastKnownStackSizes = new Dictionary<int, int>();
internal static DateTime lastKillTime = DateTime.Now;
internal static double killsPer5Min = 0;
internal static double killsPerHour = 0;
internal static DateTime statsStartTime = DateTime.Now;
internal static Timer updateTimer;
private static Timer vitalsTimer;
private static System.Windows.Forms.Timer commandTimer;
private static readonly Queue<string> pendingCommands = new Queue<string>();
public static bool RareMetaEnabled { get; set; } = true;
// VVS View Management
private static class ViewManager
{
public static void ViewInit()
{
Views.VVSTabbedMainView.ViewInit();
}
public static void ViewDestroy()
{
Views.VVSTabbedMainView.ViewDestroy();
}
public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour)
{
Views.VVSTabbedMainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
public static void UpdateElapsedTime(TimeSpan elapsed)
{
Views.VVSTabbedMainView.UpdateElapsedTime(elapsed);
}
public static void UpdateRareCount(int rareCount)
{
Views.VVSTabbedMainView.UpdateRareCount(rareCount);
}
public static void SetRareMetaToggleState(bool enabled)
{
Views.VVSTabbedMainView.SetRareMetaToggleState(enabled);
}
public static void RefreshSettingsFromConfig()
{
Views.VVSTabbedMainView.RefreshSettingsFromConfig();
}
}
public static bool RemoteCommandsEnabled { get; set; } = false;
public static bool HttpServerEnabled { get; set; } = false;
public static string CharTag { get; set; } = "";
public static bool TelemetryEnabled { get; set; } = false;
public static bool WebSocketEnabled { get; set; } = false;
public bool InventoryLogEnabled { get; set; } = false;
public static bool AggressiveChatStreamingEnabled { get; set; } = true;
private MossyInventory _inventoryLogger;
public static NavVisualization navVisualization;
// Quest Management for always-on quest streaming
public static QuestManager questManager;
private static Timer questStreamingTimer;
private static Queue<string> rareMessageQueue = new Queue<string>();
private static DateTime _lastSent = DateTime.MinValue;
private static readonly Queue<string> _chatQueue = new Queue<string>();
protected override void Startup()
{
try
{
// DEBUG: Add startup debug message
WriteToChat("[DEBUG] PluginCore.Startup() called");
// Set MyHost - for hot reload scenarios, Host might be null
if (Host != null)
{
MyHost = Host;
}
else if (MyHost == null)
{
// Hot reload fallback - this is okay, WriteToChat will handle it
MyHost = null;
}
// Check if this is a hot reload
var isCharacterLoaded = CoreManager.Current.CharacterFilter.LoginStatus == 3;
if (IsHotReload || isCharacterLoaded)
{
// Hot reload detected - reinitialize connections and state
WriteToChat("[INFO] Hot reload detected - reinitializing plugin");
// Reload settings if character is already logged in
if (isCharacterLoaded)
{
try
{
WriteToChat("Hot reload - reinitializing character-dependent systems");
// Don't call LoginComplete - create hot reload specific initialization
InitializeForHotReload();
WriteToChat("[INFO] Hot reload initialization complete");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Hot reload initialization failed: {ex.Message}");
}
}
}
// Note: Startup messages will appear after character login
// Subscribe to chat message event
WriteToChat("[DEBUG] Subscribing to events...");
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(AllChatText);
CoreManager.Current.CommandLineText += OnChatCommand;
WriteToChat("[DEBUG] About to subscribe to LoginComplete event");
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
WriteToChat("[DEBUG] LoginComplete event subscription successful");
CoreManager.Current.CharacterFilter.Death += OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject += OnSpawn;
CoreManager.Current.WorldFilter.CreateObject += OnPortalDetected;
CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn;
// Subscribe to inventory change events for taper tracking
CoreManager.Current.WorldFilter.CreateObject += OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject += OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject += OnInventoryChange;
// Initialize VVS view after character login
ViewManager.ViewInit();
// Initialize the timer
updateTimer = new Timer(1000); // Update every second
updateTimer.Elapsed += UpdateStats;
updateTimer.Start();
// Initialize vitals streaming timer
vitalsTimer = new Timer(5000); // Send vitals every 5 seconds
vitalsTimer.Elapsed += SendVitalsUpdate;
vitalsTimer.Start();
// Initialize command processing timer (Windows Forms timer for main thread)
commandTimer = new System.Windows.Forms.Timer();
commandTimer.Interval = 10; // Process commands every 10ms
commandTimer.Tick += ProcessPendingCommands;
commandTimer.Start();
// Note: View initialization moved to LoginComplete for VVS compatibility
// Enable TLS1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
//Enable vTank interface
vTank.Enable();
//lyssna på commands
WebSocket.OnServerCommand += HandleServerCommand;
//starta inventory. Hanterar subscriptions i den med
_inventoryLogger = new MossyInventory();
// Initialize navigation visualization system
navVisualization = new NavVisualization();
// Note: DECAL Harmony patches will be initialized in LoginComplete event
// where the chat system is available for error messages
}
catch (Exception ex)
{
WriteToChat("Error during startup: " + ex.Message);
}
}
protected override void Shutdown()
{
try
{
PluginSettings.Save();
if (TelemetryEnabled)
Telemetry.Stop(); // ensure no dangling timer / HttpClient
WriteToChat("Mosswart Massacre is shutting down...");
// Unsubscribe from chat message event
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
CoreManager.Current.CommandLineText -= OnChatCommand;
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(AllChatText);
CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject -= OnSpawn;
CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected;
CoreManager.Current.WorldFilter.ReleaseObject -= OnDespawn;
// Unsubscribe from inventory change events
CoreManager.Current.WorldFilter.CreateObject -= OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject -= OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject -= OnInventoryChange;
// Stop and dispose of the timers
if (updateTimer != null)
{
updateTimer.Stop();
updateTimer.Dispose();
updateTimer = null;
}
if (vitalsTimer != null)
{
vitalsTimer.Stop();
vitalsTimer.Dispose();
vitalsTimer = null;
}
if (commandTimer != null)
{
commandTimer.Stop();
commandTimer.Dispose();
commandTimer = null;
}
// Stop and dispose quest streaming timer
if (questStreamingTimer != null)
{
questStreamingTimer.Stop();
questStreamingTimer.Elapsed -= OnQuestStreamingUpdate;
questStreamingTimer.Dispose();
questStreamingTimer = null;
}
// Dispose quest manager
if (questManager != null)
{
questManager.Dispose();
questManager = null;
}
// Clean up the view
ViewManager.ViewDestroy();
//Disable vtank interface
vTank.Disable();
// sluta lyssna på commands
WebSocket.OnServerCommand -= HandleServerCommand;
WebSocket.Stop();
//shutdown inv
_inventoryLogger.Dispose();
// Clean up navigation visualization
if (navVisualization != null)
{
navVisualization.Dispose();
navVisualization = null;
}
// Clean up taper tracking
trackedTaperContainers.Clear();
lastKnownStackSizes.Clear();
// Clean up Harmony patches
DecalHarmonyClean.Cleanup();
MyHost = null;
}
catch (Exception ex)
{
WriteToChat("Error during shutdown: " + ex.Message);
}
}
private void CharacterFilter_LoginComplete(object sender, EventArgs e)
{
WriteToChat("[DEBUG] CharacterFilter_LoginComplete event fired!");
CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
WriteToChat("Mosswart Massacre has started!");
PluginSettings.Initialize(); // Safe to call now
// Apply the values
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
CharTag = PluginSettings.Instance.CharTag;
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
ViewManager.RefreshSettingsFromConfig(); // Refresh all UI settings after loading
if (TelemetryEnabled)
Telemetry.Start();
if (WebSocketEnabled)
WebSocket.Start();
// Initialize Harmony patches using UtilityBelt's loaded DLL
try
{
bool success = DecalHarmonyClean.Initialize();
if (success)
WriteToChat("[OK] Plugin message interception active");
else
WriteToChat("[FAIL] Could not initialize message interception");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Harmony initialization failed: {ex.Message}");
}
// Initialize death tracking
totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
sessionDeaths = 0;
// Initialize cached Prismatic Taper count
InitializePrismaticTaperCount();
// Initialize quest manager for always-on quest streaming
try
{
questManager = new QuestManager();
questManager.RefreshQuests();
// Initialize quest streaming timer (30 seconds)
questStreamingTimer = new Timer(30000);
questStreamingTimer.Elapsed += OnQuestStreamingUpdate;
questStreamingTimer.AutoReset = true;
questStreamingTimer.Start();
WriteToChat("[OK] Quest streaming initialized");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Quest streaming initialization failed: {ex.Message}");
}
}
#region Quest Streaming Methods
private static void OnQuestStreamingUpdate(object sender, ElapsedEventArgs e)
{
try
{
// Stream high priority quest data via WebSocket
if (WebSocketEnabled && questManager?.QuestList != null && questManager.QuestList.Count > 0)
{
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// Find and stream priority quests (deduplicated by quest ID)
var priorityQuests = questManager.QuestList
.Where(q => IsHighPriorityQuest(q.Id))
.GroupBy(q => q.Id)
.Select(g => g.First()) // Take first occurrence of each quest ID
.ToList();
foreach (var quest in priorityQuests)
{
try
{
string questName = questManager.GetFriendlyQuestName(quest.Id);
long timeRemaining = quest.ExpireTime - currentTime;
string countdown = FormatCountdown(timeRemaining);
// Stream quest data
System.Threading.Tasks.Task.Run(() => WebSocket.SendQuestDataAsync(questName, countdown));
}
catch (Exception)
{
// Silently handle individual quest streaming errors
}
}
}
}
catch (Exception)
{
// Silently handle quest streaming errors to avoid spam
}
}
private static bool IsHighPriorityQuest(string questId)
{
return questId == "stipendtimer_0812" || // Changed from stipendtimer_monthly to stipendtimer_0812
questId == "augmentationblankgemacquired" ||
questId == "insatiableeaterjaw";
}
private static string FormatCountdown(long seconds)
{
if (seconds <= 0)
return "READY";
var timeSpan = TimeSpan.FromSeconds(seconds);
if (timeSpan.TotalDays >= 1)
return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours:D2}h";
else if (timeSpan.TotalHours >= 1)
return $"{timeSpan.Hours}h {timeSpan.Minutes:D2}m";
else if (timeSpan.TotalMinutes >= 1)
return $"{timeSpan.Minutes}m {timeSpan.Seconds:D2}s";
else
return $"{timeSpan.Seconds}s";
}
#endregion
private void InitializeForHotReload()
{
// This method handles initialization that depends on character being logged in
// Similar to LoginComplete but designed for hot reload scenarios
WriteToChat("Mosswart Massacre hot reload initialization started!");
// 1. Initialize settings - CRITICAL first step
PluginSettings.Initialize();
// 2. Apply the values from settings
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
CharTag = PluginSettings.Instance.CharTag;
// 3. Update UI with current settings
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
ViewManager.RefreshSettingsFromConfig();
// 4. Restart services if they were enabled (stop first, then start)
if (TelemetryEnabled)
{
Telemetry.Stop(); // Stop existing
Telemetry.Start(); // Restart
}
if (WebSocketEnabled)
{
WebSocket.Stop(); // Stop existing
WebSocket.Start(); // Restart
}
if (HttpServerEnabled)
{
HttpCommandServer.Stop(); // Stop existing
HttpCommandServer.Start(); // Restart
}
// 5. Initialize Harmony patches (only if not already done)
// Note: Harmony patches are global and don't need reinitialization
if (!DecalHarmonyClean.IsActive())
{
try
{
bool success = DecalHarmonyClean.Initialize();
if (success)
WriteToChat("[OK] Plugin message interception active");
else
WriteToChat("[FAIL] Could not initialize message interception");
}
catch (Exception ex)
{
WriteToChat($"[ERROR] Harmony initialization failed: {ex.Message}");
}
}
// 6. Reinitialize death tracking
totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
// Don't reset sessionDeaths - keep the current session count
// 7. Reinitialize cached Prismatic Taper count
InitializePrismaticTaperCount();
WriteToChat("Hot reload initialization completed!");
}
private void InitializePrismaticTaperCount()
{
try
{
lastPrismaticCount = cachedPrismaticCount;
cachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
// Initialize tracking for existing tapers
trackedTaperContainers.Clear();
lastKnownStackSizes.Clear();
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase) &&
IsPlayerOwnedContainer(wo.Container))
{
int stackCount = wo.Values(LongValueKey.StackCount, 1);
trackedTaperContainers[wo.Id] = wo.Container;
lastKnownStackSizes[wo.Id] = stackCount;
}
}
}
catch (Exception ex)
{
WriteToChat($"[TAPER] Error initializing count: {ex.Message}");
cachedPrismaticCount = 0;
lastPrismaticCount = 0;
trackedTaperContainers.Clear();
lastKnownStackSizes.Clear();
}
}
private bool IsPlayerOwnedContainer(int containerId)
{
try
{
// Check if it's the player's main inventory
if (containerId == CoreManager.Current.CharacterFilter.Id)
return true;
// Check if it's one of the player's containers (side packs)
// Get the container object to verify it belongs to the player
WorldObject container = CoreManager.Current.WorldFilter[containerId];
if (container != null &&
container.ObjectClass == ObjectClass.Container &&
container.Container == CoreManager.Current.CharacterFilter.Id)
{
return true;
}
return false;
}
catch
{
return false;
}
}
private void OnInventoryCreate(object sender, CreateObjectEventArgs e)
{
try
{
var item = e.New;
if (IsPlayerOwnedContainer(item.Container) &&
item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
{
lastPrismaticCount = cachedPrismaticCount;
int stackCount = item.Values(LongValueKey.StackCount, 1);
cachedPrismaticCount += stackCount;
int delta = cachedPrismaticCount - lastPrismaticCount;
// Initialize tracking for this new taper
trackedTaperContainers[item.Id] = item.Container;
lastKnownStackSizes[item.Id] = stackCount;
}
}
catch (Exception ex)
{
WriteToChat($"[TAPER] Error in OnInventoryCreate: {ex.Message}");
}
}
private void OnInventoryRelease(object sender, ReleaseObjectEventArgs e)
{
try
{
var item = e.Released;
if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
{
// Check where this taper WAS before being released (not where it's going)
if (trackedTaperContainers.TryGetValue(item.Id, out int previousContainer))
{
if (IsPlayerOwnedContainer(previousContainer))
{
// This taper was in our inventory and is now being released
lastPrismaticCount = cachedPrismaticCount;
int stackCount = item.Values(LongValueKey.StackCount, 1);
cachedPrismaticCount -= stackCount;
}
// Clean up tracking
trackedTaperContainers.Remove(item.Id);
lastKnownStackSizes.Remove(item.Id);
}
else
{
// Fallback: recalculate total count when untracked taper is released
lastPrismaticCount = cachedPrismaticCount;
cachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
}
}
}
catch (Exception ex)
{
WriteToChat($"[TAPER] Error in OnInventoryRelease: {ex.Message}");
}
}
private void OnInventoryChange(object sender, ChangeObjectEventArgs e)
{
try
{
var item = e.Changed;
if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
{
bool isInPlayerContainer = IsPlayerOwnedContainer(item.Container);
// Track container location for release detection
if (isInPlayerContainer)
{
bool wasAlreadyTracked = trackedTaperContainers.ContainsKey(item.Id);
trackedTaperContainers[item.Id] = item.Container;
// Handle stack size changes with pure delta math
int currentStack = item.Values(LongValueKey.StackCount, 1);
// Check if this is a pickup from ground (item not previously tracked)
if (!wasAlreadyTracked)
{
// This is likely a pickup from ground - increment count
lastPrismaticCount = cachedPrismaticCount;
cachedPrismaticCount += currentStack;
}
else if (lastKnownStackSizes.TryGetValue(item.Id, out int previousStack))
{
int stackDelta = currentStack - previousStack;
if (stackDelta != 0)
{
lastPrismaticCount = cachedPrismaticCount;
cachedPrismaticCount += stackDelta;
}
}
lastKnownStackSizes[item.Id] = currentStack;
}
// Item is no longer in player containers
// DON'T clean up tracking here - let OnInventoryRelease handle cleanup
// This ensures tracking data is available for the Release event
}
}
catch (Exception ex)
{
WriteToChat($"[TAPER] Error in OnInventoryChange: {ex.Message}");
}
}
private async void OnSpawn(object sender, CreateObjectEventArgs e)
{
var mob = e.New;
if (mob.ObjectClass != ObjectClass.Monster) return;
try
{
// Get DECAL coordinates
var decalCoords = mob.Coordinates();
if (decalCoords == null) return;
const string fmt = "F7";
string ns = decalCoords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture);
string ew = decalCoords.EastWest.ToString(fmt, CultureInfo.InvariantCulture);
// Get Z coordinate using RawCoordinates() for accurate world Z position
string zCoord = "0";
try
{
var rawCoords = mob.RawCoordinates();
if (rawCoords != null)
{
zCoord = rawCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
else
{
// Fallback to player Z approximation if RawCoordinates fails
var playerCoords = Coordinates.Me;
if (Math.Abs(playerCoords.Z) > 0.1)
{
zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
}
}
catch
{
// Fallback to player Z approximation on error
try
{
var playerCoords = Coordinates.Me;
if (Math.Abs(playerCoords.Z) > 0.1)
{
zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
}
catch
{
zCoord = "0";
}
}
await WebSocket.SendSpawnAsync(ns, ew, zCoord, mob.Name);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WS] Spawn send failed: {ex}");
}
}
private async void OnPortalDetected(object sender, CreateObjectEventArgs e)
{
var portal = e.New;
if (portal.ObjectClass != ObjectClass.Portal) return;
try
{
// Get portal coordinates from DECAL
var decalCoords = portal.Coordinates();
if (decalCoords == null) return;
const string fmt = "F7";
string ns = decalCoords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture);
string ew = decalCoords.EastWest.ToString(fmt, CultureInfo.InvariantCulture);
// Get Z coordinate using RawCoordinates() for accurate world Z position
string zCoord = "0";
try
{
var rawCoords = portal.RawCoordinates();
if (rawCoords != null)
{
zCoord = rawCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
else
{
// Fallback to player Z approximation if RawCoordinates fails
var playerCoords = Coordinates.Me;
if (Math.Abs(playerCoords.Z) > 0.1)
{
zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
}
}
catch
{
// Fallback to player Z approximation on error
try
{
var playerCoords = Coordinates.Me;
if (Math.Abs(playerCoords.Z) > 0.1)
{
zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
}
}
catch
{
zCoord = "0";
}
}
await WebSocket.SendPortalAsync(ns, ew, zCoord, portal.Name);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[PORTAL ERROR] {ex.Message}");
PluginCore.WriteToChat($"[WS] Portal send failed: {ex}");
}
}
private void OnDespawn(object sender, ReleaseObjectEventArgs e)
{
var mob = e.Released;
if (mob.ObjectClass != ObjectClass.Monster) return;
// var c = mob.Coordinates();
// PluginCore.WriteToChat(
// $"[Despawn] {mob.Name} @ (NS={c.NorthSouth:F1}, EW={c.EastWest:F1})");
}
private async void AllChatText(object sender, ChatTextInterceptEventArgs e)
{
try
{
string cleaned = NormalizeChatLine(e.Text);
// Send to WebSocket
await WebSocket.SendChatTextAsync(e.Color, cleaned);
// Note: Plugin message analysis is now handled by Harmony patches
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WS] Chat send failed: {ex}");
}
}
private static string NormalizeChatLine(string raw)
{
if (string.IsNullOrEmpty(raw))
return raw;
// 1) Remove all <…> tags
var noTags = Regex.Replace(raw, "<[^>]+>", "");
// 2) Trim trailing newline or carriage-return
var trimmed = noTags.TrimEnd('\r', '\n');
// 3) Collapse multiple spaces into one
var collapsed = Regex.Replace(trimmed, @"[ ]{2,}", " ");
return collapsed;
}
private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e)
{
sessionDeaths++;
totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
}
private void HandleServerCommand(CommandEnvelope env)
{
// This is called from WebSocket thread - queue for main thread execution
lock (pendingCommands)
{
pendingCommands.Enqueue(env.Command);
}
}
private void ProcessPendingCommands(object sender, EventArgs e)
{
// This runs on the main UI thread via Windows Forms timer
string command = null;
lock (pendingCommands)
{
if (pendingCommands.Count > 0)
command = pendingCommands.Dequeue();
}
if (command != null)
{
try
{
// Execute ALL WebSocket commands on main thread - fast and reliable
DispatchChatToBoxWithPluginIntercept(command);
}
catch (Exception ex)
{
WriteToChat($"[WS] Command execution error: {ex.Message}");
}
}
}
private void OnChatText(object sender, ChatTextInterceptEventArgs e)
{
try
{
if (IsKilledByMeMessage(e.Text))
{
totalKills++;
lastKillTime = DateTime.Now;
CalculateKillsPerInterval();
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
if (IsRareDiscoveryMessage(e.Text, out string rareText))
{
rareCount++;
ViewManager.UpdateRareCount(rareCount);
if (RareMetaEnabled)
{
Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
}
DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
// Fire and forget: we don't await, since sending is not critical and we don't want to block.
_ = WebSocket.SendRareAsync(rareText);
}
if (e.Color == 18 && e.Text.EndsWith("!report\""))
{
TimeSpan elapsed = DateTime.Now - statsStartTime;
string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}";
WriteToChat($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
MyHost.Actions.InvokeChatParser($"/a {reportMessage}");
}
if (RemoteCommandsEnabled && e.Color == 18)
{
string characterName = Regex.Escape(CoreManager.Current.CharacterFilter.Name);
string pattern = $@"^\[Allegiance\].*Dunking Rares.*say[s]?, \""!do {characterName} (?<command>.+)\""$";
string tag = Regex.Escape(PluginCore.CharTag);
string patterntag = $@"^\[Allegiance\].*Dunking Rares.*say[s]?, \""!dot {tag} (?<command>.+)\""$";
var match = Regex.Match(e.Text, pattern);
var matchtag = Regex.Match(e.Text, patterntag);
if (match.Success)
{
string command = match.Groups["command"].Value;
DispatchChatToBoxWithPluginIntercept(command);
DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
}
else if (matchtag.Success)
{
string command = matchtag.Groups["command"].Value;
DispatchChatToBoxWithPluginIntercept(command);
DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
}
}
}
catch (Exception ex)
{
WriteToChat("Error processing chat message: " + ex.Message);
}
}
private void OnChatCommand(object sender, ChatParserInterceptEventArgs e)
{
try
{
if (e.Text.StartsWith("/mm", StringComparison.OrdinalIgnoreCase))
{
e.Eat = true; // Prevent the message from showing in chat
HandleMmCommand(e.Text);
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[Error] Failed to process /mm command: {ex.Message}");
}
}
private void UpdateStats(object sender, ElapsedEventArgs e)
{
try
{
// Update the elapsed time
TimeSpan elapsed = DateTime.Now - statsStartTime;
ViewManager.UpdateElapsedTime(elapsed);
// Recalculate kill rates
CalculateKillsPerInterval();
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
catch (Exception ex)
{
WriteToChat("Error updating stats: " + ex.Message);
}
}
private static void SendVitalsUpdate(object sender, ElapsedEventArgs e)
{
try
{
// Only send if WebSocket is enabled
if (!WebSocketEnabled)
return;
// Collect vitals data
int currentHealth = CoreManager.Current.Actions.Vital[VitalType.CurrentHealth];
int currentStamina = CoreManager.Current.Actions.Vital[VitalType.CurrentStamina];
int currentMana = CoreManager.Current.Actions.Vital[VitalType.CurrentMana];
int maxHealth = CoreManager.Current.Actions.Vital[VitalType.MaximumHealth];
int maxStamina = CoreManager.Current.Actions.Vital[VitalType.MaximumStamina];
int maxMana = CoreManager.Current.Actions.Vital[VitalType.MaximumMana];
int vitae = CoreManager.Current.CharacterFilter.Vitae;
// Create vitals data structure
var vitalsData = new
{
type = "vitals",
timestamp = DateTime.UtcNow.ToString("o"),
character_name = CoreManager.Current.CharacterFilter.Name,
health_current = currentHealth,
health_max = maxHealth,
health_percentage = maxHealth > 0 ? Math.Round((double)currentHealth / maxHealth * 100, 1) : 0,
stamina_current = currentStamina,
stamina_max = maxStamina,
stamina_percentage = maxStamina > 0 ? Math.Round((double)currentStamina / maxStamina * 100, 1) : 0,
mana_current = currentMana,
mana_max = maxMana,
mana_percentage = maxMana > 0 ? Math.Round((double)currentMana / maxMana * 100, 1) : 0,
vitae = vitae
};
// Send via WebSocket
_ = WebSocket.SendVitalsAsync(vitalsData);
}
catch (Exception ex)
{
WriteToChat($"Error sending vitals: {ex.Message}");
}
}
private void CalculateKillsPerInterval()
{
double minutesElapsed = (DateTime.Now - statsStartTime).TotalMinutes;
if (minutesElapsed > 0)
{
killsPer5Min = (totalKills / minutesElapsed) * 5;
killsPerHour = (totalKills / minutesElapsed) * 60;
}
}
private bool IsKilledByMeMessage(string text)
{
string[] killPatterns = new string[]
{
@"^You flatten (?<targetname>.+)'s body with the force of your assault!$",
@"^You bring (?<targetname>.+) to a fiery end!$",
@"^You beat (?<targetname>.+) to a lifeless pulp!$",
@"^You smite (?<targetname>.+) mightily!$",
@"^You obliterate (?<targetname>.+)!$",
@"^You run (?<targetname>.+) through!$",
@"^You reduce (?<targetname>.+) to a sizzling, oozing mass!$",
@"^You knock (?<targetname>.+) into next Morningthaw!$",
@"^You split (?<targetname>.+) apart!$",
@"^You cleave (?<targetname>.+) in twain!$",
@"^You slay (?<targetname>.+) viciously enough to impart death several times over!$",
@"^You reduce (?<targetname>.+) to a drained, twisted corpse!$",
@"^Your killing blow nearly turns (?<targetname>.+) inside-out!$",
@"^Your attack stops (?<targetname>.+) cold!$",
@"^Your lightning coruscates over (?<targetname>.+)'s mortal remains!$",
@"^Your assault sends (?<targetname>.+) to an icy death!$",
@"^You killed (?<targetname>.+)!$",
@"^The thunder of crushing (?<targetname>.+) is followed by the deafening silence of death!$",
@"^The deadly force of your attack is so strong that (?<targetname>.+)'s ancestors feel it!$",
@"^(?<targetname>.+)'s seared corpse smolders before you!$",
@"^(?<targetname>.+) is reduced to cinders!$",
@"^(?<targetname>.+) is shattered by your assault!$",
@"^(?<targetname>.+) catches your attack, with dire consequences!$",
@"^(?<targetname>.+) is utterly destroyed by your attack!$",
@"^(?<targetname>.+) suffers a frozen fate!$",
@"^(?<targetname>.+)'s perforated corpse falls before you!$",
@"^(?<targetname>.+) is fatally punctured!$",
@"^(?<targetname>.+)'s death is preceded by a sharp, stabbing pain!$",
@"^(?<targetname>.+) is torn to ribbons by your assault!$",
@"^(?<targetname>.+) is liquified by your attack!$",
@"^(?<targetname>.+)'s last strength dissolves before you!$",
@"^Electricity tears (?<targetname>.+) apart!$",
@"^Blistered by lightning, (?<targetname>.+) falls!$",
@"^(?<targetname>.+)'s last strength withers before you!$",
@"^(?<targetname>.+) is dessicated by your attack!$",
@"^(?<targetname>.+) is incinerated by your assault!$"
};
foreach (string pattern in killPatterns)
{
if (Regex.IsMatch(text, pattern))
return true;
}
return false;
}
private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
{
rareTextOnly = null;
// Match pattern: "<name> has discovered the <something>!"
string pattern = @"^(?<name>['A-Za-z ]+)\shas discovered the (?<item>.*?)!$";
Match match = Regex.Match(text, pattern);
if (match.Success && match.Groups["name"].Value == CoreManager.Current.CharacterFilter.Name)
{
rareTextOnly = match.Groups["item"].Value; // just "Ancient Pickle"
return true;
}
return false;
}
public static void WriteToChat(string message)
{
try
{
// For hot reload scenarios where MyHost might be null, use CoreManager directly
if (MyHost != null)
{
MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1);
}
else
{
// Hot reload fallback - use CoreManager directly like the original template
CoreManager.Current.Actions.AddChatText("[Mosswart Massacre] " + message, 1);
}
}
catch (Exception ex)
{
// Last resort fallback - try CoreManager even if MyHost was supposed to work
try
{
CoreManager.Current.Actions.AddChatText($"[Mosswart Massacre] {message} (WriteToChat error: {ex.Message})", 1);
}
catch
{
// Give up - can't write to chat at all
}
}
}
public static void RestartStats()
{
totalKills = 0;
rareCount = 0;
sessionDeaths = 0; // Reset session deaths only
statsStartTime = DateTime.Now;
killsPer5Min = 0;
killsPerHour = 0;
WriteToChat($"Stats have been reset. Session deaths: {sessionDeaths}, Total deaths: {totalDeaths}");
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
ViewManager.UpdateRareCount(rareCount);
}
public static void ToggleRareMeta()
{
PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
}
[DllImport("Decal.dll")]
private static extern int DispatchOnChatCommand(ref IntPtr str, [MarshalAs(UnmanagedType.U4)] int target);
public static bool Decal_DispatchOnChatCommand(string cmd)
{
IntPtr bstr = Marshal.StringToBSTR(cmd);
try
{
bool eaten = (DispatchOnChatCommand(ref bstr, 1) & 0x1) > 0;
return eaten;
}
finally
{
Marshal.FreeBSTR(bstr);
}
}
public static void DispatchChatToBoxWithPluginIntercept(string cmd)
{
if (!Decal_DispatchOnChatCommand(cmd))
CoreManager.Current.Actions.InvokeChatParser(cmd);
}
private void HandleMmCommand(string text)
{
// Remove the /mm prefix and trim extra whitespace
string[] args = text.Substring(3).Trim().Split(' ');
if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
{
WriteToChat("Usage: /mm <command>. Try /mm help");
return;
}
string subCommand = args[0].ToLower();
switch (subCommand)
{
case "telemetry":
if (args.Length > 1)
{
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
TelemetryEnabled = true;
Telemetry.Start();
PluginSettings.Instance.TelemetryEnabled = true;
WriteToChat("Telemetry streaming ENABLED.");
}
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
TelemetryEnabled = false;
Telemetry.Stop();
PluginSettings.Instance.TelemetryEnabled = false;
WriteToChat("Telemetry streaming DISABLED.");
}
else
{
WriteToChat("Usage: /mm telemetry <enable|disable>");
}
}
else
{
WriteToChat("Usage: /mm telemetry <enable|disable>");
}
break;
case "ws":
if (args.Length > 1)
{
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
WebSocketEnabled = true;
WebSocket.Start();
PluginSettings.Instance.WebSocketEnabled = true;
WriteToChat("WS streaming ENABLED.");
}
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
WebSocketEnabled = false;
WebSocket.Stop();
PluginSettings.Instance.WebSocketEnabled = false;
WriteToChat("WS streaming DISABLED.");
}
else
{
WriteToChat("Usage: /mm ws <enable|disable>");
}
}
else
{
WriteToChat("Usage: /mm ws <enable|disable>");
}
break;
case "help":
WriteToChat("Mosswart Massacre Commands:");
WriteToChat("/mm report - Show current stats");
WriteToChat("/mm loc - Show current location");
WriteToChat("/mm telemetry - Telemetry streaming enable|disable");
WriteToChat("/mm ws - Websocket streaming enable|disable");
WriteToChat("/mm reset - Reset all counters");
WriteToChat("/mm meta - Toggle rare meta state");
WriteToChat("/mm http - Local http-command server enable|disable");
WriteToChat("/mm remotecommand - Listen to allegiance !do/!dot enable|disable");
WriteToChat("/mm getmetastate - Gets the current metastate");
WriteToChat("/mm nextwp - Advance VTank to next waypoint");
WriteToChat("/mm decalstatus - Check Harmony patch status (UtilityBelt version)");
WriteToChat("/mm decaldebug - Enable/disable plugin message debug output + WebSocket streaming");
WriteToChat("/mm harmonyraw - Show raw intercepted messages (debug output)");
WriteToChat("/mm testprismatic - Test Prismatic Taper detection and icon lookup");
WriteToChat("/mm deathstats - Show current death tracking statistics");
WriteToChat("/mm testtaper - Test cached Prismatic Taper tracking");
WriteToChat("/mm debugtaper - Show detailed taper tracking debug info");
WriteToChat("/mm gui - Manually initialize/reinitialize GUI!!!");
break;
case "report":
TimeSpan elapsed = DateTime.Now - statsStartTime;
string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}, Session Deaths: {sessionDeaths}, Total Deaths: {totalDeaths}";
WriteToChat(reportMessage);
break;
case "getmetastate":
string metaState = VtankControl.VtGetMetaState();
WriteToChat(metaState);
break;
case "loc":
Coordinates here = Coordinates.Me;
var pos = Utils.GetPlayerPosition();
WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
break;
case "reset":
RestartStats();
break;
case "meta":
RareMetaEnabled = !RareMetaEnabled;
WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");
ViewManager.SetRareMetaToggleState(RareMetaEnabled); // <-- sync the UI
break;
case "http":
if (args.Length > 1)
{
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
PluginSettings.Instance.HttpServerEnabled = true;
HttpServerEnabled = true;
HttpCommandServer.Start();
}
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
PluginSettings.Instance.HttpServerEnabled = false;
HttpServerEnabled = false;
HttpCommandServer.Stop();
}
else
{
WriteToChat("Usage: /mm http <enable|disable>");
}
}
else
{
WriteToChat("Usage: /mm http <enable|disable>");
}
break;
case "remotecommands":
if (args.Length > 1)
{
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
PluginSettings.Instance.RemoteCommandsEnabled = true;
RemoteCommandsEnabled = true;
WriteToChat("Remote command listening is now ENABLED.");
}
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
PluginSettings.Instance.RemoteCommandsEnabled = false;
RemoteCommandsEnabled = false;
WriteToChat("Remote command listening is now DISABLED.");
}
else
{
WriteToChat("Invalid remotecommands argument. Use 'enable' or 'disable'.");
}
}
else
{
WriteToChat("Usage: /mm remotecommands <enable|disable>");
}
break;
case "nextwp":
double result = VtankControl.VtAdvanceWaypoint();
if (result == 1)
{
WriteToChat("Advanced VTank to next waypoint.");
}
else
{
WriteToChat("Failed to advance VTank waypoint. Is VTank running?");
}
break;
case "vtanktest":
try
{
WriteToChat("Testing VTank interface...");
WriteToChat($"VTank Instance: {(vTank.Instance != null ? "Found" : "NULL")}");
WriteToChat($"VTank Type: {vTank.Instance?.GetType()?.Name ?? "NULL"}");
WriteToChat($"NavCurrent: {vTank.Instance?.NavCurrent ?? -1}");
WriteToChat($"NavNumPoints: {vTank.Instance?.NavNumPoints ?? -1}");
WriteToChat($"NavType: {vTank.Instance?.NavType}");
WriteToChat($"MacroEnabled: {vTank.Instance?.MacroEnabled}");
}
catch (Exception ex)
{
WriteToChat($"VTank test error: {ex.Message}");
}
break;
case "decalstatus":
try
{
WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ===");
WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}");
WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}");
WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}");
// Test Harmony availability
WriteToChat("=== Harmony Version Status ===");
try
{
var harmonyTest = Harmony.HarmonyInstance.Create("test.version.check");
WriteToChat($"[OK] Harmony Available (ID: {harmonyTest.Id})");
// Check Harmony assembly version
var harmonyAssembly = typeof(Harmony.HarmonyInstance).Assembly;
WriteToChat($"[OK] Harmony Version: {harmonyAssembly.GetName().Version}");
WriteToChat($"[OK] Harmony Location: {harmonyAssembly.Location}");
}
catch (Exception harmonyEx)
{
WriteToChat($"[FAIL] Harmony Test Failed: {harmonyEx.Message}");
}
}
catch (Exception ex)
{
WriteToChat($"Status check error: {ex.Message}");
}
break;
case "decaldebug":
if (args.Length > 1)
{
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
AggressiveChatStreamingEnabled = true;
WriteToChat("[OK] DECAL debug streaming ENABLED - will show captured messages + stream via WebSocket");
}
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
AggressiveChatStreamingEnabled = false;
WriteToChat("[FAIL] DECAL debug streaming DISABLED - WebSocket streaming also disabled");
}
else
{
WriteToChat("Usage: /mm decaldebug <enable|disable>");
}
}
else
{
WriteToChat("Usage: /mm decaldebug <enable|disable>");
}
break;
case "harmonyraw":
// Debug functionality removed
break;
case "initgui":
case "gui":
try
{
WriteToChat("Attempting to manually initialize GUI...");
ViewManager.ViewDestroy(); // Clean up any existing view
ViewManager.ViewInit(); // Reinitialize
WriteToChat("GUI initialization attempt completed.");
}
catch (Exception ex)
{
WriteToChat($"GUI initialization error: {ex.Message}");
}
break;
case "testprismatic":
try
{
WriteToChat("=== FULL INVENTORY DUMP ===");
var worldFilter = CoreManager.Current.WorldFilter;
var playerInv = CoreManager.Current.CharacterFilter.Id;
WriteToChat("Listing ALL items in your main inventory:");
int itemNum = 1;
foreach (WorldObject item in worldFilter.GetByContainer(playerInv))
{
if (!string.IsNullOrEmpty(item.Name))
{
int stackCount = item.Values(LongValueKey.StackCount, 0);
WriteToChat($"{itemNum:D2}: '{item.Name}' (count: {stackCount}, icon: 0x{item.Icon:X}, class: {item.ObjectClass})");
itemNum++;
// Highlight anything that might be a taper
string nameLower = item.Name.ToLower();
if (nameLower.Contains("taper") || nameLower.Contains("prismatic") ||
nameLower.Contains("prism") || nameLower.Contains("component"))
{
WriteToChat($" *** POSSIBLE MATCH: '{item.Name}' ***");
}
}
}
WriteToChat($"=== Total items listed: {itemNum - 1} ===");
// Now test our utility functions on the found Prismatic Taper
WriteToChat("=== Testing Utility Functions on Prismatic Taper ===");
var foundItem = Utils.FindItemByName("Prismatic Taper");
if (foundItem != null)
{
WriteToChat($"SUCCESS! Found: '{foundItem.Name}'");
WriteToChat($"Utils.GetItemStackSize: {Utils.GetItemStackSize("Prismatic Taper")}");
WriteToChat($"Utils.GetItemIcon: 0x{Utils.GetItemIcon("Prismatic Taper"):X}");
WriteToChat($"Utils.GetItemDisplayIcon: 0x{Utils.GetItemDisplayIcon("Prismatic Taper"):X}");
WriteToChat("=== TELEMETRY WILL NOW WORK! ===");
}
else
{
WriteToChat("ERROR: Still can't find Prismatic Taper with utility functions!");
}
}
catch (Exception ex)
{
WriteToChat($"Search error: {ex.Message}");
}
break;
case "deathstats":
try
{
WriteToChat("=== Death Tracking Statistics ===");
WriteToChat($"Session Deaths: {sessionDeaths}");
WriteToChat($"Total Deaths: {totalDeaths}");
// Get current character death count to verify sync
int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
WriteToChat($"Character Property NumDeaths: {currentCharDeaths}");
if (currentCharDeaths != totalDeaths)
{
WriteToChat($"[WARNING] Death count sync issue detected!");
WriteToChat($"Updating totalDeaths from {totalDeaths} to {currentCharDeaths}");
totalDeaths = currentCharDeaths;
}
WriteToChat("Death tracking is active and will increment on character death.");
}
catch (Exception ex)
{
WriteToChat($"Death stats error: {ex.Message}");
}
break;
case "testdeath":
try
{
WriteToChat("=== Manual Death Test ===");
WriteToChat($"Current sessionDeaths variable: {sessionDeaths}");
WriteToChat($"Current totalDeaths variable: {totalDeaths}");
// Read directly from character property
int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
WriteToChat($"Character Property NumDeaths (43): {currentCharDeaths}");
// Manually increment session deaths for testing
sessionDeaths++;
WriteToChat($"Manually incremented sessionDeaths to: {sessionDeaths}");
WriteToChat("Note: This doesn't simulate a real death, just tests the tracking variables.");
// Check if death event is properly subscribed
WriteToChat($"Death event subscription check:");
var deathEvent = typeof(Decal.Adapter.Wrappers.CharacterFilter).GetEvent("Death");
WriteToChat($"Death event exists: {deathEvent != null}");
}
catch (Exception ex)
{
WriteToChat($"Test death error: {ex.Message}");
}
break;
case "testtaper":
try
{
WriteToChat("=== Cached Taper Tracking Test ===");
WriteToChat($"Cached Count: {cachedPrismaticCount}");
WriteToChat($"Last Count: {lastPrismaticCount}");
// Compare with Utils function
int utilsCount = Utils.GetItemStackSize("Prismatic Taper");
WriteToChat($"Utils Count: {utilsCount}");
if (cachedPrismaticCount == utilsCount)
{
WriteToChat("[OK] Cached count matches Utils count");
}
else
{
WriteToChat($"[WARNING] Count mismatch! Cached: {cachedPrismaticCount}, Utils: {utilsCount}");
WriteToChat("Refreshing cached count...");
InitializePrismaticTaperCount();
}
WriteToChat("=== Container Analysis ===");
int mainPackCount = 0;
int sidePackCount = 0;
int playerId = CoreManager.Current.CharacterFilter.Id;
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
{
int stackCount = wo.Values(LongValueKey.StackCount, 1);
if (wo.Container == playerId)
{
mainPackCount += stackCount;
}
else
{
sidePackCount += stackCount;
}
}
}
WriteToChat($"Main Pack Tapers: {mainPackCount}");
WriteToChat($"Side Pack Tapers: {sidePackCount}");
WriteToChat($"Total: {mainPackCount + sidePackCount}");
WriteToChat("=== Event System Status ===");
WriteToChat($"Tracking {trackedTaperContainers.Count} taper stacks for delta detection");
WriteToChat($"Known stack sizes: {lastKnownStackSizes.Count} items");
WriteToChat("Pure delta tracking - NO expensive inventory scans during events!");
WriteToChat("Now tracks: consumption, drops, trades, container moves");
WriteToChat("Try moving tapers between containers and casting spells!");
}
catch (Exception ex)
{
WriteToChat($"Taper test error: {ex.Message}");
}
break;
case "debugtaper":
// Debug functionality removed
break;
case "finditem":
if (args.Length > 1)
{
string itemName = string.Join(" ", args, 1, args.Length - 1).Trim('"');
WriteToChat($"=== Searching for: '{itemName}' ===");
var foundItem = Utils.FindItemByName(itemName);
if (foundItem != null)
{
WriteToChat($"FOUND: '{foundItem.Name}'");
WriteToChat($"Count: {foundItem.Values(LongValueKey.StackCount, 0)}");
WriteToChat($"Icon: 0x{foundItem.Icon:X}");
WriteToChat($"Display Icon: 0x{(foundItem.Icon + 0x6000000):X}");
WriteToChat($"Object Class: {foundItem.ObjectClass}");
}
else
{
WriteToChat($"NOT FOUND: '{itemName}'");
WriteToChat("Make sure the name is exactly as it appears in-game.");
}
}
else
{
WriteToChat("Usage: /mm finditem \"Item Name\"");
WriteToChat("Example: /mm finditem \"Prismatic Taper\"");
}
break;
default:
WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help");
break;
}
}
}
}