diff --git a/DisplayCommands/ByteGrid.cs b/DisplayCommands/ByteGrid.cs new file mode 100644 index 0000000..889d119 --- /dev/null +++ b/DisplayCommands/ByteGrid.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; + +namespace DisplayCommands; + +public class ByteGrid(ushort width, ushort height) +{ + public ushort Height { get; } = height; + + public ushort Width { get; } = width; + + internal Memory Data { get; } = new byte[width * height].AsMemory(); + + public byte this[ushort x, ushort y] + { + get => Data.Span[ GetIndex(x, y)]; + set => Data.Span[GetIndex(x, y)] = value; + } + + private int GetIndex(ushort x, ushort y) + { + Debug.Assert(x < Width); + Debug.Assert(y < Height); + return x + y * Width; + } +} diff --git a/DisplayCommands/Cp437Grid.cs b/DisplayCommands/Cp437Grid.cs new file mode 100644 index 0000000..3dd6e99 --- /dev/null +++ b/DisplayCommands/Cp437Grid.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using System.Text; + +namespace DisplayCommands; + +public sealed class Cp437Grid(ushort width, ushort height) +{ + private readonly ByteGrid _byteGrid = new(width, height); + + public ushort Height { get; } = height; + public ushort Width { get; } = width; + + internal Memory Data => _byteGrid.Data; + + private readonly Encoding _encoding = Encoding.GetEncoding(437); + + public char this[ushort x, ushort y] + { + get => ByteToChar(_byteGrid[x, y]); + set => _byteGrid[x, y] = CharToByte(value); + } + + public string this[ushort row] + { + get + { + var rowStart = row * Width; + return _encoding.GetString(_byteGrid.Data[rowStart..(rowStart + Width)].Span); + } + set + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(row, Height, nameof(row)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value.Length, Width, nameof(value)); + ushort x = 0; + for (; x < value.Length; x++) + _byteGrid[x, row] = CharToByte(value[x]); + for (; x < Width; x++) + _byteGrid[x, row] = CharToByte(' '); + } + } + + private byte CharToByte(char c) + { + ReadOnlySpan valuesStr = stackalloc char[] { c }; + Span convertedStr = stackalloc byte[1]; + var consumed = _encoding.GetBytes(valuesStr, convertedStr); + Debug.Assert(consumed == 1); + return convertedStr[0]; + + } + + private char ByteToChar(byte b) + { + ReadOnlySpan valueBytes = stackalloc byte[] { b }; + Span resultStr = stackalloc char[1]; + _encoding.GetChars(valueBytes, resultStr); + return resultStr[0]; + } +} \ No newline at end of file diff --git a/DisplayCommands/DisplayCommands.csproj b/DisplayCommands/DisplayCommands.csproj new file mode 100644 index 0000000..068e186 --- /dev/null +++ b/DisplayCommands/DisplayCommands.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + disable + enable + true + true + true + + + + Recommended + true + CA1805,CA1848 + + + + + + + + + + + + diff --git a/DisplayCommands/DisplayConfiguration.cs b/DisplayCommands/DisplayConfiguration.cs new file mode 100644 index 0000000..ff3cf05 --- /dev/null +++ b/DisplayCommands/DisplayConfiguration.cs @@ -0,0 +1,8 @@ +namespace DisplayCommands; + +public class DisplayConfiguration +{ + public string Hostname { get; set; } = "172.23.42.29"; + + public int Port { get; set; } = 2342; +} \ No newline at end of file diff --git a/DisplayCommands/DisplayExtensions.cs b/DisplayCommands/DisplayExtensions.cs new file mode 100644 index 0000000..5c94d8d --- /dev/null +++ b/DisplayCommands/DisplayExtensions.cs @@ -0,0 +1,25 @@ +using System.Text; +using DisplayCommands.Internals; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace DisplayCommands; + +public static class DisplayExtensions +{ + static DisplayExtensions() + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + } + + public static IServiceCollection AddDisplay( + this IServiceCollection services, + IConfigurationSection? configurationSection = null + ) + { + services.AddSingleton(); + if (configurationSection != null) + services.Configure(configurationSection); + return services; + } +} \ No newline at end of file diff --git a/DisplayCommands/GlobalUsings.cs b/DisplayCommands/GlobalUsings.cs new file mode 100644 index 0000000..129a986 --- /dev/null +++ b/DisplayCommands/GlobalUsings.cs @@ -0,0 +1,4 @@ +// Global using directives + +global using System; +global using System.Threading.Tasks; \ No newline at end of file diff --git a/DisplayCommands/IDisplayConnection.cs b/DisplayCommands/IDisplayConnection.cs new file mode 100644 index 0000000..5354390 --- /dev/null +++ b/DisplayCommands/IDisplayConnection.cs @@ -0,0 +1,13 @@ +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); +} \ No newline at end of file diff --git a/DisplayCommands/Internals/DisplayCommand.cs b/DisplayCommands/Internals/DisplayCommand.cs new file mode 100644 index 0000000..9574dc9 --- /dev/null +++ b/DisplayCommands/Internals/DisplayCommand.cs @@ -0,0 +1,17 @@ +namespace DisplayCommands.Internals; + +internal enum DisplayCommand: ushort +{ + Clear = 0x0002, + Cp437Data = 0x0003, + CharBrightness = 0x0005, + Brightness = 0x0007, + HardReset = 0x000b, + FadeOut = 0x000d, + BitmapLegacy = 0x0010, + BitmapLinear = 0x0012, + BitmapLinearWin = 0x0013, + BitmapLinearAnd = 0x0014, + BitmapLinearOr = 0x0015, + BitmapLinearXor = 0x0016, +} diff --git a/DisplayCommands/Internals/DisplayConnection.cs b/DisplayCommands/Internals/DisplayConnection.cs new file mode 100644 index 0000000..0e04969 --- /dev/null +++ b/DisplayCommands/Internals/DisplayConnection.cs @@ -0,0 +1,104 @@ +using System.Buffers; +using System.Diagnostics; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Options; + +namespace DisplayCommands.Internals; + +internal sealed class DisplayConnection(IOptions options) : IDisplayConnection, IDisposable +{ + private readonly UdpClient _udpClient = new(options.Value.Hostname, options.Value.Port); + private readonly ArrayPool _arrayPool = ArrayPool.Shared; + + public ValueTask SendClearAsync() + { + var header = new HeaderWindow { Command = DisplayCommand.Clear }; + return SendAsync(header, Memory.Empty); + } + + public ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid) + { + var header = new HeaderWindow + { + Command = DisplayCommand.Cp437Data, + Height = grid.Height, + Width = grid.Width, + PosX = x, + PosY = y + }; + return SendAsync(header, grid.Data); + } + + public ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma) + { + var header = new HeaderWindow + { + Command = DisplayCommand.CharBrightness, + PosX = x, + PosY = y, + Height = luma.Height, + Width = luma.Width + }; + + return SendAsync(header, luma.Data); + } + + public async ValueTask SendBrightnessAsync(byte brightness) + { + var header = new HeaderWindow { Command = DisplayCommand.Brightness }; + + var payloadBuffer = _arrayPool.Rent(1); + var payload = payloadBuffer.AsMemory(0, 1); + payload.Span[0] = brightness; + + await SendAsync(header, payload); + _arrayPool.Return(payloadBuffer); + } + + public ValueTask SendHardResetAsync() + { + var header = new HeaderWindow { Command = DisplayCommand.HardReset }; + return SendAsync(header, Memory.Empty); + } + + public async ValueTask SendFadeOutAsync(byte loops) + { + var header = new HeaderWindow { Command = DisplayCommand.FadeOut }; + + var payloadBuffer = _arrayPool.Rent(1); + var payload = payloadBuffer.AsMemory(0, 1); + payload.Span[0] = loops; + + await SendAsync(header, payload); + _arrayPool.Return(payloadBuffer); + } + + private async ValueTask SendAsync(HeaderWindow header, Memory payload) + { + int headerSize; + unsafe + { + // because we specified the struct layout, no platform-specific padding will be added and this is be safe. + headerSize = sizeof(HeaderWindow); + } + + Debug.Assert(headerSize == 10); + var messageSize = headerSize + payload.Length; + + var buffer = _arrayPool.Rent(messageSize); + var message = buffer.AsMemory(0, messageSize); + + MemoryMarshal.Write(message.Span, header); + payload.CopyTo(message[headerSize..]); + + await _udpClient.SendAsync(message); + + _arrayPool.Return(buffer); + } + + public void Dispose() + { + _udpClient.Dispose(); + } +} \ No newline at end of file diff --git a/DisplayCommands/Internals/DisplaySubCommand.cs b/DisplayCommands/Internals/DisplaySubCommand.cs new file mode 100644 index 0000000..66e0691 --- /dev/null +++ b/DisplayCommands/Internals/DisplaySubCommand.cs @@ -0,0 +1,10 @@ +namespace DisplayCommands.Internals; + +internal enum DisplaySubCommand +{ + BitmapNormal = 0x0, + BitmapCompressZ = 0x677a, + BitmapCompressBz = 0x627a, + BitmapCompressLz = 0x6c7a, + BitmapCompressZs = 0x7a73, +} \ No newline at end of file diff --git a/DisplayCommands/Internals/HeaderWindow.cs b/DisplayCommands/Internals/HeaderWindow.cs new file mode 100644 index 0000000..05d5541 --- /dev/null +++ b/DisplayCommands/Internals/HeaderWindow.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace DisplayCommands.Internals; + +[StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)] +internal struct HeaderWindow +{ + public DisplayCommand Command; + + public ushort PosX; + + public ushort PosY; + + public ushort Width; + + public ushort Height; +} \ No newline at end of file diff --git a/TanksServer.sln b/TanksServer.sln index 8f630dd..0580f66 100644 --- a/TanksServer.sln +++ b/TanksServer.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TanksServer", "TanksServer\TanksServer.csproj", "{D88BF376-47A4-4C72-ADD1-983F9285C351}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisplayCommands", "DisplayCommands\DisplayCommands.csproj", "{B4B43561-7A2C-486B-99F7-E58A67BC370A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +14,9 @@ Global {D88BF376-47A4-4C72-ADD1-983F9285C351}.Debug|Any CPU.Build.0 = Debug|Any CPU {D88BF376-47A4-4C72-ADD1-983F9285C351}.Release|Any CPU.ActiveCfg = Release|Any CPU {D88BF376-47A4-4C72-ADD1-983F9285C351}.Release|Any CPU.Build.0 = Release|Any CPU + {B4B43561-7A2C-486B-99F7-E58A67BC370A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4B43561-7A2C-486B-99F7-E58A67BC370A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index f404bf1..72ea92c 100644 --- a/TanksServer/Program.cs +++ b/TanksServer/Program.cs @@ -1,5 +1,5 @@ using System.IO; -using System.Xml; +using DisplayCommands; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -115,6 +115,7 @@ public static class Program builder.Configuration.GetSection("Tanks")); builder.Services.Configure( builder.Configuration.GetSection("Players")); + builder.Services.AddDisplay(builder.Configuration.GetSection("ServicePointDisplay")); var app = builder.Build(); diff --git a/TanksServer/TanksServer.csproj b/TanksServer/TanksServer.csproj index b6d1a42..b42f76c 100644 --- a/TanksServer/TanksServer.csproj +++ b/TanksServer/TanksServer.csproj @@ -42,4 +42,8 @@ + + + +