diff --git a/TanksServer/ClientScreenServer.cs b/TanksServer/ClientScreenServer.cs index 5ab644d..4b7fc44 100644 --- a/TanksServer/ClientScreenServer.cs +++ b/TanksServer/ClientScreenServer.cs @@ -7,8 +7,9 @@ namespace TanksServer; internal sealed class ClientScreenServer( ILogger logger, - ILoggerFactory loggerFactory -) : IHostedLifecycleService + ILoggerFactory loggerFactory, + MapDrawer drawer +) : IHostedLifecycleService, ITickStep { private readonly List _connections = new(); @@ -16,45 +17,57 @@ internal sealed class ClientScreenServer( { logger.LogDebug("HandleClient"); var connection = - new ClientScreenServerConnection(socket, loggerFactory.CreateLogger()); + new ClientScreenServerConnection(socket, loggerFactory.CreateLogger(), this); _connections.Add(connection); return connection.Done; } - public Task Send(DisplayPixelBuffer buf) - { - logger.LogDebug("Sending buffer to {} clients", _connections.Count); - return Task.WhenAll(_connections.Select(c => c.Send(buf))); - } - public Task StoppingAsync(CancellationToken cancellationToken) { logger.LogInformation("closing connections"); return Task.WhenAll(_connections.Select(c => c.CloseAsync())); } + public Task TickAsync() + { + logger.LogTrace("Sending buffer to {} clients", _connections.Count); + return Task.WhenAll(_connections.Select(c => c.SendAsync(drawer.LastFrame))); + } + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} - -internal sealed class ClientScreenServerConnection(WebSocket webSocket, ILogger logger) - : EasyWebSocket(webSocket, logger, ArraySegment.Empty) -{ - private bool _wantsNewFrame = true; - - public Task Send(DisplayPixelBuffer buf) + + private void Remove(ClientScreenServerConnection connection) => _connections.Remove(connection); + + private sealed class ClientScreenServerConnection( + WebSocket webSocket, + ILogger logger, + ClientScreenServer server + ) : EasyWebSocket(webSocket, logger, ArraySegment.Empty) { - if (!_wantsNewFrame) + private bool _wantsNewFrame = true; + + public Task SendAsync(DisplayPixelBuffer buf) + { + if (!_wantsNewFrame) + return Task.CompletedTask; + _wantsNewFrame = false; + return TrySendAsync(buf.Data); + } + + protected override Task ReceiveAsync(ArraySegment buffer) + { + _wantsNewFrame = true; return Task.CompletedTask; - return SendAsync(buf.Data); - } + } - protected override Task ReceiveAsync(ArraySegment buffer) - { - _wantsNewFrame = true; - return Task.CompletedTask; + protected override Task ClosingAsync() + { + server.Remove(this); + return Task.CompletedTask; + } } } diff --git a/TanksServer/EasyWebSocket.cs b/TanksServer/EasyWebSocket.cs index 9261f63..d043fcb 100644 --- a/TanksServer/EasyWebSocket.cs +++ b/TanksServer/EasyWebSocket.cs @@ -12,6 +12,7 @@ internal abstract class EasyWebSocket private readonly WebSocket _socket; private readonly Task _readLoop; private readonly ArraySegment _buffer; + private int _closed; protected EasyWebSocket(WebSocket socket, ILogger logger, ArraySegment buffer) { @@ -33,21 +34,38 @@ internal abstract class EasyWebSocket await ReceiveAsync(_buffer[..response.Count]); } while (_socket.State == WebSocketState.Open); + + await CloseAsync(); } protected abstract Task ReceiveAsync(ArraySegment buffer); + protected abstract Task ClosingAsync(); - protected Task SendAsync(byte[] data) + protected async Task TrySendAsync(byte[] data) { + if (_socket.State != WebSocketState.Open) + await CloseAsync(); + _logger.LogTrace("sending {} bytes of data", _buffer.Count); - return _socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None); + + try + { + await _socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None); + } + catch (WebSocketException wsEx) + { + _logger.LogDebug(wsEx, "send failed"); + } } public async Task CloseAsync() { + if (Interlocked.Exchange(ref _closed, 1) == 1) + return; _logger.LogDebug("closing socket"); await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); await _readLoop; + await ClosingAsync(); _completionSource.SetResult(); } } diff --git a/TanksServer/GameTickService.cs b/TanksServer/GameTickService.cs new file mode 100644 index 0000000..dc5d629 --- /dev/null +++ b/TanksServer/GameTickService.cs @@ -0,0 +1,38 @@ +using System.Threading; +using Microsoft.Extensions.Hosting; + +namespace TanksServer; + +public class GameTickService(IEnumerable steps) : IHostedService +{ + private readonly CancellationTokenSource _cancellation = new(); + private readonly List _steps = steps.ToList(); + private Task? _run; + + public Task StartAsync(CancellationToken cancellationToken) + { + _run = RunAsync(); + return Task.CompletedTask; + } + + private async Task RunAsync() + { + while (!_cancellation.IsCancellationRequested) + { + foreach (var step in _steps) + await step.TickAsync(); + await Task.Delay(1000 / 250); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _cancellation.CancelAsync(); + if (_run != null) await _run; + } +} + +public interface ITickStep +{ + Task TickAsync(); +} diff --git a/TanksServer/MapDrawer.cs b/TanksServer/MapDrawer.cs index 7f34012..58cc91f 100644 --- a/TanksServer/MapDrawer.cs +++ b/TanksServer/MapDrawer.cs @@ -1,10 +1,10 @@ namespace TanksServer; -internal class MapDrawer(MapService map) +internal class MapDrawer(MapService map):ITickStep { private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn; - public void DrawInto(DisplayPixelBuffer buf) + private void DrawInto(DisplayPixelBuffer buf) { for (var tileY = 0; tileY < MapService.TilesPerColumn; tileY++) for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++) @@ -23,7 +23,7 @@ internal class MapDrawer(MapService map) } } - public DisplayPixelBuffer CreateGameFieldPixelBuffer() + private DisplayPixelBuffer CreateGameFieldPixelBuffer() { var data = new byte[10 + GameFieldPixelCount / 8]; var result = new DisplayPixelBuffer(data) @@ -37,4 +37,20 @@ internal class MapDrawer(MapService map) }; return result; } + + private DisplayPixelBuffer? _lastFrame; + + public DisplayPixelBuffer LastFrame + { + get => _lastFrame ?? throw new InvalidOperationException("first frame not yet drawn"); + private set => _lastFrame = value; + } + + public Task TickAsync() + { + var buffer = CreateGameFieldPixelBuffer(); + DrawInto(buffer); + LastFrame = buffer; + return Task.CompletedTask; + } } diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index b6bb9df..f55a2c3 100644 --- a/TanksServer/Program.cs +++ b/TanksServer/Program.cs @@ -12,14 +12,8 @@ internal static class Program { var app = Configure(args); - var display = app.Services.GetRequiredService(); - var mapDrawer = app.Services.GetRequiredService(); var clientScreenServer = app.Services.GetRequiredService(); - var buffer = mapDrawer.CreateGameFieldPixelBuffer(); - mapDrawer.DrawInto(buffer); - await display.Send(buffer); - var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client")); app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider }); app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider }); @@ -46,10 +40,16 @@ internal static class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => sp.GetRequiredService()); + builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); + builder.Services.AddSingleton(sp => sp.GetRequiredService()); + builder.Services.AddHostedService(); + return builder.Build(); } }