live player info in client

This commit is contained in:
Vinzenz Schroeter 2024-04-22 19:03:07 +02:00
parent fb675e59ff
commit a50a9770c9
14 changed files with 193 additions and 70 deletions

View file

@ -7,4 +7,5 @@ namespace TanksServer.Interactivity;
[JsonSerializable(typeof(Guid))]
[JsonSerializable(typeof(NameId))]
[JsonSerializable(typeof(IEnumerable<string>))]
[JsonSerializable(typeof(PlayerInfo))]
internal sealed partial class AppSerializerContext : JsonSerializerContext;

View file

@ -7,9 +7,12 @@ internal sealed class ByteChannelWebSocket(WebSocket socket, ILogger logger, int
{
private readonly byte[] _buffer = new byte[messageSize];
public ValueTask SendAsync(ReadOnlyMemory<byte> data, bool endOfMessage = true) =>
public ValueTask SendBinaryAsync(ReadOnlyMemory<byte> data, bool endOfMessage = true) =>
socket.SendAsync(data, WebSocketMessageType.Binary, endOfMessage, CancellationToken.None);
public ValueTask SendTextAsync(ReadOnlyMemory<byte> data, bool endOfMessage = true) =>
socket.SendAsync(data, WebSocketMessageType.Text, endOfMessage, CancellationToken.None);
public async IAsyncEnumerable<Memory<byte>> ReadAllAsync()
{
while (socket.State is WebSocketState.Open or WebSocketState.CloseSent)

View file

@ -26,7 +26,7 @@ internal sealed class ClientScreenServerConnection(
if (!await _wantedFrames.WaitAsync(TimeSpan.Zero))
{
logger.LogTrace("client does not want a frame yet");
Logger.LogTrace("client does not want a frame yet");
return;
}
@ -35,17 +35,17 @@ internal sealed class ClientScreenServerConnection(
if (_playerScreenData != null)
RefreshPlayerSpecificData(gamePixelGrid);
logger.LogTrace("sending");
Logger.LogTrace("sending");
try
{
logger.LogTrace("sending {} bytes of pixel data", pixels.Data.Length);
await Socket.SendAsync(pixels.Data, _playerScreenData == null);
Logger.LogTrace("sending {} bytes of pixel data", pixels.Data.Length);
await Socket.SendBinaryAsync(pixels.Data, _playerScreenData == null);
if (_playerScreenData != null)
await Socket.SendAsync(_playerScreenData.GetPacket());
await Socket.SendBinaryAsync(_playerScreenData.GetPacket());
}
catch (WebSocketException ex)
{
logger.LogWarning(ex, "send failed");
Logger.LogWarning(ex, "send failed");
}
}
@ -61,5 +61,9 @@ internal sealed class ClientScreenServerConnection(
}
}
protected override void HandleMessage(Memory<byte> _) => _wantedFrames.Release();
protected override ValueTask HandleMessageAsync(Memory<byte> _)
{
_wantedFrames.Release();
return ValueTask.CompletedTask;
}
}

View file

@ -7,13 +7,11 @@ internal sealed class ControlsServer(
ILoggerFactory loggerFactory
) : WebsocketServer<ControlsServerConnection>(logger)
{
public async Task HandleClientAsync(WebSocket ws, Player player)
public Task HandleClientAsync(WebSocket ws, Player player)
{
logger.LogDebug("control client connected {}", player.Id);
var clientLogger = loggerFactory.CreateLogger<ControlsServerConnection>();
var sock = new ControlsServerConnection(ws, clientLogger, player);
await AddConnection(sock);
await sock.ReceiveAsync();
await RemoveConnection(sock);
return HandleClientAsync(sock);
}
}

View file

@ -23,12 +23,12 @@ internal sealed class ControlsServerConnection(
Shoot = 0x05
}
protected override void HandleMessage(Memory<byte> buffer)
protected override ValueTask HandleMessageAsync(Memory<byte> buffer)
{
var type = (MessageType)buffer.Span[0];
var control = (InputType)buffer.Span[1];
logger.LogTrace("player input {} {} {}", player.Id, type, control);
Logger.LogTrace("player input {} {} {}", player.Id, type, control);
var isEnable = type switch
{
@ -59,5 +59,7 @@ internal sealed class ControlsServerConnection(
default:
throw new ArgumentException("invalid control type");
}
return ValueTask.CompletedTask;
}
}

View file

@ -0,0 +1,58 @@
using System.Net.WebSockets;
using System.Text.Json;
namespace TanksServer.Interactivity;
internal sealed class PlayerInfoConnection(
Player player,
ILogger logger,
WebSocket rawSocket
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)), IDisposable
{
private readonly SemaphoreSlim _wantedFrames = new(1);
private readonly AppSerializerContext _context = new(new JsonSerializerOptions(JsonSerializerDefaults.Web));
private byte[] _lastMessage = [];
protected override ValueTask HandleMessageAsync(Memory<byte> buffer)
{
var response = GetMessageToSend();
if (response == null)
{
Logger.LogTrace("cannot respond directly, increasing wanted frames");
_wantedFrames.Release();
return ValueTask.CompletedTask;
}
Logger.LogTrace("responding directly");
return Socket.SendTextAsync(response);
}
public async Task OnGameTickAsync()
{
if (!await _wantedFrames.WaitAsync(TimeSpan.Zero))
return;
var response = GetMessageToSend();
if (response == null)
{
_wantedFrames.Release();
return;
}
Logger.LogTrace("responding indirectly");
await Socket.SendTextAsync(response);
}
private byte[]? GetMessageToSend()
{
var info = new PlayerInfo(player.Name, player.Scores, player.Controls);
var response = JsonSerializer.SerializeToUtf8Bytes(info, _context.PlayerInfo);
if (response.SequenceEqual(_lastMessage))
return null;
return _lastMessage = response;
}
public void Dispose() => _wantedFrames.Dispose();
}

View file

@ -1,30 +1,32 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.WebSockets;
using TanksServer.GameLogic;
namespace TanksServer.Interactivity;
internal sealed class PlayerServer(
ILogger<PlayerServer> logger,
ILogger<PlayerInfoConnection> connectionLogger,
TankSpawnQueue tankSpawnQueue
)
) : WebsocketServer<PlayerInfoConnection>(logger), ITickStep
{
private readonly ConcurrentDictionary<string, Player> _players = new();
public Player? GetOrAdd(string name, Guid id)
{
Player AddAndSpawn()
{
var player = new Player(name, id);
tankSpawnQueue.EnqueueForImmediateSpawn(player);
return player;
}
var player = _players.GetOrAdd(name, _ => AddAndSpawn());
if (player.Id != id)
var existingOrAddedPlayer = _players.GetOrAdd(name, _ => AddAndSpawn());
if (existingOrAddedPlayer.Id != id)
return null;
logger.LogInformation("player {} (re)joined", player.Id);
return player;
logger.LogInformation("player {} (re)joined", existingOrAddedPlayer.Id);
return existingOrAddedPlayer;
Player AddAndSpawn()
{
var newPlayer = new Player(name, id);
tankSpawnQueue.EnqueueForImmediateSpawn(newPlayer);
return newPlayer;
}
}
public bool TryGet(Guid? playerId, [MaybeNullWhen(false)] out Player foundPlayer)
@ -42,4 +44,10 @@ internal sealed class PlayerServer(
}
public IEnumerable<Player> GetAll() => _players.Values;
public Task HandleClientAsync(WebSocket webSocket, Player player)
=> HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket));
public Task TickAsync(TimeSpan delta)
=> ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync());
}

View file

@ -35,7 +35,7 @@ internal abstract class WebsocketServer<T>(
}
}
protected Task AddConnection(T connection) => Locked(() =>
private Task AddConnectionAsync(T connection) => Locked(() =>
{
if (_closing)
{
@ -47,7 +47,7 @@ internal abstract class WebsocketServer<T>(
return Task.CompletedTask;
}, CancellationToken.None);
protected Task RemoveConnection(T connection) => Locked(() =>
private Task RemoveConnectionAsync(T connection) => Locked(() =>
{
_connections.Remove(connection);
return Task.CompletedTask;
@ -55,9 +55,9 @@ internal abstract class WebsocketServer<T>(
protected async Task HandleClientAsync(T connection)
{
await AddConnection(connection);
await AddConnectionAsync(connection);
await connection.ReceiveAsync();
await RemoveConnection(connection);
await RemoveConnectionAsync(connection);
}
private async Task Locked(Func<Task> action, CancellationToken cancellationToken)

View file

@ -2,22 +2,24 @@ namespace TanksServer.Interactivity;
internal abstract class WebsocketServerConnection(
ILogger logger,
ByteChannelWebSocket socket)
ByteChannelWebSocket socket
)
{
protected readonly ByteChannelWebSocket Socket = socket;
protected readonly ILogger Logger = logger;
public Task CloseAsync()
{
logger.LogDebug("closing connection");
Logger.LogDebug("closing connection");
return Socket.CloseAsync();
}
public async Task ReceiveAsync()
{
await foreach (var buffer in Socket.ReadAllAsync())
HandleMessage(buffer);
logger.LogTrace("done receiving");
await HandleMessageAsync(buffer);
Logger.LogTrace("done receiving");
}
protected abstract void HandleMessage(Memory<byte> buffer);
protected abstract ValueTask HandleMessageAsync(Memory<byte> buffer);
}