198 lines
7.7 KiB
C#
198 lines
7.7 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);
|
|
}
|
|
else if (op == "ext.over") // TODO: move to rooms extending IGameRoom
|
|
{
|
|
// 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");
|
|
}
|
|
}
|
|
} 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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|