Websockets-version

This commit is contained in:
erik 2025-05-05 20:08:15 +02:00
parent 9de0d03474
commit d2e9988bdd
5 changed files with 337 additions and 3 deletions

View file

@ -92,6 +92,7 @@
<DependentUpon>Resources.resx</DependentUpon> <DependentUpon>Resources.resx</DependentUpon>
</Compile> </Compile>
<Compile Include="ViewSystemSelector.cs" /> <Compile Include="ViewSystemSelector.cs" />
<Compile Include="WebSocket.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" />

View file

@ -25,6 +25,7 @@ namespace MosswartMassacre
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; public static bool TelemetryEnabled { get; set; } = false;
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>();
@ -39,6 +40,7 @@ namespace MosswartMassacre
// Subscribe to chat message event // Subscribe to chat message event
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText); CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(AllChatText);
CoreManager.Current.CommandLineText += OnChatCommand; CoreManager.Current.CommandLineText += OnChatCommand;
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete; CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
@ -54,6 +56,8 @@ namespace MosswartMassacre
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
//Enable vTank interface //Enable vTank interface
vTank.Enable(); vTank.Enable();
//lyssna på commands
WebSocket.OnServerCommand += HandleServerCommand;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -73,6 +77,7 @@ namespace MosswartMassacre
// Unsubscribe from chat message event // Unsubscribe from chat message event
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText); CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
CoreManager.Current.CommandLineText -= OnChatCommand; CoreManager.Current.CommandLineText -= OnChatCommand;
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(AllChatText);
// Stop and dispose of the timer // Stop and dispose of the timer
if (updateTimer != null) if (updateTimer != null)
@ -86,6 +91,9 @@ namespace MosswartMassacre
MainView.ViewDestroy(); MainView.ViewDestroy();
//Disable vtank interface //Disable vtank interface
vTank.Disable(); vTank.Disable();
// sluta lyssna på commands
WebSocket.OnServerCommand -= HandleServerCommand;
WebSocket.Stop();
MyHost = null; MyHost = null;
} }
@ -102,6 +110,7 @@ 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; TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
@ -109,11 +118,47 @@ namespace MosswartMassacre
MainView.SetRareMetaToggleState(RareMetaEnabled); MainView.SetRareMetaToggleState(RareMetaEnabled);
if (TelemetryEnabled) if (TelemetryEnabled)
Telemetry.Start(); 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) private void OnChatText(object sender, ChatTextInterceptEventArgs e)
{ {
try try
@ -407,11 +452,39 @@ namespace MosswartMassacre
WriteToChat("Usage: /mm telemetry <enable|disable>"); WriteToChat("Usage: /mm telemetry <enable|disable>");
} }
break; 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": case "help":
WriteToChat("Mosswart Massacre Commands:"); WriteToChat("Mosswart Massacre Commands:");
WriteToChat("/mm report - Show current stats"); WriteToChat("/mm report - Show current stats");
WriteToChat("/mm loc - Show current location"); 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 reset - Reset all counters");
WriteToChat("/mm meta - Toggle rare meta state"); WriteToChat("/mm meta - Toggle rare meta state");
WriteToChat("/mm http - Local http-command server enable|disable"); WriteToChat("/mm http - Local http-command server enable|disable");

View file

@ -17,6 +17,7 @@ namespace MosswartMassacre
private bool _rareMetaEnabled = true; private bool _rareMetaEnabled = true;
private bool _httpServerEnabled = false; private bool _httpServerEnabled = false;
private bool _telemetryEnabled = 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
@ -123,7 +124,11 @@ namespace MosswartMassacre
get => _telemetryEnabled; get => _telemetryEnabled;
set { _telemetryEnabled = value; Save(); } set { _telemetryEnabled = value; Save(); }
} }
public bool WebSocketEnabled
{
get => _webSocketEnabled;
set { _webSocketEnabled = value; Save(); }
}
public string CharTag public string CharTag
{ {
get => _charTag; get => _charTag;

View file

@ -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);
/// <summary>
/// Fires when a valid CommandEnvelope arrives for this character.
/// </summary>
public static event Action<CommandEnvelope> 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<byte>(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<CommandEnvelope>(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<byte>(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);
}
}
}

Binary file not shown.