more commands, change display communication to new lib
This commit is contained in:
		
							parent
							
								
									38463ac109
								
							
						
					
					
						commit
						7213318838
					
				
					 31 changed files with 240 additions and 417 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,3 +1,5 @@ | |||
| bin | ||||
| obj | ||||
| .idea | ||||
| client | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,2 @@ | |||
| // Global using directives | ||||
| 
 | ||||
| global using System; | ||||
| global using System.Threading.Tasks; | ||||
| global using System.Threading.Tasks; | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Returns the IPv4 address that is associated with the interface with which the display is reachable. | ||||
|     /// </summary> | ||||
|     /// <returns>IPv4 as text</returns> | ||||
|     public string GetLocalIPv4(); | ||||
| } | ||||
|  | @ -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<DisplayConfiguration> 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<byte> payload) | ||||
|     { | ||||
|         int headerSize; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| namespace DisplayCommands.Internals; | ||||
| 
 | ||||
| internal enum DisplaySubCommand | ||||
| internal enum DisplaySubCommand : ushort | ||||
| { | ||||
|     BitmapNormal = 0x0, | ||||
|     BitmapCompressZ = 0x677a, | ||||
|  |  | |||
							
								
								
									
										17
									
								
								DisplayCommands/Internals/HeaderBitmap.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								DisplayCommands/Internals/HeaderBitmap.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
| } | ||||
							
								
								
									
										47
									
								
								DisplayCommands/PixelGrid.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								DisplayCommands/PixelGrid.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<byte> 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); | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | @ -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<IDrawStep> _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; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| using TanksServer.ServicePointDisplay; | ||||
| using DisplayCommands; | ||||
| 
 | ||||
| namespace TanksServer.Graphics; | ||||
| 
 | ||||
| internal interface IDrawStep | ||||
| { | ||||
|     void Draw(PixelDisplayBufferView buffer); | ||||
|     void Draw(PixelGrid buffer); | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | @ -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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -7,15 +7,15 @@ namespace TanksServer.Interactivity; | |||
| /// <summary> | ||||
| /// Hacky class for easier semantics | ||||
| /// </summary> | ||||
| internal sealed class ByteChannelWebSocket : Channel<byte[]> | ||||
| internal sealed class ByteChannelWebSocket : Channel<Memory<byte>> | ||||
| { | ||||
|     private readonly ILogger _logger; | ||||
|     private readonly WebSocket _socket; | ||||
|     private readonly Task _backgroundDone; | ||||
|     private readonly byte[] _buffer; | ||||
| 
 | ||||
|     private readonly Channel<byte[]> _outgoing = Channel.CreateUnbounded<byte[]>(); | ||||
|     private readonly Channel<byte[]> _incoming = Channel.CreateUnbounded<byte[]>(); | ||||
|     private readonly Channel<Memory<byte>> _outgoing = Channel.CreateUnbounded<Memory<byte>>(); | ||||
|     private readonly Channel<Memory<byte>> _incoming = Channel.CreateUnbounded<Memory<byte>>(); | ||||
| 
 | ||||
|     public ByteChannelWebSocket(WebSocket socket, ILogger logger, int messageSize) | ||||
|     { | ||||
|  |  | |||
|  | @ -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<ClientScreenServerConnection> 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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | @ -71,8 +71,8 @@ internal sealed class ControlsServer(ILogger<ControlsServer> 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); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										87
									
								
								TanksServer/Interactivity/SendToServicePointDisplay.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								TanksServer/Interactivity/SendToServicePointDisplay.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<SendToServicePointDisplay> _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<SendToServicePointDisplay> 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; | ||||
|     } | ||||
| } | ||||
|  | @ -1,3 +1,3 @@ | |||
| namespace TanksServer.Models; | ||||
| 
 | ||||
| internal record struct PixelPosition(int X, int Y); | ||||
| internal record struct PixelPosition(ushort X, ushort Y); | ||||
|  | @ -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( | ||||
|  |  | |||
|  | @ -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<IDrawStep, TankDrawer>(); | ||||
|         builder.Services.AddSingleton<IDrawStep, BulletDrawer>(); | ||||
| 
 | ||||
|         builder.Services.Configure<ServicePointDisplayConfiguration>( | ||||
|             builder.Configuration.GetSection("ServicePointDisplay")); | ||||
|         builder.Services.Configure<TanksConfiguration>( | ||||
|             builder.Configuration.GetSection("Tanks")); | ||||
|         builder.Services.Configure<PlayersConfiguration>( | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  | @ -1,23 +0,0 @@ | |||
| using System.Diagnostics; | ||||
| 
 | ||||
| namespace TanksServer.ServicePointDisplay; | ||||
| 
 | ||||
| internal sealed class FixedSizeBitGridView(Memory<byte> 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; | ||||
|     } | ||||
| } | ||||
|  | @ -1,82 +0,0 @@ | |||
| using System.Collections; | ||||
| 
 | ||||
| namespace TanksServer.ServicePointDisplay; | ||||
| 
 | ||||
| internal sealed class FixedSizeBitRowView(Memory<byte> data) : IList<bool> | ||||
| { | ||||
|     public int Count => data.Length * 8; | ||||
|     public bool IsReadOnly => false; | ||||
| 
 | ||||
|     IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | ||||
| 
 | ||||
|     public IEnumerator<bool> GetEnumerator() | ||||
|     { | ||||
|         return Enumerable().GetEnumerator(); | ||||
| 
 | ||||
|         IEnumerable<bool> 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(); | ||||
| } | ||||
|  | @ -1,39 +0,0 @@ | |||
| using System.Text; | ||||
| 
 | ||||
| namespace TanksServer.ServicePointDisplay; | ||||
| 
 | ||||
| internal sealed class FixedSizeCharGridView(Memory<byte> 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; | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | @ -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<SendToServicePointDisplay> _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<ServicePointDisplayConfiguration> options, | ||||
|         LastFinishedFrameProvider lastFinishedFrameProvider, | ||||
|         PlayerServer players, | ||||
|         ILogger<SendToServicePointDisplay> 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(); | ||||
|     } | ||||
| } | ||||
|  | @ -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; } | ||||
| } | ||||
|  | @ -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; } | ||||
| } | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|     }; | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter