using System.Net.Sockets; using System.Text.Json; using System.Text.Json.Nodes; namespace CommonwealthOnline.Server; internal sealed class SyntheticProtocolClient : IAsyncDisposable { private readonly TcpClient _client = new(); private NetworkStream? _stream; private readonly MemoryStream _buffer = new(); public uint PlayerId { get; private set; } public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken = default) { await _client.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false); _client.NoDelay = true; _stream = _client.GetStream(); var welcome = await ReceiveAsync(cancellationToken).ConfigureAwait(false); if (JsonHelpers.String(welcome["type"]) != "welcome") throw new InvalidDataException("Server did not send welcome packet."); await SendAsync(new JsonObject { ["type"] = "hello", ["protocolVersion"] = ProtocolConstants.ProtocolVersion }, cancellationToken).ConfigureAwait(false); while (true) { var packet = await ReceiveAsync(cancellationToken).ConfigureAwait(false); if (JsonHelpers.String(packet["type"]) != "sessionReady") continue; if (!JsonHelpers.TryUInt32(packet["playerId"], 1, uint.MaxValue, out var id)) throw new InvalidDataException("sessionReady did not contain a valid playerId."); PlayerId = id; break; } } public async Task SendAsync(JsonObject packet, CancellationToken cancellationToken = default) { if (_stream is null) throw new InvalidOperationException("Client is not connected."); var encoded = JsonSerializer.SerializeToUtf8Bytes(packet); if (encoded.Length > ProtocolConstants.MaxMessageBytes) throw new InvalidDataException("Synthetic packet exceeds maximum message size."); await _stream.WriteAsync(encoded, cancellationToken).ConfigureAwait(false); await _stream.WriteAsync(new byte[] { (byte)'\n' }, cancellationToken).ConfigureAwait(false); } public async Task ReceiveAsync(CancellationToken cancellationToken = default) { if (_stream is null) throw new InvalidOperationException("Client is not connected."); var one = new byte[1]; _buffer.SetLength(0); while (_buffer.Length <= ProtocolConstants.MaxMessageBytes) { var count = await _stream.ReadAsync(one, cancellationToken).ConfigureAwait(false); if (count == 0) throw new EndOfStreamException("Server closed the connection."); if (one[0] == (byte)'\n') { var data = _buffer.ToArray(); if (data.Length > 0 && data[^1] == (byte)'\r') Array.Resize(ref data, data.Length - 1); return JsonNode.Parse(data) as JsonObject ?? throw new InvalidDataException("Server message was not a JSON object."); } _buffer.WriteByte(one[0]); } throw new InvalidDataException("Server message exceeded maximum size."); } public Task SendTransformAsync(double x, double y, double z, string cellId, string worldspaceId = "", string movementType = "normal", CancellationToken cancellationToken = default) => SendAsync(new JsonObject { ["type"] = "transform", ["x"] = x, ["y"] = y, ["z"] = z, ["angleZ"] = 0.0, ["cellId"] = cellId, ["worldspaceId"] = worldspaceId, ["movementType"] = movementType }, cancellationToken); public async ValueTask DisposeAsync() { try { _stream?.Dispose(); } catch { } try { _client.Dispose(); } catch { } _buffer.Dispose(); await ValueTask.CompletedTask; } }