do not respawn inactive players

This commit is contained in:
Vinzenz Schroeter 2024-05-02 21:27:56 +02:00 committed by RobbersDaughter
parent cd12ab7bde
commit abad2c95c8
11 changed files with 91 additions and 48 deletions

View file

@ -6,5 +6,5 @@ build:
podman build . --tag=$(TAG) podman build . --tag=$(TAG)
run: build run: build
podman run -i -p 80:3000 localhost/$(TAG):latest podman run -i -p 3000:3000 localhost/$(TAG):latest

View file

@ -34,6 +34,7 @@ type PlayerInfoMessage = {
readonly scores: Scores; readonly scores: Scores;
readonly controls: string; readonly controls: string;
readonly tank?: TankInfo; readonly tank?: TankInfo;
readonly openConnections: number;
} }
export default function PlayerInfo({player}: { player: string }) { export default function PlayerInfo({player}: { player: string }) {
@ -81,6 +82,8 @@ export default function PlayerInfo({player}: { player: string }) {
<ScoreRow name="pixels moved" value={lastJsonMessage.scores.pixelsMoved}/> <ScoreRow name="pixels moved" value={lastJsonMessage.scores.pixelsMoved}/>
<ScoreRow name="score" value={lastJsonMessage.scores.overallScore}/> <ScoreRow name="score" value={lastJsonMessage.scores.overallScore}/>
<ScoreRow name="connections" value={lastJsonMessage.openConnections}/>
</tbody> </tbody>
</table> </table>
</Column>; </Column>;

View file

@ -35,7 +35,7 @@ internal sealed class TankSpawnQueue(
return false; // no one on queue return false; // no one on queue
var now = DateTime.Now; var now = DateTime.Now;
if (player.LastInput + _idleTimeout < now) if (player.OpenConnections < 1 || player.LastInput + _idleTimeout < now)
{ {
// player idle // player idle
_queue.Enqueue(player); _queue.Enqueue(player);

View file

@ -7,7 +7,8 @@ namespace TanksServer.Interactivity;
internal sealed class ClientScreenServer( internal sealed class ClientScreenServer(
ILogger<ClientScreenServer> logger, ILogger<ClientScreenServer> logger,
ILoggerFactory loggerFactory ILoggerFactory loggerFactory
) : WebsocketServer<ClientScreenServerConnection>(logger), IFrameConsumer ) : WebsocketServer<ClientScreenServerConnection>(logger),
IFrameConsumer
{ {
public Task HandleClientAsync(WebSocket socket, Player? player) public Task HandleClientAsync(WebSocket socket, Player? player)
=> base.HandleClientAsync(new ClientScreenServerConnection( => base.HandleClientAsync(new ClientScreenServerConnection(

View file

@ -1,16 +1,11 @@
using System.Buffers; using System.Buffers;
using System.Diagnostics;
using System.Net.WebSockets; using System.Net.WebSockets;
using DisplayCommands; using DisplayCommands;
using TanksServer.Graphics; using TanksServer.Graphics;
namespace TanksServer.Interactivity; namespace TanksServer.Interactivity;
internal sealed class ClientScreenServerConnection( internal sealed class ClientScreenServerConnection : WebsocketServerConnection
WebSocket webSocket,
ILogger<ClientScreenServerConnection> logger,
Player? player
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(webSocket, logger, 0))
{ {
private sealed record class Package( private sealed record class Package(
IMemoryOwner<byte> PixelsOwner, IMemoryOwner<byte> PixelsOwner,
@ -20,12 +15,21 @@ internal sealed class ClientScreenServerConnection(
); );
private readonly MemoryPool<byte> _memoryPool = MemoryPool<byte>.Shared; private readonly MemoryPool<byte> _memoryPool = MemoryPool<byte>.Shared;
private readonly PlayerScreenData? _playerDataBuilder;
private readonly Player? _player;
private int _wantsFrameOnTick = 1; private int _wantsFrameOnTick = 1;
private Package? _next; private Package? _next;
private readonly PlayerScreenData? _playerDataBuilder = player == null public ClientScreenServerConnection(WebSocket webSocket,
? null ILogger<ClientScreenServerConnection> logger,
: new PlayerScreenData(logger, player); 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<byte> _) protected override ValueTask HandleMessageAsync(Memory<byte> _)
{ {
@ -72,6 +76,12 @@ internal sealed class ClientScreenServerConnection(
oldNext?.PlayerDataOwner?.Dispose(); oldNext?.PlayerDataOwner?.Dispose();
} }
public override ValueTask RemovedAsync()
{
_player?.DecrementConnectionCount();
return ValueTask.CompletedTask;
}
private async ValueTask SendAndDisposeAsync(Package package) private async ValueTask SendAndDisposeAsync(Package package)
{ {
try try

View file

@ -2,12 +2,18 @@ using System.Net.WebSockets;
namespace TanksServer.Interactivity; namespace TanksServer.Interactivity;
internal sealed class ControlsServerConnection( internal sealed class ControlsServerConnection : WebsocketServerConnection
WebSocket socket,
ILogger<ControlsServerConnection> logger,
Player player
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(socket, logger, 2))
{ {
private readonly Player _player;
public ControlsServerConnection(WebSocket socket,
ILogger<ControlsServerConnection> logger,
Player player) : base(logger, new ByteChannelWebSocket(socket, logger, 2))
{
_player = player;
_player.IncrementConnectionCount();
}
private enum MessageType : byte private enum MessageType : byte
{ {
Enable = 0x01, Enable = 0x01,
@ -28,7 +34,7 @@ internal sealed class ControlsServerConnection(
var type = (MessageType)buffer.Span[0]; var type = (MessageType)buffer.Span[0];
var control = (InputType)buffer.Span[1]; 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 var isEnable = type switch
{ {
@ -37,24 +43,24 @@ internal sealed class ControlsServerConnection(
_ => throw new ArgumentException("invalid message type") _ => throw new ArgumentException("invalid message type")
}; };
player.LastInput = DateTime.Now; _player.LastInput = DateTime.Now;
switch (control) switch (control)
{ {
case InputType.Forward: case InputType.Forward:
player.Controls.Forward = isEnable; _player.Controls.Forward = isEnable;
break; break;
case InputType.Backward: case InputType.Backward:
player.Controls.Backward = isEnable; _player.Controls.Backward = isEnable;
break; break;
case InputType.Left: case InputType.Left:
player.Controls.TurnLeft = isEnable; _player.Controls.TurnLeft = isEnable;
break; break;
case InputType.Right: case InputType.Right:
player.Controls.TurnRight = isEnable; _player.Controls.TurnRight = isEnable;
break; break;
case InputType.Shoot: case InputType.Shoot:
player.Controls.Shoot = isEnable; _player.Controls.Shoot = isEnable;
break; break;
default: default:
throw new ArgumentException("invalid control type"); throw new ArgumentException("invalid control type");
@ -62,4 +68,10 @@ internal sealed class ControlsServerConnection(
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }
public override ValueTask RemovedAsync()
{
_player.DecrementConnectionCount();
return ValueTask.CompletedTask;
}
} }

View file

@ -4,16 +4,23 @@ using TanksServer.GameLogic;
namespace TanksServer.Interactivity; namespace TanksServer.Interactivity;
internal sealed class PlayerInfoConnection( internal sealed class PlayerInfoConnection : WebsocketServerConnection
Player player,
ILogger logger,
WebSocket rawSocket,
MapEntityManager entityManager
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0))
{ {
private int _wantsInfoOnTick = 1; private int _wantsInfoOnTick = 1;
private byte[]? _lastMessage = null; private byte[]? _lastMessage = null;
private byte[]? _nextMessage = 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<byte> buffer) protected override ValueTask HandleMessageAsync(Memory<byte> buffer)
{ {
@ -41,9 +48,15 @@ internal sealed class PlayerInfoConnection(
Interlocked.Exchange(ref _nextMessage, response); Interlocked.Exchange(ref _nextMessage, response);
} }
public override ValueTask RemovedAsync()
{
_player.DecrementConnectionCount();
return ValueTask.CompletedTask;
}
private byte[] GetMessageToSend() private byte[] GetMessageToSend()
{ {
var tank = entityManager.GetCurrentTankOfPlayer(player); var tank = _entityManager.GetCurrentTankOfPlayer(_player);
TankInfo? tankInfo = null; TankInfo? tankInfo = null;
if (tank != null) if (tank != null)
@ -52,7 +65,12 @@ internal sealed class PlayerInfoConnection(
tankInfo = new TankInfo(tank.Orientation, magazine, tank.Position.ToPixelPosition(), tank.Moving); 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 // TODO: switch to async version with pre-allocated buffer / IMemoryOwner
return JsonSerializer.SerializeToUtf8Bytes(info, AppSerializerContext.Default.PlayerInfo); return JsonSerializer.SerializeToUtf8Bytes(info, AppSerializerContext.Default.PlayerInfo);

View file

@ -47,6 +47,7 @@ internal abstract class WebsocketServer<T>(
await AddConnectionAsync(connection); await AddConnectionAsync(connection);
await connection.ReceiveAsync(); await connection.ReceiveAsync();
await RemoveConnectionAsync(connection); await RemoveConnectionAsync(connection);
await connection.RemovedAsync();
} }
private async ValueTask LockedAsync(Func<ValueTask> action, CancellationToken cancellationToken) private async ValueTask LockedAsync(Func<ValueTask> action, CancellationToken cancellationToken)
@ -62,7 +63,7 @@ internal abstract class WebsocketServer<T>(
} }
} }
public void Dispose() => _mutex.Dispose(); public virtual void Dispose() => _mutex.Dispose();
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

View file

@ -22,20 +22,9 @@ internal abstract class WebsocketServerConnection(
Logger.LogTrace("done receiving"); Logger.LogTrace("done receiving");
} }
public abstract ValueTask RemovedAsync();
protected abstract ValueTask HandleMessageAsync(Memory<byte> buffer); protected abstract ValueTask HandleMessageAsync(Memory<byte> buffer);
protected async ValueTask LockedAsync(Func<ValueTask> action) public virtual void Dispose() => _mutex.Dispose();
{
await _mutex.WaitAsync();
try
{
await action();
}
finally
{
_mutex.Release();
}
}
public void Dispose() => _mutex.Dispose();
} }

View file

@ -4,6 +4,8 @@ namespace TanksServer.Models;
internal sealed class Player : IEquatable<Player> internal sealed class Player : IEquatable<Player>
{ {
private int _openConnections;
public required string Name { get; init; } public required string Name { get; init; }
[JsonIgnore] public PlayerControls Controls { get; } = new(); [JsonIgnore] public PlayerControls Controls { get; } = new();
@ -12,6 +14,8 @@ internal sealed class Player : IEquatable<Player>
public DateTime LastInput { get; set; } = DateTime.Now; public DateTime LastInput { get; set; } = DateTime.Now;
public int OpenConnections => _openConnections;
public override bool Equals(object? obj) => obj is Player p && Equals(p); public override bool Equals(object? obj) => obj is Player p && Equals(p);
public bool Equals(Player? other) => other?.Name == Name; public bool Equals(Player? other) => other?.Name == Name;
@ -21,4 +25,8 @@ internal sealed class Player : IEquatable<Player>
public static bool operator ==(Player? left, Player? right) => Equals(left, right); public static bool operator ==(Player? left, Player? right) => Equals(left, right);
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);
} }

View file

@ -11,5 +11,6 @@ internal record struct PlayerInfo(
string Name, string Name,
Scores Scores, Scores Scores,
string Controls, string Controls,
TankInfo? Tank TankInfo? Tank,
int OpenConnections
); );