using System.Net.WebSockets; using System.Text; using Microsoft.Extensions.Logging; using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Reliability; using SVSim.BattleNode.Wire; namespace SVSim.BattleNode.Sessions; /// /// One per connected client. Owns the WebSocket + reliability ledgers + lifecycle phase. /// public sealed class BattleSession { private readonly WebSocket _ws; private readonly ILogger _log; public string BattleId { get; } public long ViewerId { get; } public BattleSessionPhase Phase { get; internal set; } = BattleSessionPhase.AwaitingInitNetwork; public InboundTracker Inbound { get; } = new(); public OutboundSequencer Outbound { get; } = new(); public BattleSession(WebSocket ws, string battleId, long viewerId, ILogger log) { _ws = ws; _log = log; BattleId = battleId; ViewerId = viewerId; } /// /// Send the EIO3 open handshake then enter the read loop. Returns when the WS closes. /// Dispatch is a no-op in this task; Task 13 fills it in. /// public async Task RunAsync(CancellationToken cancellation) { await SendEioOpenAsync(cancellation); var buffer = new byte[8192]; var pendingAttachments = new List(); SocketIoFrame? pendingFrame = null; while (_ws.State == WebSocketState.Open && !cancellation.IsCancellationRequested) { var msg = await ReadCompleteMessageAsync(buffer, cancellation); if (msg is null) break; if (msg.Value.IsText) { var text = Encoding.UTF8.GetString(msg.Value.Bytes); if (text.Length == 0) continue; var eio = EngineIoFrame.Parse(text); if (eio.Type == EngineIoPacketType.Ping) { await SendTextAsync("3", cancellation); // EIO3 pong continue; } if (eio.Type != EngineIoPacketType.Message) continue; // SIO inside the message payload. var sio = SocketIoFrame.Parse(eio.Payload); if (sio.AttachmentCount > 0) { pendingFrame = sio; pendingAttachments.Clear(); continue; } DispatchSocketIo(sio); } else { // Binary frame — an attachment for a pending binary event. pendingAttachments.Add(msg.Value.Bytes); if (pendingFrame is not null && pendingAttachments.Count == pendingFrame.AttachmentCount) { var assembled = pendingFrame.WithAttachments(pendingAttachments.ToArray()); pendingFrame = null; DispatchSocketIo(assembled); } } } } private void DispatchSocketIo(SocketIoFrame frame) { // v1 task 11 stub: log and drop. Task 13 wires the lifecycle dispatch in. _log.LogDebug("BattleSession {Bid}: received SIO {Type} event={Event}", BattleId, frame.Type, frame.EventName); } private async Task SendEioOpenAsync(CancellationToken ct) { var sid = Guid.NewGuid().ToString("N").Substring(0, 16); var handshake = new EngineIoHandshake(sid, Array.Empty(), 25000, 60000).ToJson(); await SendTextAsync($"0{handshake}", ct); } private Task SendTextAsync(string text, CancellationToken ct) { var bytes = Encoding.UTF8.GetBytes(text); return _ws.SendAsync(bytes, WebSocketMessageType.Text, endOfMessage: true, ct); } private async Task<(byte[] Bytes, bool IsText)?> ReadCompleteMessageAsync(byte[] buffer, CancellationToken ct) { using var ms = new MemoryStream(); WebSocketReceiveResult result; do { try { result = await _ws.ReceiveAsync(buffer, ct); } catch (OperationCanceledException) { return null; } catch (WebSocketException) { return null; } if (result.MessageType == WebSocketMessageType.Close) return null; ms.Write(buffer, 0, result.Count); } while (!result.EndOfMessage); return (ms.ToArray(), result.MessageType == WebSocketMessageType.Text); } }