more commands, change display communication to new lib
This commit is contained in:
parent
38463ac109
commit
7213318838
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
.idea
|
.idea
|
||||||
|
client
|
||||||
|
|
||||||
|
|
|
@ -22,4 +22,6 @@ public class ByteGrid(ushort width, ushort height)
|
||||||
Debug.Assert(y < Height);
|
Debug.Assert(y < Height);
|
||||||
return x + y * Width;
|
return x + y * Width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Clear() => Data.Span.Clear();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,2 @@
|
||||||
// Global using directives
|
|
||||||
|
|
||||||
global using System;
|
global using System;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
|
@ -7,7 +7,18 @@ public interface IDisplayConnection
|
||||||
ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid);
|
ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid);
|
||||||
|
|
||||||
ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma);
|
ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma);
|
||||||
|
|
||||||
ValueTask SendBrightnessAsync(byte brightness);
|
ValueTask SendBrightnessAsync(byte brightness);
|
||||||
|
|
||||||
ValueTask SendHardResetAsync();
|
ValueTask SendHardResetAsync();
|
||||||
|
|
||||||
ValueTask SendFadeOutAsync(byte loops);
|
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.Buffers;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
@ -74,6 +75,27 @@ internal sealed class DisplayConnection(IOptions<DisplayConfiguration> options)
|
||||||
_arrayPool.Return(payloadBuffer);
|
_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)
|
private async ValueTask SendAsync(HeaderWindow header, Memory<byte> payload)
|
||||||
{
|
{
|
||||||
int headerSize;
|
int headerSize;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
namespace DisplayCommands.Internals;
|
namespace DisplayCommands.Internals;
|
||||||
|
|
||||||
internal enum DisplaySubCommand
|
internal enum DisplaySubCommand : ushort
|
||||||
{
|
{
|
||||||
BitmapNormal = 0x0,
|
BitmapNormal = 0x0,
|
||||||
BitmapCompressZ = 0x677a,
|
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.GameLogic;
|
||||||
using TanksServer.ServicePointDisplay;
|
|
||||||
|
|
||||||
namespace TanksServer.Graphics;
|
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())
|
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.GameLogic;
|
||||||
using TanksServer.ServicePointDisplay;
|
|
||||||
|
|
||||||
namespace TanksServer.Graphics;
|
namespace TanksServer.Graphics;
|
||||||
|
|
||||||
|
@ -8,13 +8,14 @@ internal sealed class DrawStateToFrame(
|
||||||
) : ITickStep
|
) : ITickStep
|
||||||
{
|
{
|
||||||
private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
|
private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
|
||||||
|
private readonly PixelGrid _drawGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
|
||||||
|
|
||||||
public Task TickAsync()
|
public Task TickAsync()
|
||||||
{
|
{
|
||||||
var buffer = PixelDisplayBufferView.New(0, 0, MapService.TilesPerRow, MapService.PixelsPerColumn);
|
_drawGrid.Clear();
|
||||||
foreach (var step in _drawSteps)
|
foreach (var step in _drawSteps)
|
||||||
step.Draw(buffer);
|
step.Draw(_drawGrid);
|
||||||
lastFrameProvider.LastFrame = buffer;
|
lastFrameProvider.LastFrame = _drawGrid;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using TanksServer.ServicePointDisplay;
|
using DisplayCommands;
|
||||||
|
|
||||||
namespace TanksServer.Graphics;
|
namespace TanksServer.Graphics;
|
||||||
|
|
||||||
internal interface IDrawStep
|
internal interface IDrawStep
|
||||||
{
|
{
|
||||||
void Draw(PixelDisplayBufferView buffer);
|
void Draw(PixelGrid buffer);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
using TanksServer.ServicePointDisplay;
|
using DisplayCommands;
|
||||||
|
|
||||||
namespace TanksServer.Graphics;
|
namespace TanksServer.Graphics;
|
||||||
|
|
||||||
internal sealed class LastFinishedFrameProvider
|
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");
|
get => _lastFrame ?? throw new InvalidOperationException("first frame not yet drawn");
|
||||||
set => _lastFrame = value;
|
set => _lastFrame = value;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
using DisplayCommands;
|
||||||
using TanksServer.GameLogic;
|
using TanksServer.GameLogic;
|
||||||
using TanksServer.ServicePointDisplay;
|
|
||||||
|
|
||||||
namespace TanksServer.Graphics;
|
namespace TanksServer.Graphics;
|
||||||
|
|
||||||
internal sealed class MapDrawer(MapService map) : IDrawStep
|
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 tileY = 0; tileY < MapService.TilesPerColumn; tileY++)
|
||||||
for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++)
|
for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++)
|
||||||
|
@ -18,7 +18,7 @@ internal sealed class MapDrawer(MapService map) : IDrawStep
|
||||||
for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++)
|
for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++)
|
||||||
{
|
{
|
||||||
var position = tile.GetPixelRelative(pixelInTileX, pixelInTileY);
|
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;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using TanksServer.GameLogic;
|
using TanksServer.GameLogic;
|
||||||
using TanksServer.ServicePointDisplay;
|
|
||||||
|
|
||||||
namespace TanksServer.Graphics;
|
namespace TanksServer.Graphics;
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ internal sealed class TankDrawer : IDrawStep
|
||||||
_tankSpriteWidth = tankImage.Width;
|
_tankSpriteWidth = tankImage.Width;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(PixelDisplayBufferView buffer)
|
public void Draw(PixelGrid buffer)
|
||||||
{
|
{
|
||||||
foreach (var tank in _tanks)
|
foreach (var tank in _tanks)
|
||||||
{
|
{
|
||||||
|
@ -42,8 +42,8 @@ internal sealed class TankDrawer : IDrawStep
|
||||||
if (!TankSpriteAt(dx, dy, rotationVariant))
|
if (!TankSpriteAt(dx, dy, rotationVariant))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var position = new PixelPosition(pos.X + dx, pos.Y + dy);
|
var position = new PixelPosition((ushort)(pos.X + dx), (ushort)(pos.Y + dy));
|
||||||
buffer.Pixels[position] = true;
|
buffer[position.X, position.Y] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,15 @@ namespace TanksServer.Interactivity;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hacky class for easier semantics
|
/// Hacky class for easier semantics
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ByteChannelWebSocket : Channel<byte[]>
|
internal sealed class ByteChannelWebSocket : Channel<Memory<byte>>
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly WebSocket _socket;
|
private readonly WebSocket _socket;
|
||||||
private readonly Task _backgroundDone;
|
private readonly Task _backgroundDone;
|
||||||
private readonly byte[] _buffer;
|
private readonly byte[] _buffer;
|
||||||
|
|
||||||
private readonly Channel<byte[]> _outgoing = Channel.CreateUnbounded<byte[]>();
|
private readonly Channel<Memory<byte>> _outgoing = Channel.CreateUnbounded<Memory<byte>>();
|
||||||
private readonly Channel<byte[]> _incoming = Channel.CreateUnbounded<byte[]>();
|
private readonly Channel<Memory<byte>> _incoming = Channel.CreateUnbounded<Memory<byte>>();
|
||||||
|
|
||||||
public ByteChannelWebSocket(WebSocket socket, ILogger logger, int messageSize)
|
public ByteChannelWebSocket(WebSocket socket, ILogger logger, int messageSize)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
using DisplayCommands;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using TanksServer.ServicePointDisplay;
|
|
||||||
|
|
||||||
namespace TanksServer.Interactivity;
|
namespace TanksServer.Interactivity;
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ internal sealed class ClientScreenServer(
|
||||||
Done = ReceiveAsync();
|
Done = ReceiveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendAsync(PixelDisplayBufferView buf)
|
public async Task SendAsync(PixelGrid buf)
|
||||||
{
|
{
|
||||||
if (!await _wantedFrames.WaitAsync(TimeSpan.Zero))
|
if (!await _wantedFrames.WaitAsync(TimeSpan.Zero))
|
||||||
{
|
{
|
||||||
|
|
|
@ -71,8 +71,8 @@ internal sealed class ControlsServer(ILogger<ControlsServer> logger, ILoggerFact
|
||||||
{
|
{
|
||||||
await foreach (var buffer in _binaryWebSocket.Reader.ReadAllAsync())
|
await foreach (var buffer in _binaryWebSocket.Reader.ReadAllAsync())
|
||||||
{
|
{
|
||||||
var type = (MessageType)buffer[0];
|
var type = (MessageType)buffer.Span[0];
|
||||||
var control = (InputType)buffer[1];
|
var control = (InputType)buffer.Span[1];
|
||||||
|
|
||||||
_logger.LogTrace("player input {} {} {}", _player.Id, type, control);
|
_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;
|
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(subX < 8);
|
||||||
Debug.Assert(subY < 8);
|
Debug.Assert(subY < 8);
|
||||||
return new PixelPosition(
|
return new PixelPosition(
|
||||||
X: position.X * MapService.TileSize + subX,
|
X: (ushort)(position.X * MapService.TileSize + subX),
|
||||||
Y: position.Y * MapService.TileSize + subY
|
Y: (ushort)(position.Y * MapService.TileSize + subY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static PixelPosition ToPixelPosition(this FloatPosition position) => new(
|
public static PixelPosition ToPixelPosition(this FloatPosition position) => new(
|
||||||
X: (int)position.X % MapService.PixelsPerRow,
|
X: (ushort)((int)position.X % MapService.PixelsPerRow),
|
||||||
Y: (int)position.Y % MapService.PixelsPerRow
|
Y: (ushort)((int)position.Y % MapService.PixelsPerRow)
|
||||||
);
|
);
|
||||||
|
|
||||||
public static TilePosition ToTilePosition(this PixelPosition position) => new(
|
public static TilePosition ToTilePosition(this PixelPosition position) => new(
|
||||||
|
|
|
@ -8,7 +8,6 @@ using Microsoft.Extensions.FileProviders;
|
||||||
using TanksServer.GameLogic;
|
using TanksServer.GameLogic;
|
||||||
using TanksServer.Graphics;
|
using TanksServer.Graphics;
|
||||||
using TanksServer.Interactivity;
|
using TanksServer.Interactivity;
|
||||||
using TanksServer.ServicePointDisplay;
|
|
||||||
|
|
||||||
namespace TanksServer;
|
namespace TanksServer;
|
||||||
|
|
||||||
|
@ -109,8 +108,6 @@ public static class Program
|
||||||
builder.Services.AddSingleton<IDrawStep, TankDrawer>();
|
builder.Services.AddSingleton<IDrawStep, TankDrawer>();
|
||||||
builder.Services.AddSingleton<IDrawStep, BulletDrawer>();
|
builder.Services.AddSingleton<IDrawStep, BulletDrawer>();
|
||||||
|
|
||||||
builder.Services.Configure<ServicePointDisplayConfiguration>(
|
|
||||||
builder.Configuration.GetSection("ServicePointDisplay"));
|
|
||||||
builder.Services.Configure<TanksConfiguration>(
|
builder.Services.Configure<TanksConfiguration>(
|
||||||
builder.Configuration.GetSection("Tanks"));
|
builder.Configuration.GetSection("Tanks"));
|
||||||
builder.Services.Configure<PlayersConfiguration>(
|
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_SCREEN_URL=ws://vinzenz-lpt2/screen
|
||||||
VITE_TANK_CONTROLS_URL=ws://vinzenz-lpt2/controls
|
VITE_TANK_CONTROLS_URL=ws://vinzenz-lpt2/controls
|
||||||
VITE_TANK_PLAYER_URL=http://vinzenz-lpt2/player
|
VITE_TANK_PLAYER_URL=http://vinzenz-lpt2/player
|
||||||
|
|
|
@ -11,7 +11,7 @@ const offColor = [0, 0, 0, 255];
|
||||||
|
|
||||||
function getIndexes(bitIndex: number) {
|
function getIndexes(bitIndex: number) {
|
||||||
return {
|
return {
|
||||||
byteIndex: 10 + Math.floor(bitIndex / 8),
|
byteIndex: Math.floor(bitIndex / 8),
|
||||||
bitInByteIndex: 7 - bitIndex % 8
|
bitInByteIndex: 7 - bitIndex % 8
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue