MosswartMassacre/MosswartMassacre/PluginCore.cs
2025-05-27 21:29:43 +02:00

618 lines
25 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;
namespace MosswartMassacre
{
[FriendlyName("Mosswart Massacre")]
public class PluginCore : PluginBase
{
internal static PluginHost MyHost;
internal static int totalKills = 0;
internal static int rareCount = 0;
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;
public static bool RareMetaEnabled { get; set; } = true;
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 bool WebSocketEnabled { get; set; } = false;
public bool InventoryLogEnabled { get; set; } = false;
private MossyInventory _inventoryLogger;
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
{
MyHost = Host;
WriteToChat("Mosswart Massacre has started!");
// Subscribe to chat message event
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(AllChatText);
CoreManager.Current.CommandLineText += OnChatCommand;
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
CoreManager.Current.WorldFilter.CreateObject += OnSpawn;
CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn;
// Initialize the timer
updateTimer = new Timer(1000); // Update every second
updateTimer.Elapsed += UpdateStats;
updateTimer.Start();
// Initialize the view (UI)
MainView.ViewInit();
// 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();
}
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.WorldFilter.CreateObject -= OnSpawn;
CoreManager.Current.WorldFilter.ReleaseObject -= OnDespawn;
// Stop and dispose of the timer
if (updateTimer != null)
{
updateTimer.Stop();
updateTimer.Dispose();
updateTimer = null;
}
// Clean up the view
MainView.ViewDestroy();
//Disable vtank interface
vTank.Disable();
// sluta lyssna på commands
WebSocket.OnServerCommand -= HandleServerCommand;
WebSocket.Stop();
//shutdown inv
_inventoryLogger.Dispose();
MyHost = null;
}
catch (Exception ex)
{
WriteToChat("Error during shutdown: " + ex.Message);
}
}
private void CharacterFilter_LoginComplete(object sender, EventArgs e)
{
CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
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;
MainView.SetRareMetaToggleState(RareMetaEnabled);
if (TelemetryEnabled)
Telemetry.Start();
if (WebSocketEnabled)
WebSocket.Start();
}
private async void OnSpawn(object sender, CreateObjectEventArgs e)
{
var mob = e.New;
if (mob.ObjectClass != ObjectClass.Monster) return;
try
{
var coords = mob.Coordinates();
const string fmt = "F7";
string ns = coords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture);
string ew = coords.EastWest.ToString(fmt, CultureInfo.InvariantCulture);
await WebSocket.SendSpawnAsync(ns, ew, mob.Name);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WS] Spawn 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);
await WebSocket.SendChatTextAsync(e.Color, cleaned);
}
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 HandleServerCommand(CommandEnvelope env)
{
// Skicka commands
DispatchChatToBoxWithPluginIntercept(env.Command);
CoreManager.Current.Actions.InvokeChatParser($"/a Executed '{env.Command}' from Mosswart Overlord");
}
private void OnChatText(object sender, ChatTextInterceptEventArgs e)
{
try
{
// WriteToChat($"[Debug] Chat Color: {e.Color}, Message: {e.Text}");
if (IsKilledByMeMessage(e.Text))
{
totalKills++;
lastKillTime = DateTime.Now;
CalculateKillsPerInterval();
MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
if (IsRareDiscoveryMessage(e.Text, out string rareText))
{
rareCount++;
MainView.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;
MainView.UpdateElapsedTime(elapsed);
// Recalculate kill rates
CalculateKillsPerInterval();
MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
catch (Exception ex)
{
WriteToChat("Error updating stats: " + 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 ]+)\s(?<text>has discovered the .*!$)";
Match match = Regex.Match(text, pattern);
if (match.Success && match.Groups["name"].Value == CoreManager.Current.CharacterFilter.Name)
{
rareTextOnly = match.Groups["text"].Value; // just "has discovered the Ancient Pickle!"
return true;
}
return false;
}
public static void WriteToChat(string message)
{
MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1);
}
public static void RestartStats()
{
totalKills = 0;
rareCount = 0;
statsStartTime = DateTime.Now;
killsPer5Min = 0;
killsPerHour = 0;
WriteToChat("Stats have been reset.");
MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
MainView.UpdateRareCount(rareCount);
}
public static void ToggleRareMeta()
{
PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
MainView.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");
break;
case "debug":
DispatchChatToBoxWithPluginIntercept("/ub give bajs to Town Crier");
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}";
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")}");
MainView.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;
default:
WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help");
break;
}
}
}
}