1654 lines
71 KiB
C#
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;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|