diff --git a/Amf/Amf0.cs b/Amf/Amf0.cs index 47c8ae4..f05fbd0 100644 --- a/Amf/Amf0.cs +++ b/Amf/Amf0.cs @@ -119,29 +119,29 @@ public sealed class Amf0Reader case 0x06: return null; // undefined case 0x07: return _refs[U16()]; // reference case 0x08: // ECMA (associative) array - { - U32(); // approximate count; ignore - var o = new Dictionary(); _refs.Add(o); ReadProps(o); return o; - } + { + U32(); // approximate count; ignore + var o = new Dictionary(); _refs.Add(o); ReadProps(o); return o; + } case 0x0A: // strict array - { - uint n = U32(); - var list = new List((int)Math.Min(n, 4096)); _refs.Add(list); - for (uint i = 0; i < n; i++) list.Add(ReadValue()); - return list; - } + { + uint n = U32(); + var list = new List((int)Math.Min(n, 4096)); _refs.Add(list); + for (uint i = 0; i < n; i++) list.Add(ReadValue()); + return list; + } case 0x0B: // date: ms since epoch + tz (ignored) - { - double ms = F64(); U16(); - return DateTime.UnixEpoch.AddMilliseconds(ms); - } + { + double ms = F64(); U16(); + return DateTime.UnixEpoch.AddMilliseconds(ms); + } case 0x0C: return Utf8((int)U32()); // long string case 0x0F: return Utf8((int)U32()); // XML document (as text) case 0x10: // typed object - { - string cls = Utf8(U16()); - var o = new Dictionary { ["__className"] = cls }; _refs.Add(o); ReadProps(o); return o; - } + { + string cls = Utf8(U16()); + var o = new Dictionary { ["__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)"); 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 bool b: U8(0x01); U8((byte)(b ? 1 : 0)); return; case string s: - { - var bytes = Encoding.UTF8.GetBytes(s); - if (bytes.Length <= ushort.MaxValue) { U8(0x02); U16((ushort)bytes.Length); } - else { U8(0x0C); U32((uint)bytes.Length); } - _s.Write(bytes); return; - } + { + var bytes = Encoding.UTF8.GetBytes(s); + if (bytes.Length <= ushort.MaxValue) { U8(0x02); U16((ushort)bytes.Length); } + else { U8(0x0C); U32((uint)bytes.Length); } + _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: U8(0x00); F64(Convert.ToDouble(v)); 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); } EndObject(); return; case IEnumerable list: - { - var items = list.Cast().ToList(); - U8(0x0A); U32((uint)items.Count); - foreach (var item in items) WriteValue(item); - return; - } + { + var items = list.Cast().ToList(); + U8(0x0A); U32((uint)items.Count); + foreach (var item in items) WriteValue(item); + return; + } default: // POCO / anonymous object -> anonymous AMF object U8(0x03); foreach (var prop in v.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) diff --git a/Controllers/CDLController.cs b/Controllers/CDLController.cs index ad955b3..79ef4a9 100644 --- a/Controllers/CDLController.cs +++ b/Controllers/CDLController.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc; -using ReboundEmulator.Core.Rooms; using ReboundEmulator.Core; namespace ReboundEmulator.Controllers @@ -32,19 +31,11 @@ namespace ReboundEmulator.Controllers { var lkey = Sessions.Issue(name); - // more satisfying return Results.Text($"\n{lkey}", "text/xml"); } [HttpGet("/cdl/1/cdl-clientprops")] - public IActionResult CDLClientProps(string game) - { - 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 - } + 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 } } diff --git a/Controllers/ConfigAndAssetsController.cs b/Controllers/ConfigAndAssetsController.cs index 0f609b5..7d592b4 100644 --- a/Controllers/ConfigAndAssetsController.cs +++ b/Controllers/ConfigAndAssetsController.cs @@ -2,8 +2,6 @@ using ReboundEmulator.Core; using ReboundEmulator.Model; using System.IO.Compression; -using System.Net; -using System.Reflection.Metadata.Ecma335; using System.Text; namespace ReboundEmulator.Controllers diff --git a/Core/BowlingGame.cs b/Core/BowlingGame.cs index 1acb814..084275f 100644 --- a/Core/BowlingGame.cs +++ b/Core/BowlingGame.cs @@ -1,6 +1,4 @@ -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Reflection.Metadata.Ecma335; +using System.Net.Sockets; namespace ReboundEmulator.Core { diff --git a/Core/Player.cs b/Core/Player.cs index 6f86524..23c61c0 100644 --- a/Core/Player.cs +++ b/Core/Player.cs @@ -1,5 +1,4 @@ -using ReboundEmulator; -using ReboundEmulator.Core.Rooms; +using ReboundEmulator.Core.Rooms; using System.Net.Sockets; namespace ReboundEmulator.Core @@ -23,10 +22,10 @@ namespace ReboundEmulator.Core 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(); - foreach(var (k, v) in fields) msg[k] = v; + foreach (var (k, v) in fields) msg[k] = v; await SendAsync(msg); msg.TryGetValue("msg.op", out var op); @@ -37,8 +36,8 @@ namespace ReboundEmulator.Core { await _writeLock.WaitAsync(); try { await TCPProtocol.WriteMessageAsync(_stream, msg); } - catch(IOException) {} - catch(ObjectDisposedException) {} + catch (IOException) { } + catch (ObjectDisposedException) { } finally { _writeLock.Release(); } } } diff --git a/Core/Rooms/GameRoomAttribute.cs b/Core/Rooms/GameRoomAttribute.cs index d1a319e..debc7d2 100644 --- a/Core/Rooms/GameRoomAttribute.cs +++ b/Core/Rooms/GameRoomAttribute.cs @@ -1,9 +1,8 @@ namespace ReboundEmulator.Core.Rooms { [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? OpPrefix { get; init; } = opPrefix; } } \ No newline at end of file diff --git a/Core/Rooms/GameRooms/BowlingRoom.cs b/Core/Rooms/GameRooms/BowlingRoom.cs new file mode 100644 index 0000000..f8480f4 --- /dev/null +++ b/Core/Rooms/GameRooms/BowlingRoom.cs @@ -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 OnMessageAsync(Room room, Player player, string op, Dictionary msg) + { + throw new NotImplementedException(); + } + + public Task OnPlayerJoinedAsync(Room room, Player player) + { + throw new NotImplementedException(); + } + + public Task OnPlayerLeftAsync(Room room, Player player) + { + throw new NotImplementedException(); + } + } +} diff --git a/Core/Rooms/GameRooms/Lottso2Room.cs b/Core/Rooms/GameRooms/Lottso2Room.cs index 97e410d..5c593ed 100644 --- a/Core/Rooms/GameRooms/Lottso2Room.cs +++ b/Core/Rooms/GameRooms/Lottso2Room.cs @@ -1,6 +1,4 @@ -using ReboundEmulator; - -namespace ReboundEmulator.Core.Rooms.GameRooms +namespace ReboundEmulator.Core.Rooms.GameRooms { [GameRoom("lottso2")] public class Lottso2Room : IGameRoom diff --git a/Core/Rooms/GameRooms/SkeeballRoom.cs b/Core/Rooms/GameRooms/SkeeballRoom.cs index 2578337..f826c96 100644 --- a/Core/Rooms/GameRooms/SkeeballRoom.cs +++ b/Core/Rooms/GameRooms/SkeeballRoom.cs @@ -1,6 +1,6 @@ namespace ReboundEmulator.Core.Rooms.GameRooms { - [GameRoom("seaball", "sb.")] + [GameRoom("seaball")] public class SkeeballRoom : IGameRoom { public Task OnGameJoinAsync(Room room, Player player) => Task.CompletedTask; diff --git a/Core/Rooms/IGameRoom.cs b/Core/Rooms/IGameRoom.cs index a34b5a2..4a5f8ec 100644 --- a/Core/Rooms/IGameRoom.cs +++ b/Core/Rooms/IGameRoom.cs @@ -2,7 +2,6 @@ { public interface IGameRoom { - string? OpPrefix => null; Task OnPlayerJoinedAsync(Room room, Player player); Task OnPlayerLeftAsync(Room room, Player player); Task OnMessageAsync(Room room, Player player, string op, Dictionary msg); diff --git a/Core/Rooms/Room.cs b/Core/Rooms/Room.cs index 23ab372..9bd1c4a 100644 --- a/Core/Rooms/Room.cs +++ b/Core/Rooms/Room.cs @@ -1,33 +1,26 @@ -using Microsoft.AspNetCore.StaticFiles; -using System.Runtime.InteropServices.Marshalling; -using System.Reflection; -using ReboundEmulator.Core; - -namespace ReboundEmulator.Core.Rooms +namespace ReboundEmulator.Core.Rooms { public sealed class Room { public string Key { get; } public string HumanName { get; } public string GameCode { get; } - public string OpPrefix => - Game.GetType().GetCustomAttributes() - .FirstOrDefault(a => string.Equals(a.GameCode, GameCode, StringComparison.OrdinalIgnoreCase)) - ?.OpPrefix ?? GameCode + "."; public string RuleId { get; } public int SoftCapacity { get; } public int TabId { get; } public bool Live { get; set; } = true; public IGameRoom Game { get; } + public static ILogger Log { get; private set; } readonly List _players = new(); readonly object _lock = new(); 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; SoftCapacity = softCapacity; TabId = tabId; Live = live; Game = game; + Log = log; } public bool IsJoinable => Live && PlayerCount < SoftCapacity; @@ -42,7 +35,7 @@ namespace ReboundEmulator.Core.Rooms ("aepc", ""), ("acvw", ""), ("agvs", ""), ("alrc", ""), ]; - public static (string, string)[][] PlayerState(Player p) => + public static (string, string)[][] PlayerState(Player p) => [ [("msg.op", "pl.rm.stte"), ("plid", p.Id.ToString()), ("gmpf", ""), ("glpf", "..."), ("frls", "")], [("msg.op", "pl.rm.stte"), ("plid", p.Id.ToString()), ("tokb", "9999")], @@ -126,7 +119,7 @@ namespace ReboundEmulator.Core.Rooms public async Task HandleAsync(Player player, string op, Dictionary msg) { - switch(op) + switch (op) { case "gm.join": await GameJoinAsync(player); @@ -135,11 +128,11 @@ namespace ReboundEmulator.Core.Rooms msg.TryGetValue("text", out var text); await BroadcastAsync( - [("msg.op", "say"), - ("chnl", msg.GetValueOrDefault("chnl", "")), - ("plid", player.Id.ToString()), - ("scrn", player.ScreenName), - ("type", "stnd"), + [("msg.op", "say"), + ("chnl", msg.GetValueOrDefault("chnl", "")), + ("plid", player.Id.ToString()), + ("scrn", player.ScreenName), + ("type", "stnd"), ("text", text ?? "")] ); @@ -150,8 +143,8 @@ namespace ReboundEmulator.Core.Rooms return; } - if (!await Game.OnMessageAsync(this, player, op, msg)) - Console.WriteLine($"[Room {Key}] Unhandled OP '{op}' From {player.ScreenName}"); + if (!await Game.OnMessageAsync(this, player, op, msg) && Game is not ChatOnlyRoom) + Log.LogInformation("[Room {Key}] Unhandled OP '{OP}' From {ScreenName}", Key, op, player.ScreenName); } public async Task GameJoinAsync(Player player) @@ -159,32 +152,32 @@ namespace ReboundEmulator.Core.Rooms string plid = player.Id.ToString(); await player.SendAsync( - ("msg.op", "gm.join"), - ("plid", plid), - ("snme", player.ScreenName), - ("gmid", "0"), - ("scrn", player.ScreenName), + ("msg.op", "gm.join"), + ("plid", plid), + ("snme", player.ScreenName), + ("gmid", "0"), + ("scrn", player.ScreenName), ("join", "join") ); await player.SendAsync( - ("msg.op", "gm.lnch"), + ("msg.op", "gm.lnch"), ("gmid", "0") ); await player.SendAsync( - ("msg.op", "tokb"), + ("msg.op", "tokb"), ("tokb", "9999") ); await player.SendAsync( - ("msg.op", "tick"), - ("scrn", player.ScreenName), + ("msg.op", "tick"), + ("scrn", player.ScreenName), ("text", "Welcome to Not Pogo!") ); await player.SendAsync( - ("msg.op", "cnfg"), + ("msg.op", "cnfg"), ("impd", "40") ); diff --git a/Core/Rooms/RoomManager.cs b/Core/Rooms/RoomManager.cs index 1190c1e..a4fb675 100644 --- a/Core/Rooms/RoomManager.cs +++ b/Core/Rooms/RoomManager.cs @@ -12,14 +12,14 @@ namespace ReboundEmulator.Core.Rooms 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 games = JsonSerializer.Deserialize>(File.ReadAllText(path), opts)!; foreach (var (gameCode, rooms) in games) 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> BuildFactories() @@ -54,11 +54,11 @@ namespace ReboundEmulator.Core.Rooms return gameRoom; } - public static Room Define(string key, string humanName, string gameCode, string ruleId, bool isLive, int softCapacity = 50, int tabId = 1) => - Rooms.GetOrAdd(key, _ => new Room(key, humanName, gameCode, ruleId, softCapacity, tabId, isLive, CreateGame(gameCode, ruleId))); + 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), log)); - public static Room GetOrCreate(string key, string gameCode, string ruleId = "") => - Rooms.GetOrAdd(key, _ => new Room(key, key, gameCode, ruleId, 50, 1, false, CreateGame(gameCode, 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), log)); public static Room? Find(string key) => Rooms.TryGetValue(key, out var r) ? r : null; public static IEnumerable ForGame(string gameCode) => Rooms.Values.Where(r => r.GameCode == gameCode); diff --git a/Core/Sessions.cs b/Core/Sessions.cs index 46dfa81..06da2cb 100644 --- a/Core/Sessions.cs +++ b/Core/Sessions.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.StaticFiles; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; diff --git a/FlashGateway.cs b/FlashGateway.cs index 5a57db2..b91520b 100644 --- a/FlashGateway.cs +++ b/FlashGateway.cs @@ -1,7 +1,7 @@ // /flashservices/gateway endpoint for roomsel.swf (com.pogo.roomsel.RoomTabs). // Register in Program.cs: app.MapFlashGateway(); -using ReboundEmulator.Core.Rooms; using ReboundEmulator.Amf; +using ReboundEmulator.Core.Rooms; namespace ReboundEmulator; diff --git a/Program.cs b/Program.cs index ca519c1..bb36d64 100644 --- a/Program.cs +++ b/Program.cs @@ -53,7 +53,7 @@ namespace ReboundEmulator app.MapFlashGateway(); // 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>()!); app.Run(); } diff --git a/TCPGameServer.cs b/TCPGameServer.cs index 7e6daa8..68a8ee4 100644 --- a/TCPGameServer.cs +++ b/TCPGameServer.cs @@ -1,7 +1,7 @@ -using System.Net.Sockets; -using System.Net; -using ReboundEmulator.Core; +using ReboundEmulator.Core; using ReboundEmulator.Core.Rooms; +using System.Net; +using System.Net.Sockets; namespace ReboundEmulator { @@ -17,8 +17,6 @@ namespace ReboundEmulator public static List Clients { get; } = []; public static BowlingGame? SingleplayerBowlingGame; - static readonly HashSet RoomOps = ["say", "rm.left", "disc", "gm.join", "ext.over"]; - protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _tcpPort = _configuration.GetValue("GameServerPort"); @@ -55,9 +53,9 @@ namespace ReboundEmulator catch (EndOfStreamException) { break; } if (!received.TryGetValue("msg.op", out var op)) continue; - _logger.LogInformation("[plid {Plid}] <- {Op} {Fields}", - player.Id, - op, + _logger.LogInformation("[plid {Plid}] <- {Op} {Fields}", + player.Id, + op, string.Join(" ", received.Where(kv => kv.Key != "msg.op").Select(kv => $"{kv.Key}='{kv.Value}'"))); @@ -73,13 +71,14 @@ namespace ReboundEmulator { 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); - } } - } 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); } @@ -89,7 +88,7 @@ namespace ReboundEmulator { 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); } } } @@ -111,10 +110,10 @@ namespace ReboundEmulator 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); + var room = !string.IsNullOrEmpty(rkey) ? + RoomManager.Find(rkey) ?? + RoomManager.GetOrCreate(rkey, game, _logger) : + RoomManager.GetOrCreate($"{(game == "" ? "default" : game)}_lobby", game, _logger); string plid = player.Id.ToString(); @@ -145,7 +144,7 @@ namespace ReboundEmulator public override Task StopAsync(CancellationToken cancellationToken) { - if(!cancellationToken.IsCancellationRequested) + if (!cancellationToken.IsCancellationRequested) { Clients.Clear(); Listener?.Stop(); diff --git a/TCPProtocol.cs b/TCPProtocol.cs index fd0c4de..440f93d 100644 --- a/TCPProtocol.cs +++ b/TCPProtocol.cs @@ -1,6 +1,4 @@ -using Microsoft.AspNetCore.Routing.Constraints; -using Microsoft.AspNetCore.Server.HttpSys; -using System.Net.Sockets; +using System.Net.Sockets; using System.Text; namespace ReboundEmulator @@ -22,7 +20,7 @@ namespace ReboundEmulator var props = new Dictionary(); using var ms = new MemoryStream(payload); - while(ms.Position < ms.Length) + while (ms.Position < ms.Length) { string key = ReadJavaUtf(ms); string value = ReadJavaUtf(ms); @@ -40,7 +38,7 @@ namespace ReboundEmulator public static async Task WriteMessageAsync(NetworkStream stream, IEnumerable> props) { using var payload = new MemoryStream(); - foreach(var kv in props) + foreach (var kv in props) { WriteJavaUtf(payload, kv.Key); WriteJavaUtf(payload, kv.Value); @@ -141,7 +139,7 @@ namespace ReboundEmulator private static void WriteJavaUtf(Stream s, string str) { using var buf = new MemoryStream(); - foreach(char ch in str) + foreach (char ch in str) { if (ch >= 0x001 && ch <= 0x007F) buf.WriteByte((byte)ch);