diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index ac9acd5..33a1c87 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -92,6 +92,7 @@
Resources.resx
+
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index 95e1858..b12285f 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -25,6 +25,7 @@ namespace MosswartMassacre
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;
private static Queue rareMessageQueue = new Queue();
private static DateTime _lastSent = DateTime.MinValue;
private static readonly Queue _chatQueue = new Queue();
@@ -39,6 +40,7 @@ namespace MosswartMassacre
// Subscribe to chat message event
CoreManager.Current.ChatBoxMessage += new EventHandler(OnChatText);
+ CoreManager.Current.ChatBoxMessage += new EventHandler(AllChatText);
CoreManager.Current.CommandLineText += OnChatCommand;
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
@@ -54,6 +56,8 @@ namespace MosswartMassacre
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
//Enable vTank interface
vTank.Enable();
+ //lyssna på commands
+ WebSocket.OnServerCommand += HandleServerCommand;
}
catch (Exception ex)
{
@@ -73,6 +77,7 @@ namespace MosswartMassacre
// Unsubscribe from chat message event
CoreManager.Current.ChatBoxMessage -= new EventHandler(OnChatText);
CoreManager.Current.CommandLineText -= OnChatCommand;
+ CoreManager.Current.ChatBoxMessage -= new EventHandler(AllChatText);
// Stop and dispose of the timer
if (updateTimer != null)
@@ -86,6 +91,9 @@ namespace MosswartMassacre
MainView.ViewDestroy();
//Disable vtank interface
vTank.Disable();
+ // sluta lyssna på commands
+ WebSocket.OnServerCommand -= HandleServerCommand;
+ WebSocket.Stop();
MyHost = null;
}
@@ -102,6 +110,7 @@ namespace MosswartMassacre
// Apply the values
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
@@ -109,11 +118,47 @@ namespace MosswartMassacre
MainView.SetRareMetaToggleState(RareMetaEnabled);
if (TelemetryEnabled)
Telemetry.Start();
-
+ if (WebSocketEnabled)
+ WebSocket.Start();
}
+ 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 AllChatText(object sender, ChatTextInterceptEventArgs e)
+ {
+ try
+ {
+
+ var cleaned = NormalizeChatLine(e.Text);
+
+ _ = WebSocket.SendChatTextAsync(e.Color, cleaned);
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat("Error sending chat over WS: " + ex.Message);
+ }
+ }
+ 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
@@ -407,11 +452,39 @@ namespace MosswartMassacre
WriteToChat("Usage: /mm telemetry ");
}
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 ");
+ }
+ }
+ else
+ {
+ WriteToChat("Usage: /mm ws ");
+ }
+ 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 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");
diff --git a/MosswartMassacre/PluginSettings.cs b/MosswartMassacre/PluginSettings.cs
index e59f09e..37d1826 100644
--- a/MosswartMassacre/PluginSettings.cs
+++ b/MosswartMassacre/PluginSettings.cs
@@ -17,6 +17,7 @@ namespace MosswartMassacre
private bool _rareMetaEnabled = true;
private bool _httpServerEnabled = false;
private bool _telemetryEnabled = false;
+ private bool _webSocketEnabled = false;
private string _charTag = "default";
public static PluginSettings Instance => _instance
@@ -123,7 +124,11 @@ namespace MosswartMassacre
get => _telemetryEnabled;
set { _telemetryEnabled = value; Save(); }
}
-
+ public bool WebSocketEnabled
+ {
+ get => _webSocketEnabled;
+ set { _webSocketEnabled = value; Save(); }
+ }
public string CharTag
{
get => _charTag;
diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs
new file mode 100644
index 0000000..fd641cf
--- /dev/null
+++ b/MosswartMassacre/WebSocket.cs
@@ -0,0 +1,255 @@
+// WebSocket.cs
+using System;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Decal.Adapter;
+using Newtonsoft.Json;
+
+namespace MosswartMassacre
+{
+ // 1) The envelope type for incoming commands
+ public class CommandEnvelope
+ {
+ [JsonProperty("player_name")]
+ public string PlayerName { get; set; }
+
+ [JsonProperty("command")]
+ public string Command { get; set; }
+ }
+
+ public static class WebSocket
+ {
+ // ─── configuration ──────────────────────────
+ private static readonly Uri WsEndpoint = new Uri("wss://mosswart.snakedesert.se/websocket/");
+ private const string SharedSecret = "your_shared_secret";
+ private const int IntervalSec = 5;
+ private static string SessionId = "";
+
+ // ─── runtime state ──────────────────────────
+ private static ClientWebSocket _ws;
+ private static CancellationTokenSource _cts;
+ private static bool _enabled;
+ private static readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
+
+ ///
+ /// Fires when a valid CommandEnvelope arrives for this character.
+ ///
+ public static event Action OnServerCommand;
+
+ // ─── public API ─────────────────────────────
+
+ public static void Start()
+ {
+ if (_enabled) return;
+ _enabled = true;
+ _cts = new CancellationTokenSource();
+
+ PluginCore.WriteToChat("[WebSocket] connecting…");
+ _ = Task.Run(ConnectAndLoopAsync);
+
+ }
+
+ public static void Stop()
+ {
+ if (!_enabled) return;
+ _enabled = false;
+
+ _cts.Cancel();
+ _ws?.Abort();
+ _ws?.Dispose();
+ _ws = null;
+
+ PluginCore.WriteToChat("[WebSocket] DISABLED");
+ }
+
+ // ─── connect / receive / telemetry loop ──────────────────────
+
+ private static async Task ConnectAndLoopAsync()
+ {
+ while (_enabled && !_cts.IsCancellationRequested)
+ {
+ try
+ {
+ // 1) Establish connection
+ _ws = new ClientWebSocket();
+ _ws.Options.SetRequestHeader("X-Plugin-Secret", SharedSecret);
+ await _ws.ConnectAsync(WsEndpoint, _cts.Token);
+ PluginCore.WriteToChat("[WebSocket] CONNECTED");
+ SessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
+
+ // ─── Register this socket under our character name ───
+ var registerEnvelope = new
+ {
+ type = "register",
+ player_name = CoreManager.Current.CharacterFilter.Name
+ };
+ var regJson = JsonConvert.SerializeObject(registerEnvelope);
+ await SendEncodedAsync(regJson, _cts.Token);
+ PluginCore.WriteToChat("[WebSocket] REGISTERED");
+
+ var buffer = new byte[4096];
+
+ // 2) Fire-and-forget receive loop
+ var receiveTask = Task.Run(async () =>
+ {
+ while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested)
+ {
+ WebSocketReceiveResult result;
+ try
+ {
+ result = await _ws.ReceiveAsync(new ArraySegment(buffer), _cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[WebSocket] receive error: {ex.Message}");
+ break;
+ }
+
+ if (result.MessageType == WebSocketMessageType.Close)
+ break;
+
+ var msg = Encoding.UTF8.GetString(buffer, 0, result.Count).Trim();
+
+ // 3) Parse into CommandEnvelope
+ CommandEnvelope env;
+ try
+ {
+ env = JsonConvert.DeserializeObject(msg);
+ }
+ catch (JsonException)
+ {
+ continue; // skip malformed JSON
+ }
+
+ // 4) Filter by this character name
+ if (string.Equals(
+ env.PlayerName,
+ CoreManager.Current.CharacterFilter.Name,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ OnServerCommand?.Invoke(env);
+ }
+ }
+ });
+
+ // 5) Inline telemetry loop
+ while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested)
+ {
+ var json = BuildPayloadJson();
+ await SendEncodedAsync(json, _cts.Token);
+
+ try
+ {
+ await Task.Delay(TimeSpan.FromSeconds(IntervalSec), _cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ }
+
+ // Wait for receive loop to finish
+ await receiveTask;
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[WebSocket] error: {ex.Message}");
+ }
+ finally
+ {
+ _ws?.Abort();
+ _ws?.Dispose();
+ _ws = null;
+ }
+
+ // Pause before reconnecting
+ try { await Task.Delay(2000, CancellationToken.None); } catch { }
+ }
+ }
+
+
+ // ─── fire-and-forget chat sender ────────────────────
+
+ public static async Task SendChatTextAsync(int colorIndex, string chatText)
+ {
+ var envelope = new
+ {
+ type = "chat",
+ character_name = CoreManager.Current.CharacterFilter.Name,
+ text = chatText,
+ color = colorIndex
+
+ };
+ var json = JsonConvert.SerializeObject(envelope);
+ await SendEncodedAsync(json, CancellationToken.None);
+ }
+
+ // ─── shared send helper with locking ───────────────
+
+ private static async Task SendEncodedAsync(string text, CancellationToken token)
+ {
+ await _sendLock.WaitAsync(token);
+ try
+ {
+ if (_ws == null || _ws.State != WebSocketState.Open)
+ return;
+
+ var bytes = Encoding.UTF8.GetBytes(text);
+ await _ws.SendAsync(new ArraySegment(bytes),
+ WebSocketMessageType.Text,
+ true,
+ token);
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat("[WebSocket] send error: " + ex.Message);
+ _ws?.Abort();
+ _ws?.Dispose();
+ _ws = null;
+ }
+ finally
+ {
+ _sendLock.Release();
+ }
+ }
+
+ // ─── payload builder ──────────────────────────────
+
+
+
+ private static string BuildPayloadJson()
+ {
+ 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 = 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()
+ };
+ return JsonConvert.SerializeObject(payload);
+ }
+ }
+}
diff --git a/MosswartMassacre/lib/utank2-i.dll b/MosswartMassacre/lib/utank2-i.dll
new file mode 100644
index 0000000..7696f05
Binary files /dev/null and b/MosswartMassacre/lib/utank2-i.dll differ