Implement Experimental Pogo Bowl Support (and in turn Leaderboard games in general) Remove "StaticFiles" In Favor Of App Base Directory Storage New `logn` Sequence Fixed Incorrect Jackpot Result Response (although it did uncover lost media with spin.rslt=1)
167 lines
8.7 KiB
C#
167 lines
8.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", "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<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;
|
|
}
|
|
}
|
|
}
|