Launcher milestone 2: player list, kick/ban, live stats

Adds an admin-protocol client (token-authed TCP JSON on 127.0.0.1:AdminPort,
reusing the server's .admin-token) that polls connected players and status
every 3s, plus Kick/Ban on the selected player and a players/uptime readout.
This commit is contained in:
NomadsReach
2026-08-16 18:22:03 -04:00
parent 1f01e68794
commit 16c101a4bc
4 changed files with 276 additions and 19 deletions
+67
View File
@@ -0,0 +1,67 @@
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
namespace CommonwealthOnline.Host.Services;
// Speaks the server's token-authenticated admin protocol on 127.0.0.1:AdminPort:
// read .admin-token, send {..,"adminToken"}\n, read one newline-terminated JSON reply.
public sealed class AdminClient
{
private const int MaxResponseBytes = 1_000_000;
private readonly int _port;
private readonly string _tokenPath;
public AdminClient(int adminPort, string tokenPath)
{
_port = adminPort;
_tokenPath = tokenPath;
}
public async Task<JsonObject?> SendAsync(JsonObject request, CancellationToken ct = default)
{
var token = (await File.ReadAllTextAsync(_tokenPath, ct).ConfigureAwait(false)).Trim();
var authenticated = (JsonObject)request.DeepClone();
authenticated["adminToken"] = token;
var payload = JsonSerializer.SerializeToUtf8Bytes(authenticated);
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, _port, ct).ConfigureAwait(false);
var stream = client.GetStream();
await stream.WriteAsync(payload, ct).ConfigureAwait(false);
await stream.WriteAsync(new byte[] { (byte)'\n' }, ct).ConfigureAwait(false);
using var buffer = new MemoryStream();
var one = new byte[1];
while (buffer.Length < MaxResponseBytes)
{
var read = await stream.ReadAsync(one, ct).ConfigureAwait(false);
if (read == 0 || one[0] == (byte)'\n')
{
break;
}
buffer.WriteByte(one[0]);
}
return JsonNode.Parse(buffer.ToArray()) as JsonObject;
}
public Task<JsonObject?> StatusAsync(CancellationToken ct = default) =>
SendAsync(new JsonObject { ["cmd"] = "status" }, ct);
public Task<JsonObject?> ClientsAsync(CancellationToken ct = default) =>
SendAsync(new JsonObject { ["cmd"] = "clients" }, ct);
public Task<JsonObject?> KickAsync(uint playerId, string reason, CancellationToken ct = default) =>
SendAsync(new JsonObject { ["cmd"] = "kick", ["playerId"] = playerId, ["reason"] = reason }, ct);
public Task<JsonObject?> BanAsync(uint playerId, string reason, CancellationToken ct = default) =>
SendAsync(new JsonObject { ["cmd"] = "ban", ["playerId"] = playerId, ["reason"] = reason }, ct);
}