From 72133188384333919587804819253b687a3950bf Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 12 Apr 2024 16:05:24 +0200 Subject: [PATCH] more commands, change display communication to new lib --- .gitignore | 2 + DisplayCommands/ByteGrid.cs | 2 + DisplayCommands/GlobalUsings.cs | 4 +- DisplayCommands/IDisplayConnection.cs | 13 ++- .../Internals/DisplayConnection.cs | 22 ++++ .../Internals/DisplaySubCommand.cs | 2 +- DisplayCommands/Internals/HeaderBitmap.cs | 17 +++ DisplayCommands/PixelGrid.cs | 47 ++++++++ TanksServer/Graphics/BulletDrawer.cs | 13 ++- TanksServer/Graphics/DrawStateToFrame.cs | 9 +- TanksServer/Graphics/IDrawStep.cs | 4 +- .../Graphics/LastFinishedFrameProvider.cs | 6 +- TanksServer/Graphics/MapDrawer.cs | 8 +- TanksServer/Graphics/TankDrawer.cs | 10 +- .../Interactivity/ByteChannelWebSocket.cs | 6 +- .../Interactivity/ClientScreenServer.cs | 14 +-- TanksServer/Interactivity/ControlsServer.cs | 4 +- .../SendToServicePointDisplay.cs | 87 ++++++++++++++ TanksServer/Models/PixelPosition.cs | 2 +- TanksServer/Models/PositionHelpers.cs | 8 +- TanksServer/Program.cs | 3 - .../ServicePointDisplay/DisplayBufferView.cs | 62 ---------- .../FixedSizeBitGridView.cs | 23 ---- .../FixedSizeBitRowView.cs | 82 ------------- .../FixedSizeCharGridView.cs | 39 ------- .../PixelDisplayBufferView.cs | 28 ----- .../SendToServicePointDisplay.cs | 110 ------------------ .../ServicePointDisplayConfiguration.cs | 8 -- .../ServicePointDisplay/TextDisplayBuffer.cs | 16 --- tank-frontend/.env | 4 + tank-frontend/src/ClientScreen.tsx | 2 +- 31 files changed, 240 insertions(+), 417 deletions(-) create mode 100644 DisplayCommands/Internals/HeaderBitmap.cs create mode 100644 DisplayCommands/PixelGrid.cs create mode 100644 TanksServer/Interactivity/SendToServicePointDisplay.cs delete mode 100644 TanksServer/ServicePointDisplay/DisplayBufferView.cs delete mode 100644 TanksServer/ServicePointDisplay/FixedSizeBitGridView.cs delete mode 100644 TanksServer/ServicePointDisplay/FixedSizeBitRowView.cs delete mode 100644 TanksServer/ServicePointDisplay/FixedSizeCharGridView.cs delete mode 100644 TanksServer/ServicePointDisplay/PixelDisplayBufferView.cs delete mode 100644 TanksServer/ServicePointDisplay/SendToServicePointDisplay.cs delete mode 100644 TanksServer/ServicePointDisplay/ServicePointDisplayConfiguration.cs delete mode 100644 TanksServer/ServicePointDisplay/TextDisplayBuffer.cs diff --git a/.gitignore b/.gitignore index cb1cef4..8a0cf38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ bin obj .idea +client + diff --git a/DisplayCommands/ByteGrid.cs b/DisplayCommands/ByteGrid.cs index 889d119..2572098 100644 --- a/DisplayCommands/ByteGrid.cs +++ b/DisplayCommands/ByteGrid.cs @@ -22,4 +22,6 @@ public class ByteGrid(ushort width, ushort height) Debug.Assert(y < Height); return x + y * Width; } + + public void Clear() => Data.Span.Clear(); } diff --git a/DisplayCommands/GlobalUsings.cs b/DisplayCommands/GlobalUsings.cs index 129a986..b124d17 100644 --- a/DisplayCommands/GlobalUsings.cs +++ b/DisplayCommands/GlobalUsings.cs @@ -1,4 +1,2 @@ -// Global using directives - global using System; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; diff --git a/DisplayCommands/IDisplayConnection.cs b/DisplayCommands/IDisplayConnection.cs index 5354390..db53477 100644 --- a/DisplayCommands/IDisplayConnection.cs +++ b/DisplayCommands/IDisplayConnection.cs @@ -3,11 +3,22 @@ namespace DisplayCommands; public interface IDisplayConnection { ValueTask SendClearAsync(); - + ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid); ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma); + ValueTask SendBrightnessAsync(byte brightness); + ValueTask SendHardResetAsync(); + ValueTask SendFadeOutAsync(byte loops); + + public ValueTask SendBitmapLinearWindowAsync(ushort x, ushort y, PixelGrid pixels); + + /// + /// Returns the IPv4 address that is associated with the interface with which the display is reachable. + /// + /// IPv4 as text + public string GetLocalIPv4(); } \ No newline at end of file diff --git a/DisplayCommands/Internals/DisplayConnection.cs b/DisplayCommands/Internals/DisplayConnection.cs index 0e04969..5083b8b 100644 --- a/DisplayCommands/Internals/DisplayConnection.cs +++ b/DisplayCommands/Internals/DisplayConnection.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Diagnostics; +using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using Microsoft.Extensions.Options; @@ -74,6 +75,27 @@ internal sealed class DisplayConnection(IOptions options) _arrayPool.Return(payloadBuffer); } + public ValueTask SendBitmapLinearWindowAsync(ushort x, ushort y, PixelGrid pixels) + { + var header = new HeaderWindow + { + Command = DisplayCommand.BitmapLinearWin, + PosX = x, PosY = y, + Width = pixels.Width, + Height = pixels.Height + }; + + return SendAsync(header, pixels.Data); + } + + public string GetLocalIPv4() + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0); + socket.Connect(options.Value.Hostname, options.Value.Port); + var endPoint = socket.LocalEndPoint as IPEndPoint ?? throw new NotSupportedException(); + return endPoint.Address.ToString(); + } + private async ValueTask SendAsync(HeaderWindow header, Memory payload) { int headerSize; diff --git a/DisplayCommands/Internals/DisplaySubCommand.cs b/DisplayCommands/Internals/DisplaySubCommand.cs index 66e0691..57fe8f3 100644 --- a/DisplayCommands/Internals/DisplaySubCommand.cs +++ b/DisplayCommands/Internals/DisplaySubCommand.cs @@ -1,6 +1,6 @@ namespace DisplayCommands.Internals; -internal enum DisplaySubCommand +internal enum DisplaySubCommand : ushort { BitmapNormal = 0x0, BitmapCompressZ = 0x677a, diff --git a/DisplayCommands/Internals/HeaderBitmap.cs b/DisplayCommands/Internals/HeaderBitmap.cs new file mode 100644 index 0000000..e3bdd43 --- /dev/null +++ b/DisplayCommands/Internals/HeaderBitmap.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace DisplayCommands.Internals; + +[StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)] +internal struct HeaderBitmap +{ + public DisplayCommand Command; + + public ushort Offset; + + public ushort Length; + + public DisplaySubCommand SubCommand; + + public ushort Reserved; +} \ No newline at end of file diff --git a/DisplayCommands/PixelGrid.cs b/DisplayCommands/PixelGrid.cs new file mode 100644 index 0000000..e84c84e --- /dev/null +++ b/DisplayCommands/PixelGrid.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; + +namespace DisplayCommands; + +public sealed class PixelGrid(ushort width, ushort height) +{ + private readonly ByteGrid _byteGrid = new((ushort)(width / 8u), height); + + public ushort Width { get; } = width; + + public ushort Height { get; } = height; + + public Memory Data => _byteGrid.Data; + + public bool this[ushort x, ushort y] + { + get + { + Debug.Assert(y < Height); + var (byteIndex, bitInByteMask) = GetIndexes(x); + var byteVal = _byteGrid[byteIndex, y]; + return (byteVal & bitInByteMask) != 0; + } + set + { + Debug.Assert(y < Height); + var (byteIndex, bitInByteMask) = GetIndexes(x); + if (value) + _byteGrid[byteIndex, y] |= bitInByteMask; + else + _byteGrid[byteIndex, y] &= (byte)(ushort.MaxValue ^ bitInByteMask); + } + } + + public void Clear() => _byteGrid.Clear(); + + private (ushort byteIndex, byte bitInByteMask) GetIndexes(int x) + { + Debug.Assert(x < Width); + var byteIndex = (ushort)(x / 8); + Debug.Assert(byteIndex < Width); + var bitInByteIndex = (byte)(7 - x % 8); + Debug.Assert(bitInByteIndex < 8); + var bitInByteMask = (byte)(1 << bitInByteIndex); + return (byteIndex, bitInByteMask); + } +} \ No newline at end of file diff --git a/TanksServer/Graphics/BulletDrawer.cs b/TanksServer/Graphics/BulletDrawer.cs index fcfa562..bce2b36 100644 --- a/TanksServer/Graphics/BulletDrawer.cs +++ b/TanksServer/Graphics/BulletDrawer.cs @@ -1,13 +1,16 @@ +using DisplayCommands; using TanksServer.GameLogic; -using TanksServer.ServicePointDisplay; namespace TanksServer.Graphics; -internal sealed class BulletDrawer(BulletManager bullets): IDrawStep +internal sealed class BulletDrawer(BulletManager bullets) : IDrawStep { - public void Draw(PixelDisplayBufferView buffer) + public void Draw(PixelGrid buffer) { foreach (var bullet in bullets.GetAll()) - buffer.Pixels[bullet.Position.ToPixelPosition()] = true; + { + var pos = bullet.Position.ToPixelPosition(); + buffer[pos.X, pos.Y] = true; + } } -} +} \ No newline at end of file diff --git a/TanksServer/Graphics/DrawStateToFrame.cs b/TanksServer/Graphics/DrawStateToFrame.cs index a793322..dba1a4a 100644 --- a/TanksServer/Graphics/DrawStateToFrame.cs +++ b/TanksServer/Graphics/DrawStateToFrame.cs @@ -1,5 +1,5 @@ +using DisplayCommands; using TanksServer.GameLogic; -using TanksServer.ServicePointDisplay; namespace TanksServer.Graphics; @@ -8,13 +8,14 @@ internal sealed class DrawStateToFrame( ) : ITickStep { private readonly List _drawSteps = drawSteps.ToList(); + private readonly PixelGrid _drawGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn); public Task TickAsync() { - var buffer = PixelDisplayBufferView.New(0, 0, MapService.TilesPerRow, MapService.PixelsPerColumn); + _drawGrid.Clear(); foreach (var step in _drawSteps) - step.Draw(buffer); - lastFrameProvider.LastFrame = buffer; + step.Draw(_drawGrid); + lastFrameProvider.LastFrame = _drawGrid; return Task.CompletedTask; } } diff --git a/TanksServer/Graphics/IDrawStep.cs b/TanksServer/Graphics/IDrawStep.cs index 9366adf..0067912 100644 --- a/TanksServer/Graphics/IDrawStep.cs +++ b/TanksServer/Graphics/IDrawStep.cs @@ -1,8 +1,8 @@ -using TanksServer.ServicePointDisplay; +using DisplayCommands; namespace TanksServer.Graphics; internal interface IDrawStep { - void Draw(PixelDisplayBufferView buffer); + void Draw(PixelGrid buffer); } diff --git a/TanksServer/Graphics/LastFinishedFrameProvider.cs b/TanksServer/Graphics/LastFinishedFrameProvider.cs index 70c101d..eb58723 100644 --- a/TanksServer/Graphics/LastFinishedFrameProvider.cs +++ b/TanksServer/Graphics/LastFinishedFrameProvider.cs @@ -1,12 +1,12 @@ -using TanksServer.ServicePointDisplay; +using DisplayCommands; namespace TanksServer.Graphics; internal sealed class LastFinishedFrameProvider { - private PixelDisplayBufferView? _lastFrame; + private PixelGrid? _lastFrame; - public PixelDisplayBufferView LastFrame + public PixelGrid LastFrame { get => _lastFrame ?? throw new InvalidOperationException("first frame not yet drawn"); set => _lastFrame = value; diff --git a/TanksServer/Graphics/MapDrawer.cs b/TanksServer/Graphics/MapDrawer.cs index d40fef6..df0d786 100644 --- a/TanksServer/Graphics/MapDrawer.cs +++ b/TanksServer/Graphics/MapDrawer.cs @@ -1,11 +1,11 @@ +using DisplayCommands; using TanksServer.GameLogic; -using TanksServer.ServicePointDisplay; namespace TanksServer.Graphics; internal sealed class MapDrawer(MapService map) : IDrawStep { - public void Draw(PixelDisplayBufferView buffer) + public void Draw(PixelGrid buffer) { for (var tileY = 0; tileY < MapService.TilesPerColumn; tileY++) for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++) @@ -18,8 +18,8 @@ internal sealed class MapDrawer(MapService map) : IDrawStep for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++) { var position = tile.GetPixelRelative(pixelInTileX, pixelInTileY); - buffer.Pixels[position] = pixelInTileX % 2 == pixelInTileY % 2; + buffer[position.X, position.Y] = pixelInTileX % 2 == pixelInTileY % 2; } } } -} +} \ No newline at end of file diff --git a/TanksServer/Graphics/TankDrawer.cs b/TanksServer/Graphics/TankDrawer.cs index 61e3d0c..706b4b1 100644 --- a/TanksServer/Graphics/TankDrawer.cs +++ b/TanksServer/Graphics/TankDrawer.cs @@ -1,7 +1,7 @@ +using DisplayCommands; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using TanksServer.GameLogic; -using TanksServer.ServicePointDisplay; namespace TanksServer.Graphics; @@ -29,21 +29,21 @@ internal sealed class TankDrawer : IDrawStep _tankSpriteWidth = tankImage.Width; } - public void Draw(PixelDisplayBufferView buffer) + public void Draw(PixelGrid buffer) { foreach (var tank in _tanks) { var pos = tank.Position.ToPixelPosition(); var rotationVariant = (int)Math.Round(tank.Rotation) % 16; - + for (var dy = 0; dy < MapService.TileSize; dy++) for (var dx = 0; dx < MapService.TileSize; dx++) { if (!TankSpriteAt(dx, dy, rotationVariant)) continue; - var position = new PixelPosition(pos.X + dx, pos.Y + dy); - buffer.Pixels[position] = true; + var position = new PixelPosition((ushort)(pos.X + dx), (ushort)(pos.Y + dy)); + buffer[position.X, position.Y] = true; } } } diff --git a/TanksServer/Interactivity/ByteChannelWebSocket.cs b/TanksServer/Interactivity/ByteChannelWebSocket.cs index 0408fc0..67d4e56 100644 --- a/TanksServer/Interactivity/ByteChannelWebSocket.cs +++ b/TanksServer/Interactivity/ByteChannelWebSocket.cs @@ -7,15 +7,15 @@ namespace TanksServer.Interactivity; /// /// Hacky class for easier semantics /// -internal sealed class ByteChannelWebSocket : Channel +internal sealed class ByteChannelWebSocket : Channel> { private readonly ILogger _logger; private readonly WebSocket _socket; private readonly Task _backgroundDone; private readonly byte[] _buffer; - private readonly Channel _outgoing = Channel.CreateUnbounded(); - private readonly Channel _incoming = Channel.CreateUnbounded(); + private readonly Channel> _outgoing = Channel.CreateUnbounded>(); + private readonly Channel> _incoming = Channel.CreateUnbounded>(); public ByteChannelWebSocket(WebSocket socket, ILogger logger, int messageSize) { diff --git a/TanksServer/Interactivity/ClientScreenServer.cs b/TanksServer/Interactivity/ClientScreenServer.cs index 7c8c1db..efbf76d 100644 --- a/TanksServer/Interactivity/ClientScreenServer.cs +++ b/TanksServer/Interactivity/ClientScreenServer.cs @@ -1,8 +1,8 @@ using System.Diagnostics; using System.Net.WebSockets; using System.Threading.Channels; +using DisplayCommands; using Microsoft.Extensions.Hosting; -using TanksServer.ServicePointDisplay; namespace TanksServer.Interactivity; @@ -44,10 +44,10 @@ internal sealed class ClientScreenServer( public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; private void Remove(ClientScreenServerConnection connection) => _connections.TryRemove(connection, out _); - + public IEnumerable GetConnections() => _connections.Keys; - internal sealed class ClientScreenServerConnection: IDisposable + internal sealed class ClientScreenServerConnection : IDisposable { private readonly ByteChannelWebSocket _channel; private readonly SemaphoreSlim _wantedFrames = new(1); @@ -64,7 +64,7 @@ internal sealed class ClientScreenServer( Done = ReceiveAsync(); } - public async Task SendAsync(PixelDisplayBufferView buf) + public async Task SendAsync(PixelGrid buf) { if (!await _wantedFrames.WaitAsync(TimeSpan.Zero)) { @@ -85,9 +85,9 @@ internal sealed class ClientScreenServer( private async Task ReceiveAsync() { - await foreach (var _ in _channel.Reader.ReadAllAsync()) + await foreach (var _ in _channel.Reader.ReadAllAsync()) _wantedFrames.Release(); - + _logger.LogTrace("done receiving"); _server.Remove(this); } @@ -106,4 +106,4 @@ internal sealed class ClientScreenServer( Done.Dispose(); } } -} +} \ No newline at end of file diff --git a/TanksServer/Interactivity/ControlsServer.cs b/TanksServer/Interactivity/ControlsServer.cs index a279a2e..f23b66a 100644 --- a/TanksServer/Interactivity/ControlsServer.cs +++ b/TanksServer/Interactivity/ControlsServer.cs @@ -71,8 +71,8 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact { await foreach (var buffer in _binaryWebSocket.Reader.ReadAllAsync()) { - var type = (MessageType)buffer[0]; - var control = (InputType)buffer[1]; + var type = (MessageType)buffer.Span[0]; + var control = (InputType)buffer.Span[1]; _logger.LogTrace("player input {} {} {}", _player.Id, type, control); diff --git a/TanksServer/Interactivity/SendToServicePointDisplay.cs b/TanksServer/Interactivity/SendToServicePointDisplay.cs new file mode 100644 index 0000000..0061a02 --- /dev/null +++ b/TanksServer/Interactivity/SendToServicePointDisplay.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using System.Net.Sockets; +using DisplayCommands; +using TanksServer.GameLogic; +using TanksServer.Graphics; + +namespace TanksServer.Interactivity; + +internal sealed class SendToServicePointDisplay : ITickStep +{ + private readonly LastFinishedFrameProvider _lastFinishedFrameProvider; + private readonly Cp437Grid _scoresBuffer; + private readonly PlayerServer _players; + private readonly ILogger _logger; + private readonly IDisplayConnection _displayConnection; + + private DateTime _nextFailLog = DateTime.Now; + + private const int ScoresWidth = 12; + private const int ScoresHeight = 20; + private const int ScoresPlayerRows = ScoresHeight - 5; + + public SendToServicePointDisplay( + LastFinishedFrameProvider lastFinishedFrameProvider, + PlayerServer players, + ILogger logger, + IDisplayConnection displayConnection + ) + { + _lastFinishedFrameProvider = lastFinishedFrameProvider; + _players = players; + _logger = logger; + _displayConnection = displayConnection; + + var localIp = _displayConnection.GetLocalIPv4().Split('.'); + Debug.Assert(localIp.Length == 4); + _scoresBuffer = new Cp437Grid(12, 20) + { + [00] = "== TANKS! ==", + [01] = "-- scores --", + [17] = "-- join --", + [18] = string.Join('.', localIp[..2]), + [19] = string.Join('.', localIp[2..]) + }; + } + + public async Task TickAsync() + { + RefreshScores(); + try + { + await _displayConnection.SendCp437DataAsync(MapService.TilesPerRow, 0, _scoresBuffer); + await _displayConnection.SendBitmapLinearWindowAsync(0, 0, _lastFinishedFrameProvider.LastFrame); + } + catch (SocketException ex) + { + if (DateTime.Now > _nextFailLog) + { + _logger.LogWarning("could not send data to service point display: {}", ex.Message); + _nextFailLog = DateTime.Now + TimeSpan.FromSeconds(5); + } + } + } + + private void RefreshScores() + { + var playersToDisplay = _players.GetAll() + .OrderByDescending(p => p.Kills) + .Take(ScoresPlayerRows); + + ushort row = 2; + foreach (var p in playersToDisplay) + { + var score = p.Kills.ToString(); + var nameLength = Math.Min(p.Name.Length, ScoresWidth - score.Length - 1); + + var name = p.Name[..nameLength]; + var spaces = new string(' ', ScoresWidth - score.Length - nameLength); + + _scoresBuffer[row] = name + spaces + score; + row++; + } + + for (; row < 17; row++) + _scoresBuffer[row] = string.Empty; + } +} \ No newline at end of file diff --git a/TanksServer/Models/PixelPosition.cs b/TanksServer/Models/PixelPosition.cs index 116ac0e..106525d 100644 --- a/TanksServer/Models/PixelPosition.cs +++ b/TanksServer/Models/PixelPosition.cs @@ -1,3 +1,3 @@ namespace TanksServer.Models; -internal record struct PixelPosition(int X, int Y); +internal record struct PixelPosition(ushort X, ushort Y); \ No newline at end of file diff --git a/TanksServer/Models/PositionHelpers.cs b/TanksServer/Models/PositionHelpers.cs index ae15bf0..8c4e02c 100644 --- a/TanksServer/Models/PositionHelpers.cs +++ b/TanksServer/Models/PositionHelpers.cs @@ -10,15 +10,15 @@ internal static class PositionHelpers Debug.Assert(subX < 8); Debug.Assert(subY < 8); return new PixelPosition( - X: position.X * MapService.TileSize + subX, - Y: position.Y * MapService.TileSize + subY + X: (ushort)(position.X * MapService.TileSize + subX), + Y: (ushort)(position.Y * MapService.TileSize + subY) ); } public static PixelPosition ToPixelPosition(this FloatPosition position) => new( - X: (int)position.X % MapService.PixelsPerRow, - Y: (int)position.Y % MapService.PixelsPerRow + X: (ushort)((int)position.X % MapService.PixelsPerRow), + Y: (ushort)((int)position.Y % MapService.PixelsPerRow) ); public static TilePosition ToTilePosition(this PixelPosition position) => new( diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index 72ea92c..611889f 100644 --- a/TanksServer/Program.cs +++ b/TanksServer/Program.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.FileProviders; using TanksServer.GameLogic; using TanksServer.Graphics; using TanksServer.Interactivity; -using TanksServer.ServicePointDisplay; namespace TanksServer; @@ -109,8 +108,6 @@ public static class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.Configure( - builder.Configuration.GetSection("ServicePointDisplay")); builder.Services.Configure( builder.Configuration.GetSection("Tanks")); builder.Services.Configure( diff --git a/TanksServer/ServicePointDisplay/DisplayBufferView.cs b/TanksServer/ServicePointDisplay/DisplayBufferView.cs deleted file mode 100644 index 8d1dde8..0000000 --- a/TanksServer/ServicePointDisplay/DisplayBufferView.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace TanksServer.ServicePointDisplay; - -internal class DisplayBufferView(byte[] data) -{ - public byte[] Data => data; - - public ushort Mode - { - get => GetTwoBytes(0); - set => SetTwoBytes(0, value); - } - - public ushort TileX - { - get => GetTwoBytes(2); - set => SetTwoBytes(2, value); - } - - public ushort TileY - { - get => GetTwoBytes(4); - set => SetTwoBytes(4, value); - } - - public ushort WidthInTiles - { - get => GetTwoBytes(6); - set => SetTwoBytes(6, value); - } - - public ushort RowCount - { - get => GetTwoBytes(8); - set => SetTwoBytes(8, value); - } - - public TilePosition Position - { - get => new(TileX, TileY); - set - { - ArgumentOutOfRangeException.ThrowIfGreaterThan(value.X, ushort.MaxValue); - ArgumentOutOfRangeException.ThrowIfGreaterThan(value.Y, ushort.MaxValue); - ArgumentOutOfRangeException.ThrowIfNegative(value.X); - ArgumentOutOfRangeException.ThrowIfNegative(value.Y); - - TileX = (ushort)value.X; - TileY = (ushort)value.Y; - } - } - - private ushort GetTwoBytes(int index) - { - return (ushort)(data[index] * byte.MaxValue + data[index + 1]); - } - - private void SetTwoBytes(int index, ushort value) - { - data[index] = (byte)(value / byte.MaxValue); - data[index + 1] = (byte)(value % byte.MaxValue); - } -} \ No newline at end of file diff --git a/TanksServer/ServicePointDisplay/FixedSizeBitGridView.cs b/TanksServer/ServicePointDisplay/FixedSizeBitGridView.cs deleted file mode 100644 index bcd598f..0000000 --- a/TanksServer/ServicePointDisplay/FixedSizeBitGridView.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Diagnostics; - -namespace TanksServer.ServicePointDisplay; - -internal sealed class FixedSizeBitGridView(Memory data, int columns, int rows) -{ - private readonly FixedSizeBitRowView _bits = new(data); - - public bool this[PixelPosition position] - { - get => _bits[ToPixelIndex(position)]; - set => _bits[ToPixelIndex(position)] = value; - } - - private int ToPixelIndex(PixelPosition position) - { - Debug.Assert(position.X < columns); - Debug.Assert(position.Y < rows); - var index = position.Y * columns + position.X; - ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(position)); - return index; - } -} \ No newline at end of file diff --git a/TanksServer/ServicePointDisplay/FixedSizeBitRowView.cs b/TanksServer/ServicePointDisplay/FixedSizeBitRowView.cs deleted file mode 100644 index b3ccbdf..0000000 --- a/TanksServer/ServicePointDisplay/FixedSizeBitRowView.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections; - -namespace TanksServer.ServicePointDisplay; - -internal sealed class FixedSizeBitRowView(Memory data) : IList -{ - public int Count => data.Length * 8; - public bool IsReadOnly => false; - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public IEnumerator GetEnumerator() - { - return Enumerable().GetEnumerator(); - - IEnumerable Enumerable() - { - for (var i = 0; i < Count; i++) - yield return this[i]; - } - } - - public void Clear() - { - var span = data.Span; - for (var i = 0; i < data.Length; i++) - span[i] = 0; - } - - public void CopyTo(bool[] array, int arrayIndex) - { - for (var i = 0; i < Count && i + arrayIndex < array.Length; i++) - array[i + arrayIndex] = this[i]; - } - - private (int byteIndex, int bitInByteIndex) GetIndexes(int bitIndex) - { - var byteIndex = bitIndex / 8; - var bitInByteIndex = 7 - bitIndex % 8; - if (byteIndex >= data.Length) - { - throw new ArgumentOutOfRangeException(nameof(bitIndex), - $"accessing this bit field at position {bitIndex} would result in an access to byte " + - $"{byteIndex} but byte length is {data.Length}"); - } - - return (byteIndex, bitInByteIndex); - } - - public bool this[int bitIndex] - { - get - { - var (byteIndex, bitInByteIndex) = GetIndexes(bitIndex); - var bitInByteMask = (byte)(1 << bitInByteIndex); - return (data.Span[byteIndex] & bitInByteMask) != 0; - } - - set - { - var (byteIndex, bitInByteIndex) = GetIndexes(bitIndex); - var bitInByteMask = (byte)(1 << bitInByteIndex); - - if (value) - { - data.Span[byteIndex] |= bitInByteMask; - } - else - { - var withoutBitMask = (byte)(ushort.MaxValue ^ bitInByteMask); - data.Span[byteIndex] &= withoutBitMask; - } - } - } - - public void Add(bool item) => throw new NotSupportedException(); - public bool Contains(bool item) => throw new NotSupportedException(); - public bool Remove(bool item) => throw new NotSupportedException(); - public int IndexOf(bool item) => throw new NotSupportedException(); - public void Insert(int index, bool item) => throw new NotSupportedException(); - public void RemoveAt(int index) => throw new NotSupportedException(); -} \ No newline at end of file diff --git a/TanksServer/ServicePointDisplay/FixedSizeCharGridView.cs b/TanksServer/ServicePointDisplay/FixedSizeCharGridView.cs deleted file mode 100644 index 9f1dae8..0000000 --- a/TanksServer/ServicePointDisplay/FixedSizeCharGridView.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Text; - -namespace TanksServer.ServicePointDisplay; - -internal sealed class FixedSizeCharGridView(Memory data, ushort rowLength, ushort rowCount) -{ - public char this[int x, int y] - { - get => (char)data.Span[x + y * rowLength]; - set => data.Span[x + y * rowLength] = CharToByte(value); - } - - public string this[int row] - { - get - { - var rowStart = row * rowLength; - return Encoding.UTF8.GetString(data[rowStart..(rowStart + rowLength)].Span); - } - set - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(row, rowCount, nameof(row)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(value.Length, rowLength, nameof(value)); - var x = 0; - for (; x < value.Length; x++) - this[x, row] = value[x]; - for (; x < rowLength; x++) - this[x, row] = ' '; - } - } - - private static byte CharToByte(char c) - { - ArgumentOutOfRangeException.ThrowIfNegative(c); - ArgumentOutOfRangeException.ThrowIfGreaterThan(c, (char)byte.MaxValue, nameof(c)); - // c# strings are UTF-16 - return (byte)c; - } -} \ No newline at end of file diff --git a/TanksServer/ServicePointDisplay/PixelDisplayBufferView.cs b/TanksServer/ServicePointDisplay/PixelDisplayBufferView.cs deleted file mode 100644 index 3532a7c..0000000 --- a/TanksServer/ServicePointDisplay/PixelDisplayBufferView.cs +++ /dev/null @@ -1,28 +0,0 @@ -using TanksServer.GameLogic; - -namespace TanksServer.ServicePointDisplay; - -internal sealed class PixelDisplayBufferView : DisplayBufferView -{ - private PixelDisplayBufferView(byte[] data, int columns, int pixelRows) : base(data) - { - Pixels = new FixedSizeBitGridView(Data.AsMemory(10), columns, pixelRows); - } - - // ReSharper disable once CollectionNeverQueried.Global (setting values in collection updates underlying byte array) - public FixedSizeBitGridView Pixels { get; } - - public static PixelDisplayBufferView New(ushort x, ushort y, ushort widthInTiles, ushort pixelRows) - { - // 10 bytes header, one byte per tile row (with one bit each pixel) after that - var size = 10 + widthInTiles * pixelRows; - return new PixelDisplayBufferView(new byte[size], widthInTiles * MapService.TileSize, pixelRows) - { - Mode = 19, - TileX = x, - TileY = y, - WidthInTiles = widthInTiles, - RowCount = pixelRows - }; - } -} \ No newline at end of file diff --git a/TanksServer/ServicePointDisplay/SendToServicePointDisplay.cs b/TanksServer/ServicePointDisplay/SendToServicePointDisplay.cs deleted file mode 100644 index 24ab1ab..0000000 --- a/TanksServer/ServicePointDisplay/SendToServicePointDisplay.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; -using TanksServer.GameLogic; -using TanksServer.Graphics; -using TanksServer.Interactivity; - -namespace TanksServer.ServicePointDisplay; - -internal sealed class SendToServicePointDisplay : ITickStep, IDisposable -{ - private readonly UdpClient? _udpClient; - private readonly LastFinishedFrameProvider _lastFinishedFrameProvider; - private readonly TextDisplayBuffer _scoresBuffer; - private readonly PlayerServer _players; - private readonly ILogger _logger; - private DateTime _nextFailLog = DateTime.Now; - - private const int ScoresWidth = 12; - private const int ScoresHeight = 20; - private const int ScoresPlayerRows = ScoresHeight - 5; - - public SendToServicePointDisplay( - IOptions options, - LastFinishedFrameProvider lastFinishedFrameProvider, - PlayerServer players, - ILogger logger - ) - { - _lastFinishedFrameProvider = lastFinishedFrameProvider; - _players = players; - _logger = logger; - _udpClient = options.Value.Enable - ? new UdpClient(options.Value.Hostname, options.Value.Port) - : null; - - var localIp = GetLocalIp(options.Value.Hostname, options.Value.Port).Split('.'); - Debug.Assert(localIp.Length == 4); // were talking legacy ip - _scoresBuffer = new TextDisplayBuffer(new TilePosition(MapService.TilesPerRow, 0), 12, 20) - { - Rows = - { - [00] = "== TANKS! ==", - [01] = "-- scores --", - [17] = "-- join --", - [18] = string.Join('.', localIp[..2]), - [19] = string.Join('.', localIp[2..]) - } - }; - } - - private static string GetLocalIp(string host, int port) - { - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0); - socket.Connect(host, port); - var endPoint = socket.LocalEndPoint as IPEndPoint ?? throw new NotSupportedException(); - return endPoint.Address.ToString(); - } - - public Task TickAsync() - { - return _udpClient == null ? Task.CompletedTask : Core(); - - async Task Core() - { - RefreshScores(); - try - { - await _udpClient.SendAsync(_scoresBuffer.Data); - await _udpClient.SendAsync(_lastFinishedFrameProvider.LastFrame.Data); - } - catch (SocketException ex) - { - if (DateTime.Now > _nextFailLog) - { - _logger.LogWarning("could not send data to service point display: {}", ex.Message); - _nextFailLog = DateTime.Now + TimeSpan.FromSeconds(5); - } - } - } - } - - private void RefreshScores() - { - var playersToDisplay = _players.GetAll() - .OrderByDescending(p => p.Kills) - .Take(ScoresPlayerRows); - - var row = 2; - foreach (var p in playersToDisplay) - { - var score = p.Kills.ToString(); - var nameLength = Math.Min(p.Name.Length, ScoresWidth - score.Length - 1); - - var name = p.Name[..nameLength]; - var spaces = new string(' ', ScoresWidth - score.Length - nameLength); - - _scoresBuffer.Rows[row] = name + spaces + score; - row++; - } - - for (; row < 17; row++) - _scoresBuffer.Rows[row] = string.Empty; - } - - public void Dispose() - { - _udpClient?.Dispose(); - } -} \ No newline at end of file diff --git a/TanksServer/ServicePointDisplay/ServicePointDisplayConfiguration.cs b/TanksServer/ServicePointDisplay/ServicePointDisplayConfiguration.cs deleted file mode 100644 index 6a71db9..0000000 --- a/TanksServer/ServicePointDisplay/ServicePointDisplayConfiguration.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TanksServer.ServicePointDisplay; - -internal sealed class ServicePointDisplayConfiguration -{ - public bool Enable { get; set; } = true; - public string Hostname { get; set; } = string.Empty; - public int Port { get; set; } -} diff --git a/TanksServer/ServicePointDisplay/TextDisplayBuffer.cs b/TanksServer/ServicePointDisplay/TextDisplayBuffer.cs deleted file mode 100644 index ace76a7..0000000 --- a/TanksServer/ServicePointDisplay/TextDisplayBuffer.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace TanksServer.ServicePointDisplay; - -internal sealed class TextDisplayBuffer : DisplayBufferView -{ - public TextDisplayBuffer(TilePosition position, ushort charsPerRow, ushort rows) - : base(new byte[10 + charsPerRow * rows]) - { - Mode = 3; - WidthInTiles = charsPerRow; - RowCount = rows; - Position = position; - Rows = new FixedSizeCharGridView(Data.AsMemory(10), charsPerRow, rows); - } - - public FixedSizeCharGridView Rows { get; set; } -} \ No newline at end of file diff --git a/tank-frontend/.env b/tank-frontend/.env index d8123a6..d835d34 100644 --- a/tank-frontend/.env +++ b/tank-frontend/.env @@ -1,3 +1,7 @@ +#VITE_TANK_SCREEN_URL=ws://172.23.43.79/screen +#VITE_TANK_CONTROLS_URL=ws://172.23.43.79/controls +#VITE_TANK_PLAYER_URL=http://172.23.43.79/player + VITE_TANK_SCREEN_URL=ws://vinzenz-lpt2/screen VITE_TANK_CONTROLS_URL=ws://vinzenz-lpt2/controls VITE_TANK_PLAYER_URL=http://vinzenz-lpt2/player diff --git a/tank-frontend/src/ClientScreen.tsx b/tank-frontend/src/ClientScreen.tsx index 0a5e1b2..e5353a0 100644 --- a/tank-frontend/src/ClientScreen.tsx +++ b/tank-frontend/src/ClientScreen.tsx @@ -11,7 +11,7 @@ const offColor = [0, 0, 0, 255]; function getIndexes(bitIndex: number) { return { - byteIndex: 10 + Math.floor(bitIndex / 8), + byteIndex: Math.floor(bitIndex / 8), bitInByteIndex: 7 - bitIndex % 8 }; }