using System.Net.Sockets; using System.Net; using NotPogo.Core; 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; 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) { using (client) await using (var stream = client.GetStream()) { try { while (true) { Dictionary received; try { received = await TCPProtocol.ReadMessageAsync(stream); } catch (EndOfStreamException) { break; } _logger.LogInformation("Received:"); foreach (var kv in received) _logger.LogInformation(" {Key} = {Value}", kv.Key, kv.Value); if (!received.TryGetValue("msg.op", out var op)) continue; 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); // send logn response chain await SendAsync(stream, ("msg.op", "logn"), ("flags", "65535"), ("time", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())); await SendAsync(stream, ("msg.op", "init"), ("mxpl", "70"), ("plid", "1"), ("mxtb", "70"), ("mxst", "4"), ("cnid", "1623")); await SendAsync(stream, ("msg.op", "load"), ("rmst", "start")); await SendAsync(stream, ("msg.op", "rm.join"), ("scrn", "NotPogoUser"), ("pfid", "1000000000"), ("plid", "1"), ("entl", "ictsebpaulovfFmMrRG"), ("snme", "NotPogoUser"), ("priv", "0"), ("aepc", ""), ("acvw", ""), ("agvs", ""), ("alrc", "")); await SendAsync(stream, ("msg.op", "gm.crea"), ("launch", "y"), ("live", "0"), ("port", "0"), ("iali", "86400000"), ("gins", "NotPogoTempRoom"), ("dead", "0"), ("iaid", "15000"), ("shut", ""), ("gmid", "0")); await SendAsync(stream, ("msg.op", "load"), ("rmst", "end")); await SendAsync(stream, ("msg.op", "say"), ("chnl", "serv"), ("scrn", "NotPogoUser"), ("type", "stnd"), ("text", "Welcome to Not Pogo, the best place for games, fun and friends!")); await SendAsync(stream, ("msg.op", "say"), ("chnl", "serv"), ("scrn", "NotPogoUser"), ("type", "stnd"), ("text", "Caution: This emulator is in Alpha Stages. Some things may not work, even with proper game properties.")); await SendAsync(stream, ("msg.op", "pl.rm.stte"), ("plid", "1"), ("gmpf", ""), ("glpf", "..."), ("frls", "")); await SendAsync(stream, ("msg.op", "pl.rm.stte"), ("plid", "1"), ("tokb", "9999")); await SendAsync(stream, ("msg.op", "pl.rm.stte"), ("plid", "1"), ("avtr", ""), ("icon", "avatarminighost")); _logger.LogInformation("Sent 'logn' Response Sequence"); } else if (op == "gm.join") // Room Join Request { // send gm.join response chain await SendAsync(stream, ("msg.op", "gm.join"), ("plid", "1"), ("snme", "NotPogoUser"), ("gmid", "0"), ("scrn", "NotPogoUser"), ("join", "join")); await SendAsync(stream, ("msg.op", "gm.lnch"), ("gmid", "0")); await SendAsync(stream, ("msg.op", "tokb"), ("tokb", "9999")); await SendAsync(stream, ("msg.op", "tick"), ("scrn", "NotPogoUser"), ("text", "Welcome to Not Pogo!")); await SendAsync(stream, ("msg.op", "cnfg"), ("impd", "40")); await SendAsync(stream, ("msg.op", "ext.pref"), ("pref", "")); _logger.LogInformation("Sent 'gm.join' Response Sequence (Launches Game)"); } else if (op == "ext.over") { // because we aren't actually keeping track of anything going on in the games, we can just send the correct placeholders bool isJackpot = (received["gm.won"] == "true") ? true : false; int tokensWon = Random.Shared.Next(0, 999); await SendAsync(stream, ("msg.op", "ext.rslt"), ("tokb", "9999"), ("tokn.won", isJackpot ? "100" : "0"), ("spin.rslt", isJackpot ? "2" : "0"), // we aren't doing anything with the other outcomes so always issue tokens ("spin.tokn", tokensWon.ToString()), // standalone reference implementation ("inst.desc", ""), ("jpot", "0"), ("pzpm", ""), ("bmlt", "1"), ("bspo", "0"), ("xpup", "0") ); _logger.LogInformation("Results Sent"); } else if (op == "co.redy") { // when client is ready, start game loop SingleplayerBowlingGame ??= new(); _ = SingleplayerBowlingGame.StartGameLoop(stream); } else if (op == "co.turn") { // increment score SingleplayerBowlingGame?.RecordThrow(int.Parse(received["pins"])); } } } catch (Exception e) { _logger.LogError(e, "Error Handling Client"); } } Clients.Remove(client); _logger.LogInformation("Client Disconnected"); } 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; } } }