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