potential fix for locking issues
This commit is contained in:
		
							parent
							
								
									7044ffda79
								
							
						
					
					
						commit
						c0172963d5
					
				
					 20 changed files with 112 additions and 141 deletions
				
			
		| 
						 | 
					@ -80,8 +80,6 @@ internal sealed class Endpoints(
 | 
				
			||||||
        if (name.Length > 12) return TypedResults.BadRequest("name too long");
 | 
					        if (name.Length > 12) return TypedResults.BadRequest("name too long");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var player = playerService.GetOrAdd(name);
 | 
					        var player = playerService.GetOrAdd(name);
 | 
				
			||||||
        return player != null
 | 
					        return TypedResults.Ok(player.Name);
 | 
				
			||||||
            ? TypedResults.Ok(player.Name)
 | 
					 | 
				
			||||||
            : TypedResults.Unauthorized();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,10 +4,10 @@ internal sealed class CollectPowerUp(
 | 
				
			||||||
    MapEntityManager entityManager
 | 
					    MapEntityManager entityManager
 | 
				
			||||||
) : ITickStep
 | 
					) : ITickStep
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public Task TickAsync(TimeSpan delta)
 | 
					    public ValueTask TickAsync(TimeSpan delta)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        entityManager.RemoveWhere(TryCollect);
 | 
					        entityManager.RemoveWhere(TryCollect);
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private bool TryCollect(PowerUp obj)
 | 
					    private bool TryCollect(PowerUp obj)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,12 +9,12 @@ internal sealed class CollideBullets(
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private const int ExplosionRadius = 3;
 | 
					    private const int ExplosionRadius = 3;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task TickAsync(TimeSpan _)
 | 
					    public ValueTask TickAsync(TimeSpan _)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        entityManager.RemoveWhere(BulletHitsTank);
 | 
					        entityManager.RemoveWhere(BulletHitsTank);
 | 
				
			||||||
        entityManager.RemoveWhere(BulletHitsWall);
 | 
					        entityManager.RemoveWhere(BulletHitsWall);
 | 
				
			||||||
        entityManager.RemoveWhere(BulletTimesOut);
 | 
					        entityManager.RemoveWhere(BulletTimesOut);
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private bool BulletTimesOut(Bullet bullet)
 | 
					    private bool BulletTimesOut(Bullet bullet)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,5 +2,5 @@ namespace TanksServer.GameLogic;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public interface ITickStep
 | 
					public interface ITickStep
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    Task TickAsync(TimeSpan delta);
 | 
					    ValueTask TickAsync(TimeSpan delta);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,12 +5,12 @@ internal sealed class MoveBullets(
 | 
				
			||||||
    IOptions<GameRules> config
 | 
					    IOptions<GameRules> config
 | 
				
			||||||
) : ITickStep
 | 
					) : ITickStep
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public Task TickAsync(TimeSpan delta)
 | 
					    public ValueTask TickAsync(TimeSpan delta)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        foreach (var bullet in entityManager.Bullets)
 | 
					        foreach (var bullet in entityManager.Bullets)
 | 
				
			||||||
            MoveBullet(bullet, delta);
 | 
					            MoveBullet(bullet, delta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void MoveBullet(Bullet bullet, TimeSpan delta)
 | 
					    private void MoveBullet(Bullet bullet, TimeSpan delta)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,12 +8,12 @@ internal sealed class MoveTanks(
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly GameRules _config = options.Value;
 | 
					    private readonly GameRules _config = options.Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task TickAsync(TimeSpan delta)
 | 
					    public ValueTask TickAsync(TimeSpan delta)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        foreach (var tank in entityManager.Tanks)
 | 
					        foreach (var tank in entityManager.Tanks)
 | 
				
			||||||
            tank.Moving = TryMoveTank(tank, delta);
 | 
					            tank.Moving = TryMoveTank(tank, delta);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private bool TryMoveTank(Tank tank, TimeSpan delta)
 | 
					    private bool TryMoveTank(Tank tank, TimeSpan delta)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ internal sealed class RotateTanks(
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly GameRules _config = options.Value;
 | 
					    private readonly GameRules _config = options.Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task TickAsync(TimeSpan delta)
 | 
					    public ValueTask TickAsync(TimeSpan delta)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        foreach (var tank in entityManager.Tanks)
 | 
					        foreach (var tank in entityManager.Tanks)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
| 
						 | 
					@ -30,6 +30,6 @@ internal sealed class RotateTanks(
 | 
				
			||||||
            logger.LogTrace("rotated tank to {}", tank.Rotation);
 | 
					            logger.LogTrace("rotated tank to {}", tank.Rotation);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,12 +9,12 @@ internal sealed class ShootFromTanks(
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly GameRules _config = options.Value;
 | 
					    private readonly GameRules _config = options.Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task TickAsync(TimeSpan _)
 | 
					    public ValueTask TickAsync(TimeSpan _)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        foreach (var tank in entityManager.Tanks.Where(t => !t.Moving))
 | 
					        foreach (var tank in entityManager.Tanks.Where(t => !t.Moving))
 | 
				
			||||||
            Shoot(tank);
 | 
					            Shoot(tank);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void Shoot(Tank tank)
 | 
					    private void Shoot(Tank tank)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,14 +8,14 @@ internal sealed class SpawnPowerUp(
 | 
				
			||||||
    private readonly double _spawnChance = options.Value.PowerUpSpawnChance;
 | 
					    private readonly double _spawnChance = options.Value.PowerUpSpawnChance;
 | 
				
			||||||
    private readonly int _maxCount = options.Value.MaxPowerUpCount;
 | 
					    private readonly int _maxCount = options.Value.MaxPowerUpCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task TickAsync(TimeSpan delta)
 | 
					    public ValueTask TickAsync(TimeSpan delta)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (entityManager.PowerUps.Count() >= _maxCount)
 | 
					        if (entityManager.PowerUps.Count() >= _maxCount)
 | 
				
			||||||
            return Task.CompletedTask;
 | 
					            return ValueTask.CompletedTask;
 | 
				
			||||||
        if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds)
 | 
					        if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds)
 | 
				
			||||||
            return Task.CompletedTask;
 | 
					            return ValueTask.CompletedTask;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        entityManager.SpawnPowerUp();
 | 
					        entityManager.SpawnPowerUp();
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,12 +43,12 @@ internal sealed class TankSpawnQueue(
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task TickAsync(TimeSpan _)
 | 
					    public ValueTask TickAsync(TimeSpan _)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (!TryDequeueNext(out var player))
 | 
					        if (!TryDequeueNext(out var player))
 | 
				
			||||||
            return Task.CompletedTask;
 | 
					            return ValueTask.CompletedTask;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        entityManager.SpawnTank(player);
 | 
					        entityManager.SpawnTank(player);
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ internal sealed class GeneratePixelsTickStep(
 | 
				
			||||||
    private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
 | 
					    private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
 | 
				
			||||||
    private readonly List<IFrameConsumer> _consumers = consumers.ToList();
 | 
					    private readonly List<IFrameConsumer> _consumers = consumers.ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task TickAsync(TimeSpan _)
 | 
					    public async ValueTask TickAsync(TimeSpan _)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        PixelGrid observerPixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
 | 
					        PixelGrid observerPixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,5 +4,5 @@ namespace TanksServer.Graphics;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
internal interface IFrameConsumer
 | 
					internal interface IFrameConsumer
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels);
 | 
					    ValueTask OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,6 @@ internal sealed class ClientScreenServer(
 | 
				
			||||||
            player
 | 
					            player
 | 
				
			||||||
        ));
 | 
					        ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
 | 
					    public ValueTask OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
 | 
				
			||||||
        => ParallelForEachConnectionAsync(c => c.OnGameTickAsync(observerPixels, gamePixelGrid).AsTask());
 | 
					        => ParallelForEachConnectionAsync(c => c.OnGameTickAsync(observerPixels, gamePixelGrid).AsTask());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,45 +8,26 @@ internal sealed class ClientScreenServerConnection(
 | 
				
			||||||
    WebSocket webSocket,
 | 
					    WebSocket webSocket,
 | 
				
			||||||
    ILogger<ClientScreenServerConnection> logger,
 | 
					    ILogger<ClientScreenServerConnection> logger,
 | 
				
			||||||
    string? playerName = null
 | 
					    string? playerName = null
 | 
				
			||||||
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(webSocket, logger, 0)),
 | 
					) : WebsocketServerConnection(logger, new ByteChannelWebSocket(webSocket, logger, 0))
 | 
				
			||||||
    IDisposable
 | 
					 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly SemaphoreSlim _wantedFramesOnTick = new(0, 2);
 | 
					    private bool _wantsFrameOnTick = true;
 | 
				
			||||||
    private readonly SemaphoreSlim _mutex = new(1);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private PixelGrid? _lastSentPixels = null;
 | 
					    private PixelGrid? _lastSentPixels;
 | 
				
			||||||
    private PixelGrid? _nextPixels = null;
 | 
					    private PixelGrid? _nextPixels;
 | 
				
			||||||
    private readonly PlayerScreenData? _nextPlayerData = playerName != null ? new PlayerScreenData(logger) : null;
 | 
					    private readonly PlayerScreenData? _nextPlayerData = playerName != null ? new PlayerScreenData(logger) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected override async ValueTask HandleMessageAsync(Memory<byte> _)
 | 
					    protected override async ValueTask HandleMessageLockedAsync(Memory<byte> _)
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        await _mutex.WaitAsync();
 | 
					 | 
				
			||||||
        try
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (_nextPixels == null)
 | 
					        if (_nextPixels == null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
                _wantedFramesOnTick.Release();
 | 
					            _wantsFrameOnTick = true;
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _lastSentPixels = _nextPixels;
 | 
					        await SendNowAsync();
 | 
				
			||||||
            _nextPixels = null;
 | 
					 | 
				
			||||||
            await SendNowAsync(_lastSentPixels);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        catch (SemaphoreFullException)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            logger.LogWarning("ignoring request for more frames");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        finally
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _mutex.Release();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async ValueTask OnGameTickAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid)
 | 
					    public ValueTask OnGameTickAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid) => LockedAsync(async () =>
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        await _mutex.WaitAsync();
 | 
					 | 
				
			||||||
        try
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (pixels == _lastSentPixels)
 | 
					        if (pixels == _lastSentPixels)
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
| 
						 | 
					@ -62,39 +43,31 @@ internal sealed class ClientScreenServerConnection(
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var sendImmediately = await _wantedFramesOnTick.WaitAsync(TimeSpan.Zero);
 | 
					 | 
				
			||||||
            if (sendImmediately)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                await SendNowAsync(pixels);
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            _wantedFramesOnTick.Release();
 | 
					 | 
				
			||||||
        _nextPixels = pixels;
 | 
					        _nextPixels = pixels;
 | 
				
			||||||
        }
 | 
					        if (_wantsFrameOnTick)
 | 
				
			||||||
        finally
 | 
					            _ = await SendNowAsync();
 | 
				
			||||||
        {
 | 
					    });
 | 
				
			||||||
            _mutex.Release();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async ValueTask SendNowAsync(PixelGrid pixels)
 | 
					    private async ValueTask<bool> SendNowAsync()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Logger.LogTrace("sending");
 | 
					        var pixels = _nextPixels
 | 
				
			||||||
 | 
					                     ?? throw new InvalidOperationException("next pixels not set");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Logger.LogTrace("sending {} bytes of pixel data", pixels.Data.Length);
 | 
					 | 
				
			||||||
            await Socket.SendBinaryAsync(pixels.Data, _nextPlayerData == null);
 | 
					            await Socket.SendBinaryAsync(pixels.Data, _nextPlayerData == null);
 | 
				
			||||||
            if (_nextPlayerData != null)
 | 
					            if (_nextPlayerData != null)
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                await Socket.SendBinaryAsync(_nextPlayerData.GetPacket());
 | 
					                await Socket.SendBinaryAsync(_nextPlayerData.GetPacket());
 | 
				
			||||||
            }
 | 
					
 | 
				
			||||||
 | 
					            _lastSentPixels = _nextPixels;
 | 
				
			||||||
 | 
					            _nextPixels = null;
 | 
				
			||||||
 | 
					            _wantsFrameOnTick = false;
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (WebSocketException ex)
 | 
					        catch (WebSocketException ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Logger.LogWarning(ex, "send failed");
 | 
					            Logger.LogWarning(ex, "send failed");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public void Dispose() => _wantedFramesOnTick.Dispose();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ internal sealed class ControlsServerConnection(
 | 
				
			||||||
        Shoot = 0x05
 | 
					        Shoot = 0x05
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected override ValueTask HandleMessageAsync(Memory<byte> buffer)
 | 
					    protected override ValueTask HandleMessageLockedAsync(Memory<byte> buffer)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var type = (MessageType)buffer.Span[0];
 | 
					        var type = (MessageType)buffer.Span[0];
 | 
				
			||||||
        var control = (InputType)buffer.Span[1];
 | 
					        var control = (InputType)buffer.Span[1];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,19 +10,19 @@ internal sealed class PlayerInfoConnection(
 | 
				
			||||||
    ILogger logger,
 | 
					    ILogger logger,
 | 
				
			||||||
    WebSocket rawSocket,
 | 
					    WebSocket rawSocket,
 | 
				
			||||||
    MapEntityManager entityManager
 | 
					    MapEntityManager entityManager
 | 
				
			||||||
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)), IDisposable
 | 
					) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0))
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly SemaphoreSlim _wantedFrames = new(1);
 | 
					 | 
				
			||||||
    private readonly AppSerializerContext _context = new(new JsonSerializerOptions(JsonSerializerDefaults.Web));
 | 
					    private readonly AppSerializerContext _context = new(new JsonSerializerOptions(JsonSerializerDefaults.Web));
 | 
				
			||||||
 | 
					    private bool _wantsInfoOnTick;
 | 
				
			||||||
    private byte[] _lastMessage = [];
 | 
					    private byte[] _lastMessage = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected override ValueTask HandleMessageAsync(Memory<byte> buffer)
 | 
					    protected override ValueTask HandleMessageLockedAsync(Memory<byte> buffer)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var response = GetMessageToSend();
 | 
					        var response = GetMessageToSend();
 | 
				
			||||||
        if (response == null)
 | 
					        if (response == null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Logger.LogTrace("cannot respond directly, increasing wanted frames");
 | 
					            Logger.LogTrace("cannot respond directly, increasing wanted frames");
 | 
				
			||||||
            _wantedFrames.Release();
 | 
					            _wantsInfoOnTick = true;
 | 
				
			||||||
            return ValueTask.CompletedTask;
 | 
					            return ValueTask.CompletedTask;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,21 +30,18 @@ internal sealed class PlayerInfoConnection(
 | 
				
			||||||
        return Socket.SendTextAsync(response);
 | 
					        return Socket.SendTextAsync(response);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task OnGameTickAsync()
 | 
					    public ValueTask OnGameTickAsync() => LockedAsync(() =>
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (!await _wantedFrames.WaitAsync(TimeSpan.Zero))
 | 
					        if (!_wantsInfoOnTick)
 | 
				
			||||||
            return;
 | 
					            return ValueTask.CompletedTask;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var response = GetMessageToSend();
 | 
					        var response = GetMessageToSend();
 | 
				
			||||||
        if (response == null)
 | 
					        if (response == null)
 | 
				
			||||||
        {
 | 
					            return ValueTask.CompletedTask;
 | 
				
			||||||
            _wantedFrames.Release();
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Logger.LogTrace("responding indirectly");
 | 
					        Logger.LogTrace("responding indirectly");
 | 
				
			||||||
        await Socket.SendTextAsync(response);
 | 
					        return Socket.SendTextAsync(response);
 | 
				
			||||||
    }
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private byte[]? GetMessageToSend()
 | 
					    private byte[]? GetMessageToSend()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -77,6 +74,4 @@ internal sealed class PlayerInfoConnection(
 | 
				
			||||||
        str.Append(']');
 | 
					        str.Append(']');
 | 
				
			||||||
        return str.ToString();
 | 
					        return str.ToString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public void Dispose() => _wantedFrames.Dispose();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ internal sealed class PlayerServer(
 | 
				
			||||||
    private readonly Dictionary<string, Player> _players = [];
 | 
					    private readonly Dictionary<string, Player> _players = [];
 | 
				
			||||||
    private readonly SemaphoreSlim _mutex = new(1, 1);
 | 
					    private readonly SemaphoreSlim _mutex = new(1, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Player? GetOrAdd(string name)
 | 
					    public Player GetOrAdd(string name)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        _mutex.Wait();
 | 
					        _mutex.Wait();
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
| 
						 | 
					@ -67,6 +67,6 @@ internal sealed class PlayerServer(
 | 
				
			||||||
    public Task HandleClientAsync(WebSocket webSocket, Player player)
 | 
					    public Task HandleClientAsync(WebSocket webSocket, Player player)
 | 
				
			||||||
        => HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket, entityManager));
 | 
					        => HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket, entityManager));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task TickAsync(TimeSpan delta)
 | 
					    public ValueTask TickAsync(TimeSpan delta)
 | 
				
			||||||
        => ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync());
 | 
					        => ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync().AsTask());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,7 +48,7 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
 | 
					    public async ValueTask OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (DateTime.Now < _nextFrameAfter)
 | 
					        if (DateTime.Now < _nextFrameAfter)
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ internal abstract class WebsocketServer<T>(
 | 
				
			||||||
    public async Task StoppingAsync(CancellationToken cancellationToken)
 | 
					    public async Task StoppingAsync(CancellationToken cancellationToken)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        logger.LogInformation("closing connections");
 | 
					        logger.LogInformation("closing connections");
 | 
				
			||||||
        await Locked(async () =>
 | 
					        await LockedAsync(async () =>
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _closing = true;
 | 
					            _closing = true;
 | 
				
			||||||
            await Task.WhenAll(_connections.Select(c => c.CloseAsync()));
 | 
					            await Task.WhenAll(_connections.Select(c => c.CloseAsync()));
 | 
				
			||||||
| 
						 | 
					@ -22,35 +22,24 @@ internal abstract class WebsocketServer<T>(
 | 
				
			||||||
        logger.LogInformation("closed connections");
 | 
					        logger.LogInformation("closed connections");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected Task ParallelForEachConnectionAsync(Func<T, Task> body)
 | 
					    protected ValueTask ParallelForEachConnectionAsync(Func<T, Task> body) =>
 | 
				
			||||||
    {
 | 
					        LockedAsync(async () => await Task.WhenAll(_connections.Select(body)), CancellationToken.None);
 | 
				
			||||||
        _mutex.Wait();
 | 
					 | 
				
			||||||
        try
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return Task.WhenAll(_connections.Select(body));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        finally
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _mutex.Release();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private Task AddConnectionAsync(T connection) => Locked(() =>
 | 
					    private ValueTask AddConnectionAsync(T connection) => LockedAsync(async () =>
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (_closing)
 | 
					        if (_closing)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.LogWarning("refusing connection because server is shutting down");
 | 
					            logger.LogWarning("refusing connection because server is shutting down");
 | 
				
			||||||
            return connection.CloseAsync();
 | 
					            await connection.CloseAsync();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        _connections.Add(connection);
 | 
					        _connections.Add(connection);
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					 | 
				
			||||||
    }, CancellationToken.None);
 | 
					    }, CancellationToken.None);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private Task RemoveConnectionAsync(T connection) => Locked(() =>
 | 
					    private ValueTask RemoveConnectionAsync(T connection) => LockedAsync(() =>
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        _connections.Remove(connection);
 | 
					        _connections.Remove(connection);
 | 
				
			||||||
        return Task.CompletedTask;
 | 
					        return ValueTask.CompletedTask;
 | 
				
			||||||
    }, CancellationToken.None);
 | 
					    }, CancellationToken.None);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected async Task HandleClientAsync(T connection)
 | 
					    protected async Task HandleClientAsync(T connection)
 | 
				
			||||||
| 
						 | 
					@ -60,7 +49,7 @@ internal abstract class WebsocketServer<T>(
 | 
				
			||||||
        await RemoveConnectionAsync(connection);
 | 
					        await RemoveConnectionAsync(connection);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async Task Locked(Func<Task> action, CancellationToken cancellationToken)
 | 
					    private async ValueTask LockedAsync(Func<ValueTask> action, CancellationToken cancellationToken)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        await _mutex.WaitAsync(cancellationToken);
 | 
					        await _mutex.WaitAsync(cancellationToken);
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,9 @@ namespace TanksServer.Interactivity;
 | 
				
			||||||
internal abstract class WebsocketServerConnection(
 | 
					internal abstract class WebsocketServerConnection(
 | 
				
			||||||
    ILogger logger,
 | 
					    ILogger logger,
 | 
				
			||||||
    ByteChannelWebSocket socket
 | 
					    ByteChannelWebSocket socket
 | 
				
			||||||
)
 | 
					) : IDisposable
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    private readonly SemaphoreSlim _mutex = new(1);
 | 
				
			||||||
    protected readonly ByteChannelWebSocket Socket = socket;
 | 
					    protected readonly ByteChannelWebSocket Socket = socket;
 | 
				
			||||||
    protected readonly ILogger Logger = logger;
 | 
					    protected readonly ILogger Logger = logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,9 +18,24 @@ internal abstract class WebsocketServerConnection(
 | 
				
			||||||
    public async Task ReceiveAsync()
 | 
					    public async Task ReceiveAsync()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        await foreach (var buffer in Socket.ReadAllAsync())
 | 
					        await foreach (var buffer in Socket.ReadAllAsync())
 | 
				
			||||||
            await HandleMessageAsync(buffer);
 | 
					            await LockedAsync(() => HandleMessageLockedAsync(buffer));
 | 
				
			||||||
        Logger.LogTrace("done receiving");
 | 
					        Logger.LogTrace("done receiving");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected abstract ValueTask HandleMessageAsync(Memory<byte> buffer);
 | 
					    protected abstract ValueTask HandleMessageLockedAsync(Memory<byte> buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected async ValueTask LockedAsync(Func<ValueTask> action)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        await _mutex.WaitAsync();
 | 
				
			||||||
 | 
					        try
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            await action();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        finally
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _mutex.Release();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void Dispose() => _mutex.Dispose();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue