do not respawn inactive players
This commit is contained in:
parent
cd12ab7bde
commit
abad2c95c8
2
Makefile
2
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
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -11,5 +11,6 @@ internal record struct PlayerInfo(
|
|||
string Name,
|
||||
Scores Scores,
|
||||
string Controls,
|
||||
TankInfo? Tank
|
||||
TankInfo? Tank,
|
||||
int OpenConnections
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue