send scores to big display
This commit is contained in:
parent
a89392beb8
commit
7f00160780
22 changed files with 305 additions and 126 deletions
64
TanksServer/ServicePointDisplay/DisplayBufferView.cs
Normal file
64
TanksServer/ServicePointDisplay/DisplayBufferView.cs
Normal 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);
|
||||
}
|
||||
}
|
82
TanksServer/ServicePointDisplay/FixedSizeBitFieldView.cs
Normal file
82
TanksServer/ServicePointDisplay/FixedSizeBitFieldView.cs
Normal file
|
@ -0,0 +1,82 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace TanksServer.ServicePointDisplay;
|
||||
|
||||
internal sealed class FixedSizeBitFieldView(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();
|
||||
}
|
39
TanksServer/ServicePointDisplay/FixedSizeCharGridView.cs
Normal file
39
TanksServer/ServicePointDisplay/FixedSizeCharGridView.cs
Normal 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;
|
||||
}
|
||||
}
|
29
TanksServer/ServicePointDisplay/PixelDisplayBufferView.cs
Normal file
29
TanksServer/ServicePointDisplay/PixelDisplayBufferView.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
103
TanksServer/ServicePointDisplay/SendToServicePointDisplay.cs
Normal file
103
TanksServer/ServicePointDisplay/SendToServicePointDisplay.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
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; }
|
||||
}
|
18
TanksServer/ServicePointDisplay/TextDisplayBuffer.cs
Normal file
18
TanksServer/ServicePointDisplay/TextDisplayBuffer.cs
Normal 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; }
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue