Compare commits
7 commits
master
...
awesome-fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61c74be07e | ||
|
|
aa744a8edc | ||
|
|
aede9d8bdd | ||
|
|
4e2bb68e86 | ||
|
|
2e12766f8b | ||
|
|
60d06341dd | ||
|
|
d2e9988bdd |
15 changed files with 850 additions and 495 deletions
140
MosswartMassacre/ChatManager.cs
Normal file
140
MosswartMassacre/ChatManager.cs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Decal.Adapter;
|
||||||
|
|
||||||
|
namespace MosswartMassacre
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles game chat events: kill/rare detection, forwarding chat to WebSocket, and "/mm" command parsing.
|
||||||
|
/// </summary>
|
||||||
|
internal class ChatManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly StatsManager _statsManager;
|
||||||
|
private readonly IWebSocketService _wsService;
|
||||||
|
private readonly Action<string> _mmCommandHandler;
|
||||||
|
private bool _isStarted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new ChatManager.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="statsManager">Stats manager for registering kills/rares.</param>
|
||||||
|
/// <param name="mmCommandHandler">Delegate to handle '/mm' commands.</param>
|
||||||
|
public ChatManager(StatsManager statsManager, IWebSocketService wsService, Action<string> mmCommandHandler)
|
||||||
|
{
|
||||||
|
_statsManager = statsManager ?? throw new ArgumentNullException(nameof(statsManager));
|
||||||
|
_wsService = wsService ?? throw new ArgumentNullException(nameof(wsService));
|
||||||
|
_mmCommandHandler = mmCommandHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes to chat and command events.
|
||||||
|
/// </summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_isStarted) return;
|
||||||
|
_isStarted = true;
|
||||||
|
CoreManager.Current.ChatBoxMessage += OnChatIntercept;
|
||||||
|
CoreManager.Current.CommandLineText += OnChatCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribes from chat and command events.
|
||||||
|
/// </summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (!_isStarted) return;
|
||||||
|
CoreManager.Current.ChatBoxMessage -= OnChatIntercept;
|
||||||
|
CoreManager.Current.CommandLineText -= OnChatCommand;
|
||||||
|
_isStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChatIntercept(object sender, ChatTextInterceptEventArgs e)
|
||||||
|
{
|
||||||
|
// Forward chat text asynchronously to WebSocket
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string cleaned = NormalizeChatLine(e.Text);
|
||||||
|
_ = _wsService.SendChatTextAsync(e.Color, cleaned);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat("Error sending chat over WS: " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill detection
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (PluginCore.IsKilledByMeMessage(e.Text))
|
||||||
|
{
|
||||||
|
_statsManager.RegisterKill();
|
||||||
|
}
|
||||||
|
// Rare discovery detection
|
||||||
|
if (PluginCore.IsRareDiscoveryMessage(e.Text, out string rareText))
|
||||||
|
{
|
||||||
|
_statsManager.RegisterRare();
|
||||||
|
if (PluginSettings.Instance.RareMetaEnabled)
|
||||||
|
PluginCore.Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
|
||||||
|
DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
|
||||||
|
}
|
||||||
|
// Remote commands from allegiance
|
||||||
|
if (PluginSettings.Instance.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;
|
||||||
|
PluginCore.Decal_DispatchOnChatCommand(command);
|
||||||
|
DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
|
||||||
|
}
|
||||||
|
else if (matchtag.Success)
|
||||||
|
{
|
||||||
|
string command = matchtag.Groups["command"].Value;
|
||||||
|
PluginCore.Decal_DispatchOnChatCommand(command);
|
||||||
|
DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginCore.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;
|
||||||
|
_mmCommandHandler?.Invoke(e.Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat("[Error] Failed to process /mm command: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeChatLine(string raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(raw)) return raw;
|
||||||
|
// Remove tags
|
||||||
|
string noTags = Regex.Replace(raw, "<[^>]+>", "");
|
||||||
|
// Trim newlines
|
||||||
|
string trimmed = noTags.TrimEnd('\r', '\n');
|
||||||
|
// Collapse spaces
|
||||||
|
return Regex.Replace(trimmed, "[ ]{2,}", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
MosswartMassacre/CommandHandler.cs
Normal file
137
MosswartMassacre/CommandHandler.cs
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Decal.Adapter;
|
||||||
|
|
||||||
|
namespace MosswartMassacre
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles "/mm" commands from chat.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandHandler : ICommandHandler
|
||||||
|
{
|
||||||
|
private readonly StatsManager _statsManager;
|
||||||
|
private readonly IWebSocketService _wsService;
|
||||||
|
|
||||||
|
public CommandHandler(StatsManager statsManager, IWebSocketService wsService)
|
||||||
|
{
|
||||||
|
_statsManager = statsManager ?? throw new ArgumentNullException(nameof(statsManager));
|
||||||
|
_wsService = wsService ?? throw new ArgumentNullException(nameof(wsService));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Handle(string commandText)
|
||||||
|
{
|
||||||
|
// Remove the /mm prefix and trim
|
||||||
|
string[] args = commandText.Length > 3
|
||||||
|
? commandText.Substring(3).Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
: Array.Empty<string>();
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat("Usage: /mm <command>. Try /mm help");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string sub = args[0].ToLowerInvariant();
|
||||||
|
switch (sub)
|
||||||
|
{
|
||||||
|
case "ws":
|
||||||
|
if (args.Length > 1)
|
||||||
|
{
|
||||||
|
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_wsService.Start();
|
||||||
|
PluginSettings.Instance.WebSocketEnabled = true;
|
||||||
|
PluginCore.WriteToChat("WS streaming ENABLED.");
|
||||||
|
}
|
||||||
|
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_wsService.Stop();
|
||||||
|
PluginSettings.Instance.WebSocketEnabled = false;
|
||||||
|
PluginCore.WriteToChat("WS streaming DISABLED.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat("Usage: /mm ws <enable|disable>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat("Usage: /mm ws <enable|disable>");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "help":
|
||||||
|
PluginCore.WriteToChat("Mosswart Massacre Commands:");
|
||||||
|
PluginCore.WriteToChat("/mm report - Show current stats");
|
||||||
|
PluginCore.WriteToChat("/mm loc - Show current location");
|
||||||
|
PluginCore.WriteToChat("/mm ws - Websocket streaming enable|disable");
|
||||||
|
PluginCore.WriteToChat("/mm reset - Reset all counters");
|
||||||
|
PluginCore.WriteToChat("/mm meta - Toggle rare meta state");
|
||||||
|
PluginCore.WriteToChat("/mm http - Local http-command server enable|disable");
|
||||||
|
PluginCore.WriteToChat("/mm remotecommand - Listen to allegiance !do/!dot enable|disable");
|
||||||
|
PluginCore.WriteToChat("/mm getmetastate - Gets the current metastate");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "report":
|
||||||
|
var elapsed = DateTime.Now - _statsManager.StatsStartTime;
|
||||||
|
var report = $"Total Kills: {_statsManager.TotalKills}, Kills per Hour: {_statsManager.KillsPerHour:F2}, Elapsed Time: {elapsed:dd.hh:mm:ss}, Rares Found: {_statsManager.RareCount}";
|
||||||
|
PluginCore.WriteToChat(report);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "getmetastate":
|
||||||
|
var state = VtankControl.VtGetMetaState();
|
||||||
|
PluginCore.WriteToChat(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "loc":
|
||||||
|
var here = Coordinates.Me;
|
||||||
|
var pos = Utils.GetPlayerPosition();
|
||||||
|
PluginCore.WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "reset":
|
||||||
|
_statsManager.Restart();
|
||||||
|
PluginCore.WriteToChat("Stats have been reset.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "meta":
|
||||||
|
PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
|
||||||
|
PluginCore.WriteToChat($"Rare meta state is now {(PluginSettings.Instance.RareMetaEnabled ? "ON" : "OFF")}");
|
||||||
|
MainView.SetRareMetaToggleState(PluginSettings.Instance.RareMetaEnabled);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "http":
|
||||||
|
if (args.Length > 1)
|
||||||
|
{
|
||||||
|
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
PluginSettings.Instance.HttpServerEnabled = true;
|
||||||
|
HttpCommandServer.Start();
|
||||||
|
}
|
||||||
|
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
PluginSettings.Instance.HttpServerEnabled = false;
|
||||||
|
HttpCommandServer.Stop();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat("Usage: /mm http <enable|disable>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat("Usage: /mm http <enable|disable>");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "remotecommands":
|
||||||
|
PluginSettings.Instance.RemoteCommandsEnabled = !PluginSettings.Instance.RemoteCommandsEnabled;
|
||||||
|
PluginCore.WriteToChat($"Remote command listening is now {(PluginSettings.Instance.RemoteCommandsEnabled ? "ENABLED" : "DISABLED")}.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
PluginCore.WriteToChat($"Unknown /mm command: {sub}. Try /mm help");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,18 @@ namespace MosswartMassacre
|
||||||
PluginCore.WriteToChat("Error in delayed command system: " + ex.Message);
|
PluginCore.WriteToChat("Error in delayed command system: " + ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Shutdown the delayed command system: unsubscribe from RenderFrame and clear pending commands.
|
||||||
|
/// </summary>
|
||||||
|
public static void Shutdown()
|
||||||
|
{
|
||||||
|
if (isDelayListening)
|
||||||
|
{
|
||||||
|
CoreManager.Current.RenderFrame -= Core_RenderFrame_Delay;
|
||||||
|
isDelayListening = false;
|
||||||
|
}
|
||||||
|
delayedCommands.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DelayedCommand
|
public class DelayedCommand
|
||||||
|
|
|
||||||
14
MosswartMassacre/ICommandHandler.cs
Normal file
14
MosswartMassacre/ICommandHandler.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace MosswartMassacre
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles plugin commands (e.g., /mm commands).
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Processes a command text, e.g. "/mm report".
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandText">The full command text.</param>
|
||||||
|
void Handle(string commandText);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
MosswartMassacre/IStreamService.cs
Normal file
18
MosswartMassacre/IStreamService.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
namespace MosswartMassacre
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a start/stop service with life-cycle management.
|
||||||
|
/// </summary>
|
||||||
|
public interface IStreamService : System.IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the service.
|
||||||
|
/// </summary>
|
||||||
|
void Start();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the service.
|
||||||
|
/// </summary>
|
||||||
|
void Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
MosswartMassacre/IWebSocketService.cs
Normal file
23
MosswartMassacre/IWebSocketService.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MosswartMassacre
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service for WebSocket streaming and command handling.
|
||||||
|
/// </summary>
|
||||||
|
public interface IWebSocketService : IStreamService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fires when a valid command envelope arrives from the server for this character.
|
||||||
|
/// </summary>
|
||||||
|
event Action<CommandEnvelope> OnServerCommand;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends chat text asynchronously over the WebSocket.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="colorIndex">Chat color index.</param>
|
||||||
|
/// <param name="chatText">Text to send.</param>
|
||||||
|
Task SendChatTextAsync(int colorIndex, string chatText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,9 @@
|
||||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||||
<HintPath>lib\Decal.Interop.Inject.dll</HintPath>
|
<HintPath>lib\Decal.Interop.Inject.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="System.Configuration">
|
||||||
|
<HintPath>$(FrameworkPathOverride)\System.Configuration.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|
@ -76,7 +79,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="vTank.cs" />
|
<Compile Include="vTank.cs" />
|
||||||
<Compile Include="VtankControl.cs" />
|
<Compile Include="VtankControl.cs" />
|
||||||
<Compile Include="Telemetry.cs" />
|
|
||||||
<Compile Include="Coordinates.cs" />
|
<Compile Include="Coordinates.cs" />
|
||||||
<Compile Include="Geometry.cs" />
|
<Compile Include="Geometry.cs" />
|
||||||
<Compile Include="Utils.cs" />
|
<Compile Include="Utils.cs" />
|
||||||
|
|
@ -92,6 +94,14 @@
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="ViewSystemSelector.cs" />
|
<Compile Include="ViewSystemSelector.cs" />
|
||||||
|
<!-- WebSocket.cs replaced by WebSocketService -->
|
||||||
|
<Compile Include="StatsManager.cs" />
|
||||||
|
<Compile Include="ChatManager.cs" />
|
||||||
|
<Compile Include="ICommandHandler.cs" />
|
||||||
|
<Compile Include="CommandHandler.cs" />
|
||||||
|
<Compile Include="IStreamService.cs" />
|
||||||
|
<Compile Include="IWebSocketService.cs" />
|
||||||
|
<Compile Include="WebSocketService.cs" />
|
||||||
<Compile Include="Wrapper.cs" />
|
<Compile Include="Wrapper.cs" />
|
||||||
<Compile Include="Wrapper_Decal.cs" />
|
<Compile Include="Wrapper_Decal.cs" />
|
||||||
<Compile Include="Wrapper_MyHuds.cs" />
|
<Compile Include="Wrapper_MyHuds.cs" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
@ -9,25 +9,76 @@ using Decal.Adapter.Wrappers;
|
||||||
|
|
||||||
namespace MosswartMassacre
|
namespace MosswartMassacre
|
||||||
{
|
{
|
||||||
|
[ComVisible(true)]
|
||||||
|
[Guid("be0a51b6-cf1f-4318-ad49-e40d6aebe14b")]
|
||||||
[FriendlyName("Mosswart Massacre")]
|
[FriendlyName("Mosswart Massacre")]
|
||||||
public class PluginCore : PluginBase
|
public class PluginCore : PluginBase
|
||||||
{
|
{
|
||||||
internal static PluginHost MyHost;
|
internal static PluginHost MyHost;
|
||||||
internal static int totalKills = 0;
|
// Stats manager for kills and rares
|
||||||
internal static int rareCount = 0;
|
private static StatsManager StatsMgr;
|
||||||
internal static DateTime lastKillTime = DateTime.Now;
|
// Chat and command handling
|
||||||
internal static double killsPer5Min = 0;
|
private ChatManager chatManager;
|
||||||
internal static double killsPerHour = 0;
|
private IWebSocketService _wsService;
|
||||||
internal static DateTime statsStartTime = DateTime.Now;
|
private ICommandHandler _commandHandler;
|
||||||
internal static Timer updateTimer;
|
|
||||||
public static bool RareMetaEnabled { get; set; } = true;
|
public static bool RareMetaEnabled { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Handles server-sent commands via WebSocket.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleServerCommand(CommandEnvelope env)
|
||||||
|
{
|
||||||
|
DispatchChatToBoxWithPluginIntercept(env.Command);
|
||||||
|
CoreManager.Current.Actions.InvokeChatParser($"/a Executed '{env.Command}' from Mosswart Overlord");
|
||||||
|
}
|
||||||
public static bool RemoteCommandsEnabled { get; set; } = false;
|
public static bool RemoteCommandsEnabled { get; set; } = false;
|
||||||
public static bool HttpServerEnabled { get; set; } = false;
|
public static bool HttpServerEnabled { get; set; } = false;
|
||||||
public static string CharTag { get; set; } = "";
|
public static string CharTag { get; set; } = "";
|
||||||
public static bool TelemetryEnabled { get; set; } = false;
|
// Telemetry is removed; HTTP posting no longer supported
|
||||||
|
public bool WebSocketEnabled { get; set; } = false;
|
||||||
private static Queue<string> rareMessageQueue = new Queue<string>();
|
private static Queue<string> rareMessageQueue = new Queue<string>();
|
||||||
private static DateTime _lastSent = DateTime.MinValue;
|
private static DateTime _lastSent = DateTime.MinValue;
|
||||||
private static readonly Queue<string> _chatQueue = new Queue<string>();
|
private static readonly Queue<string> _chatQueue = new Queue<string>();
|
||||||
|
// Precompiled regex patterns for detecting kill messages
|
||||||
|
private static readonly Regex[] KillRegexes = new Regex[]
|
||||||
|
{
|
||||||
|
new Regex(@"^You flatten (?<targetname>.+)'s body with the force of your assault!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You bring (?<targetname>.+) to a fiery end!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You beat (?<targetname>.+) to a lifeless pulp!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You smite (?<targetname>.+) mightily!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You obliterate (?<targetname>.+)!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You run (?<targetname>.+) through!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You reduce (?<targetname>.+) to a sizzling, oozing mass!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You knock (?<targetname>.+) into next Morningthaw!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You split (?<targetname>.+) apart!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You cleave (?<targetname>.+) in twain!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You slay (?<targetname>.+) viciously enough to impart death several times over!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You reduce (?<targetname>.+) to a drained, twisted corpse!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^Your killing blow nearly turns (?<targetname>.+) inside-out!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^Your attack stops (?<targetname>.+) cold!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^Your lightning coruscates over (?<targetname>.+)'s mortal remains!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^Your assault sends (?<targetname>.+) to an icy death!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^You killed (?<targetname>.+)!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^The thunder of crushing (?<targetname>.+) is followed by the deafening silence of death!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^The deadly force of your attack is so strong that (?<targetname>.+)'s ancestors feel it!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+)'s seared corpse smolders before you!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) is reduced to cinders!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) is shattered by your assault!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) catches your attack, with dire consequences!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) is utterly destroyed by your attack!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) suffers a frozen fate!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+)'s perforated corpse falls before you!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) is fatally punctured!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+)'s last strength dissolves before you!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) is torn to ribbons by your assault!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) is liquified by your attack!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+)'s last strength withers before you!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) is dessicated by your attack!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^(?<targetname>.+) is incinerated by your assault!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
// Additional patterns from original killPatterns
|
||||||
|
new Regex(@"^(?<targetname>.+)'s death is preceded by a sharp, stabbing pain!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^Electricity tears (?<targetname>.+) apart!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
new Regex(@"^Blistered by lightning, (?<targetname>.+) falls!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||||
|
};
|
||||||
|
|
||||||
protected override void Startup()
|
protected override void Startup()
|
||||||
{
|
{
|
||||||
|
|
@ -37,15 +88,27 @@ namespace MosswartMassacre
|
||||||
|
|
||||||
WriteToChat("Mosswart Massacre has started!");
|
WriteToChat("Mosswart Massacre has started!");
|
||||||
|
|
||||||
// Subscribe to chat message event
|
// Initialize WebSocket streaming service
|
||||||
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
|
_wsService = new WebSocketService(StatsMgr);
|
||||||
CoreManager.Current.CommandLineText += OnChatCommand;
|
if (WebSocketEnabled)
|
||||||
|
{
|
||||||
|
_wsService.Start();
|
||||||
|
}
|
||||||
|
_wsService.OnServerCommand += HandleServerCommand;
|
||||||
|
// Initialize command handler for "/mm" commands
|
||||||
|
_commandHandler = new CommandHandler(StatsMgr, _wsService);
|
||||||
|
|
||||||
|
// Initialize chat handling
|
||||||
|
chatManager = new ChatManager(StatsMgr, _wsService, _commandHandler.Handle);
|
||||||
|
chatManager.Start();
|
||||||
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
|
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
|
||||||
|
|
||||||
// Initialize the timer
|
// Initialize stats manager
|
||||||
updateTimer = new Timer(1000); // Update every second
|
StatsMgr = new StatsManager();
|
||||||
updateTimer.Elapsed += UpdateStats;
|
StatsMgr.KillStatsUpdated += (total, per5, perHour) => MainView.UpdateKillStats(total, per5, perHour);
|
||||||
updateTimer.Start();
|
StatsMgr.ElapsedTimeUpdated += elapsed => MainView.UpdateElapsedTime(elapsed);
|
||||||
|
StatsMgr.RareCountUpdated += rare => MainView.UpdateRareCount(rare);
|
||||||
|
StatsMgr.Start();
|
||||||
|
|
||||||
// Initialize the view (UI)
|
// Initialize the view (UI)
|
||||||
MainView.ViewInit();
|
MainView.ViewInit();
|
||||||
|
|
@ -66,27 +129,28 @@ namespace MosswartMassacre
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
PluginSettings.Save();
|
PluginSettings.Save();
|
||||||
if (TelemetryEnabled)
|
|
||||||
Telemetry.Stop(); // ensure no dangling timer / HttpClient
|
|
||||||
WriteToChat("Mosswart Massacre is shutting down...");
|
WriteToChat("Mosswart Massacre is shutting down...");
|
||||||
|
// clean up any pending delayed commands
|
||||||
|
DelayedCommandManager.Shutdown();
|
||||||
|
|
||||||
// Unsubscribe from chat message event
|
// Stop chat handling
|
||||||
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
|
chatManager?.Stop();
|
||||||
CoreManager.Current.CommandLineText -= OnChatCommand;
|
|
||||||
|
|
||||||
// Stop and dispose of the timer
|
// Stop and dispose of stats manager
|
||||||
if (updateTimer != null)
|
StatsMgr?.Stop();
|
||||||
{
|
StatsMgr?.Dispose();
|
||||||
updateTimer.Stop();
|
|
||||||
updateTimer.Dispose();
|
|
||||||
updateTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up the view
|
// Clean up the view
|
||||||
MainView.ViewDestroy();
|
MainView.ViewDestroy();
|
||||||
//Disable vtank interface
|
// Disable vtank interface
|
||||||
vTank.Disable();
|
vTank.Disable();
|
||||||
|
// Stop WebSocket service
|
||||||
|
if (_wsService != null)
|
||||||
|
{
|
||||||
|
_wsService.OnServerCommand -= HandleServerCommand;
|
||||||
|
_wsService.Stop();
|
||||||
|
_wsService.Dispose();
|
||||||
|
}
|
||||||
MyHost = null;
|
MyHost = null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -102,209 +166,34 @@ namespace MosswartMassacre
|
||||||
|
|
||||||
// Apply the values
|
// Apply the values
|
||||||
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
|
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
|
||||||
|
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
|
||||||
RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
|
RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
|
||||||
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
|
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
|
||||||
TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
|
|
||||||
CharTag = PluginSettings.Instance.CharTag;
|
CharTag = PluginSettings.Instance.CharTag;
|
||||||
MainView.SetRareMetaToggleState(RareMetaEnabled);
|
MainView.SetRareMetaToggleState(RareMetaEnabled);
|
||||||
if (TelemetryEnabled)
|
// HTTP Telemetry removed
|
||||||
Telemetry.Start();
|
if (WebSocketEnabled)
|
||||||
|
_wsService?.Start();
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
// if (e.Text.EndsWith("!testrare\""))
|
|
||||||
// {
|
|
||||||
// string simulatedText = $"{CoreManager.Current.CharacterFilter.Name} has discovered the Ancient Pickle!";
|
|
||||||
//
|
|
||||||
// if (IsRareDiscoveryMessage(simulatedText, out string simulatedRareText))
|
|
||||||
// {
|
|
||||||
// rareCount++;
|
|
||||||
// MainView.UpdateRareCount(rareCount);
|
|
||||||
//
|
|
||||||
// if (RareMetaEnabled)
|
|
||||||
// {
|
|
||||||
// Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// DelayedCommandManager.AddDelayedCommand($"/a {simulatedRareText}", 3000);
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// WriteToChat("[Test] Simulated rare message didn't match the regex.");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
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)
|
/// <summary>
|
||||||
|
/// Determines if the chat text indicates a kill by this character.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsKilledByMeMessage(string text)
|
||||||
{
|
{
|
||||||
WriteToChat("Error processing chat message: " + ex.Message);
|
// Check each precompiled kill message pattern
|
||||||
}
|
foreach (var regex in KillRegexes)
|
||||||
}
|
|
||||||
private void OnChatCommand(object sender, ChatParserInterceptEventArgs e)
|
|
||||||
{
|
{
|
||||||
try
|
if (regex.IsMatch(text))
|
||||||
{
|
|
||||||
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 true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
|
/// <summary>
|
||||||
|
/// Determines if the chat text indicates a rare discovery by this character.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
|
||||||
{
|
{
|
||||||
rareTextOnly = null;
|
rareTextOnly = null;
|
||||||
|
|
||||||
|
|
@ -320,21 +209,20 @@ namespace MosswartMassacre
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Prefix for all chat messages from this plugin
|
||||||
|
private const string ChatPrefix = "[Mosswart Massacre] ";
|
||||||
|
/// <summary>
|
||||||
|
/// Write a message to the chat with plugin prefix.
|
||||||
|
/// </summary>
|
||||||
public static void WriteToChat(string message)
|
public static void WriteToChat(string message)
|
||||||
{
|
{
|
||||||
MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1);
|
MyHost.Actions.AddChatText(ChatPrefix + message, 0, 1);
|
||||||
}
|
}
|
||||||
public static void RestartStats()
|
public static void RestartStats()
|
||||||
{
|
{
|
||||||
totalKills = 0;
|
// Reset stats via StatsManager
|
||||||
rareCount = 0;
|
StatsMgr?.Restart();
|
||||||
statsStartTime = DateTime.Now;
|
|
||||||
killsPer5Min = 0;
|
|
||||||
killsPerHour = 0;
|
|
||||||
|
|
||||||
WriteToChat("Stats have been reset.");
|
WriteToChat("Stats have been reset.");
|
||||||
MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
|
|
||||||
MainView.UpdateRareCount(rareCount);
|
|
||||||
}
|
}
|
||||||
public static void ToggleRareMeta()
|
public static void ToggleRareMeta()
|
||||||
{
|
{
|
||||||
|
|
@ -365,141 +253,6 @@ namespace MosswartMassacre
|
||||||
if (!Decal_DispatchOnChatCommand(cmd))
|
if (!Decal_DispatchOnChatCommand(cmd))
|
||||||
CoreManager.Current.Actions.InvokeChatParser(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 "help":
|
|
||||||
WriteToChat("Mosswart Massacre Commands:");
|
|
||||||
WriteToChat("/mm report - Show current stats");
|
|
||||||
WriteToChat("/mm loc - Show current location");
|
|
||||||
WriteToChat("/mm telemetry - Telemetry streaming enable|disable"); // NEW
|
|
||||||
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 "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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,32 @@ namespace MosswartMassacre
|
||||||
private static PluginSettings _instance;
|
private static PluginSettings _instance;
|
||||||
private static string _filePath;
|
private static string _filePath;
|
||||||
private static readonly object _sync = new object();
|
private static readonly object _sync = new object();
|
||||||
|
// Timer to debounce saving settings to disk
|
||||||
|
private static readonly System.Timers.Timer _saveTimer;
|
||||||
|
// Reuse YAML serializer/deserializer to avoid rebuilding per operation
|
||||||
|
private static readonly IDeserializer _deserializer = new DeserializerBuilder()
|
||||||
|
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||||
|
.Build();
|
||||||
|
private static readonly ISerializer _serializer = new SerializerBuilder()
|
||||||
|
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||||
|
.Build();
|
||||||
|
|
||||||
// backing fields
|
// backing fields
|
||||||
private bool _remoteCommandsEnabled = false;
|
private bool _remoteCommandsEnabled = false;
|
||||||
private bool _rareMetaEnabled = true;
|
private bool _rareMetaEnabled = true;
|
||||||
private bool _httpServerEnabled = false;
|
private bool _httpServerEnabled = false;
|
||||||
private bool _telemetryEnabled = false;
|
private bool _webSocketEnabled = false;
|
||||||
private string _charTag = "default";
|
private string _charTag = "default";
|
||||||
|
|
||||||
public static PluginSettings Instance => _instance
|
public static PluginSettings Instance => _instance
|
||||||
?? throw new InvalidOperationException("PluginSettings not initialized");
|
?? throw new InvalidOperationException("PluginSettings not initialized");
|
||||||
|
|
||||||
|
// Static constructor to initialize save debounce timer
|
||||||
|
static PluginSettings()
|
||||||
|
{
|
||||||
|
_saveTimer = new System.Timers.Timer(1000) { AutoReset = false };
|
||||||
|
_saveTimer.Elapsed += (s, e) => Save();
|
||||||
|
}
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
// determine settings file path
|
// determine settings file path
|
||||||
|
|
@ -29,10 +44,7 @@ namespace MosswartMassacre
|
||||||
string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||||
_filePath = Path.Combine(pluginFolder, $"{characterName}.yaml");
|
_filePath = Path.Combine(pluginFolder, $"{characterName}.yaml");
|
||||||
|
|
||||||
// build serializer/deserializer once
|
// use shared deserializer
|
||||||
var builder = new DeserializerBuilder()
|
|
||||||
.WithNamingConvention(UnderscoredNamingConvention.Instance);
|
|
||||||
var deserializer = builder.Build();
|
|
||||||
|
|
||||||
PluginSettings loaded = null;
|
PluginSettings loaded = null;
|
||||||
|
|
||||||
|
|
@ -41,7 +53,7 @@ namespace MosswartMassacre
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string yaml = File.ReadAllText(_filePath);
|
string yaml = File.ReadAllText(_filePath);
|
||||||
loaded = deserializer.Deserialize<PluginSettings>(yaml);
|
loaded = _deserializer.Deserialize<PluginSettings>(yaml);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -69,11 +81,8 @@ namespace MosswartMassacre
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// serialize to YAML
|
// serialize to YAML using shared serializer
|
||||||
var serializer = new SerializerBuilder()
|
var yaml = _serializer.Serialize(_instance);
|
||||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
|
||||||
.Build();
|
|
||||||
var yaml = serializer.Serialize(_instance);
|
|
||||||
|
|
||||||
// write temp file
|
// write temp file
|
||||||
var temp = _filePath + ".tmp";
|
var temp = _filePath + ".tmp";
|
||||||
|
|
@ -98,36 +107,43 @@ namespace MosswartMassacre
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Schedule settings to be saved after debounce interval.
|
||||||
|
/// </summary>
|
||||||
|
private static void ScheduleSave()
|
||||||
|
{
|
||||||
|
_saveTimer.Stop();
|
||||||
|
_saveTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
// public properties
|
// public properties
|
||||||
public bool RemoteCommandsEnabled
|
public bool RemoteCommandsEnabled
|
||||||
{
|
{
|
||||||
get => _remoteCommandsEnabled;
|
get => _remoteCommandsEnabled;
|
||||||
set { _remoteCommandsEnabled = value; Save(); }
|
set { _remoteCommandsEnabled = value; ScheduleSave(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RareMetaEnabled
|
public bool RareMetaEnabled
|
||||||
{
|
{
|
||||||
get => _rareMetaEnabled;
|
get => _rareMetaEnabled;
|
||||||
set { _rareMetaEnabled = value; Save(); }
|
set { _rareMetaEnabled = value; ScheduleSave(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HttpServerEnabled
|
public bool HttpServerEnabled
|
||||||
{
|
{
|
||||||
get => _httpServerEnabled;
|
get => _httpServerEnabled;
|
||||||
set { _httpServerEnabled = value; Save(); }
|
set { _httpServerEnabled = value; ScheduleSave(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TelemetryEnabled
|
public bool WebSocketEnabled
|
||||||
{
|
{
|
||||||
get => _telemetryEnabled;
|
get => _webSocketEnabled;
|
||||||
set { _telemetryEnabled = value; Save(); }
|
set { _webSocketEnabled = value; ScheduleSave(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CharTag
|
public string CharTag
|
||||||
{
|
{
|
||||||
get => _charTag;
|
get => _charTag;
|
||||||
set { _charTag = value; Save(); }
|
set { _charTag = value; ScheduleSave(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ using System.Runtime.InteropServices;
|
||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
[assembly: Guid("9b6a07e1-ae78-47f4-b09c-174f6a27d7a3")]
|
[assembly: Guid("be0a51b6-cf1f-4318-ad49-e40d6aebe14b")]
|
||||||
|
|
||||||
// Version information for an assembly consists of the following four values:
|
// Version information for an assembly consists of the following four values:
|
||||||
// Major Version
|
// Major Version
|
||||||
|
|
|
||||||
120
MosswartMassacre/StatsManager.cs
Normal file
120
MosswartMassacre/StatsManager.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
using System;
|
||||||
|
using System.Timers;
|
||||||
|
|
||||||
|
namespace MosswartMassacre
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages kill and rare statistics, with periodic updates.
|
||||||
|
/// </summary>
|
||||||
|
public class StatsManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Timer _timer;
|
||||||
|
|
||||||
|
public int TotalKills { get; private set; }
|
||||||
|
public int RareCount { get; private set; }
|
||||||
|
public double KillsPer5Min { get; private set; }
|
||||||
|
public double KillsPerHour { get; private set; }
|
||||||
|
public DateTime StatsStartTime { get; private set; }
|
||||||
|
public DateTime LastKillTime { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when kill statistics change: total kills, kills per 5 min, kills per hour.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<int, double, double> KillStatsUpdated;
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when elapsed time updates (periodic timer).
|
||||||
|
/// </summary>
|
||||||
|
public event Action<TimeSpan> ElapsedTimeUpdated;
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when rare count changes.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<int> RareCountUpdated;
|
||||||
|
|
||||||
|
public StatsManager()
|
||||||
|
{
|
||||||
|
StatsStartTime = DateTime.Now;
|
||||||
|
LastKillTime = DateTime.Now;
|
||||||
|
_timer = new Timer(1000);
|
||||||
|
_timer.Elapsed += OnTimerElapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start periodic updates.
|
||||||
|
/// </summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop periodic updates.
|
||||||
|
/// </summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_timer.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
RecalculateRates();
|
||||||
|
ElapsedTimeUpdated?.Invoke(DateTime.Now - StatsStartTime);
|
||||||
|
KillStatsUpdated?.Invoke(TotalKills, KillsPer5Min, KillsPerHour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a new kill, updating counts and raising events.
|
||||||
|
/// </summary>
|
||||||
|
public void RegisterKill()
|
||||||
|
{
|
||||||
|
TotalKills++;
|
||||||
|
LastKillTime = DateTime.Now;
|
||||||
|
RecalculateRates();
|
||||||
|
KillStatsUpdated?.Invoke(TotalKills, KillsPer5Min, KillsPerHour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a rare discovery and raise event.
|
||||||
|
/// </summary>
|
||||||
|
public void RegisterRare()
|
||||||
|
{
|
||||||
|
RareCount++;
|
||||||
|
RareCountUpdated?.Invoke(RareCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset all statistics to zero and starting point.
|
||||||
|
/// </summary>
|
||||||
|
public void Restart()
|
||||||
|
{
|
||||||
|
TotalKills = 0;
|
||||||
|
RareCount = 0;
|
||||||
|
StatsStartTime = DateTime.Now;
|
||||||
|
KillsPer5Min = 0;
|
||||||
|
KillsPerHour = 0;
|
||||||
|
LastKillTime = DateTime.Now;
|
||||||
|
KillStatsUpdated?.Invoke(TotalKills, KillsPer5Min, KillsPerHour);
|
||||||
|
RareCountUpdated?.Invoke(RareCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecalculateRates()
|
||||||
|
{
|
||||||
|
double minutesElapsed = (DateTime.Now - StatsStartTime).TotalMinutes;
|
||||||
|
if (minutesElapsed > 0)
|
||||||
|
{
|
||||||
|
KillsPer5Min = (TotalKills / minutesElapsed) * 5;
|
||||||
|
KillsPerHour = (TotalKills / minutesElapsed) * 60;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
KillsPer5Min = 0;
|
||||||
|
KillsPerHour = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_timer.Elapsed -= OnTimerElapsed;
|
||||||
|
_timer.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
// Telemetry.cs ───────────────────────────────────────────────────────────────
|
|
||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Decal.Adapter;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace MosswartMassacre
|
|
||||||
{
|
|
||||||
public static class Telemetry
|
|
||||||
{
|
|
||||||
/* ───────────── configuration ───────────── */
|
|
||||||
private const string Endpoint = "https://mosswart.snakedesert.se/position/"; // <- trailing slash!
|
|
||||||
private const string SharedSecret = "your_shared_secret"; // <- keep in sync
|
|
||||||
private const int IntervalSec = 5; // seconds between posts
|
|
||||||
|
|
||||||
/* ───────────── runtime state ───────────── */
|
|
||||||
private static readonly HttpClient _http = new HttpClient();
|
|
||||||
private static string _sessionId;
|
|
||||||
private static CancellationTokenSource _cts;
|
|
||||||
private static bool _enabled;
|
|
||||||
|
|
||||||
/* ───────────── public API ───────────── */
|
|
||||||
public static void Start()
|
|
||||||
{
|
|
||||||
if (_enabled) return;
|
|
||||||
|
|
||||||
_enabled = true;
|
|
||||||
_sessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
|
|
||||||
_cts = new CancellationTokenSource();
|
|
||||||
|
|
||||||
PluginCore.WriteToChat("[Telemetry] HTTP streaming ENABLED");
|
|
||||||
|
|
||||||
_ = Task.Run(() => LoopAsync(_cts.Token)); // fire-and-forget
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Stop()
|
|
||||||
{
|
|
||||||
if (!_enabled) return;
|
|
||||||
_cts.Cancel();
|
|
||||||
_enabled = false;
|
|
||||||
PluginCore.WriteToChat("[Telemetry] HTTP streaming DISABLED");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ───────────── async loop ───────────── */
|
|
||||||
private static async Task LoopAsync(CancellationToken token)
|
|
||||||
{
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await SendSnapshotAsync(token);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PluginCore.WriteToChat($"[Telemetry] send failed: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(IntervalSec), token);
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException) { } // expected on Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ───────────── single POST ───────────── */
|
|
||||||
private static async Task SendSnapshotAsync(CancellationToken token)
|
|
||||||
{
|
|
||||||
var coords = Coordinates.Me;
|
|
||||||
|
|
||||||
var payload = new
|
|
||||||
{
|
|
||||||
character_name = CoreManager.Current.CharacterFilter.Name,
|
|
||||||
char_tag = PluginCore.CharTag,
|
|
||||||
session_id = _sessionId,
|
|
||||||
timestamp = DateTime.UtcNow.ToString("o"),
|
|
||||||
|
|
||||||
ew = coords.EW,
|
|
||||||
ns = coords.NS,
|
|
||||||
z = coords.Z,
|
|
||||||
|
|
||||||
kills = PluginCore.totalKills,
|
|
||||||
onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"),
|
|
||||||
kills_per_hour = PluginCore.killsPerHour.ToString("F0"),
|
|
||||||
deaths = 0,
|
|
||||||
rares_found = PluginCore.rareCount,
|
|
||||||
prismatic_taper_count = 0,
|
|
||||||
vt_state = VtankControl.VtGetMetaState(),
|
|
||||||
};
|
|
||||||
|
|
||||||
string json = JsonConvert.SerializeObject(payload);
|
|
||||||
var req = new HttpRequestMessage(HttpMethod.Post, Endpoint)
|
|
||||||
{
|
|
||||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
|
||||||
};
|
|
||||||
req.Headers.Add("X-Plugin-Secret", SharedSecret);
|
|
||||||
|
|
||||||
using var resp = await _http.SendAsync(req, token);
|
|
||||||
|
|
||||||
if (!resp.IsSuccessStatusCode) // stay quiet on success
|
|
||||||
{
|
|
||||||
PluginCore.WriteToChat($"[Telemetry] server replied {resp.StatusCode}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
215
MosswartMassacre/WebSocketService.cs
Normal file
215
MosswartMassacre/WebSocketService.cs
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Configuration;
|
||||||
|
using Decal.Adapter;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace MosswartMassacre
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Envelope for commands received via WebSocket.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandEnvelope
|
||||||
|
{
|
||||||
|
[JsonProperty("player_name")]
|
||||||
|
public string PlayerName { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("command")]
|
||||||
|
public string Command { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket service for sending chat and receiving server commands.
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketService : IWebSocketService
|
||||||
|
{
|
||||||
|
private readonly StatsManager _statsManager;
|
||||||
|
private readonly Uri _endpoint;
|
||||||
|
private readonly string _secret;
|
||||||
|
private readonly int _intervalSec;
|
||||||
|
private ClientWebSocket _ws;
|
||||||
|
private CancellationTokenSource _cts;
|
||||||
|
private bool _enabled;
|
||||||
|
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||||
|
private string _sessionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when a command arrives for this character.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<CommandEnvelope> OnServerCommand;
|
||||||
|
|
||||||
|
public WebSocketService(StatsManager statsManager)
|
||||||
|
{
|
||||||
|
_statsManager = statsManager ?? throw new ArgumentNullException(nameof(statsManager));
|
||||||
|
// Load configuration
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var endpoint = ConfigurationManager.AppSettings["WebSocketEndpoint"];
|
||||||
|
_endpoint = !string.IsNullOrEmpty(endpoint)
|
||||||
|
? new Uri(endpoint)
|
||||||
|
: new Uri("wss://mosswart.snakedesert.se/websocket/");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_endpoint = new Uri("wss://mosswart.snakedesert.se/websocket/");
|
||||||
|
}
|
||||||
|
_secret = ConfigurationManager.AppSettings["WebSocketSharedSecret"]
|
||||||
|
?? "your_shared_secret";
|
||||||
|
if (!int.TryParse(ConfigurationManager.AppSettings["WebSocketIntervalSec"], out _intervalSec))
|
||||||
|
{
|
||||||
|
_intervalSec = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_enabled) return;
|
||||||
|
_enabled = true;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
PluginCore.WriteToChat("[WebSocket] connecting…");
|
||||||
|
_ = Task.Run(() => ConnectLoopAsync(_cts.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (!_enabled) return;
|
||||||
|
_enabled = false;
|
||||||
|
_cts.Cancel();
|
||||||
|
_ws?.Abort();
|
||||||
|
_ws?.Dispose();
|
||||||
|
_ws = null;
|
||||||
|
PluginCore.WriteToChat("[WebSocket] DISABLED");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendChatTextAsync(int colorIndex, string chatText)
|
||||||
|
{
|
||||||
|
await _sendLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_ws == null || _ws.State != WebSocketState.Open)
|
||||||
|
return;
|
||||||
|
var envelope = new
|
||||||
|
{
|
||||||
|
type = "chat",
|
||||||
|
character_name = CoreManager.Current.CharacterFilter.Name,
|
||||||
|
text = chatText,
|
||||||
|
color = colorIndex
|
||||||
|
};
|
||||||
|
var json = JsonConvert.SerializeObject(envelope);
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
await _ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cts.Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat("[WebSocket] send error: " + ex.Message);
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_sendLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConnectLoopAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (_enabled && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ws = new ClientWebSocket();
|
||||||
|
_ws.Options.SetRequestHeader("X-Plugin-Secret", _secret);
|
||||||
|
await _ws.ConnectAsync(_endpoint, token);
|
||||||
|
PluginCore.WriteToChat("[WebSocket] CONNECTED");
|
||||||
|
_sessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
|
||||||
|
|
||||||
|
// Register this client
|
||||||
|
var registerMsg = new { type = "register", player_name = CoreManager.Current.CharacterFilter.Name };
|
||||||
|
await SendChatTextAsync(0, JsonConvert.SerializeObject(registerMsg));
|
||||||
|
PluginCore.WriteToChat("[WebSocket] REGISTERED");
|
||||||
|
|
||||||
|
var buffer = new byte[4096];
|
||||||
|
var receiveTask = ReceiveLoopAsync(buffer, token);
|
||||||
|
|
||||||
|
// Telemetry loop
|
||||||
|
while (_ws.State == WebSocketState.Open && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var coords = Coordinates.Me;
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
type = "telemetry",
|
||||||
|
character_name = CoreManager.Current.CharacterFilter.Name,
|
||||||
|
char_tag = PluginCore.CharTag,
|
||||||
|
session_id = _sessionId,
|
||||||
|
timestamp = DateTime.UtcNow.ToString("o"),
|
||||||
|
ew = coords.EW,
|
||||||
|
ns = coords.NS,
|
||||||
|
z = coords.Z,
|
||||||
|
kills = _statsManager.TotalKills,
|
||||||
|
onlinetime = (DateTime.Now - _statsManager.StatsStartTime).ToString(@"dd\.hh\:mm\:ss"),
|
||||||
|
kills_per_hour = _statsManager.KillsPerHour,
|
||||||
|
deaths = 0,
|
||||||
|
rares_found = _statsManager.RareCount,
|
||||||
|
prismatic_taper_count = 0,
|
||||||
|
vt_state = VtankControl.VtGetMetaState()
|
||||||
|
};
|
||||||
|
var json = JsonConvert.SerializeObject(payload);
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
await _ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, token);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(_intervalSec), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
await receiveTask;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat($"[WebSocket] error: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_ws?.Abort();
|
||||||
|
_ws?.Dispose();
|
||||||
|
_ws = null;
|
||||||
|
}
|
||||||
|
// wait before reconnect
|
||||||
|
await Task.Delay(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReceiveLoopAsync(byte[] buffer, CancellationToken token)
|
||||||
|
{
|
||||||
|
while (_ws.State == WebSocketState.Open && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _ws.ReceiveAsync(new ArraySegment<byte>(buffer), token);
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close) break;
|
||||||
|
var msg = Encoding.UTF8.GetString(buffer, 0, result.Count).Trim();
|
||||||
|
CommandEnvelope env;
|
||||||
|
try { env = JsonConvert.DeserializeObject<CommandEnvelope>(msg); }
|
||||||
|
catch { continue; }
|
||||||
|
if (string.Equals(env.PlayerName, CoreManager.Current.CharacterFilter.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
OnServerCommand?.Invoke(env);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginCore.WriteToChat($"[WebSocket] receive error: {ex.Message}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
_sendLock.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
|
<appSettings>
|
||||||
|
<!-- WebSocket settings -->
|
||||||
|
<add key="WebSocketEndpoint" value="wss://mosswart.snakedesert.se/websocket/" />
|
||||||
|
<add key="WebSocketSharedSecret" value="your_shared_secret" />
|
||||||
|
<add key="WebSocketIntervalSec" value="5" />
|
||||||
|
</appSettings>
|
||||||
<runtime>
|
<runtime>
|
||||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
|
|
|
||||||
BIN
MosswartMassacre/lib/utank2-i.dll
Normal file
BIN
MosswartMassacre/lib/utank2-i.dll
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue