send scores to big display

This commit is contained in:
Vinzenz Schroeter 2024-04-09 22:38:56 +02:00
parent a89392beb8
commit 7f00160780
22 changed files with 305 additions and 126 deletions

View file

@ -1,11 +1,12 @@
using TanksServer.Helpers;
using TanksServer.ServicePointDisplay;
using TanksServer.Services;
namespace TanksServer.DrawSteps;
internal sealed class BulletDrawer(BulletManager bullets): IDrawStep
{
public void Draw(DisplayPixelBuffer buffer)
public void Draw(PixelDisplayBufferView buffer)
{
foreach (var bullet in bullets.GetAll())
buffer.Pixels[bullet.Position.ToPixelPosition().ToPixelIndex()] = true;

View file

@ -1,8 +1,8 @@
using TanksServer.Helpers;
using TanksServer.ServicePointDisplay;
namespace TanksServer.DrawSteps;
internal interface IDrawStep
{
void Draw(DisplayPixelBuffer buffer);
void Draw(PixelDisplayBufferView buffer);
}

View file

@ -1,12 +1,13 @@
using TanksServer.Helpers;
using TanksServer.Models;
using TanksServer.ServicePointDisplay;
using TanksServer.Services;
namespace TanksServer.DrawSteps;
internal sealed class MapDrawer(MapService map) : IDrawStep
{
public void Draw(DisplayPixelBuffer buffer)
public void Draw(PixelDisplayBufferView buffer)
{
for (var tileY = 0; tileY < MapService.TilesPerColumn; tileY++)
for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++)

View file

@ -1,6 +1,7 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TanksServer.Helpers;
using TanksServer.ServicePointDisplay;
using TanksServer.Services;
namespace TanksServer.DrawSteps;
@ -29,7 +30,7 @@ internal sealed class TankDrawer : IDrawStep
_tankSpriteWidth = tankImage.Width;
}
public void Draw(DisplayPixelBuffer buffer)
public void Draw(PixelDisplayBufferView buffer)
{
foreach (var tank in _tanks)
{

View file

@ -1,55 +0,0 @@
namespace TanksServer.Helpers;
internal sealed class DisplayPixelBuffer(byte[] data)
{
public byte[] Data => data;
public byte Magic1
{
get => data[0];
set => data[0] = value;
}
public byte Magic2
{
get => data[1];
set => data[1] = value;
}
public ushort X
{
get => GetTwoBytes(2);
set => SetTwoBytes(2, value);
}
public ushort Y
{
get => GetTwoBytes(4);
set => SetTwoBytes(4, value);
}
public ushort WidthInTiles
{
get => GetTwoBytes(6);
set => SetTwoBytes(6, value);
}
public ushort HeightInPixels
{
get => GetTwoBytes(8);
set => SetTwoBytes(8, value);
}
public FixedSizeBitFieldView Pixels { get; } = new(data.AsMemory(10));
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);
}
}

View file

@ -6,8 +6,8 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using TanksServer.DrawSteps;
using TanksServer.Helpers;
using TanksServer.Models;
using TanksServer.Servers;
using TanksServer.ServicePointDisplay;
using TanksServer.Services;
using TanksServer.TickSteps;
@ -81,7 +81,6 @@ internal static class Program
builder.Services.AddSingleton<MapService>();
builder.Services.AddSingleton<BulletManager>();
builder.Services.AddSingleton<TankManager>();
builder.Services.AddSingleton<SpawnNewTanks>();
builder.Services.AddSingleton<ControlsServer>();
builder.Services.AddSingleton<PlayerServer>();
builder.Services.AddSingleton<ClientScreenServer>();
@ -98,7 +97,7 @@ internal static class Program
builder.Services.AddSingleton<ITickStep, RotateTanks>();
builder.Services.AddSingleton<ITickStep, MoveTanks>();
builder.Services.AddSingleton<ITickStep, ShootFromTanks>();
builder.Services.AddSingleton<ITickStep>(sp => sp.GetRequiredService<SpawnNewTanks>());
builder.Services.AddSingleton<ITickStep, SpawnNewTanks>();
builder.Services.AddSingleton<ITickStep, DrawStateToFrame>();
builder.Services.AddSingleton<ITickStep, SendToServicePointDisplay>();
builder.Services.AddSingleton<ITickStep, SendToClientScreen>();

View file

@ -4,6 +4,7 @@ using System.Threading.Channels;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TanksServer.Helpers;
using TanksServer.ServicePointDisplay;
namespace TanksServer.Servers;
@ -65,7 +66,7 @@ internal sealed class ClientScreenServer(
Done = ReceiveAsync();
}
public async Task SendAsync(DisplayPixelBuffer buf)
public async Task SendAsync(PixelDisplayBufferView buf)
{
if (!await _wantedFrames.WaitAsync(TimeSpan.Zero))
{

View file

@ -29,6 +29,8 @@ internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueueProvi
foundPlayer = null;
return false;
}
public IEnumerable<Player> GetAll() => _players.Values;
private Player AddAndSpawn(string name)
{

View file

@ -0,0 +1,64 @@
using TanksServer.Models;
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);
}
}

View file

@ -1,6 +1,6 @@
using System.Collections;
namespace TanksServer.Helpers;
namespace TanksServer.ServicePointDisplay;
internal sealed class FixedSizeBitFieldView(Memory<byte> data) : IList<bool>
{
@ -8,6 +8,7 @@ internal sealed class FixedSizeBitFieldView(Memory<byte> data) : IList<bool>
public bool IsReadOnly => false;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<bool> GetEnumerator()
{
return Enumerable().GetEnumerator();
@ -32,10 +33,17 @@ internal sealed class FixedSizeBitFieldView(Memory<byte> data) : IList<bool>
array[i + arrayIndex] = this[i];
}
private static (int byteIndex, int bitInByteIndex) GetIndexes(int bitIndex)
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);
}
@ -71,4 +79,4 @@ internal sealed class FixedSizeBitFieldView(Memory<byte> data) : IList<bool>
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();
}
}

View file

@ -0,0 +1,39 @@
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;
}
}

View file

@ -0,0 +1,29 @@
using TanksServer.Helpers;
using TanksServer.Services;
namespace TanksServer.ServicePointDisplay;
internal sealed class PixelDisplayBufferView : DisplayBufferView
{
private PixelDisplayBufferView(byte[] data) : base(data)
{
Pixels = new FixedSizeBitFieldView(Data.AsMemory(10));
}
// ReSharper disable once CollectionNeverQueried.Global (setting values in collection updates underlying byte array)
public FixedSizeBitFieldView 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])
{
Mode = 19,
TileX = x,
TileY = y,
WidthInTiles = widthInTiles,
RowCount = pixelRows
};
}
}

View file

@ -0,0 +1,103 @@
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using TanksServer.Servers;
using TanksServer.Services;
using TanksServer.TickSteps;
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 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;
_scoresBuffer = new(new(MapService.TilesPerRow, 0), 12, 20);
_scoresBuffer.Rows[0] = "== TANKS! ==";
_scoresBuffer.Rows[1] = "-- scores --";
_scoresBuffer.Rows[17] = "-- join --";
var localIp = GetLocalIp(options.Value.Hostname, options.Value.Port).Split('.');
Debug.Assert(localIp.Length == 4); // were talking legacy ip
_scoresBuffer.Rows[18] = string.Join('.', localIp[..2]);
_scoresBuffer.Rows[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)
{
_logger.LogWarning(ex, "could not send data to service point display");
}
}
}
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 = ScoresWidth - score.Length;
var name = p.Name[..nameLength];
var spaces = new string(' ', nameLength - name.Length + 1);
_scoresBuffer.Rows[row] = name + spaces + score;
row++;
}
for (; row < 17; row++)
_scoresBuffer.Rows[row] = string.Empty;
}
public void Dispose()
{
_udpClient?.Dispose();
}
}

View file

@ -1,4 +1,4 @@
namespace TanksServer.Models;
namespace TanksServer.ServicePointDisplay;
internal sealed class ServicePointDisplayConfiguration
{

View file

@ -0,0 +1,18 @@
using TanksServer.Models;
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; }
}

View file

@ -3,12 +3,16 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TanksServer.TickSteps;
namespace TanksServer;
namespace TanksServer.Services;
internal sealed class GameTickWorker(
IEnumerable<ITickStep> steps, IHostApplicationLifetime lifetime, ILogger<GameTickWorker> logger
IEnumerable<ITickStep> steps,
IHostApplicationLifetime lifetime,
ILogger<GameTickWorker> logger
) : IHostedService, IDisposable
{
private const int TicksPerSecond = 25;
private static readonly TimeSpan TickPacing = TimeSpan.FromMilliseconds((int)(1000 / TicksPerSecond));
private readonly CancellationTokenSource _cancellation = new();
private readonly List<ITickStep> _steps = steps.ToList();
private Task? _run;
@ -31,8 +35,10 @@ internal sealed class GameTickWorker(
foreach (var step in _steps)
await step.TickAsync();
await Task.Delay(TimeSpan.FromMilliseconds(1000 / 25) - sw.Elapsed);
var wantedDelay = TickPacing - sw.Elapsed;
if (wantedDelay.Ticks > 0)
await Task.Delay(wantedDelay);
}
}
catch (Exception ex)
@ -53,4 +59,4 @@ internal sealed class GameTickWorker(
_cancellation.Dispose();
_run?.Dispose();
}
}
}

View file

@ -1,12 +1,12 @@
using TanksServer.Helpers;
using TanksServer.ServicePointDisplay;
namespace TanksServer.Services;
internal sealed class LastFinishedFrameProvider
{
private DisplayPixelBuffer? _lastFrame;
private PixelDisplayBufferView? _lastFrame;
public DisplayPixelBuffer LastFrame
public PixelDisplayBufferView LastFrame
{
get => _lastFrame ?? throw new InvalidOperationException("first frame not yet drawn");
set => _lastFrame = value;

View file

@ -1,5 +1,5 @@
using TanksServer.DrawSteps;
using TanksServer.Helpers;
using TanksServer.ServicePointDisplay;
using TanksServer.Services;
namespace TanksServer.TickSteps;
@ -8,30 +8,14 @@ internal sealed class DrawStateToFrame(
IEnumerable<IDrawStep> drawSteps, LastFinishedFrameProvider lastFrameProvider
) : ITickStep
{
private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn;
private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
public Task TickAsync()
{
var buffer = CreateGameFieldPixelBuffer();
var buffer = PixelDisplayBufferView.New(0, 0, MapService.TilesPerRow, MapService.PixelsPerColumn);
foreach (var step in _drawSteps)
step.Draw(buffer);
lastFrameProvider.LastFrame = buffer;
return Task.CompletedTask;
}
private static DisplayPixelBuffer CreateGameFieldPixelBuffer()
{
var data = new byte[10 + GameFieldPixelCount / 8];
var result = new DisplayPixelBuffer(data)
{
Magic1 = 0,
Magic2 = 19,
X = 0,
Y = 0,
WidthInTiles = MapService.TilesPerRow,
HeightInPixels = MapService.PixelsPerColumn
};
return result;
}
}

View file

@ -1,25 +0,0 @@
using System.Net.Sockets;
using TanksServer.Models;
using TanksServer.Services;
namespace TanksServer.TickSteps;
internal sealed class SendToServicePointDisplay(
IOptions<ServicePointDisplayConfiguration> options,
LastFinishedFrameProvider lastFinishedFrameProvider
) : ITickStep, IDisposable
{
private readonly UdpClient? _udpClient = options.Value.Enable
? new(options.Value.Hostname, options.Value.Port)
: null;
public Task TickAsync()
{
return _udpClient?.SendAsync(lastFinishedFrameProvider.LastFrame.Data).AsTask() ?? Task.CompletedTask;
}
public void Dispose()
{
_udpClient?.Dispose();
}
}

View file

@ -4,7 +4,9 @@ using TanksServer.Services;
namespace TanksServer.TickSteps;
internal sealed class ShootFromTanks(
TankManager tanks, IOptions<TanksConfiguration> options, BulletManager bulletManager
TankManager tanks,
IOptions<TanksConfiguration> options,
BulletManager bulletManager
) : ITickStep
{
private readonly TanksConfiguration _config = options.Value;
@ -34,4 +36,4 @@ internal sealed class ShootFromTanks(
bulletManager.Spawn(new Bullet(tank.Owner, position, tank.Rotation));
}
}
}

View file

@ -15,8 +15,9 @@
}
},
"ServicePointDisplay": {
"Enable": false,
"Hostname": "172.23.42.29",
"Enable": true,
//"Hostname": "172.23.42.29",
"Hostname": "localhost",
"Port": 2342
}
}

View file

@ -1,3 +1,3 @@
VITE_TANK_SCREEN_URL=ws://localhost:3000/screen
VITE_TANK_CONTROLS_URL=ws://localhost:3000/controls
VITE_TANK_PLAYER_URL=http://localhost:3000/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