Implement Multiplayer Support
Implement Custom Screen Name Support
This commit is contained in:
parent
7e58c49ce6
commit
2631d3199c
@ -1,4 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NotPogo.Core;
|
||||
using NotPogo.Core.Rooms;
|
||||
|
||||
namespace NotPogo.Controllers
|
||||
{
|
||||
@ -8,18 +10,30 @@ namespace NotPogo.Controllers
|
||||
[Route("/cdl/cdl-info.jsp")]
|
||||
public IResult CDLInfo(string lkey)
|
||||
{
|
||||
// this is just to satisfy download clients
|
||||
if (string.IsNullOrEmpty(lkey))
|
||||
return Results.Empty;
|
||||
else
|
||||
return Results.Text("error none screenname 'NotPogoUser' site 'pogo' tokens 9999 enti 'oava 1 obad 1'");
|
||||
var acct = Sessions.Find(lkey);
|
||||
if (acct is null)
|
||||
return Results.Text("error notloggedin", "text/plain"); // see note below
|
||||
|
||||
var enti = $"oava {(acct.OwnsPremiumAvatar ? 1 : 0)} obad {(acct.OwnsBadges ? 1 : 0)}";
|
||||
var body = string.Join(" ",
|
||||
"error", "none",
|
||||
"screenname", Q(acct.ScreenName),
|
||||
"site", Q(acct.Site),
|
||||
"tokens", acct.Tokens.ToString(),
|
||||
"enti", Q(enti),
|
||||
"avatar", Q(acct.Avatar));
|
||||
return Results.Text(body, "text/plain");
|
||||
}
|
||||
|
||||
static string Q(string s) => "'" + s.Replace("\\", "\\\\").Replace("'", "\\'") + "'";
|
||||
|
||||
[HttpGet("/cdl/1/login")]
|
||||
public IResult CDLLogin()
|
||||
public IResult CDLLogin(string name)
|
||||
{
|
||||
var lkey = Sessions.Issue(name);
|
||||
|
||||
// more satisfying
|
||||
return Results.Text($"<LoginResponse error=\"none\">\n<LoginSessionKey>{Guid.NewGuid()}</LoginSessionKey></LoginResponse>", "text/xml");
|
||||
return Results.Text($"<LoginResponse error=\"none\">\n<LoginSessionKey>{lkey}</LoginSessionKey></LoginResponse>", "text/xml");
|
||||
}
|
||||
|
||||
[HttpGet("/cdl/1/cdl-clientprops")]
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NotPogo.Core;
|
||||
using NotPogo.Model;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
@ -27,6 +28,7 @@ namespace NotPogo.Controllers
|
||||
string site,
|
||||
string locale,
|
||||
string hostedGameClass,
|
||||
string scrn = null!, // user will get a username assigned if screenName is not set
|
||||
bool isSingleApplet = true)
|
||||
{
|
||||
string baseUrl = $"{Request.Scheme}://{Request.Host.Value}";
|
||||
@ -40,6 +42,8 @@ namespace NotPogo.Controllers
|
||||
// stay one 776-wide <applet> tag with no second one alongside it.
|
||||
int gameWidth = isSingleApplet ? 776 : 564;
|
||||
|
||||
string lkey = Sessions.Issue(scrn); // since there isn't a database
|
||||
|
||||
var gameApplet = new AppletDefinition
|
||||
{
|
||||
Name = "game",
|
||||
@ -59,10 +63,10 @@ namespace NotPogo.Controllers
|
||||
{ "site", site },
|
||||
{ "rkey", "" },
|
||||
{ "unid", "" },
|
||||
{ "scrn", "NotPogoUser" },
|
||||
{ "scrn", scrn! },
|
||||
{ "sourceid", "" },
|
||||
{ "launchPage", "" },
|
||||
{ "lkey", "" },
|
||||
{ "lkey", lkey },
|
||||
{ "cnid", "" },
|
||||
{ "isSingleApplet", isSingleApplet ? "true" : "false" },
|
||||
{ "browserInfo", "" },
|
||||
|
||||
39
Core/Player.cs
Normal file
39
Core/Player.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using NotPogo.Core.Rooms;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace NotPogo.Core
|
||||
{
|
||||
public sealed class Player
|
||||
{
|
||||
static int _nextId;
|
||||
|
||||
public int Id { get; } = Interlocked.Increment(ref _nextId);
|
||||
public Account? Account { get; set; }
|
||||
public string ScreenName { get; set; } = "Guest";
|
||||
public string ProfileId { get; set; } = "100000000000";
|
||||
public string LoginKey { get; set; } = "";
|
||||
public Room? Room { get; set; }
|
||||
public DateTime? LastSeen { get; set; }
|
||||
|
||||
readonly NetworkStream _stream;
|
||||
readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
|
||||
public Player(NetworkStream stream) => _stream = stream;
|
||||
|
||||
public async Task SendAsync(params(string key, string value)[] fields)
|
||||
{
|
||||
var msg = new Dictionary<string, string>();
|
||||
foreach(var (k, v) in fields) msg[k] = v;
|
||||
await SendAsync(msg);
|
||||
}
|
||||
|
||||
public async Task SendAsync(Dictionary<string, string> msg)
|
||||
{
|
||||
await _writeLock.WaitAsync();
|
||||
try { await TCPProtocol.WriteMessageAsync(_stream, msg); }
|
||||
catch(IOException) {}
|
||||
catch(ObjectDisposedException) {}
|
||||
finally { _writeLock.Release(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Core/Rooms/GameRoomAttribute.cs
Normal file
8
Core/Rooms/GameRoomAttribute.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace NotPogo.Core.Rooms
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
public sealed class GameRoomAttribute(string gameCode) : Attribute
|
||||
{
|
||||
public string GameCode { get; } = gameCode;
|
||||
}
|
||||
}
|
||||
23
Core/Rooms/GameRooms/WordRiotRoom.cs
Normal file
23
Core/Rooms/GameRooms/WordRiotRoom.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace NotPogo.Core.Rooms.GameRooms
|
||||
{
|
||||
[GameRoom("wordriot_cdl")]
|
||||
public class WordRiotRoom(string ruleId) : IGameRoom
|
||||
{
|
||||
public Task OnGameJoinAsync(Room room, Player player) => Task.CompletedTask;
|
||||
|
||||
public Task<bool> OnMessageAsync(Room room, Player player, string op, Dictionary<string, string> msg) => Task.FromResult(true);
|
||||
|
||||
public async Task OnPlayerJoinedAsync(Room room, Player player)
|
||||
{
|
||||
await player.SendAsync(
|
||||
("msg.op", "say"),
|
||||
("chnl", "serv"),
|
||||
("scrn", ""),
|
||||
("type", "stnd"),
|
||||
("text", "Welcome To Word Riot! Please Wait For Players To Join")
|
||||
);
|
||||
}
|
||||
|
||||
public Task OnPlayerLeftAsync(Room room, Player player) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
18
Core/Rooms/IGameRoom.cs
Normal file
18
Core/Rooms/IGameRoom.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace NotPogo.Core.Rooms
|
||||
{
|
||||
public interface IGameRoom
|
||||
{
|
||||
Task OnPlayerJoinedAsync(Room room, Player player);
|
||||
Task OnPlayerLeftAsync(Room room, Player player);
|
||||
Task<bool> OnMessageAsync(Room room, Player player, string op, Dictionary<string, string> msg);
|
||||
Task OnGameJoinAsync(Room room, Player player);
|
||||
}
|
||||
|
||||
public sealed class ChatOnlyRoom : IGameRoom
|
||||
{
|
||||
public Task<bool> OnMessageAsync(Room room, Player player, string op, Dictionary<string, string> msg) => Task.FromResult(false);
|
||||
public Task OnPlayerJoinedAsync(Room room, Player player) => Task.CompletedTask;
|
||||
public Task OnPlayerLeftAsync(Room room, Player player) => Task.CompletedTask;
|
||||
public Task OnGameJoinAsync(Room room, Player player) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
191
Core/Rooms/Room.cs
Normal file
191
Core/Rooms/Room.cs
Normal file
@ -0,0 +1,191 @@
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
|
||||
namespace NotPogo.Core.Rooms
|
||||
{
|
||||
public sealed class Room
|
||||
{
|
||||
public string Key { get; }
|
||||
public string HumanName { get; }
|
||||
public string GameCode { get; }
|
||||
public string RuleId { get; }
|
||||
public int SoftCapacity { get; }
|
||||
public int TabId { get; }
|
||||
public bool Live { get; set; } = true;
|
||||
public IGameRoom Game { get; }
|
||||
|
||||
readonly List<Player> _players = new();
|
||||
readonly object _lock = new();
|
||||
|
||||
public Room(string key, string humanName, string gameCode, string ruleId,
|
||||
int softCapacity, int tabId, bool live, IGameRoom game)
|
||||
{
|
||||
Key = key; HumanName = humanName; GameCode = gameCode; RuleId = ruleId;
|
||||
SoftCapacity = softCapacity; TabId = tabId; Live = live; Game = game;
|
||||
}
|
||||
|
||||
public bool IsJoinable => Live && PlayerCount < SoftCapacity;
|
||||
|
||||
public int PlayerCount { get { lock (_lock) return _players.Count; } }
|
||||
public Player[] Snapshot() { lock (_lock) { return [.. _players]; } }
|
||||
|
||||
public static (string, string)[] PresenceFields(Player p) =>
|
||||
[
|
||||
("msg.op", "rm.join"), ("scrn", p.ScreenName), ("snme", p.ScreenName), ("pfid", p.ProfileId),
|
||||
("plid", p.Id.ToString()), ("entl", "ictsebaulovfFmMrRG"), ("priv", "0"),
|
||||
("aepc", ""), ("acvw", ""), ("agvs", ""), ("alrc", ""),
|
||||
];
|
||||
|
||||
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")],
|
||||
[("msg.op", "pl.rm.stte"), ("plid", p.Id.ToString()), ("avtr", ""), ("icon", "avatarminighost")]
|
||||
];
|
||||
|
||||
public async Task JoinAsync(Player player)
|
||||
{
|
||||
Player[] others;
|
||||
lock (_lock)
|
||||
{
|
||||
others = [.. _players];
|
||||
_players.Add(player);
|
||||
player.Room = this;
|
||||
}
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "load"),
|
||||
("rmst", "start")
|
||||
);
|
||||
|
||||
await player.SendAsync(PresenceFields(player));
|
||||
foreach (var other in others)
|
||||
await player.SendAsync(PresenceFields(other));
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "gm.crea"),
|
||||
("launch", "y"),
|
||||
("live", "0"),
|
||||
("port", "0"),
|
||||
("iali", "86400000"),
|
||||
("gins", HumanName),
|
||||
("dead", "0"),
|
||||
("iaid", "15000"),
|
||||
("shut", ""),
|
||||
("gmid", "0")
|
||||
);
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "load"),
|
||||
("rmst", "end")
|
||||
);
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "say"),
|
||||
("chnl", "serv"),
|
||||
("scrn", ""),
|
||||
("type", "stnd"),
|
||||
("text", "NotPogo Is Alpha Software! Some Things May Not Function Correctly.")
|
||||
);
|
||||
|
||||
foreach (var other in others)
|
||||
foreach (var m in PlayerState(other)) await player.SendAsync(m);
|
||||
await BroadcastAsync(PresenceFields(player), except: player);
|
||||
foreach (var m in PlayerState(player)) await BroadcastAsync(m);
|
||||
|
||||
await Game.OnPlayerJoinedAsync(this, player);
|
||||
}
|
||||
|
||||
public async Task LeaveAsync(Player player)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_players.Remove(player)) return;
|
||||
player.Room = null;
|
||||
}
|
||||
|
||||
await BroadcastAsync([("msg.op", "rm.left"), ("plid", player.Id.ToString()), ("scrn", player.ScreenName)]);
|
||||
await Game.OnPlayerLeftAsync(this, player);
|
||||
}
|
||||
|
||||
public Task BroadcastAsync((string, string)[] values, Player? except = null)
|
||||
{
|
||||
var msg = new Dictionary<string, string>();
|
||||
foreach (var (k, v) in values) msg[k] = v;
|
||||
return BroadcastAsync(msg, except);
|
||||
}
|
||||
|
||||
public Task BroadcastAsync(Dictionary<string, string> msg, Player? except = null) =>
|
||||
Task.WhenAll(Snapshot().Where(p => p != except).Select(p => p.SendAsync(msg)));
|
||||
|
||||
public async Task HandleAsync(Player player, string op, Dictionary<string, string> msg)
|
||||
{
|
||||
switch(op)
|
||||
{
|
||||
case "gm.join":
|
||||
await GameJoinAsync(player);
|
||||
return;
|
||||
case "say":
|
||||
msg.TryGetValue("text", out var text);
|
||||
|
||||
await BroadcastAsync(
|
||||
[("msg.op", "say"),
|
||||
("chnl", msg.GetValueOrDefault("chnl", "")),
|
||||
("plid", player.Id.ToString()),
|
||||
("scrn", player.ScreenName),
|
||||
("type", "stnd"),
|
||||
("text", text ?? "")]
|
||||
);
|
||||
|
||||
return;
|
||||
case "rm.left":
|
||||
case "disc":
|
||||
await LeaveAsync(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await Game.OnMessageAsync(this, player, op, msg))
|
||||
Console.WriteLine($"[Room {Key}] Unhandled OP '{op}' From {player.ScreenName}");
|
||||
}
|
||||
|
||||
public async Task GameJoinAsync(Player player)
|
||||
{
|
||||
string plid = player.Id.ToString();
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "gm.join"),
|
||||
("plid", plid),
|
||||
("snme", player.ScreenName),
|
||||
("gmid", "0"),
|
||||
("scrn", player.ScreenName),
|
||||
("join", "join")
|
||||
);
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "gm.lnch"),
|
||||
("gmid", "0")
|
||||
);
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "tokb"),
|
||||
("tokb", "9999")
|
||||
);
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "tick"),
|
||||
("scrn", player.ScreenName),
|
||||
("text", "Welcome to Not Pogo!")
|
||||
);
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "cnfg"),
|
||||
("impd", "40")
|
||||
);
|
||||
|
||||
await player.SendAsync(
|
||||
("msg.op", "ext.pref"),
|
||||
("pref", "")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Core/Rooms/RoomManager.cs
Normal file
47
Core/Rooms/RoomManager.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using NotPogo.Core.Rooms.GameRooms;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
|
||||
namespace NotPogo.Core.Rooms
|
||||
{
|
||||
public static class RoomManager
|
||||
{
|
||||
static readonly ConcurrentDictionary<string, Room> Rooms = new();
|
||||
|
||||
static readonly Dictionary<string, Func<string, IGameRoom>> Factories = BuildFactories();
|
||||
|
||||
static Dictionary<string, Func<string, IGameRoom>> BuildFactories()
|
||||
{
|
||||
var map = new Dictionary<string, Func<string, IGameRoom>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var type in typeof(IGameRoom).Assembly.GetTypes()
|
||||
.Where(t => typeof(IGameRoom).IsAssignableFrom(t) && !t.IsAbstract))
|
||||
{
|
||||
foreach (var attr in type.GetCustomAttributes<GameRoomAttribute>())
|
||||
{
|
||||
// constructor convention: (string ruleId) if the game has modes, otherwise parameterless
|
||||
Func<string, IGameRoom> factory =
|
||||
type.GetConstructor([typeof(string)]) is not null ? rule => (IGameRoom)Activator.CreateInstance(type, rule)!
|
||||
: type.GetConstructor(Type.EmptyTypes) is not null ? _ => (IGameRoom)Activator.CreateInstance(type)!
|
||||
: throw new InvalidOperationException($"{type.Name} needs a (string ruleId) or parameterless constructor");
|
||||
|
||||
if (!map.TryAdd(attr.GameCode, factory))
|
||||
throw new InvalidOperationException($"Game code '{attr.GameCode}' is claimed by more than one room class");
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
static IGameRoom CreateGame(string gameCode, string ruleId) =>
|
||||
Factories.TryGetValue(gameCode, out var create) ? create(ruleId) : new ChatOnlyRoom();
|
||||
|
||||
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 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? 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);
|
||||
}
|
||||
}
|
||||
57
Core/Sessions.cs
Normal file
57
Core/Sessions.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace NotPogo.Core
|
||||
{
|
||||
public sealed class Account
|
||||
{
|
||||
public required string ScreenName { get; init; }
|
||||
public string ProfileId { get; init; } = "1000000000"; // pfid
|
||||
public string Site { get; init; } = "pogo";
|
||||
public int Tokens { get; set; } = 9999;
|
||||
public string Avatar { get; set; } = "";
|
||||
public bool OwnsPremiumAvatar { get; set; } = true; // enti.oava
|
||||
public bool OwnsBadges { get; set; } = true; // enti.obad
|
||||
}
|
||||
|
||||
public static class Sessions
|
||||
{
|
||||
static byte[] _secret = []; // set once at startup
|
||||
static readonly ConcurrentDictionary<string, Account> Live = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static void Init(string secret) => _secret = Encoding.UTF8.GetBytes(secret);
|
||||
|
||||
// lkey = base64url(screenName) + "." + base64url(HMAC(screenName))
|
||||
public static string Issue(string screenName)
|
||||
{
|
||||
var name = Encoding.UTF8.GetBytes(screenName);
|
||||
return B64(name) + "." + B64(HMACSHA256.HashData(_secret, name));
|
||||
}
|
||||
|
||||
public static Account? Find(string? lkey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(lkey)) return null;
|
||||
var parts = lkey.Split('.');
|
||||
if (parts.Length != 2) return null;
|
||||
try
|
||||
{
|
||||
var name = UnB64(parts[0]);
|
||||
if (!CryptographicOperations.FixedTimeEquals(UnB64(parts[1]), HMACSHA256.HashData(_secret, name)))
|
||||
return null; // forged or from another secret
|
||||
var screenName = Encoding.UTF8.GetString(name);
|
||||
// in-memory state for this run; recreated with defaults after a restart
|
||||
return Live.GetOrAdd(screenName, n => new Account { ScreenName = n });
|
||||
}
|
||||
catch (FormatException) { return null; }
|
||||
}
|
||||
|
||||
static string B64(byte[] b) => Convert.ToBase64String(b).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
static byte[] UnB64(string s)
|
||||
{
|
||||
s = s.Replace('-', '+').Replace('_', '/');
|
||||
return Convert.FromBase64String(s + new string('=', (4 - s.Length % 4) % 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,21 @@
|
||||
using NotPogo.Amf;
|
||||
|
||||
using NotPogo;
|
||||
using NotPogo.Core.Rooms;
|
||||
|
||||
namespace NotPogo;
|
||||
|
||||
public static class FlashGateway
|
||||
{
|
||||
static string _gameHost = "127.0.0.1";
|
||||
static int _gamePort = 11200;
|
||||
|
||||
static string[] RuleOrder(string gameCode) =>
|
||||
[.. RoomManager.ForGame(gameCode) // all Word Riot rooms
|
||||
.Select(r => r.RuleId) // "classic", "rally", "advanced", "classic", ...
|
||||
.Distinct() // each mode once
|
||||
.OrderBy(r => r, StringComparer.Ordinal)];
|
||||
|
||||
// "Service.method" -> handler(args). Args arrive as AMF0 values: strings, doubles, etc.
|
||||
static readonly Dictionary<string, Func<List<object?>, object?>> Handlers = new()
|
||||
{
|
||||
@ -15,46 +25,66 @@ public static class FlashGateway
|
||||
["com.pogo.flash.FlashRoomsFeed.generateFeed"] = args =>
|
||||
{
|
||||
string gameCode = Str(args, 0), siteCode = Str(args, 1);
|
||||
var rooms = RoomManager.ForGame(gameCode).ToArray();
|
||||
return new
|
||||
{
|
||||
errorMsg = (string?)null, // non-null = SWF shows the error
|
||||
tabs = new[] { new { tabId = 1, index = 0 } },
|
||||
rooms = new[]
|
||||
tabs = rooms.Select(r => r.TabId).Distinct()
|
||||
.OrderBy(t => t)
|
||||
.Select((tabId, i) => new { tabId, index = i })
|
||||
.ToArray(),
|
||||
rooms = rooms.Select(r => new
|
||||
{
|
||||
// shown only if tabId == selected tab AND ruleId == gRuleId (omit ruleId if noruleid)
|
||||
// clickable only if numPeople < softCapacity && live
|
||||
new { key = "wr_classic_1", humanName = "NotPogo WR Classic", numPeople = 0, softCapacity = 50, live = true, tabId = 1, ruleId = "classic" },
|
||||
new { key = "wr_rally_1", humanName = "NotPogo WR Rally", numPeople = 0, softCapacity = 50, live = true, tabId = 1, ruleId = "rally" },
|
||||
new { key = "wr_advanced_1", humanName = "NotPogo WR Expert", numPeople = 0, softCapacity = 50, live = true, tabId = 1, ruleId = "advanced" },
|
||||
},
|
||||
key = r.Key,
|
||||
humanName = r.HumanName,
|
||||
ruleId = r.RuleId,
|
||||
tabId = r.TabId,
|
||||
numPeople = r.PlayerCount,
|
||||
softCapacity = r.SoftCapacity,
|
||||
live = r.Live,
|
||||
}).ToArray(),
|
||||
rules = Array.Empty<object>(), // stored, never read by the SWF
|
||||
ruleTabs = new[]
|
||||
{
|
||||
new { id = "classic", index = 0 },
|
||||
new { id = "rally", index = 1 },
|
||||
new { id = "advanced", index = 2 },
|
||||
}, // [{ id = <ruleId>, index = n }] -> ruleIdx for Play Now
|
||||
ruleTabs = RuleOrder(gameCode).Select((id, i) => new { id, index = i })
|
||||
};
|
||||
},
|
||||
|
||||
// RoomTabs.goRoom -> connectRoom(roomKey, gameCode, siteCode, lkey)
|
||||
["com.pogo.flash.FlashRoomConnect.connectRoom"] = args =>
|
||||
RoomInfo(Str(args, 0)),
|
||||
RoomInfo(RoomManager.Find(Str(args, 0))),
|
||||
|
||||
// RoomTabs.playNow -> playNow(gameCode, siteCode, lkey, tabIdx, ruleIdx)
|
||||
["com.pogo.flash.FlashRoomConnect.playNow"] = args =>
|
||||
RoomInfo("wr_room_1"), // pick a room server-side
|
||||
{
|
||||
string gameCode = Str(args, 0);
|
||||
int ruleIdx = args.Count > 4 && args[4] is double d ? (int)d : -1;
|
||||
var rules = RuleOrder(gameCode);
|
||||
string? ruleId = ruleIdx >= 0 && ruleIdx < rules.Length ? rules[ruleIdx] : null;
|
||||
|
||||
var pick = RoomManager.ForGame(gameCode)
|
||||
.Where(r => r.IsJoinable && (ruleId is null || r.RuleId == ruleId))
|
||||
.OrderByDescending(r => r.PlayerCount) // fill busy rooms first
|
||||
.FirstOrDefault();
|
||||
return RoomInfo(pick, notFound: "No rooms are available right now.");
|
||||
}
|
||||
};
|
||||
|
||||
// roomInfo becomes FSCommand goRoom "humanName '..' key '..' host '..' port '..'"; the exe connects TCP to host:port
|
||||
static object RoomInfo(string roomKey) => new
|
||||
static object RoomInfo(Room? room, string notFound = "That room no longer exists.")
|
||||
{
|
||||
errorMsg = (string?)null, // null or "NO ERROR" = success
|
||||
roomInfo = new { humanName = "NotPogo Room", key = roomKey, host = "downld.pogo.com", port = 11200 },
|
||||
};
|
||||
if (room is null) return new { errorMsg = notFound };
|
||||
if (!room.IsJoinable) return new { errorMsg = "That room is full." };
|
||||
return new
|
||||
{
|
||||
errorMsg = (string?)null,
|
||||
roomInfo = new { humanName = room.HumanName, key = room.Key, host = _gameHost, port = _gamePort },
|
||||
};
|
||||
}
|
||||
|
||||
public static void MapFlashGateway(this IEndpointRouteBuilder app)
|
||||
public static void MapFlashGateway(this WebApplication app)
|
||||
{
|
||||
_gameHost = app.Configuration.GetValue("GameServerHost", "127.0.0.1")!;
|
||||
_gamePort = app.Configuration.GetValue("GameServerPort", 11200);
|
||||
|
||||
app.MapPost("/flashservices/gateway", async (HttpContext ctx, ILoggerFactory lf) =>
|
||||
{
|
||||
var log = lf.CreateLogger("FlashGateway");
|
||||
|
||||
10
Program.cs
10
Program.cs
@ -1,5 +1,7 @@
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using NotPogo.Core;
|
||||
using NotPogo.Core.Rooms;
|
||||
|
||||
namespace NotPogo
|
||||
{
|
||||
@ -7,8 +9,11 @@ namespace NotPogo
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
Sessions.Init(builder.Configuration["SessionSecret"] ?? "change-me");
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllersWithViews();
|
||||
@ -47,6 +52,11 @@ namespace NotPogo
|
||||
app.MapControllers();
|
||||
app.MapFlashGateway();
|
||||
|
||||
// Define Rooms For Word Riot Deluxe
|
||||
RoomManager.Define("npwr_classic_1", "NotPogo Classic Room 1", "wordriot_cdl", "classic", true);
|
||||
RoomManager.Define("npwr_rally_1", "NotPogo Rally Room 1", "wordriot_cdl", "rally", true);
|
||||
RoomManager.Define("npwr_advanced_1", "NotPogo Advanced Room 1", "wordriot_cdl", "advanced", true);
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
|
||||
130
TCPGameServer.cs
130
TCPGameServer.cs
@ -1,6 +1,7 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Net;
|
||||
using NotPogo.Core;
|
||||
using NotPogo.Core.Rooms;
|
||||
|
||||
namespace NotPogo
|
||||
{
|
||||
@ -16,9 +17,12 @@ namespace NotPogo
|
||||
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);
|
||||
@ -33,9 +37,12 @@ namespace NotPogo
|
||||
|
||||
private async Task HandleClientAsync(TcpClient client)
|
||||
{
|
||||
Player? player = null;
|
||||
|
||||
using (client)
|
||||
await using (var stream = client.GetStream())
|
||||
{
|
||||
player = new(stream);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
@ -47,64 +54,30 @@ namespace NotPogo
|
||||
}
|
||||
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;
|
||||
_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);
|
||||
|
||||
// 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", "ictsebaulovfFmMrRG"),
|
||||
("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");
|
||||
// handle logn
|
||||
await HandleLoginAsync(player, received);
|
||||
}
|
||||
else if (op == "gm.join") // Room Join Request
|
||||
else if (op == "ping")
|
||||
{
|
||||
// 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", ""));
|
||||
|
||||
// send lottso config, more than likely will be ignored by other clients
|
||||
// this is mostly to get lottso express to work
|
||||
await SendAsync(stream,
|
||||
("msg.op", "lb.cfg"),
|
||||
("lb.cmn.rack.ballnum", "6"),
|
||||
("lb.cmn.ball.range", "1,75"),
|
||||
("lb.cmn.ball.starting", "30"),
|
||||
("lb.cmn.ball.extra", "1"),
|
||||
("lb.cmn.jackpot.goal", "50"),
|
||||
("lb.cmn.score.to.token", "100"),
|
||||
("lb.cmn.rank.points", "10"),
|
||||
("lb.cmn.scoring.goal", "100,200,300,400,500"),
|
||||
("lb.cmn.cheat", "false"),
|
||||
("rank.maxOrd", "10")
|
||||
);
|
||||
|
||||
_logger.LogInformation("Sent 'gm.join' Response Sequence (Launches Game)");
|
||||
player.LastSeen = DateTime.UtcNow;
|
||||
}
|
||||
else if (op == "ext.over")
|
||||
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
|
||||
|
||||
@ -126,24 +99,65 @@ namespace NotPogo
|
||||
);
|
||||
|
||||
_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"); }
|
||||
} 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>();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user