327 lines
12 KiB
C#
327 lines
12 KiB
C#
// WebSocket.cs
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net.WebSockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Decal.Adapter;
|
|
using Newtonsoft.Json;
|
|
using uTank2;
|
|
|
|
namespace MosswartMassacre
|
|
{
|
|
internal static class SessionInfo
|
|
{
|
|
|
|
internal static readonly Guid Guid = Guid.NewGuid();
|
|
|
|
internal static readonly string GuidString = Guid.ToString("N");
|
|
}
|
|
// 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://overlord.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);
|
|
private static readonly SynchronizationContext _uiCtx = SynchronizationContext.Current;
|
|
|
|
/// <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))
|
|
{
|
|
_uiCtx.Post(_ =>
|
|
{
|
|
try
|
|
{
|
|
OnServerCommand?.Invoke(env); // now on the correct thread
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PluginCore.WriteToChat($"[CMD] {ex.Message}");
|
|
}
|
|
}, null);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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",
|
|
timestamp = DateTime.UtcNow.ToString("o"),
|
|
character_name = CoreManager.Current.CharacterFilter.Name,
|
|
text = chatText,
|
|
color = colorIndex
|
|
|
|
};
|
|
var json = JsonConvert.SerializeObject(envelope);
|
|
await SendEncodedAsync(json, CancellationToken.None);
|
|
}
|
|
public static async Task SendSpawnAsync(string nsCoord, string ewCoord, string monster)
|
|
{
|
|
var envelope = new
|
|
{
|
|
type = "spawn",
|
|
timestamp = DateTime.UtcNow.ToString("o"),
|
|
character_name = CoreManager.Current.CharacterFilter.Name,
|
|
mob = monster,
|
|
ns = nsCoord,
|
|
ew = ewCoord
|
|
|
|
};
|
|
var json = JsonConvert.SerializeObject(envelope);
|
|
await SendEncodedAsync(json, CancellationToken.None);
|
|
}
|
|
public static async Task SendRareAsync(string rare)
|
|
{
|
|
var coords = Coordinates.Me;
|
|
var envelope = new
|
|
{
|
|
type = "rare",
|
|
timestamp = DateTime.UtcNow.ToString("o"),
|
|
character_name = CoreManager.Current.CharacterFilter.Name,
|
|
name = rare,
|
|
ew = coords.EW,
|
|
ns = coords.NS,
|
|
z = coords.Z
|
|
|
|
};
|
|
var json = JsonConvert.SerializeObject(envelope);
|
|
await SendEncodedAsync(json, CancellationToken.None);
|
|
}
|
|
|
|
public static async Task SendFullInventoryAsync(List<Mag.Shared.MyWorldObject> inventory)
|
|
{
|
|
var envelope = new
|
|
{
|
|
type = "full_inventory",
|
|
timestamp = DateTime.UtcNow.ToString("o"),
|
|
character_name = CoreManager.Current.CharacterFilter.Name,
|
|
item_count = inventory.Count,
|
|
items = inventory
|
|
|
|
};
|
|
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 tele = new ClientTelemetry();
|
|
var coords = Coordinates.Me;
|
|
var payload = new
|
|
{
|
|
type = "telemetry",
|
|
character_name = CoreManager.Current.CharacterFilter.Name,
|
|
char_tag = PluginCore.CharTag,
|
|
session_id = SessionInfo.GuidString,
|
|
timestamp = DateTime.UtcNow.ToString("o"),
|
|
ew = coords.EW,
|
|
ns = coords.NS,
|
|
z = coords.Z,
|
|
kills = PluginCore.totalKills,
|
|
kills_per_hour = PluginCore.killsPerHour.ToString("F0"),
|
|
onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"),
|
|
deaths = PluginCore.sessionDeaths.ToString(),
|
|
total_deaths = PluginCore.totalDeaths.ToString(),
|
|
prismatic_taper_count = Utils.GetItemStackSize("Prismatic Taper").ToString(),
|
|
vt_state = VtankControl.VtGetMetaState(),
|
|
mem_mb = tele.MemoryBytes,
|
|
cpu_pct = tele.GetCpuUsage(),
|
|
mem_handles = tele.HandleCount
|
|
|
|
};
|
|
return JsonConvert.SerializeObject(payload);
|
|
}
|
|
}
|
|
}
|