ReboundEmulator/TCPGameServer.cs
2026-09-29 11:58:29 -07:00

184 lines
9.7 KiB
C#

using System.Net.Sockets;
using System.Net;
using NotPogo.Core;
namespace NotPogo
{
public class TCPGameServer(ILogger<TCPGameServer> logger, IConfiguration configuration) : BackgroundService
{
private readonly ILogger<TCPGameServer> _logger = logger;
private readonly IConfiguration _configuration = configuration;
private int _tcpPort;
public TcpListener? Listener { get; private set; }
public static List<TcpClient> Clients { get; } = [];
public static BowlingGame? SingleplayerBowlingGame;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_tcpPort = _configuration.GetValue<int>("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<string, string> 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", "ictsebaulovfFmMrRG"),
("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", ""));
// send lottso config, more than likely will be ignored by other clients
// this is mostly to get lottso express to work
await SendAsync(stream,
("msg.op", "lb.cfg"),
("lb.cmn.rack.ballnum", "6"),
("lb.cmn.ball.range", "1,75"),
("lb.cmn.ball.starting", "30"),
("lb.cmn.ball.extra", "1"),
("lb.cmn.jackpot.goal", "50"),
("lb.cmn.score.to.token", "100"),
("lb.cmn.rank.points", "10"),
("lb.cmn.scoring.goal", "100,200,300,400,500"),
("lb.cmn.cheat", "false"),
("rank.maxOrd", "10")
);
_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 = isJackpot ? Random.Shared.Next(0, 999) : 0;
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<string, string>();
foreach (var (key, value) in fields) msg[key] = value;
return TCPProtocol.WriteMessageAsync(stream, msg);
}
static Dictionary<string, int> ParseInlineMap(string raw)
{
var result = new Dictionary<string, int>();
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;
}
}
}