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(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue