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)
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 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 }) {
<ScoreRow name="pixels moved" value={lastJsonMessage.scores.pixelsMoved}/>
<ScoreRow name="score" value={lastJsonMessage.scores.overallScore}/>
<ScoreRow name="connections" value={lastJsonMessage.openConnections}/>
</tbody>
</table>
</Column>;

View file

@ -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);

View file

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

View file

@ -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<ClientScreenServerConnection> logger,
Player? player
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(webSocket, logger, 0))
internal sealed class ClientScreenServerConnection : WebsocketServerConnection
{
private sealed record class Package(
IMemoryOwner<byte> PixelsOwner,
@ -20,12 +15,21 @@ internal sealed class ClientScreenServerConnection(
);
private readonly MemoryPool<byte> _memoryPool = MemoryPool<byte>.Shared;
private readonly PlayerScreenData? _playerDataBuilder;
private readonly Player? _player;
private int _wantsFrameOnTick = 1;
private Package? _next;
private readonly PlayerScreenData? _playerDataBuilder = player == null
public ClientScreenServerConnection(WebSocket webSocket,
ILogger<ClientScreenServerConnection> 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<byte> _)
{
@ -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

View file

@ -2,12 +2,18 @@ using System.Net.WebSockets;
namespace TanksServer.Interactivity;
internal sealed class ControlsServerConnection(
WebSocket socket,
ILogger<ControlsServerConnection> logger,
Player player
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(socket, logger, 2))
internal sealed class ControlsServerConnection : WebsocketServerConnection
{
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
{
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;
}
}

View file

@ -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<byte> 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);

View file

@ -47,6 +47,7 @@ internal abstract class WebsocketServer<T>(
await AddConnectionAsync(connection);
await connection.ReceiveAsync();
await RemoveConnectionAsync(connection);
await connection.RemovedAsync();
}
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;

View file

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

View file

@ -4,6 +4,8 @@ namespace TanksServer.Models;
internal sealed class Player : IEquatable<Player>
{
private int _openConnections;
public required string Name { get; init; }
[JsonIgnore] public PlayerControls Controls { get; } = new();
@ -12,6 +14,8 @@ internal sealed class Player : IEquatable<Player>
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<Player>
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,
Scores Scores,
string Controls,
TankInfo? Tank
TankInfo? Tank,
int OpenConnections
);