ReboundEmulator/TCPGameServer.cs
Moonbase b5c9f3dd08 Move ParseInlineMap To TCPProtocol
Implement Lottso! Deluxe Singleplayer Mode Support
2026-10-07 13:21:55 -07:00

158 lines
5.8 KiB
C#

using System.Net.Sockets;
using System.Net;
using NotPogo.Core;
using NotPogo.Core.Rooms;
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;
static readonly HashSet<string> RoomOps = ["say", "rm.left", "disc", "gm.join"];
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)
{
Player? player = null;
using (client)
await using (var stream = client.GetStream())
{
player = new(stream);
try
{
while (true)
{
Dictionary<string, string> 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<string, string> 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<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;
}
}
}