ReboundEmulator/TCPGameServer.cs
2026-09-14 17:05:08 -07:00

123 lines
6.4 KiB
C#

using System.Net.Sockets;
using System.Net;
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; } = [];
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", "init"), ("plid", "1"), ("cnid", "1")); // PLID and CNID are placeholder values here
await SendAsync(stream, ("msg.op", "load"), ("rmst", "start")); // indicate the backend is putting you in a room
await SendAsync(stream, ("msg.op", "rm.join"), ("scrn", "standalone"), ("plid", "1")); // SCRN and PLID are placeholder values here (PLID = Room ID?)
await SendAsync(stream, ("msg.op", "gm.crea")); // I don't know what this does but apparently it's required
await SendAsync(stream, ("msg.op", "load"), ("rmst", "end")); // room placement complete
await SendAsync(stream, ("msg.op", "say"), ("chnl", "serv"), ("text", "Welcome to Not Pogo! The best place to go for games and friends!")); // MOTD
await SendAsync(stream, ("msg.op", "say"), ("chnl", "serv"), ("text", "WARNING: This Emulator Is A WIP. Some Things May Not Work As Expected Even With Proper Client Props")); // Alpha Disclaimer
await SendAsync(stream, ("msg.op", "pl.rm.stte"), ("plid", "1"), ("tokb", "9999")); // PLID and TOKB are placeholder values here
_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"), ("scrn", "standalone")); // yay more placeholders!
await SendAsync(stream, ("msg.op", "jcfg"), ("jpot", "1234.56"), ("jmlt", "1.0"), ("cap", "4999.0"), ("intv", "10000"), ("incr", "0.01")); // more than likely jackpot config
await SendAsync(stream, ("msg.op", "cnfg"), ("impd", "15")); // placeholder, idk what this is
await SendAsync(stream, ("msg.op", "tokb"), ("tokb", "9999")); // token balance again for whatever reason
await SendAsync(stream, ("msg.op", "gm.lnch")); // more than likely this tells the shell to launch the game
await SendAsync(stream, ("msg.op", "ext.pref"), ("pref", "")); // game preferences
_logger.LogInformation("Sent 'gm.join' Response Sequence (Launches Game)");
}
else if (op == "ext.over")
{
bool winMet = (received["gm.won"] == "true");
if (winMet)
_logger.LogInformation("Jackpot Won!");
// send stubbed results
await SendAsync(stream, ("msg.op", "ext.rslt"), ("tokb", "999"), ("tokn.won", "0"), // Token Earnings
("spin.rslt", winMet ? "1" : "0"), ("spin.tokn", "50"), ("inst.desc", ""), // Jackpot?
("jpot", "1234.0"), ("pzpm", $"conf={Guid.NewGuid()}"), ("bmlt", "1"), ("bspo", "")); // Jackpot Confirmation Stuff - placeholder
_logger.LogInformation("Results Sent");
}
}
} catch (Exception e) { _logger.LogError(e, "Error Handling Client"); }
}
Clients.Remove(client);
_logger.LogInformation("Client Disconnected");
}
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);
}
public override Task StopAsync(CancellationToken cancellationToken)
{
if(!cancellationToken.IsCancellationRequested)
{
Clients.Clear();
Listener?.Stop();
Listener?.Dispose();
}
return Task.CompletedTask;
}
}
}