From 531a3b5866e2724c87e93c5535f9806f063ccce7 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 3 May 2024 16:43:15 +0200 Subject: [PATCH 1/9] add different icon for smart bullet --- .../TanksServer/Graphics/DrawPowerUpsStep.cs | 34 +++++++++++------- .../TanksServer/assets/powerup_smart.png | Bin 0 -> 126 bytes 2 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 tanks-backend/TanksServer/assets/powerup_smart.png diff --git a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs index c67a5e6..10cbdae 100644 --- a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs @@ -4,26 +4,34 @@ namespace TanksServer.Graphics; internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawStep { - private readonly Sprite _explosiveSprite = Sprite.FromImageFile("assets/powerup_explosive.png"); + private readonly Sprite _genericSprite = Sprite.FromImageFile("assets/powerup_explosive.png"); + private readonly Sprite _smartSprite = Sprite.FromImageFile("assets/powerup_smart.png"); public void Draw(GamePixelGrid pixels) { foreach (var powerUp in entityManager.PowerUps) { - var position = powerUp.Bounds.TopLeft; + var sprite = _genericSprite; + if (powerUp is { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart }) + sprite = _smartSprite; - for (byte dy = 0; dy < MapService.TileSize; dy++) - for (byte dx = 0; dx < MapService.TileSize; dx++) - { - var pixelState = _explosiveSprite[dx, dy]; - if (!pixelState.HasValue) - continue; + DrawPowerUp(pixels, sprite, powerUp.Bounds.TopLeft); + } + } - var (x, y) = position.GetPixelRelative(dx, dy); - pixels[x, y].EntityType = pixelState.Value - ? GamePixelEntityType.PowerUp - : null; - } + private static void DrawPowerUp(GamePixelGrid pixels, Sprite sprite, PixelPosition position) + { + for (byte dy = 0; dy < MapService.TileSize; dy++) + for (byte dx = 0; dx < MapService.TileSize; dx++) + { + var pixelState = sprite[dx, dy]; + if (!pixelState.HasValue) + continue; + + var (x, y) = position.GetPixelRelative(dx, dy); + pixels[x, y].EntityType = pixelState.Value + ? GamePixelEntityType.PowerUp + : null; } } } diff --git a/tanks-backend/TanksServer/assets/powerup_smart.png b/tanks-backend/TanksServer/assets/powerup_smart.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ceff5cff32ea537dbfe25c242cc014771fa9b6 GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#Vf Date: Fri, 3 May 2024 16:45:35 +0200 Subject: [PATCH 2/9] update generic power up shape --- .../TanksServer/Graphics/DrawPowerUpsStep.cs | 10 ++++++---- .../TanksServer/assets/powerup_explosive.png | Bin 112 -> 0 bytes .../TanksServer/assets/powerup_generic.png | Bin 0 -> 113 bytes 3 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 tanks-backend/TanksServer/assets/powerup_explosive.png create mode 100644 tanks-backend/TanksServer/assets/powerup_generic.png diff --git a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs index 10cbdae..a49f77f 100644 --- a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs @@ -4,16 +4,18 @@ namespace TanksServer.Graphics; internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawStep { - private readonly Sprite _genericSprite = Sprite.FromImageFile("assets/powerup_explosive.png"); + private readonly Sprite _genericSprite = Sprite.FromImageFile("assets/powerup_generic.png"); private readonly Sprite _smartSprite = Sprite.FromImageFile("assets/powerup_smart.png"); public void Draw(GamePixelGrid pixels) { foreach (var powerUp in entityManager.PowerUps) { - var sprite = _genericSprite; - if (powerUp is { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart }) - sprite = _smartSprite; + var sprite = powerUp switch + { + { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart } => _smartSprite, + _ => _genericSprite + }; DrawPowerUp(pixels, sprite, powerUp.Bounds.TopLeft); } diff --git a/tanks-backend/TanksServer/assets/powerup_explosive.png b/tanks-backend/TanksServer/assets/powerup_explosive.png deleted file mode 100644 index f1854d2afabcb3d6fab333b0b5788f8750fbebb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#VfMnx@Lw(4T?>GE7(8A5T-G@y GGywp70v*)= literal 0 HcmV?d00001 From 57d2a11fd55b0cfa58ebdeaeca016ca50d3acf4b Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 4 May 2024 13:16:40 +0200 Subject: [PATCH 3/9] deduplicate player info message --- .../TanksServer/Interactivity/PlayerInfoConnection.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs index cd4d461..29ac8b6 100644 --- a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs @@ -48,6 +48,14 @@ internal sealed class PlayerInfoConnection : WebsocketServerConnection await Task.Yield(); var response = await GenerateMessageAsync(); + + var shouldDropPacket = _lastMessage != null && response.Memory.Span.SequenceEqual(_lastMessage.Memory.Span); + if (shouldDropPacket) + { + response.Dispose(); + return; + } + var wantsNow = Interlocked.Exchange(ref _wantsInfoOnTick, 0) != 0; if (wantsNow) From f477d1e5de4696eff5e9d8b35a8096ece655705d Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 4 May 2024 13:38:13 +0200 Subject: [PATCH 4/9] seems like I am still not done with the connections --- .../ClientScreenServerConnection.cs | 85 ++++++++++++------- tanks-backend/TanksServer/TanksServer.csproj | 7 +- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs b/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs index 4cda7bb..6a0616d 100644 --- a/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs @@ -1,18 +1,20 @@ using System.Buffers; +using System.Diagnostics; using System.Net.WebSockets; using DisplayCommands; +using DotNext.Threading; using TanksServer.Graphics; namespace TanksServer.Interactivity; -internal sealed class ClientScreenServerConnection : WebsocketServerConnection +internal sealed class ClientScreenServerConnection + : WebsocketServerConnection, IDisposable { - private sealed record class Package(IMemoryOwner Pixels, IMemoryOwner? PlayerData); - private readonly BufferPool _bufferPool; private readonly PlayerScreenData? _playerDataBuilder; private readonly Player? _player; - private int _wantsFrameOnTick = 1; + private readonly AsyncAutoResetEvent _nextPackageEvent = new(false, 1); + private int _runningMessageHandlers = 0; private Package? _next; public ClientScreenServerConnection( @@ -32,23 +34,49 @@ internal sealed class ClientScreenServerConnection : WebsocketServerConnection protected override ValueTask HandleMessageAsync(Memory _) { - if (_wantsFrameOnTick != 0) - return ValueTask.CompletedTask; + if (Interlocked.Increment(ref _runningMessageHandlers) == 1) + return Core(); - var package = Interlocked.Exchange(ref _next, null); - if (package != null) - return SendAndDisposeAsync(package); - - // the delay between one exchange and this set could be enough for another frame to complete - // this would mean the client simply drops a frame, so this should be fine - _wantsFrameOnTick = 1; + Interlocked.Decrement(ref _runningMessageHandlers); return ValueTask.CompletedTask; + + async ValueTask Core() + { + await _nextPackageEvent.WaitAsync(); + var package = Interlocked.Exchange(ref _next, null); + if (package == null) + throw new UnreachableException("package should be set here"); + await SendAndDisposeAsync(package); + Interlocked.Decrement(ref _runningMessageHandlers); + } } public async Task OnGameTickAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid) { await Task.Yield(); + var next = BuildNextPackage(pixels, gamePixelGrid); + var oldNext = Interlocked.Exchange(ref _next, next); + + _nextPackageEvent.Set(); + + oldNext?.Dispose(); + } + + public override ValueTask RemovedAsync() + { + _player?.DecrementConnectionCount(); + return ValueTask.CompletedTask; + } + + public void Dispose() + { + _nextPackageEvent.Dispose(); + Interlocked.Exchange(ref _next, null)?.Dispose(); + } + + private Package BuildNextPackage(PixelGrid pixels, GamePixelGrid gamePixelGrid) + { var nextPixels = _bufferPool.Rent(pixels.Data.Length); pixels.Data.CopyTo(nextPixels.Memory); @@ -61,21 +89,7 @@ internal sealed class ClientScreenServerConnection : WebsocketServerConnection } var next = new Package(nextPixels, nextPlayerData); - if (Interlocked.Exchange(ref _wantsFrameOnTick, 0) != 0) - { - await SendAndDisposeAsync(next); - return; - } - - var oldNext = Interlocked.Exchange(ref _next, next); - oldNext?.Pixels.Dispose(); - oldNext?.PlayerData?.Dispose(); - } - - public override ValueTask RemovedAsync() - { - _player?.DecrementConnectionCount(); - return ValueTask.CompletedTask; + return next; } private async ValueTask SendAndDisposeAsync(Package package) @@ -92,8 +106,19 @@ internal sealed class ClientScreenServerConnection : WebsocketServerConnection } finally { - package.Pixels.Dispose(); - package.PlayerData?.Dispose(); + package.Dispose(); + } + } + + private sealed record class Package( + IMemoryOwner Pixels, + IMemoryOwner? PlayerData + ) : IDisposable + { + public void Dispose() + { + Pixels.Dispose(); + PlayerData?.Dispose(); } } } diff --git a/tanks-backend/TanksServer/TanksServer.csproj b/tanks-backend/TanksServer/TanksServer.csproj index 307de71..4cce028 100644 --- a/tanks-backend/TanksServer/TanksServer.csproj +++ b/tanks-backend/TanksServer/TanksServer.csproj @@ -7,9 +7,10 @@ - - - + + + + From fa8a723ff99b8aaf7e4e361b87c8ec01e0dcaf66 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 4 May 2024 14:25:37 +0200 Subject: [PATCH 5/9] deduplicate connection logic --- tank-frontend/src/ClientScreen.tsx | 21 +++++- tank-frontend/src/PlayerInfo.tsx | 4 +- tank-frontend/src/serverCalls.tsx | 4 +- .../ClientScreenServerConnection.cs | 75 +++++-------------- .../Interactivity/ControlsServerConnection.cs | 6 +- .../DroppablePackageRequestConnection.cs | 50 +++++++++++++ .../Interactivity/PlayerInfoConnection.cs | 53 ++++--------- .../Interactivity/WebsocketServer.cs | 2 +- .../WebsocketServerConnection.cs | 6 +- 9 files changed, 108 insertions(+), 113 deletions(-) create mode 100644 tanks-backend/TanksServer/Interactivity/DroppablePackageRequestConnection.cs diff --git a/tank-frontend/src/ClientScreen.tsx b/tank-frontend/src/ClientScreen.tsx index df0e871..ba78edd 100644 --- a/tank-frontend/src/ClientScreen.tsx +++ b/tank-frontend/src/ClientScreen.tsx @@ -1,7 +1,8 @@ -import {useEffect, useRef} from 'react'; +import {useEffect, useRef, useState} from 'react'; import './ClientScreen.css'; import {hslToString, Theme} from './theme.ts'; import {makeApiUrl, useMyWebSocket} from './serverCalls.tsx'; +import {ReadyState} from 'react-use-websocket'; const pixelsPerRow = 352; const pixelsPerCol = 160; @@ -101,6 +102,7 @@ export default function ClientScreen({theme, player}: { player: string | null }) { const canvasRef = useRef(null); + const [shouldSendMessage, setShouldSendMessage] = useState(false); const url = makeApiUrl('/screen', 'ws'); if (player && player !== '') @@ -109,13 +111,24 @@ export default function ClientScreen({theme, player}: { const { lastMessage, sendMessage, - getWebSocket - } = useMyWebSocket(url.toString(), {}); + getWebSocket, + readyState + } = useMyWebSocket(url.toString(), { + onOpen: _ => setShouldSendMessage(true) + }); const socket = getWebSocket(); if (socket) (socket as WebSocket).binaryType = 'arraybuffer'; + useEffect(() => { + if (!shouldSendMessage || readyState !== ReadyState.OPEN) + return; + setShouldSendMessage(false); + sendMessage(''); + }, [readyState, shouldSendMessage]); + + useEffect(() => { if (lastMessage === null) return; @@ -155,7 +168,7 @@ export default function ClientScreen({theme, player}: { if (ignore) return; - sendMessage(''); + setShouldSendMessage(true); }; start(); diff --git a/tank-frontend/src/PlayerInfo.tsx b/tank-frontend/src/PlayerInfo.tsx index 17e67d1..2916d04 100644 --- a/tank-frontend/src/PlayerInfo.tsx +++ b/tank-frontend/src/PlayerInfo.tsx @@ -28,7 +28,6 @@ type TankInfo = { readonly moving: boolean; } - type PlayerInfoMessage = { readonly name: string; readonly scores: Scores; @@ -48,7 +47,8 @@ export default function PlayerInfo({player}: { player: string }) { readyState, sendMessage } = useMyWebSocket(url.toString(), { - onMessage: () => setShouldSendMessage(true) + onMessage: () => setShouldSendMessage(true), + onOpen: _ => setShouldSendMessage(true) }); useEffect(() => { diff --git a/tank-frontend/src/serverCalls.tsx b/tank-frontend/src/serverCalls.tsx index d2cc9f5..6e003fb 100644 --- a/tank-frontend/src/serverCalls.tsx +++ b/tank-frontend/src/serverCalls.tsx @@ -19,10 +19,10 @@ export type Player = { readonly scores: Scores; }; -export function useMyWebSocket(url: string, options: Options) { +export function useMyWebSocket(url: string, options: Options = {}) { return useWebSocket(url, { shouldReconnect: () => true, - reconnectAttempts: 5, + reconnectAttempts: 2, onReconnectStop: () => alert('server connection failed. please reload.'), ...options }); diff --git a/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs b/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs index 6a0616d..6208cbf 100644 --- a/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs @@ -1,21 +1,16 @@ using System.Buffers; -using System.Diagnostics; using System.Net.WebSockets; using DisplayCommands; -using DotNext.Threading; using TanksServer.Graphics; namespace TanksServer.Interactivity; internal sealed class ClientScreenServerConnection - : WebsocketServerConnection, IDisposable + : DroppablePackageRequestConnection { private readonly BufferPool _bufferPool; private readonly PlayerScreenData? _playerDataBuilder; private readonly Player? _player; - private readonly AsyncAutoResetEvent _nextPackageEvent = new(false, 1); - private int _runningMessageHandlers = 0; - private Package? _next; public ClientScreenServerConnection( WebSocket webSocket, @@ -32,47 +27,11 @@ internal sealed class ClientScreenServerConnection : new PlayerScreenData(logger, player); } - protected override ValueTask HandleMessageAsync(Memory _) - { - if (Interlocked.Increment(ref _runningMessageHandlers) == 1) - return Core(); - - Interlocked.Decrement(ref _runningMessageHandlers); - return ValueTask.CompletedTask; - - async ValueTask Core() - { - await _nextPackageEvent.WaitAsync(); - var package = Interlocked.Exchange(ref _next, null); - if (package == null) - throw new UnreachableException("package should be set here"); - await SendAndDisposeAsync(package); - Interlocked.Decrement(ref _runningMessageHandlers); - } - } - public async Task OnGameTickAsync(PixelGrid pixels, GamePixelGrid gamePixelGrid) { await Task.Yield(); - var next = BuildNextPackage(pixels, gamePixelGrid); - var oldNext = Interlocked.Exchange(ref _next, next); - - _nextPackageEvent.Set(); - - oldNext?.Dispose(); - } - - public override ValueTask RemovedAsync() - { - _player?.DecrementConnectionCount(); - return ValueTask.CompletedTask; - } - - public void Dispose() - { - _nextPackageEvent.Dispose(); - Interlocked.Exchange(ref _next, null)?.Dispose(); + SetNextPackage(next); } private Package BuildNextPackage(PixelGrid pixels, GamePixelGrid gamePixelGrid) @@ -80,19 +39,17 @@ internal sealed class ClientScreenServerConnection var nextPixels = _bufferPool.Rent(pixels.Data.Length); pixels.Data.CopyTo(nextPixels.Memory); - IMemoryOwner? nextPlayerData = null; - if (_playerDataBuilder != null) - { - var data = _playerDataBuilder.Build(gamePixelGrid); - nextPlayerData = _bufferPool.Rent(data.Length); - data.CopyTo(nextPlayerData.Memory); - } + if (_playerDataBuilder == null) + return new Package(nextPixels, null); - var next = new Package(nextPixels, nextPlayerData); - return next; + var data = _playerDataBuilder.Build(gamePixelGrid); + var nextPlayerData = _bufferPool.Rent(data.Length); + data.CopyTo(nextPlayerData.Memory); + + return new Package(nextPixels, nextPlayerData); } - private async ValueTask SendAndDisposeAsync(Package package) + protected override async ValueTask SendPackageAsync(Package package) { try { @@ -104,13 +61,15 @@ internal sealed class ClientScreenServerConnection { Logger.LogWarning(ex, "send failed"); } - finally - { - package.Dispose(); - } } - private sealed record class Package( + public override void Dispose() + { + base.Dispose(); + _player?.DecrementConnectionCount(); + } + + internal sealed record class Package( IMemoryOwner Pixels, IMemoryOwner? PlayerData ) : IDisposable diff --git a/tanks-backend/TanksServer/Interactivity/ControlsServerConnection.cs b/tanks-backend/TanksServer/Interactivity/ControlsServerConnection.cs index edceb3d..3059dad 100644 --- a/tanks-backend/TanksServer/Interactivity/ControlsServerConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/ControlsServerConnection.cs @@ -69,9 +69,5 @@ internal sealed class ControlsServerConnection : WebsocketServerConnection return ValueTask.CompletedTask; } - public override ValueTask RemovedAsync() - { - _player.DecrementConnectionCount(); - return ValueTask.CompletedTask; - } + public override void Dispose() => _player.DecrementConnectionCount(); } diff --git a/tanks-backend/TanksServer/Interactivity/DroppablePackageRequestConnection.cs b/tanks-backend/TanksServer/Interactivity/DroppablePackageRequestConnection.cs new file mode 100644 index 0000000..19956a9 --- /dev/null +++ b/tanks-backend/TanksServer/Interactivity/DroppablePackageRequestConnection.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; +using DotNext.Threading; + +namespace TanksServer.Interactivity; + +internal abstract class DroppablePackageRequestConnection( + ILogger logger, + ByteChannelWebSocket socket +) : WebsocketServerConnection(logger, socket), IDisposable + where TPackage : class, IDisposable +{ + private readonly AsyncAutoResetEvent _nextPackageEvent = new(false, 1); + private int _runningMessageHandlers = 0; + private TPackage? _next; + + protected override ValueTask HandleMessageAsync(Memory _) + { + if (Interlocked.Increment(ref _runningMessageHandlers) == 1) + return Core(); + + // client has requested multiple frames, ignoring duplicate requests + Interlocked.Decrement(ref _runningMessageHandlers); + return ValueTask.CompletedTask; + + async ValueTask Core() + { + await _nextPackageEvent.WaitAsync(); + var package = Interlocked.Exchange(ref _next, null); + if (package == null) + throw new UnreachableException("package should be set here"); + await SendPackageAsync(package); + Interlocked.Decrement(ref _runningMessageHandlers); + } + } + + protected void SetNextPackage(TPackage next) + { + var oldNext = Interlocked.Exchange(ref _next, next); + _nextPackageEvent.Set(); + oldNext?.Dispose(); + } + + protected abstract ValueTask SendPackageAsync(TPackage package); + + public override void Dispose() + { + _nextPackageEvent.Dispose(); + Interlocked.Exchange(ref _next, null)?.Dispose(); + } +} diff --git a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs index 29ac8b6..04661c3 100644 --- a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs @@ -6,18 +6,14 @@ using TanksServer.GameLogic; namespace TanksServer.Interactivity; -// MemoryStream is IDisposable but does not need to be disposed -#pragma warning disable CA1001 -internal sealed class PlayerInfoConnection : WebsocketServerConnection -#pragma warning restore CA1001 +internal sealed class PlayerInfoConnection + : DroppablePackageRequestConnection> { private readonly Player _player; private readonly MapEntityManager _entityManager; private readonly BufferPool _bufferPool; private readonly MemoryStream _tempStream = new(); - private int _wantsInfoOnTick = 1; private IMemoryOwner? _lastMessage = null; - private IMemoryOwner? _nextMessage = null; public PlayerInfoConnection( Player player, @@ -33,47 +29,22 @@ internal sealed class PlayerInfoConnection : WebsocketServerConnection _player.IncrementConnectionCount(); } - protected override ValueTask HandleMessageAsync(Memory buffer) - { - var next = Interlocked.Exchange(ref _nextMessage, null); - if (next != null) - return SendAndDisposeAsync(next); - - _wantsInfoOnTick = 1; - return ValueTask.CompletedTask; - } - public async Task OnGameTickAsync() { await Task.Yield(); var response = await GenerateMessageAsync(); - - var shouldDropPacket = _lastMessage != null && response.Memory.Span.SequenceEqual(_lastMessage.Memory.Span); - if (shouldDropPacket) - { - response.Dispose(); - return; - } - - var wantsNow = Interlocked.Exchange(ref _wantsInfoOnTick, 0) != 0; - - if (wantsNow) - { - await SendAndDisposeAsync(response); - return; - } - - Interlocked.Exchange(ref _nextMessage, response); + if (response != null) + SetNextPackage(response); } - public override ValueTask RemovedAsync() + public override void Dispose() { + base.Dispose(); _player.DecrementConnectionCount(); - return ValueTask.CompletedTask; } - private async ValueTask> GenerateMessageAsync() + private async ValueTask?> GenerateMessageAsync() { var tank = _entityManager.GetCurrentTankOfPlayer(_player); @@ -97,12 +68,18 @@ internal sealed class PlayerInfoConnection : WebsocketServerConnection var messageLength = (int)_tempStream.Position; var owner = _bufferPool.Rent(messageLength); + _tempStream.Position = 0; await _tempStream.ReadExactlyAsync(owner.Memory); - return owner; + + if (_lastMessage == null || !owner.Memory.Span.SequenceEqual(_lastMessage.Memory.Span)) + return owner; + + owner.Dispose(); + return null; } - private async ValueTask SendAndDisposeAsync(IMemoryOwner data) + protected override async ValueTask SendPackageAsync(IMemoryOwner data) { await Socket.SendTextAsync(data.Memory); Interlocked.Exchange(ref _lastMessage, data)?.Dispose(); diff --git a/tanks-backend/TanksServer/Interactivity/WebsocketServer.cs b/tanks-backend/TanksServer/Interactivity/WebsocketServer.cs index 19f7e7a..0c33e4c 100644 --- a/tanks-backend/TanksServer/Interactivity/WebsocketServer.cs +++ b/tanks-backend/TanksServer/Interactivity/WebsocketServer.cs @@ -37,7 +37,7 @@ internal abstract class WebsocketServer( await connection.ReceiveAsync(); _ = _connections.TryRemove(connection, out _); - await connection.RemovedAsync(); + connection.Dispose(); } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/tanks-backend/TanksServer/Interactivity/WebsocketServerConnection.cs b/tanks-backend/TanksServer/Interactivity/WebsocketServerConnection.cs index 6be215c..893b7a5 100644 --- a/tanks-backend/TanksServer/Interactivity/WebsocketServerConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/WebsocketServerConnection.cs @@ -3,7 +3,7 @@ namespace TanksServer.Interactivity; internal abstract class WebsocketServerConnection( ILogger logger, ByteChannelWebSocket socket -) +): IDisposable { protected readonly ByteChannelWebSocket Socket = socket; protected readonly ILogger Logger = logger; @@ -21,7 +21,7 @@ internal abstract class WebsocketServerConnection( Logger.LogTrace("done receiving"); } - public abstract ValueTask RemovedAsync(); - protected abstract ValueTask HandleMessageAsync(Memory buffer); + + public abstract void Dispose(); } From 516b9538c35dd0f2f6e57f025d2a02a580b44530 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 4 May 2024 14:34:06 +0200 Subject: [PATCH 6/9] add mag size upgrade icon --- .../TanksServer/Graphics/DrawPowerUpsStep.cs | 2 ++ .../TanksServer/assets/powerup_magazine.png | Bin 0 -> 127 bytes 2 files changed, 2 insertions(+) create mode 100644 tanks-backend/TanksServer/assets/powerup_magazine.png diff --git a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs index a49f77f..1723288 100644 --- a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs @@ -6,6 +6,7 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt { private readonly Sprite _genericSprite = Sprite.FromImageFile("assets/powerup_generic.png"); private readonly Sprite _smartSprite = Sprite.FromImageFile("assets/powerup_smart.png"); + private readonly Sprite _magazineSprite = Sprite.FromImageFile("assets/powerup_magazine.png"); public void Draw(GamePixelGrid pixels) { @@ -13,6 +14,7 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt { var sprite = powerUp switch { + { Type: PowerUpType.MagazineSize } => _magazineSprite, { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart } => _smartSprite, _ => _genericSprite }; diff --git a/tanks-backend/TanksServer/assets/powerup_magazine.png b/tanks-backend/TanksServer/assets/powerup_magazine.png new file mode 100644 index 0000000000000000000000000000000000000000..30c021c0a5d108640851dede6a3b38fbf8426dfa GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#Vf Date: Sat, 4 May 2024 14:36:53 +0200 Subject: [PATCH 7/9] add explosive icon --- .../TanksServer/Graphics/DrawPowerUpsStep.cs | 2 ++ .../TanksServer/assets/powerup_explosive.png | Bin 0 -> 130 bytes 2 files changed, 2 insertions(+) create mode 100644 tanks-backend/TanksServer/assets/powerup_explosive.png diff --git a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs index 1723288..3c67069 100644 --- a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs @@ -7,6 +7,7 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt private readonly Sprite _genericSprite = Sprite.FromImageFile("assets/powerup_generic.png"); private readonly Sprite _smartSprite = Sprite.FromImageFile("assets/powerup_smart.png"); private readonly Sprite _magazineSprite = Sprite.FromImageFile("assets/powerup_magazine.png"); + private readonly Sprite _explosiveSprite = Sprite.FromImageFile("assets/powerup_explosive.png"); public void Draw(GamePixelGrid pixels) { @@ -16,6 +17,7 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt { { Type: PowerUpType.MagazineSize } => _magazineSprite, { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart } => _smartSprite, + { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Explosive } => _explosiveSprite, _ => _genericSprite }; diff --git a/tanks-backend/TanksServer/assets/powerup_explosive.png b/tanks-backend/TanksServer/assets/powerup_explosive.png new file mode 100644 index 0000000000000000000000000000000000000000..09cdc5bc7245082432b2a2414b773ec0323f0f71 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#VfxZV^H95vcLQP+!h!A=pI&4k;I0e8)d>OjB7fUtIuf-)r;v{Dfw!{vaQ_tXRc3H ZHoN~vhO<>^_d}o|44$rjF6*2UngGilCRG3c literal 0 HcmV?d00001 From fe851ffc17206e8267c8f5163cc9657be81a38d9 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 4 May 2024 14:41:03 +0200 Subject: [PATCH 8/9] add fast bullet icon --- .../TanksServer/Graphics/DrawPowerUpsStep.cs | 2 ++ .../TanksServer/assets/powerup_fastbullet.png | Bin 0 -> 124 bytes 2 files changed, 2 insertions(+) create mode 100644 tanks-backend/TanksServer/assets/powerup_fastbullet.png diff --git a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs index 3c67069..6d47060 100644 --- a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs @@ -8,6 +8,7 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt private readonly Sprite _smartSprite = Sprite.FromImageFile("assets/powerup_smart.png"); private readonly Sprite _magazineSprite = Sprite.FromImageFile("assets/powerup_magazine.png"); private readonly Sprite _explosiveSprite = Sprite.FromImageFile("assets/powerup_explosive.png"); + private readonly Sprite _fastSprite = Sprite.FromImageFile("assets/powerup_fastbullet.png"); public void Draw(GamePixelGrid pixels) { @@ -18,6 +19,7 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt { Type: PowerUpType.MagazineSize } => _magazineSprite, { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart } => _smartSprite, { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Explosive } => _explosiveSprite, + { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Fast } => _fastSprite, _ => _genericSprite }; diff --git a/tanks-backend/TanksServer/assets/powerup_fastbullet.png b/tanks-backend/TanksServer/assets/powerup_fastbullet.png new file mode 100644 index 0000000000000000000000000000000000000000..256f6a87043c63b33161329aec6910dec4073405 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#Vf Date: Sun, 5 May 2024 13:01:37 +0200 Subject: [PATCH 9/9] allocate predicate once instead of per tick --- .../TanksServer/GameLogic/CollectPowerUp.cs | 66 ++++++++++--------- .../TanksServer/GameLogic/CollideBullets.cs | 39 +++++++---- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs index cf1fc04..3b3571f 100644 --- a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs @@ -6,16 +6,18 @@ internal sealed class CollectPowerUp( MapEntityManager entityManager ) : ITickStep { + private readonly Predicate _collectPredicate = b => TryCollect(b, entityManager.Tanks); + public ValueTask TickAsync(TimeSpan delta) { - entityManager.RemoveWhere(TryCollect); + entityManager.RemoveWhere(_collectPredicate); return ValueTask.CompletedTask; } - private bool TryCollect(PowerUp obj) + private static bool TryCollect(PowerUp powerUp, IEnumerable tanks) { - var position = obj.Position; - foreach (var tank in entityManager.Tanks) + var position = powerUp.Position; + foreach (var tank in tanks) { var (topLeft, bottomRight) = tank.Bounds; if (position.X < topLeft.X || position.X > bottomRight.X || @@ -24,36 +26,40 @@ internal sealed class CollectPowerUp( // now the tank overlaps the power up by at least 0.5 tiles - switch (obj.Type) - { - case PowerUpType.MagazineType: - if (obj.MagazineType == null) - throw new UnreachableException(); - - tank.Magazine = tank.Magazine with - { - Type = tank.Magazine.Type | obj.MagazineType.Value, - UsedBullets = 0 - }; - - if (tank.ReloadingUntil >= DateTime.Now) - tank.ReloadingUntil = DateTime.Now; - - break; - case PowerUpType.MagazineSize: - tank.Magazine = tank.Magazine with - { - MaxBullets = (byte)int.Clamp(tank.Magazine.MaxBullets + 1, 1, 32) - }; - break; - default: - throw new UnreachableException(); - } - + ApplyPowerUpEffect(powerUp, tank); tank.Owner.Scores.PowerUpsCollected++; return true; } return false; } + + private static void ApplyPowerUpEffect(PowerUp powerUp, Tank tank) + { + switch (powerUp.Type) + { + case PowerUpType.MagazineType: + if (powerUp.MagazineType == null) + throw new UnreachableException(); + + tank.Magazine = tank.Magazine with + { + Type = tank.Magazine.Type | powerUp.MagazineType.Value, + UsedBullets = 0 + }; + + if (tank.ReloadingUntil >= DateTime.Now) + tank.ReloadingUntil = DateTime.Now; + + break; + case PowerUpType.MagazineSize: + tank.Magazine = tank.Magazine with + { + MaxBullets = (byte)int.Clamp(tank.Magazine.MaxBullets + 1, 1, 32) + }; + break; + default: + throw new UnreachableException(); + } + } } diff --git a/tanks-backend/TanksServer/GameLogic/CollideBullets.cs b/tanks-backend/TanksServer/GameLogic/CollideBullets.cs index aefd13e..4be0dca 100644 --- a/tanks-backend/TanksServer/GameLogic/CollideBullets.cs +++ b/tanks-backend/TanksServer/GameLogic/CollideBullets.cs @@ -2,20 +2,31 @@ using TanksServer.Graphics; namespace TanksServer.GameLogic; -internal sealed class CollideBullets( - MapEntityManager entityManager, - MapService map, - IOptions options, - TankSpawnQueue tankSpawnQueue -) : ITickStep +internal sealed class CollideBullets : ITickStep { private readonly Sprite _explosiveSprite = Sprite.FromImageFile("assets/explosion.png"); + private readonly Predicate _removeBulletsPredicate; + private readonly MapEntityManager _entityManager; + private readonly MapService _map; + private readonly bool _destructibleWalls; + private readonly TankSpawnQueue _tankSpawnQueue; + + public CollideBullets(MapEntityManager entityManager, + MapService map, + IOptions options, + TankSpawnQueue tankSpawnQueue) + { + _entityManager = entityManager; + _map = map; + _tankSpawnQueue = tankSpawnQueue; + + _destructibleWalls = options.Value.DestructibleWalls; + _removeBulletsPredicate = b => BulletHitsTank(b) || BulletHitsWall(b) || BulletTimesOut(b); + } public ValueTask TickAsync(TimeSpan _) { - entityManager.RemoveWhere(BulletHitsTank); - entityManager.RemoveWhere(BulletHitsWall); - entityManager.RemoveWhere(BulletTimesOut); + _entityManager.RemoveWhere(_removeBulletsPredicate); return ValueTask.CompletedTask; } @@ -31,7 +42,7 @@ internal sealed class CollideBullets( private bool BulletHitsWall(Bullet bullet) { var pixel = bullet.Position.ToPixelPosition(); - if (!map.Current.IsWall(pixel)) + if (!_map.Current.IsWall(pixel)) return false; ExplodeAt(pixel, bullet.IsExplosive, bullet.Owner); @@ -50,7 +61,7 @@ internal sealed class CollideBullets( private Tank? GetTankAt(FloatPosition position, Player owner, bool canHitOwnTank) { - foreach (var tank in entityManager.Tanks) + foreach (var tank in _entityManager.Tanks) { var hitsOwnTank = owner == tank.Owner; if (hitsOwnTank && !canHitOwnTank) @@ -89,7 +100,7 @@ internal sealed class CollideBullets( void Core(PixelPosition position) { - if (options.Value.DestructibleWalls && map.Current.TryDestroyWallAt(position)) + if (_destructibleWalls && _map.Current.TryDestroyWallAt(position)) owner.Scores.WallsDestroyed++; var tank = GetTankAt(position.ToFloatPosition(), owner, true); @@ -100,8 +111,8 @@ internal sealed class CollideBullets( owner.Scores.Kills++; tank.Owner.Scores.Deaths++; - entityManager.Remove(tank); - tankSpawnQueue.EnqueueForDelayedSpawn(tank.Owner); + _entityManager.Remove(tank); + _tankSpawnQueue.EnqueueForDelayedSpawn(tank.Owner); } } }