diff --git a/TableturfBattleClient/src/Pages/PreGamePage.ts b/TableturfBattleClient/src/Pages/PreGamePage.ts index 8c0da69..ec91bea 100644 --- a/TableturfBattleClient/src/Pages/PreGamePage.ts +++ b/TableturfBattleClient/src/Pages/PreGamePage.ts @@ -47,7 +47,9 @@ preGameForm.addEventListener('submit', e => { setGameUrl(response.gameID); getGameInfo(response.gameID, 0); - } else + } else if (request.status == 503) + communicationError('The server is temporarily locked for an update. Please try again soon.', true, () => setLoadingMessage(null)); + else communicationError('Unable to create the room.', true, () => setLoadingMessage(null)); }); request.addEventListener('error', () => { diff --git a/TableturfBattleServer/Game.cs b/TableturfBattleServer/Game.cs index eb74ec5..03a152f 100644 --- a/TableturfBattleServer/Game.cs +++ b/TableturfBattleServer/Game.cs @@ -24,6 +24,9 @@ public class Game { [JsonProperty("startSpaces")] public Point[]? StartSpaces; + [JsonIgnore] + internal DateTime AbandonedSince = DateTime.UtcNow; + public Game(int maxPlayers) => this.MaxPlayers = maxPlayers; public bool TryAddPlayer(Player player, out int playerIndex, out Error error) { diff --git a/TableturfBattleServer/Program.cs b/TableturfBattleServer/Program.cs index 3d296de..df06fc9 100644 --- a/TableturfBattleServer/Program.cs +++ b/TableturfBattleServer/Program.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Net; using System.Reflection; using System.Text; @@ -22,7 +23,12 @@ internal class Program { internal static HttpServer? httpServer; internal static Dictionary games = new(); + internal static Dictionary inactiveGames = new(); internal static readonly Timer timer = new(500); + private static bool lockdown; + + private const int InactiveGameLimit = 1000; + private static readonly TimeSpan InactiveGameTimeout = TimeSpan.FromMinutes(5); private static string? GetClientRootPath() { var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -49,7 +55,21 @@ internal class Program { else Console.WriteLine($"Client files were not found."); - Thread.Sleep(Timeout.Infinite); + while (true) { + var s = Console.ReadLine(); + if (s == null) + Thread.Sleep(Timeout.Infinite); + else { + s = s.Trim().ToLower(); + if (s == "update") { + if (games.Count == 0) + Environment.Exit(2); + lockdown = true; + Console.WriteLine("Locking server for update."); + } + } + + } } private static void Timer_Elapsed(object? sender, ElapsedEventArgs e) { @@ -57,8 +77,20 @@ internal class Program { foreach (var (id, game) in games) { lock (game.Players) { game.Tick(); + if (DateTime.UtcNow - game.AbandonedSince >= InactiveGameTimeout) { + games.Remove(id); + inactiveGames.Add(id, game); + Console.WriteLine($"{games.Count} games active; {inactiveGames.Count} inactive"); + if (lockdown && games.Count == 0) + Environment.Exit(2); + } } } + if (inactiveGames.Count >= InactiveGameLimit) { + foreach (var (k, _) in inactiveGames.Select(e => (e.Key, e.Value.AbandonedSince)).OrderBy(e => e.AbandonedSince).Take(InactiveGameLimit / 2)) + inactiveGames.Remove(k); + Console.WriteLine($"{games.Count} games active; {inactiveGames.Count} inactive"); + } } } @@ -85,6 +117,8 @@ internal class Program { if (e.Request.HttpMethod != "POST") { e.Response.AddHeader("Allow", "POST"); SetErrorResponse(e.Response, new(HttpStatusCode.MethodNotAllowed, "MethodNotAllowed", "Invalid request method for this endpoint.")); + } else if (lockdown) { + SetErrorResponse(e.Response, new(HttpStatusCode.ServiceUnavailable, "ServerLocked", "The server is temporarily locked for an update. Please try again soon.")); } else if (e.Request.ContentLength64 >= 65536) { e.Response.StatusCode = (int) HttpStatusCode.RequestEntityTooLarge; } else { @@ -112,8 +146,10 @@ internal class Program { var game = new Game(maxPlayers); game.Players.Add(new(game, name, clientToken)); games.Add(game.ID, game); + timer.Start(); SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { gameID = game.ID, clientToken, maxPlayers })); + Console.WriteLine($"New game started: {game.ID}; {games.Count} games active; {inactiveGames.Count} inactive"); } catch (ArgumentException) { SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidRequestData", "Invalid form data")); } @@ -130,7 +166,7 @@ internal class Program { return; } lock (games) { - if (!games.TryGetValue(gameID, out var game)) { + if (!games.TryGetValue(gameID, out var game) && !inactiveGames.TryGetValue(gameID, out game)) { SetErrorResponse(e.Response, new(HttpStatusCode.NotFound, "GameNotFound", "Game not found.")); return; } @@ -502,4 +538,17 @@ internal class Program { SetResponse(response, HttpStatusCode.OK, "application/json", jsonContent); } } + + internal static bool TryGetGame(Guid gameID, [MaybeNullWhen(false)] out Game game) { + if (games.TryGetValue(gameID, out game)) { + game.AbandonedSince = DateTime.UtcNow; + return true; + } else if (inactiveGames.TryGetValue(gameID, out game)) { + inactiveGames.Remove(gameID); + games[gameID] = game; + game.AbandonedSince = DateTime.UtcNow; + return true; + } + return false; + } } diff --git a/TableturfBattleServer/TableturfWebSocketBehaviour.cs b/TableturfBattleServer/TableturfWebSocketBehaviour.cs index 9fb32f1..8c84612 100644 --- a/TableturfBattleServer/TableturfWebSocketBehaviour.cs +++ b/TableturfBattleServer/TableturfWebSocketBehaviour.cs @@ -5,8 +5,10 @@ using WebSocketSharp.Server; namespace TableturfBattleServer; internal class TableturfWebSocketBehaviour : WebSocketBehavior { - public Guid GameID { get; set; } - public Guid ClientToken { get; set; } + public Guid GameID { get; private set; } + public Guid ClientToken { get; private set; } + public Game? Game { get; private set; } + public Player? Player { get; private set; } protected override void OnOpen() { var args = this.Context.RequestUri.Query[1..].Split('&').Select(s => s.Split('=', 2)).Where(a => a.Length == 2) @@ -17,11 +19,13 @@ internal class TableturfWebSocketBehaviour : WebSocketBehavior { this.ClientToken = clientToken; // Send an initial state payload. - if (Program.games.TryGetValue(this.GameID, out var game)) { + if (Program.TryGetGame(this.GameID, out var game)) { + this.Game = game; DTO.PlayerData? playerData = null; for (int i = 0; i < game.Players.Count; i++) { var player = game.Players[i]; if (player.Token == this.ClientToken) { + this.Player = player; playerData = new(i, player); break; }