diff --git a/Makefile b/Makefile index 4e76abc..9432c05 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,5 @@ build: podman build . --tag=$(TAG) run: build - podman run -i -p 80:3000 localhost/$(TAG):latest + podman run -i -p 3000:3000 localhost/$(TAG):latest diff --git a/tank-frontend/src/PlayerInfo.tsx b/tank-frontend/src/PlayerInfo.tsx index 04e3457..17e67d1 100644 --- a/tank-frontend/src/PlayerInfo.tsx +++ b/tank-frontend/src/PlayerInfo.tsx @@ -34,6 +34,7 @@ type PlayerInfoMessage = { readonly scores: Scores; readonly controls: string; readonly tank?: TankInfo; + readonly openConnections: number; } export default function PlayerInfo({player}: { player: string }) { @@ -81,6 +82,8 @@ export default function PlayerInfo({player}: { player: string }) { + + ; diff --git a/tanks-backend/TanksServer/GameLogic/TankSpawnQueue.cs b/tanks-backend/TanksServer/GameLogic/TankSpawnQueue.cs index ef146d2..1037623 100644 --- a/tanks-backend/TanksServer/GameLogic/TankSpawnQueue.cs +++ b/tanks-backend/TanksServer/GameLogic/TankSpawnQueue.cs @@ -35,7 +35,7 @@ internal sealed class TankSpawnQueue( return false; // no one on queue var now = DateTime.Now; - if (player.LastInput + _idleTimeout < now) + if (player.OpenConnections < 1 || player.LastInput + _idleTimeout < now) { // player idle _queue.Enqueue(player); diff --git a/tanks-backend/TanksServer/Interactivity/ClientScreenServer.cs b/tanks-backend/TanksServer/Interactivity/ClientScreenServer.cs index 1f9f1cf..2f82b3a 100644 --- a/tanks-backend/TanksServer/Interactivity/ClientScreenServer.cs +++ b/tanks-backend/TanksServer/Interactivity/ClientScreenServer.cs @@ -7,7 +7,8 @@ namespace TanksServer.Interactivity; internal sealed class ClientScreenServer( ILogger logger, ILoggerFactory loggerFactory -) : WebsocketServer(logger), IFrameConsumer +) : WebsocketServer(logger), + IFrameConsumer { public Task HandleClientAsync(WebSocket socket, Player? player) => base.HandleClientAsync(new ClientScreenServerConnection( diff --git a/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs b/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs index 38efa55..d76eda5 100644 --- a/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs @@ -1,16 +1,11 @@ using System.Buffers; -using System.Diagnostics; using System.Net.WebSockets; using DisplayCommands; using TanksServer.Graphics; namespace TanksServer.Interactivity; -internal sealed class ClientScreenServerConnection( - WebSocket webSocket, - ILogger logger, - Player? player -) : WebsocketServerConnection(logger, new ByteChannelWebSocket(webSocket, logger, 0)) +internal sealed class ClientScreenServerConnection : WebsocketServerConnection { private sealed record class Package( IMemoryOwner PixelsOwner, @@ -20,12 +15,21 @@ internal sealed class ClientScreenServerConnection( ); private readonly MemoryPool _memoryPool = MemoryPool.Shared; + private readonly PlayerScreenData? _playerDataBuilder; + private readonly Player? _player; private int _wantsFrameOnTick = 1; private Package? _next; - private readonly PlayerScreenData? _playerDataBuilder = player == null - ? null - : new PlayerScreenData(logger, player); + public ClientScreenServerConnection(WebSocket webSocket, + ILogger logger, + Player? player) : base(logger, new ByteChannelWebSocket(webSocket, logger, 0)) + { + _player = player; + _player?.IncrementConnectionCount(); + _playerDataBuilder = player == null + ? null + : new PlayerScreenData(logger, player); + } protected override ValueTask HandleMessageAsync(Memory _) { @@ -72,6 +76,12 @@ internal sealed class ClientScreenServerConnection( oldNext?.PlayerDataOwner?.Dispose(); } + public override ValueTask RemovedAsync() + { + _player?.DecrementConnectionCount(); + return ValueTask.CompletedTask; + } + private async ValueTask SendAndDisposeAsync(Package package) { try diff --git a/tanks-backend/TanksServer/Interactivity/ControlsServerConnection.cs b/tanks-backend/TanksServer/Interactivity/ControlsServerConnection.cs index 35f57f4..edceb3d 100644 --- a/tanks-backend/TanksServer/Interactivity/ControlsServerConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/ControlsServerConnection.cs @@ -2,12 +2,18 @@ using System.Net.WebSockets; namespace TanksServer.Interactivity; -internal sealed class ControlsServerConnection( - WebSocket socket, - ILogger logger, - Player player -) : WebsocketServerConnection(logger, new ByteChannelWebSocket(socket, logger, 2)) +internal sealed class ControlsServerConnection : WebsocketServerConnection { + private readonly Player _player; + + public ControlsServerConnection(WebSocket socket, + ILogger logger, + Player player) : base(logger, new ByteChannelWebSocket(socket, logger, 2)) + { + _player = player; + _player.IncrementConnectionCount(); + } + private enum MessageType : byte { Enable = 0x01, @@ -28,7 +34,7 @@ internal sealed class ControlsServerConnection( var type = (MessageType)buffer.Span[0]; var control = (InputType)buffer.Span[1]; - Logger.LogTrace("player input {} {} {}", player.Name, type, control); + Logger.LogTrace("player input {} {} {}", _player.Name, type, control); var isEnable = type switch { @@ -37,24 +43,24 @@ internal sealed class ControlsServerConnection( _ => throw new ArgumentException("invalid message type") }; - player.LastInput = DateTime.Now; + _player.LastInput = DateTime.Now; switch (control) { case InputType.Forward: - player.Controls.Forward = isEnable; + _player.Controls.Forward = isEnable; break; case InputType.Backward: - player.Controls.Backward = isEnable; + _player.Controls.Backward = isEnable; break; case InputType.Left: - player.Controls.TurnLeft = isEnable; + _player.Controls.TurnLeft = isEnable; break; case InputType.Right: - player.Controls.TurnRight = isEnable; + _player.Controls.TurnRight = isEnable; break; case InputType.Shoot: - player.Controls.Shoot = isEnable; + _player.Controls.Shoot = isEnable; break; default: throw new ArgumentException("invalid control type"); @@ -62,4 +68,10 @@ internal sealed class ControlsServerConnection( return ValueTask.CompletedTask; } + + public override ValueTask RemovedAsync() + { + _player.DecrementConnectionCount(); + return ValueTask.CompletedTask; + } } diff --git a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs index b35e759..e37aecd 100644 --- a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs @@ -4,16 +4,23 @@ using TanksServer.GameLogic; namespace TanksServer.Interactivity; -internal sealed class PlayerInfoConnection( - Player player, - ILogger logger, - WebSocket rawSocket, - MapEntityManager entityManager -) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)) +internal sealed class PlayerInfoConnection : WebsocketServerConnection { private int _wantsInfoOnTick = 1; private byte[]? _lastMessage = null; private byte[]? _nextMessage = null; + private readonly Player _player; + private readonly MapEntityManager _entityManager; + + public PlayerInfoConnection(Player player, + ILogger logger, + WebSocket rawSocket, + MapEntityManager entityManager) : base(logger, new ByteChannelWebSocket(rawSocket, logger, 0)) + { + _player = player; + _entityManager = entityManager; + _player.IncrementConnectionCount(); + } protected override ValueTask HandleMessageAsync(Memory buffer) { @@ -41,9 +48,15 @@ internal sealed class PlayerInfoConnection( Interlocked.Exchange(ref _nextMessage, response); } + public override ValueTask RemovedAsync() + { + _player.DecrementConnectionCount(); + return ValueTask.CompletedTask; + } + private byte[] GetMessageToSend() { - var tank = entityManager.GetCurrentTankOfPlayer(player); + var tank = _entityManager.GetCurrentTankOfPlayer(_player); TankInfo? tankInfo = null; if (tank != null) @@ -52,7 +65,12 @@ internal sealed class PlayerInfoConnection( tankInfo = new TankInfo(tank.Orientation, magazine, tank.Position.ToPixelPosition(), tank.Moving); } - var info = new PlayerInfo(player.Name, player.Scores, player.Controls.ToDisplayString(), tankInfo); + var info = new PlayerInfo( + _player.Name, + _player.Scores, + _player.Controls.ToDisplayString(), + tankInfo, + _player.OpenConnections); // TODO: switch to async version with pre-allocated buffer / IMemoryOwner return JsonSerializer.SerializeToUtf8Bytes(info, AppSerializerContext.Default.PlayerInfo); diff --git a/tanks-backend/TanksServer/Interactivity/WebsocketServer.cs b/tanks-backend/TanksServer/Interactivity/WebsocketServer.cs index f8c6719..8912b35 100644 --- a/tanks-backend/TanksServer/Interactivity/WebsocketServer.cs +++ b/tanks-backend/TanksServer/Interactivity/WebsocketServer.cs @@ -47,6 +47,7 @@ internal abstract class WebsocketServer( await AddConnectionAsync(connection); await connection.ReceiveAsync(); await RemoveConnectionAsync(connection); + await connection.RemovedAsync(); } private async ValueTask LockedAsync(Func action, CancellationToken cancellationToken) @@ -62,7 +63,7 @@ internal abstract class WebsocketServer( } } - public void Dispose() => _mutex.Dispose(); + public virtual void Dispose() => _mutex.Dispose(); public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/tanks-backend/TanksServer/Interactivity/WebsocketServerConnection.cs b/tanks-backend/TanksServer/Interactivity/WebsocketServerConnection.cs index 23b698c..d501c1d 100644 --- a/tanks-backend/TanksServer/Interactivity/WebsocketServerConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/WebsocketServerConnection.cs @@ -22,20 +22,9 @@ internal abstract class WebsocketServerConnection( Logger.LogTrace("done receiving"); } + public abstract ValueTask RemovedAsync(); + protected abstract ValueTask HandleMessageAsync(Memory buffer); - protected async ValueTask LockedAsync(Func action) - { - await _mutex.WaitAsync(); - try - { - await action(); - } - finally - { - _mutex.Release(); - } - } - - public void Dispose() => _mutex.Dispose(); + public virtual void Dispose() => _mutex.Dispose(); } diff --git a/tanks-backend/TanksServer/Models/Player.cs b/tanks-backend/TanksServer/Models/Player.cs index de158d4..6938146 100644 --- a/tanks-backend/TanksServer/Models/Player.cs +++ b/tanks-backend/TanksServer/Models/Player.cs @@ -4,6 +4,8 @@ namespace TanksServer.Models; internal sealed class Player : IEquatable { + private int _openConnections; + public required string Name { get; init; } [JsonIgnore] public PlayerControls Controls { get; } = new(); @@ -12,6 +14,8 @@ internal sealed class Player : IEquatable public DateTime LastInput { get; set; } = DateTime.Now; + public int OpenConnections => _openConnections; + public override bool Equals(object? obj) => obj is Player p && Equals(p); public bool Equals(Player? other) => other?.Name == Name; @@ -21,4 +25,8 @@ internal sealed class Player : IEquatable public static bool operator ==(Player? left, Player? right) => Equals(left, right); public static bool operator !=(Player? left, Player? right) => !Equals(left, right); + + internal void IncrementConnectionCount() => Interlocked.Increment(ref _openConnections); + + internal void DecrementConnectionCount() => Interlocked.Decrement(ref _openConnections); } diff --git a/tanks-backend/TanksServer/Models/PlayerInfo.cs b/tanks-backend/TanksServer/Models/PlayerInfo.cs index 118eb42..a8545f3 100644 --- a/tanks-backend/TanksServer/Models/PlayerInfo.cs +++ b/tanks-backend/TanksServer/Models/PlayerInfo.cs @@ -11,5 +11,6 @@ internal record struct PlayerInfo( string Name, Scores Scores, string Controls, - TankInfo? Tank + TankInfo? Tank, + int OpenConnections );