diff --git a/TanksServer/GameLogic/CollideBulletsWithMap.cs b/TanksServer/GameLogic/CollideBulletsWithMap.cs index f53faa3..f4934a7 100644 --- a/TanksServer/GameLogic/CollideBulletsWithMap.cs +++ b/TanksServer/GameLogic/CollideBulletsWithMap.cs @@ -1,6 +1,10 @@ namespace TanksServer.GameLogic; -internal sealed class CollideBulletsWithMap(BulletManager bullets, MapService map) : ITickStep +internal sealed class CollideBulletsWithMap( + BulletManager bullets, + MapService map, + IOptions options +) : ITickStep { public Task TickAsync(TimeSpan _) { @@ -14,7 +18,8 @@ internal sealed class CollideBulletsWithMap(BulletManager bullets, MapService ma if (!map.Current.IsWall(pixel)) return false; - map.Current.DestroyWallAt(pixel); + if (options.Value.DestructibleWalls) + map.Current.DestroyWallAt(pixel); return true; } } diff --git a/TanksServer/GameLogic/GameRulesConfiguration.cs b/TanksServer/GameLogic/GameRulesConfiguration.cs new file mode 100644 index 0000000..a52d793 --- /dev/null +++ b/TanksServer/GameLogic/GameRulesConfiguration.cs @@ -0,0 +1,6 @@ +namespace TanksServer.GameLogic; + +public class GameRulesConfiguration +{ + public bool DestructibleWalls { get; set; } = true; +} diff --git a/TanksServer/Graphics/DrawTanksStep.cs b/TanksServer/Graphics/DrawTanksStep.cs index 0283cda..739b913 100644 --- a/TanksServer/Graphics/DrawTanksStep.cs +++ b/TanksServer/Graphics/DrawTanksStep.cs @@ -1,4 +1,3 @@ -using DisplayCommands; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using TanksServer.GameLogic; diff --git a/TanksServer/Graphics/IDrawStep.cs b/TanksServer/Graphics/IDrawStep.cs index 2913304..0d61c77 100644 --- a/TanksServer/Graphics/IDrawStep.cs +++ b/TanksServer/Graphics/IDrawStep.cs @@ -1,5 +1,3 @@ -using DisplayCommands; - namespace TanksServer.Graphics; internal interface IDrawStep diff --git a/TanksServer/Interactivity/ClientScreenServer.cs b/TanksServer/Interactivity/ClientScreenServer.cs index c853e7a..e3861be 100644 --- a/TanksServer/Interactivity/ClientScreenServer.cs +++ b/TanksServer/Interactivity/ClientScreenServer.cs @@ -8,10 +8,12 @@ namespace TanksServer.Interactivity; internal sealed class ClientScreenServer( ILogger logger, - ILoggerFactory loggerFactory + ILoggerFactory loggerFactory, + IOptions hostConfig ) : IHostedLifecycleService, IFrameConsumer { private readonly ConcurrentDictionary _connections = new(); + private readonly TimeSpan _minFrameTime = TimeSpan.FromMilliseconds(hostConfig.Value.ClientDisplayMinFrameTimeMs); private bool _closing; public Task StoppingAsync(CancellationToken cancellationToken) @@ -34,6 +36,7 @@ internal sealed class ClientScreenServer( socket, loggerFactory.CreateLogger(), this, + _minFrameTime, playerGuid); var added = _connections.TryAdd(connection, 0); Debug.Assert(added); diff --git a/TanksServer/Interactivity/ClientScreenServerConnection.cs b/TanksServer/Interactivity/ClientScreenServerConnection.cs index 1fbe72b..75e97d7 100644 --- a/TanksServer/Interactivity/ClientScreenServerConnection.cs +++ b/TanksServer/Interactivity/ClientScreenServerConnection.cs @@ -11,18 +11,23 @@ internal sealed class ClientScreenServerConnection : IDisposable private readonly ILogger _logger; private readonly ClientScreenServer _server; private readonly SemaphoreSlim _wantedFrames = new(1); - private readonly Guid? _playerGuid = null; - private readonly PlayerScreenData? _playerScreenData = null; + private readonly Guid? _playerGuid; + private readonly PlayerScreenData? _playerScreenData; + private readonly TimeSpan _minFrameTime; + + private DateTime _nextFrameAfter = DateTime.Now; public ClientScreenServerConnection( WebSocket webSocket, ILogger logger, ClientScreenServer server, + TimeSpan minFrameTime, Guid? playerGuid = null ) { _server = server; _logger = logger; + _minFrameTime = minFrameTime; _playerGuid = playerGuid; if (playerGuid.HasValue) @@ -42,12 +47,17 @@ internal sealed class ClientScreenServerConnection : IDisposable public async Task SendAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid) { + if (_nextFrameAfter > DateTime.Now) + return; + if (!await _wantedFrames.WaitAsync(TimeSpan.Zero)) { _logger.LogTrace("client does not want a frame yet"); return; } + _nextFrameAfter = DateTime.Today + _minFrameTime; + if (_playerScreenData != null) RefreshPlayerSpecificData(gamePixelGrid); diff --git a/TanksServer/Interactivity/SendToServicePointDisplay.cs b/TanksServer/Interactivity/SendToServicePointDisplay.cs index af77c66..7773834 100644 --- a/TanksServer/Interactivity/SendToServicePointDisplay.cs +++ b/TanksServer/Interactivity/SendToServicePointDisplay.cs @@ -16,17 +16,22 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer private readonly ILogger _logger; private readonly PlayerServer _players; private readonly Cp437Grid _scoresBuffer; - private DateTime _nextFailLog = DateTime.Now; + private readonly TimeSpan _minFrameTime; + + private DateTime _nextFailLogAfter = DateTime.Now; + private DateTime _nextFrameAfter = DateTime.Now; public SendToServicePointDisplay( PlayerServer players, ILogger logger, - IDisplayConnection displayConnection + IDisplayConnection displayConnection, + IOptions hostOptions ) { _players = players; _logger = logger; _displayConnection = displayConnection; + _minFrameTime = TimeSpan.FromMilliseconds(hostOptions.Value.ServicePointDisplayMinFrameTimeMs); var localIp = _displayConnection.GetLocalIPv4().Split('.'); Debug.Assert(localIp.Length == 4); @@ -42,7 +47,12 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer public async Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels) { + if (DateTime.Now < _nextFrameAfter) + return; + _nextFrameAfter = DateTime.Now + _minFrameTime; + RefreshScores(); + try { await _displayConnection.SendBitmapLinearWindowAsync(0, 0, observerPixels); @@ -50,10 +60,10 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer } catch (SocketException ex) { - if (DateTime.Now > _nextFailLog) + if (DateTime.Now > _nextFailLogAfter) { _logger.LogWarning("could not send data to service point display: {}", ex.Message); - _nextFailLog = DateTime.Now + TimeSpan.FromSeconds(5); + _nextFailLogAfter = DateTime.Now + TimeSpan.FromSeconds(5); } } } diff --git a/TanksServer/Models/HostConfiguration.cs b/TanksServer/Models/HostConfiguration.cs new file mode 100644 index 0000000..41e3d5a --- /dev/null +++ b/TanksServer/Models/HostConfiguration.cs @@ -0,0 +1,10 @@ +namespace TanksServer.Models; + +public class HostConfiguration +{ + public bool EnableServicePointDisplay { get; set; } = true; + + public int ServicePointDisplayMinFrameTimeMs { get; set; } = 25; + + public int ClientDisplayMinFrameTimeMs { get; set; } = 25; +} diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index 934443e..ca15443 100644 --- a/TanksServer/Program.cs +++ b/TanksServer/Program.cs @@ -3,6 +3,7 @@ using DisplayCommands; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using TanksServer.GameLogic; @@ -73,7 +74,7 @@ public static class Program return Results.Empty; }); - app.MapGet("/map", () =>mapService.MapNames); + app.MapGet("/map", () => mapService.MapNames); app.MapPost("/map", ([FromQuery] string name) => { @@ -112,6 +113,11 @@ public static class Program builder.Services.AddHttpLogging(_ => { }); + builder.Services.Configure(builder.Configuration.GetSection("Host")); + var hostConfiguration = builder.Configuration.GetSection("Host").Get(); + if (hostConfiguration == null) + throw new InvalidOperationException("'Host' configuration missing"); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -137,7 +143,6 @@ public static class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); @@ -145,7 +150,13 @@ public static class Program builder.Configuration.GetSection("Tanks")); builder.Services.Configure( builder.Configuration.GetSection("Players")); - builder.Services.AddDisplay(builder.Configuration.GetSection("ServicePointDisplay")); + builder.Services.Configure(builder.Configuration.GetSection("GameRules")); + + if (hostConfiguration.EnableServicePointDisplay) + { + builder.Services.AddSingleton(); + builder.Services.AddDisplay(builder.Configuration.GetSection("ServicePointDisplay")); + } var app = builder.Build(); diff --git a/TanksServer/appsettings.json b/TanksServer/appsettings.json index f086ed1..7e04eb8 100644 --- a/TanksServer/appsettings.json +++ b/TanksServer/appsettings.json @@ -1,33 +1,40 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "TanksServer": "Debug", - "Microsoft.AspNetCore.HttpLogging": "Information" + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "TanksServer": "Debug", + "Microsoft.AspNetCore.HttpLogging": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://localhost:3000" + } + } + }, + "ServicePointDisplay": { + "Hostname": "172.23.42.29", + "Port": 2342 + }, + "Tanks": { + "MoveSpeed": 37.5, + "TurnSpeed": 0.5, + "ShootDelayMs": 450, + "BulletSpeed": 75 + }, + "GameRules": { + "DestructibleWalls": true + }, + "Players": { + "SpawnDelayMs": 3000, + "IdleTimeoutMs": 30000 + }, + "Host": { + "EnableServicePointDisplay": true, + "ServicePointDisplayMinFrameTimeMs": 25, + "ClientScreenMinFrameTime": 5 } - }, - "AllowedHosts": "*", - "Kestrel": { - "Endpoints": { - "Http": { - "Url": "http://localhost:3000" - } - } - }, - "ServicePointDisplay": { - "Enable": true, - "Hostname": "172.23.42.29", - "Port": 2342 - }, - "Tanks": { - "MoveSpeed": 37.5, - "TurnSpeed": 0.5, - "ShootDelayMs": 450, - "BulletSpeed": 75 - }, - "Players": { - "SpawnDelayMs": 3000, - "IdleTimeoutMs": 30000 - } }