using System.Net.Sockets; using System.Net; using NotPogo.Core; using NotPogo.Core.Rooms; namespace NotPogo { public class TCPGameServer(ILogger logger, IConfiguration configuration) : BackgroundService { private readonly ILogger _logger = logger; private readonly IConfiguration _configuration = configuration; private int _tcpPort; public TcpListener? Listener { get; private set; } public static List Clients { get; } = []; public static BowlingGame? SingleplayerBowlingGame; static readonly HashSet RoomOps = ["say", "rm.left", "disc", "gm.join"]; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _tcpPort = _configuration.GetValue("GameServerPort"); Listener = new(IPAddress.Any, _tcpPort); Listener.Start(); _logger.LogInformation("Game Server Listening On Port {Port}", _tcpPort); while (!stoppingToken.IsCancellationRequested) { var client = await Listener.AcceptTcpClientAsync(stoppingToken); _logger.LogInformation("Client Connected: {RemoteEndPoint}", client.Client.RemoteEndPoint); _ = HandleClientAsync(client); } } private async Task HandleClientAsync(TcpClient client) { Player? player = null; using (client) await using (var stream = client.GetStream()) { player = new(stream); try { while (true) { Dictionary received; try { received = await TCPProtocol.ReadMessageAsync(stream); } catch (EndOfStreamException) { break; } if (!received.TryGetValue("msg.op", out var op)) continue; _logger.LogInformation("[plid {Plid}] <- {Op} {Fields}", player.Id, op, string.Join(" ", received.Where(kv => kv.Key != "msg.op").Select(kv => $"{kv.Key}='{kv.Value}'"))); if (op == "logn") // Login Request { // we know this is a compatible client now so let's add it to the client list Clients.Add(client); // handle logn await HandleLoginAsync(player, received); } else if (op == "ping") { player.LastSeen = DateTime.UtcNow; } else if (player.Room is not null && (RoomOps.Contains(op) || op.StartsWith(player.Room.GameCode + "."))) { await player.Room.HandleAsync(player, op, received); } } } catch (Exception e) when (e is EndOfStreamException or IOException or SocketException) { _logger.LogInformation("plid {Plid} disconnected ({Reason})", player.Id, e.GetBaseException().Message); } catch(Exception e) { _logger.LogError(e, "Error Handling plid {Plid}", player.Id); } finally { try { if (player.Room is { } room) await room.LeaveAsync(player); } catch(Exception e) { _logger.LogError(e, "Cleanup Failed For plid {Plid}", player.Id); } } } Clients.Remove(client); _logger.LogInformation("Client Disconnected"); } private async Task HandleLoginAsync(Player player, Dictionary logn) { var accnt = Sessions.Find(logn.GetValueOrDefault("lkey")); // populate player information player.Account = accnt; player.ScreenName = accnt?.ScreenName ?? $"Guest_{player.Id}"; // get game code and rule id string game = logn.GetValueOrDefault("game", ""); string rkey = logn.GetValueOrDefault("rkey", ""); // pick the room var room = !string.IsNullOrEmpty(rkey) ? RoomManager.Find(rkey) ?? RoomManager.GetOrCreate(rkey, game) : RoomManager.GetOrCreate($"{(game == "" ? "default" : game)}_lobby", game); string plid = player.Id.ToString(); await player.SendAsync( ("msg.op", "logn"), ("flags", "65535"), ("time", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()) ); await player.SendAsync( ("msg.op", "init"), ("mxpl", "70"), ("plid", plid), ("mxtb", "70"), ("mxst", "4"), ("cnid", "1623") ); await room.JoinAsync(player); } public static Task SendAsync(NetworkStream stream, params (string key, string value)[] fields) { var msg = new Dictionary(); foreach (var (key, value) in fields) msg[key] = value; return TCPProtocol.WriteMessageAsync(stream, msg); } static Dictionary ParseInlineMap(string raw) { var result = new Dictionary(); if (string.IsNullOrEmpty(raw)) return result; var trimmed = raw.Trim('{', '}'); foreach(var pair in trimmed.Split(',')) { var kv = pair.Split('='); if(kv.Length != 2) continue; var key = kv[0].Trim(); if (int.TryParse(kv[1].Trim(), out int value)) result[key] = value; } return result; } public override Task StopAsync(CancellationToken cancellationToken) { if(!cancellationToken.IsCancellationRequested) { Clients.Clear(); Listener?.Stop(); Listener?.Dispose(); } return Task.CompletedTask; } } }