Code Cleanup

Rework How Room OPs Are Handled Per Room (if the game server itself isn't handling it, just send it to the room every time)
This commit is contained in:
Alan Moon 2026-10-09 14:36:32 -07:00
parent a3ac23d492
commit 64273ab651
17 changed files with 121 additions and 124 deletions

View File

@ -119,29 +119,29 @@ public sealed class Amf0Reader
case 0x06: return null; // undefined case 0x06: return null; // undefined
case 0x07: return _refs[U16()]; // reference case 0x07: return _refs[U16()]; // reference
case 0x08: // ECMA (associative) array case 0x08: // ECMA (associative) array
{ {
U32(); // approximate count; ignore U32(); // approximate count; ignore
var o = new Dictionary<string, object?>(); _refs.Add(o); ReadProps(o); return o; var o = new Dictionary<string, object?>(); _refs.Add(o); ReadProps(o); return o;
} }
case 0x0A: // strict array case 0x0A: // strict array
{ {
uint n = U32(); uint n = U32();
var list = new List<object?>((int)Math.Min(n, 4096)); _refs.Add(list); var list = new List<object?>((int)Math.Min(n, 4096)); _refs.Add(list);
for (uint i = 0; i < n; i++) list.Add(ReadValue()); for (uint i = 0; i < n; i++) list.Add(ReadValue());
return list; return list;
} }
case 0x0B: // date: ms since epoch + tz (ignored) case 0x0B: // date: ms since epoch + tz (ignored)
{ {
double ms = F64(); U16(); double ms = F64(); U16();
return DateTime.UnixEpoch.AddMilliseconds(ms); return DateTime.UnixEpoch.AddMilliseconds(ms);
} }
case 0x0C: return Utf8((int)U32()); // long string case 0x0C: return Utf8((int)U32()); // long string
case 0x0F: return Utf8((int)U32()); // XML document (as text) case 0x0F: return Utf8((int)U32()); // XML document (as text)
case 0x10: // typed object case 0x10: // typed object
{ {
string cls = Utf8(U16()); string cls = Utf8(U16());
var o = new Dictionary<string, object?> { ["__className"] = cls }; _refs.Add(o); ReadProps(o); return o; var o = new Dictionary<string, object?> { ["__className"] = cls }; _refs.Add(o); ReadProps(o); return o;
} }
case 0x11: throw new NotSupportedException("AMF3 value inside AMF0 (Flash 9+ only; AS2 never sends this)"); case 0x11: throw new NotSupportedException("AMF3 value inside AMF0 (Flash 9+ only; AS2 never sends this)");
default: throw new FormatException($"Unknown AMF0 marker 0x{marker:X2} at offset {_p - 1}"); default: throw new FormatException($"Unknown AMF0 marker 0x{marker:X2} at offset {_p - 1}");
} }
@ -184,12 +184,12 @@ public sealed class Amf0Writer
case null: U8(0x05); return; case null: U8(0x05); return;
case bool b: U8(0x01); U8((byte)(b ? 1 : 0)); return; case bool b: U8(0x01); U8((byte)(b ? 1 : 0)); return;
case string s: case string s:
{ {
var bytes = Encoding.UTF8.GetBytes(s); var bytes = Encoding.UTF8.GetBytes(s);
if (bytes.Length <= ushort.MaxValue) { U8(0x02); U16((ushort)bytes.Length); } if (bytes.Length <= ushort.MaxValue) { U8(0x02); U16((ushort)bytes.Length); }
else { U8(0x0C); U32((uint)bytes.Length); } else { U8(0x0C); U32((uint)bytes.Length); }
_s.Write(bytes); return; _s.Write(bytes); return;
} }
case byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal: case byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal:
U8(0x00); F64(Convert.ToDouble(v)); return; U8(0x00); F64(Convert.ToDouble(v)); return;
case Enum e: U8(0x00); F64(Convert.ToDouble(e)); return; case Enum e: U8(0x00); F64(Convert.ToDouble(e)); return;
@ -200,12 +200,12 @@ public sealed class Amf0Writer
foreach (DictionaryEntry kv in dict) { ShortString(kv.Key.ToString()!); WriteValue(kv.Value); } foreach (DictionaryEntry kv in dict) { ShortString(kv.Key.ToString()!); WriteValue(kv.Value); }
EndObject(); return; EndObject(); return;
case IEnumerable list: case IEnumerable list:
{ {
var items = list.Cast<object?>().ToList(); var items = list.Cast<object?>().ToList();
U8(0x0A); U32((uint)items.Count); U8(0x0A); U32((uint)items.Count);
foreach (var item in items) WriteValue(item); foreach (var item in items) WriteValue(item);
return; return;
} }
default: // POCO / anonymous object -> anonymous AMF object default: // POCO / anonymous object -> anonymous AMF object
U8(0x03); U8(0x03);
foreach (var prop in v.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) foreach (var prop in v.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))

View File

@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using ReboundEmulator.Core.Rooms;
using ReboundEmulator.Core; using ReboundEmulator.Core;
namespace ReboundEmulator.Controllers namespace ReboundEmulator.Controllers
@ -32,19 +31,11 @@ namespace ReboundEmulator.Controllers
{ {
var lkey = Sessions.Issue(name); var lkey = Sessions.Issue(name);
// more satisfying
return Results.Text($"<LoginResponse error=\"none\">\n<LoginSessionKey>{lkey}</LoginSessionKey></LoginResponse>", "text/xml"); return Results.Text($"<LoginResponse error=\"none\">\n<LoginSessionKey>{lkey}</LoginSessionKey></LoginResponse>", "text/xml");
} }
[HttpGet("/cdl/1/cdl-clientprops")] [HttpGet("/cdl/1/cdl-clientprops")]
public IActionResult CDLClientProps(string game) public IActionResult CDLClientProps() { return Ok(); } // CDL games don't really need the client props to be present on the server,
{ // it was there so Pogo could update assets for CDL games on the fly
string pathToProps = Path.Combine(AppContext.BaseDirectory, "StaticFiles", "Configs", "CDL", $"{game}.properties");
if (System.IO.File.Exists(pathToProps))
return Ok(System.IO.File.ReadAllText(pathToProps));
else
return Ok(); // don't send anything since we don't have the config for it on the server yet
}
} }
} }

View File

@ -2,8 +2,6 @@
using ReboundEmulator.Core; using ReboundEmulator.Core;
using ReboundEmulator.Model; using ReboundEmulator.Model;
using System.IO.Compression; using System.IO.Compression;
using System.Net;
using System.Reflection.Metadata.Ecma335;
using System.Text; using System.Text;
namespace ReboundEmulator.Controllers namespace ReboundEmulator.Controllers

View File

@ -1,6 +1,4 @@
using System.Net.NetworkInformation; using System.Net.Sockets;
using System.Net.Sockets;
using System.Reflection.Metadata.Ecma335;
namespace ReboundEmulator.Core namespace ReboundEmulator.Core
{ {

View File

@ -1,5 +1,4 @@
using ReboundEmulator; using ReboundEmulator.Core.Rooms;
using ReboundEmulator.Core.Rooms;
using System.Net.Sockets; using System.Net.Sockets;
namespace ReboundEmulator.Core namespace ReboundEmulator.Core
@ -23,10 +22,10 @@ namespace ReboundEmulator.Core
public Player(NetworkStream stream) => _stream = stream; public Player(NetworkStream stream) => _stream = stream;
public async Task SendAsync(params(string key, string value)[] fields) public async Task SendAsync(params (string key, string value)[] fields)
{ {
var msg = new Dictionary<string, string>(); var msg = new Dictionary<string, string>();
foreach(var (k, v) in fields) msg[k] = v; foreach (var (k, v) in fields) msg[k] = v;
await SendAsync(msg); await SendAsync(msg);
msg.TryGetValue("msg.op", out var op); msg.TryGetValue("msg.op", out var op);
@ -37,8 +36,8 @@ namespace ReboundEmulator.Core
{ {
await _writeLock.WaitAsync(); await _writeLock.WaitAsync();
try { await TCPProtocol.WriteMessageAsync(_stream, msg); } try { await TCPProtocol.WriteMessageAsync(_stream, msg); }
catch(IOException) {} catch (IOException) { }
catch(ObjectDisposedException) {} catch (ObjectDisposedException) { }
finally { _writeLock.Release(); } finally { _writeLock.Release(); }
} }
} }

View File

@ -1,9 +1,8 @@
namespace ReboundEmulator.Core.Rooms namespace ReboundEmulator.Core.Rooms
{ {
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class GameRoomAttribute(string gameCode, string? opPrefix = null) : Attribute public sealed class GameRoomAttribute(string gameCode) : Attribute
{ {
public string GameCode { get; } = gameCode; public string GameCode { get; } = gameCode;
public string? OpPrefix { get; init; } = opPrefix;
} }
} }

View File

@ -0,0 +1,26 @@
namespace ReboundEmulator.Core.Rooms.GameRooms
{
[GameRoom("bowling")]
public class BowlingRoom : IGameRoom
{
public Task OnGameJoinAsync(Room room, Player player)
{
throw new NotImplementedException();
}
public Task<bool> OnMessageAsync(Room room, Player player, string op, Dictionary<string, string> msg)
{
throw new NotImplementedException();
}
public Task OnPlayerJoinedAsync(Room room, Player player)
{
throw new NotImplementedException();
}
public Task OnPlayerLeftAsync(Room room, Player player)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,6 +1,4 @@
using ReboundEmulator; namespace ReboundEmulator.Core.Rooms.GameRooms
namespace ReboundEmulator.Core.Rooms.GameRooms
{ {
[GameRoom("lottso2")] [GameRoom("lottso2")]
public class Lottso2Room : IGameRoom public class Lottso2Room : IGameRoom

View File

@ -1,6 +1,6 @@
namespace ReboundEmulator.Core.Rooms.GameRooms namespace ReboundEmulator.Core.Rooms.GameRooms
{ {
[GameRoom("seaball", "sb.")] [GameRoom("seaball")]
public class SkeeballRoom : IGameRoom public class SkeeballRoom : IGameRoom
{ {
public Task OnGameJoinAsync(Room room, Player player) => Task.CompletedTask; public Task OnGameJoinAsync(Room room, Player player) => Task.CompletedTask;

View File

@ -2,7 +2,6 @@
{ {
public interface IGameRoom public interface IGameRoom
{ {
string? OpPrefix => null;
Task OnPlayerJoinedAsync(Room room, Player player); Task OnPlayerJoinedAsync(Room room, Player player);
Task OnPlayerLeftAsync(Room room, Player player); Task OnPlayerLeftAsync(Room room, Player player);
Task<bool> OnMessageAsync(Room room, Player player, string op, Dictionary<string, string> msg); Task<bool> OnMessageAsync(Room room, Player player, string op, Dictionary<string, string> msg);

View File

@ -1,33 +1,26 @@
using Microsoft.AspNetCore.StaticFiles; namespace ReboundEmulator.Core.Rooms
using System.Runtime.InteropServices.Marshalling;
using System.Reflection;
using ReboundEmulator.Core;
namespace ReboundEmulator.Core.Rooms
{ {
public sealed class Room public sealed class Room
{ {
public string Key { get; } public string Key { get; }
public string HumanName { get; } public string HumanName { get; }
public string GameCode { get; } public string GameCode { get; }
public string OpPrefix =>
Game.GetType().GetCustomAttributes<GameRoomAttribute>()
.FirstOrDefault(a => string.Equals(a.GameCode, GameCode, StringComparison.OrdinalIgnoreCase))
?.OpPrefix ?? GameCode + ".";
public string RuleId { get; } public string RuleId { get; }
public int SoftCapacity { get; } public int SoftCapacity { get; }
public int TabId { get; } public int TabId { get; }
public bool Live { get; set; } = true; public bool Live { get; set; } = true;
public IGameRoom Game { get; } public IGameRoom Game { get; }
public static ILogger Log { get; private set; }
readonly List<Player> _players = new(); readonly List<Player> _players = new();
readonly object _lock = new(); readonly object _lock = new();
public Room(string key, string humanName, string gameCode, string ruleId, public Room(string key, string humanName, string gameCode, string ruleId,
int softCapacity, int tabId, bool live, IGameRoom game) int softCapacity, int tabId, bool live, IGameRoom game, ILogger log)
{ {
Key = key; HumanName = humanName; GameCode = gameCode; RuleId = ruleId; Key = key; HumanName = humanName; GameCode = gameCode; RuleId = ruleId;
SoftCapacity = softCapacity; TabId = tabId; Live = live; Game = game; SoftCapacity = softCapacity; TabId = tabId; Live = live; Game = game;
Log = log;
} }
public bool IsJoinable => Live && PlayerCount < SoftCapacity; public bool IsJoinable => Live && PlayerCount < SoftCapacity;
@ -126,7 +119,7 @@ namespace ReboundEmulator.Core.Rooms
public async Task HandleAsync(Player player, string op, Dictionary<string, string> msg) public async Task HandleAsync(Player player, string op, Dictionary<string, string> msg)
{ {
switch(op) switch (op)
{ {
case "gm.join": case "gm.join":
await GameJoinAsync(player); await GameJoinAsync(player);
@ -150,8 +143,8 @@ namespace ReboundEmulator.Core.Rooms
return; return;
} }
if (!await Game.OnMessageAsync(this, player, op, msg)) if (!await Game.OnMessageAsync(this, player, op, msg) && Game is not ChatOnlyRoom)
Console.WriteLine($"[Room {Key}] Unhandled OP '{op}' From {player.ScreenName}"); Log.LogInformation("[Room {Key}] Unhandled OP '{OP}' From {ScreenName}", Key, op, player.ScreenName);
} }
public async Task GameJoinAsync(Player player) public async Task GameJoinAsync(Player player)

View File

@ -12,14 +12,14 @@ namespace ReboundEmulator.Core.Rooms
public sealed record RoomDef(string Key, string Name, string OpCode, string Rule, int Capacity = 50); public sealed record RoomDef(string Key, string Name, string OpCode, string Rule, int Capacity = 50);
public static void LoadFromFile(string path) public static void LoadFromFile(string path, ILogger log)
{ {
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true };
var games = JsonSerializer.Deserialize<Dictionary<string, RoomDef[]>>(File.ReadAllText(path), opts)!; var games = JsonSerializer.Deserialize<Dictionary<string, RoomDef[]>>(File.ReadAllText(path), opts)!;
foreach (var (gameCode, rooms) in games) foreach (var (gameCode, rooms) in games)
foreach (var r in rooms) foreach (var r in rooms)
Define(r.Key, r.Name, gameCode, r.Rule, true, r.Capacity); Define(r.Key, r.Name, gameCode, r.Rule, true, log, r.Capacity);
} }
static Dictionary<string, Func<string, IGameRoom>> BuildFactories() static Dictionary<string, Func<string, IGameRoom>> BuildFactories()
@ -54,11 +54,11 @@ namespace ReboundEmulator.Core.Rooms
return gameRoom; return gameRoom;
} }
public static Room Define(string key, string humanName, string gameCode, string ruleId, bool isLive, int softCapacity = 50, int tabId = 1) => public static Room Define(string key, string humanName, string gameCode, string ruleId, bool isLive, ILogger log, int softCapacity = 50, int tabId = 1) =>
Rooms.GetOrAdd(key, _ => new Room(key, humanName, gameCode, ruleId, softCapacity, tabId, isLive, CreateGame(gameCode, ruleId))); Rooms.GetOrAdd(key, _ => new Room(key, humanName, gameCode, ruleId, softCapacity, tabId, isLive, CreateGame(gameCode, ruleId), log));
public static Room GetOrCreate(string key, string gameCode, string ruleId = "") => public static Room GetOrCreate(string key, string gameCode, ILogger log, string ruleId = "") =>
Rooms.GetOrAdd(key, _ => new Room(key, key, gameCode, ruleId, 50, 1, false, CreateGame(gameCode, ruleId))); Rooms.GetOrAdd(key, _ => new Room(key, key, gameCode, ruleId, 50, 1, false, CreateGame(gameCode, ruleId), log));
public static Room? Find(string key) => Rooms.TryGetValue(key, out var r) ? r : null; public static Room? Find(string key) => Rooms.TryGetValue(key, out var r) ? r : null;
public static IEnumerable<Room> ForGame(string gameCode) => Rooms.Values.Where(r => r.GameCode == gameCode); public static IEnumerable<Room> ForGame(string gameCode) => Rooms.Values.Where(r => r.GameCode == gameCode);

View File

@ -1,5 +1,4 @@
using Microsoft.AspNetCore.StaticFiles; using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;

View File

@ -1,7 +1,7 @@
// /flashservices/gateway endpoint for roomsel.swf (com.pogo.roomsel.RoomTabs). // /flashservices/gateway endpoint for roomsel.swf (com.pogo.roomsel.RoomTabs).
// Register in Program.cs: app.MapFlashGateway(); // Register in Program.cs: app.MapFlashGateway();
using ReboundEmulator.Core.Rooms;
using ReboundEmulator.Amf; using ReboundEmulator.Amf;
using ReboundEmulator.Core.Rooms;
namespace ReboundEmulator; namespace ReboundEmulator;

View File

@ -53,7 +53,7 @@ namespace ReboundEmulator
app.MapFlashGateway(); app.MapFlashGateway();
// Define Rooms For CDL Games // Define Rooms For CDL Games
RoomManager.LoadFromFile(Path.Combine(AppContext.BaseDirectory, "StaticFiles", "rooms.json")); RoomManager.LoadFromFile(Path.Combine(AppContext.BaseDirectory, "StaticFiles", "rooms.json"), app.Services.GetService<ILogger<TCPGameServer>>()!);
app.Run(); app.Run();
} }

View File

@ -1,7 +1,7 @@
using System.Net.Sockets; using ReboundEmulator.Core;
using System.Net;
using ReboundEmulator.Core;
using ReboundEmulator.Core.Rooms; using ReboundEmulator.Core.Rooms;
using System.Net;
using System.Net.Sockets;
namespace ReboundEmulator namespace ReboundEmulator
{ {
@ -17,8 +17,6 @@ namespace ReboundEmulator
public static List<TcpClient> Clients { get; } = []; public static List<TcpClient> Clients { get; } = [];
public static BowlingGame? SingleplayerBowlingGame; public static BowlingGame? SingleplayerBowlingGame;
static readonly HashSet<string> RoomOps = ["say", "rm.left", "disc", "gm.join", "ext.over"];
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
_tcpPort = _configuration.GetValue<int>("GameServerPort"); _tcpPort = _configuration.GetValue<int>("GameServerPort");
@ -73,13 +71,14 @@ namespace ReboundEmulator
{ {
player.LastSeen = DateTime.UtcNow; player.LastSeen = DateTime.UtcNow;
} }
else if (player.Room is not null && (RoomOps.Contains(op) || op.StartsWith(player.Room.OpPrefix)))
{ // if we aren't handling a command here, just send it to the player's room
if (player.Room is not null)
await player.Room.HandleAsync(player, op, received); 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) 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); _logger.LogError(e, "Error Handling plid {Plid}", player.Id);
} }
@ -89,7 +88,7 @@ namespace ReboundEmulator
{ {
if (player.Room is { } room) await room.LeaveAsync(player); if (player.Room is { } room) await room.LeaveAsync(player);
} }
catch(Exception e) { _logger.LogError(e, "Cleanup Failed For plid {Plid}", player.Id); } catch (Exception e) { _logger.LogError(e, "Cleanup Failed For plid {Plid}", player.Id); }
} }
} }
@ -113,8 +112,8 @@ namespace ReboundEmulator
// pick the room // pick the room
var room = !string.IsNullOrEmpty(rkey) ? var room = !string.IsNullOrEmpty(rkey) ?
RoomManager.Find(rkey) ?? RoomManager.Find(rkey) ??
RoomManager.GetOrCreate(rkey, game) : RoomManager.GetOrCreate(rkey, game, _logger) :
RoomManager.GetOrCreate($"{(game == "" ? "default" : game)}_lobby", game); RoomManager.GetOrCreate($"{(game == "" ? "default" : game)}_lobby", game, _logger);
string plid = player.Id.ToString(); string plid = player.Id.ToString();
@ -145,7 +144,7 @@ namespace ReboundEmulator
public override Task StopAsync(CancellationToken cancellationToken) public override Task StopAsync(CancellationToken cancellationToken)
{ {
if(!cancellationToken.IsCancellationRequested) if (!cancellationToken.IsCancellationRequested)
{ {
Clients.Clear(); Clients.Clear();
Listener?.Stop(); Listener?.Stop();

View File

@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Routing.Constraints; using System.Net.Sockets;
using Microsoft.AspNetCore.Server.HttpSys;
using System.Net.Sockets;
using System.Text; using System.Text;
namespace ReboundEmulator namespace ReboundEmulator
@ -22,7 +20,7 @@ namespace ReboundEmulator
var props = new Dictionary<string, string>(); var props = new Dictionary<string, string>();
using var ms = new MemoryStream(payload); using var ms = new MemoryStream(payload);
while(ms.Position < ms.Length) while (ms.Position < ms.Length)
{ {
string key = ReadJavaUtf(ms); string key = ReadJavaUtf(ms);
string value = ReadJavaUtf(ms); string value = ReadJavaUtf(ms);
@ -40,7 +38,7 @@ namespace ReboundEmulator
public static async Task WriteMessageAsync(NetworkStream stream, IEnumerable<KeyValuePair<string, string>> props) public static async Task WriteMessageAsync(NetworkStream stream, IEnumerable<KeyValuePair<string, string>> props)
{ {
using var payload = new MemoryStream(); using var payload = new MemoryStream();
foreach(var kv in props) foreach (var kv in props)
{ {
WriteJavaUtf(payload, kv.Key); WriteJavaUtf(payload, kv.Key);
WriteJavaUtf(payload, kv.Value); WriteJavaUtf(payload, kv.Value);
@ -141,7 +139,7 @@ namespace ReboundEmulator
private static void WriteJavaUtf(Stream s, string str) private static void WriteJavaUtf(Stream s, string str)
{ {
using var buf = new MemoryStream(); using var buf = new MemoryStream();
foreach(char ch in str) foreach (char ch in str)
{ {
if (ch >= 0x001 && ch <= 0x007F) if (ch >= 0x001 && ch <= 0x007F)
buf.WriteByte((byte)ch); buf.WriteByte((byte)ch);