WIP switch to ServicePoint2 library
This commit is contained in:
		
							parent
							
								
									eb999b0d1a
								
							
						
					
					
						commit
						e1cfd714c1
					
				
					 31 changed files with 66 additions and 791 deletions
				
			
		|  | @ -1,42 +0,0 @@ | |||
| using System.Diagnostics; | ||||
| 
 | ||||
| namespace DisplayCommands; | ||||
| 
 | ||||
| public sealed class ByteGrid(ushort width, ushort height) : IEquatable<ByteGrid> | ||||
| { | ||||
|     public ushort Height { get; } = height; | ||||
| 
 | ||||
|     public ushort Width { get; } = width; | ||||
| 
 | ||||
|     internal Memory<byte> 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; | ||||
|     } | ||||
| 
 | ||||
|     public bool Equals(ByteGrid? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) return false; | ||||
|         if (ReferenceEquals(this, other)) return true; | ||||
|         return Height == other.Height && Width == other.Width && Data.Span.SequenceEqual(other.Data.Span); | ||||
|     } | ||||
| 
 | ||||
|     private int GetIndex(ushort x, ushort y) | ||||
|     { | ||||
|         Debug.Assert(x < Width); | ||||
|         Debug.Assert(y < Height); | ||||
|         return x + y * Width; | ||||
|     } | ||||
| 
 | ||||
|     public void Clear() => Data.Span.Clear(); | ||||
| 
 | ||||
|     public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is ByteGrid other && Equals(other)); | ||||
| 
 | ||||
|     public override int GetHashCode() => HashCode.Combine(Height, Width, Data); | ||||
| 
 | ||||
|     public static bool operator ==(ByteGrid? left, ByteGrid? right) => Equals(left, right); | ||||
| 
 | ||||
|     public static bool operator !=(ByteGrid? left, ByteGrid? right) => !Equals(left, right); | ||||
| } | ||||
|  | @ -1,71 +0,0 @@ | |||
| using System.Diagnostics; | ||||
| using System.Text; | ||||
| 
 | ||||
| namespace DisplayCommands; | ||||
| 
 | ||||
| public sealed class Cp437Grid(ushort width, ushort height) : IEquatable<Cp437Grid> | ||||
| { | ||||
|     private readonly ByteGrid _byteGrid = new(width, height); | ||||
|     private readonly Encoding _encoding = Encoding.GetEncoding(437); | ||||
| 
 | ||||
|     public ushort Height { get; } = height; | ||||
| 
 | ||||
|     public ushort Width { get; } = width; | ||||
| 
 | ||||
|     internal Memory<byte> Data => _byteGrid.Data; | ||||
| 
 | ||||
|     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<char> valuesStr = stackalloc char[] { c }; | ||||
|         Span<byte> convertedStr = stackalloc byte[1]; | ||||
|         var consumed = _encoding.GetBytes(valuesStr, convertedStr); | ||||
|         Debug.Assert(consumed == 1); | ||||
|         return convertedStr[0]; | ||||
|     } | ||||
| 
 | ||||
|     private char ByteToChar(byte b) | ||||
|     { | ||||
|         ReadOnlySpan<byte> valueBytes = stackalloc byte[] { b }; | ||||
|         Span<char> resultStr = stackalloc char[1]; | ||||
|         var consumed = _encoding.GetChars(valueBytes, resultStr); | ||||
|         Debug.Assert(consumed == 1); | ||||
|         return resultStr[0]; | ||||
|     } | ||||
| 
 | ||||
|     public bool Equals(Cp437Grid? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) return false; | ||||
|         if (ReferenceEquals(this, other)) return true; | ||||
|         return Height == other.Height && Width == other.Width && _byteGrid.Equals(other._byteGrid); | ||||
|     } | ||||
| 
 | ||||
|     public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is Cp437Grid other && Equals(other)); | ||||
|     public override int GetHashCode() => HashCode.Combine(_byteGrid, Height, Width); | ||||
|     public static bool operator ==(Cp437Grid? left, Cp437Grid? right) => Equals(left, right); | ||||
|     public static bool operator !=(Cp437Grid? left, Cp437Grid? right) => !Equals(left, right); | ||||
| } | ||||
|  | @ -1,23 +0,0 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
| 
 | ||||
|     <Import Project="../shared.props" /> | ||||
| 
 | ||||
|     <PropertyGroup> | ||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
|         <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0"/> | ||||
|         <PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2"/> | ||||
|         <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0"/> | ||||
|         <PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0"/> | ||||
|         <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/> | ||||
|         <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1"/> | ||||
| 
 | ||||
|         <ProjectReference Include="../EndiannessSourceGenerator/EndiannessSourceGenerator.csproj" | ||||
|                           OutputItemType="Analyzer" | ||||
|                           ReferenceOutputAssembly="false"/> | ||||
|     </ItemGroup> | ||||
| 
 | ||||
| </Project> | ||||
|  | @ -1,25 +0,0 @@ | |||
| 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<IDisplayConnection, DisplayConnection>(); | ||||
|         if (configurationSection != null) | ||||
|             services.Configure<DisplayConfiguration>(configurationSection); | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| global using System; | ||||
| global using System.Threading.Tasks; | ||||
|  | @ -1,24 +0,0 @@ | |||
| namespace DisplayCommands; | ||||
| 
 | ||||
| public interface IDisplayConnection | ||||
| { | ||||
|     ValueTask SendClearAsync(); | ||||
| 
 | ||||
|     ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid); | ||||
| 
 | ||||
|     ValueTask SendBrightnessAsync(byte brightness); | ||||
| 
 | ||||
|     ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma); | ||||
| 
 | ||||
|     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,17 +0,0 @@ | |||
| namespace DisplayCommands.Internals; | ||||
| 
 | ||||
| internal enum DisplayCommand : ushort | ||||
| { | ||||
|     Clear = 0x0002, | ||||
|     Cp437Data = 0x0003, | ||||
|     CharBrightness = 0x0005, | ||||
|     Brightness = 0x0007, | ||||
|     HardReset = 0x000b, | ||||
|     FadeOut = 0x000d, | ||||
|     [Obsolete("ignored by display code")] BitmapLegacy = 0x0010, | ||||
|     BitmapLinear = 0x0012, | ||||
|     BitmapLinearWin = 0x0013, | ||||
|     BitmapLinearAnd = 0x0014, | ||||
|     BitmapLinearOr = 0x0015, | ||||
|     BitmapLinearXor = 0x0016 | ||||
| } | ||||
|  | @ -1,126 +0,0 @@ | |||
| using System.Buffers; | ||||
| using System.Diagnostics; | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
| using System.Runtime.InteropServices; | ||||
| using Microsoft.Extensions.Options; | ||||
| 
 | ||||
| namespace DisplayCommands.Internals; | ||||
| 
 | ||||
| internal sealed class DisplayConnection(IOptions<DisplayConfiguration> options) : IDisplayConnection, IDisposable | ||||
| { | ||||
|     private readonly ArrayPool<byte> _arrayPool = ArrayPool<byte>.Shared; | ||||
|     private readonly UdpClient _udpClient = new(options.Value.Hostname, options.Value.Port); | ||||
| 
 | ||||
|     public ValueTask SendClearAsync() | ||||
|     { | ||||
|         var header = new HeaderWindow { Command = (ushort)DisplayCommand.Clear }; | ||||
| 
 | ||||
|         return SendAsync(header, Memory<byte>.Empty); | ||||
|     } | ||||
| 
 | ||||
|     public ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid) | ||||
|     { | ||||
|         var header = new HeaderWindow | ||||
|         { | ||||
|             Command = (ushort)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 = (ushort)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 = (ushort)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 = (ushort)DisplayCommand.HardReset }; | ||||
|         return SendAsync(header, Memory<byte>.Empty); | ||||
|     } | ||||
| 
 | ||||
|     public async ValueTask SendFadeOutAsync(byte loops) | ||||
|     { | ||||
|         var header = new HeaderWindow { Command = (ushort)DisplayCommand.FadeOut }; | ||||
| 
 | ||||
|         var payloadBuffer = _arrayPool.Rent(1); | ||||
|         var payload = payloadBuffer.AsMemory(0, 1); | ||||
|         payload.Span[0] = loops; | ||||
| 
 | ||||
|         await SendAsync(header, payload); | ||||
|         _arrayPool.Return(payloadBuffer); | ||||
|     } | ||||
| 
 | ||||
|     public ValueTask SendBitmapLinearWindowAsync(ushort x, ushort y, PixelGrid pixels) | ||||
|     { | ||||
|         var header = new HeaderWindow | ||||
|         { | ||||
|             Command = (ushort)DisplayCommand.BitmapLinearWin, | ||||
|             PosX = x, | ||||
|             PosY = y, | ||||
|             Width = (ushort)(pixels.Width / 8), | ||||
|             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; | ||||
|         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(); | ||||
| } | ||||
|  | @ -1,10 +0,0 @@ | |||
| namespace DisplayCommands.Internals; | ||||
| 
 | ||||
| internal enum DisplaySubCommand : ushort | ||||
| { | ||||
|     BitmapNormal = 0x0, | ||||
|     BitmapCompressZ = 0x677a, | ||||
|     BitmapCompressBz = 0x627a, | ||||
|     BitmapCompressLz = 0x6c7a, | ||||
|     BitmapCompressZs = 0x7a73 | ||||
| } | ||||
|  | @ -1,19 +0,0 @@ | |||
| using System.Runtime.InteropServices; | ||||
| using EndiannessSourceGenerator; | ||||
| 
 | ||||
| namespace DisplayCommands.Internals; | ||||
| 
 | ||||
| [StructEndianness(IsLittleEndian = false)] | ||||
| [StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)] | ||||
| internal partial struct HeaderBitmap | ||||
| { | ||||
|     private ushort _command; | ||||
| 
 | ||||
|     private ushort _offset; | ||||
| 
 | ||||
|     private ushort _length; | ||||
| 
 | ||||
|     private ushort _subCommand; | ||||
| 
 | ||||
|     private ushort _reserved; | ||||
| } | ||||
|  | @ -1,19 +0,0 @@ | |||
| using System.Runtime.InteropServices; | ||||
| using EndiannessSourceGenerator; | ||||
| 
 | ||||
| namespace DisplayCommands.Internals; | ||||
| 
 | ||||
| [StructEndianness(IsLittleEndian = false)] | ||||
| [StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)] | ||||
| internal partial struct HeaderWindow | ||||
| { | ||||
|     private ushort _command; | ||||
| 
 | ||||
|     private ushort _posX; | ||||
| 
 | ||||
|     private ushort _posY; | ||||
| 
 | ||||
|     private ushort _width; | ||||
| 
 | ||||
|     private ushort _height; | ||||
| } | ||||
|  | @ -1,59 +0,0 @@ | |||
| using System.Diagnostics; | ||||
| 
 | ||||
| namespace DisplayCommands; | ||||
| 
 | ||||
| public sealed class PixelGrid(ushort width, ushort height) : IEquatable<PixelGrid> | ||||
| { | ||||
|     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(); | ||||
| 
 | ||||
|     public bool Equals(PixelGrid? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) return false; | ||||
|         if (ReferenceEquals(this, other)) return true; | ||||
|         return Width == other.Width && Height == other.Height && _byteGrid.Equals(other._byteGrid); | ||||
|     } | ||||
| 
 | ||||
|     public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is PixelGrid other && Equals(other)); | ||||
|     public override int GetHashCode() => HashCode.Combine(_byteGrid, Width, Height); | ||||
|     public static bool operator ==(PixelGrid? left, PixelGrid? right) => Equals(left, right); | ||||
|     public static bool operator !=(PixelGrid? left, PixelGrid? right) => !Equals(left, right); | ||||
| 
 | ||||
|     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,260 +0,0 @@ | |||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
| using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||||
| 
 | ||||
| namespace EndiannessSourceGenerator; | ||||
| 
 | ||||
| internal class DebugMeException(string message) : Exception(message); | ||||
| 
 | ||||
| internal class InvalidUsageException(string message) : Exception(message); | ||||
| 
 | ||||
| [Generator] | ||||
| public class StructEndiannessSourceGenerator : ISourceGenerator | ||||
| { | ||||
|     private const string Namespace = "EndiannessSourceGenerator"; | ||||
|     private const string AttributeName = "StructEndiannessAttribute"; | ||||
|     private const string IsLittleEndianProperty = "IsLittleEndian"; | ||||
| 
 | ||||
|     private const string AttributeSourceCode = | ||||
|         $$"""
 | ||||
|           // <auto-generated/> | ||||
|           namespace {{Namespace}} | ||||
|           { | ||||
|               [System.AttributeUsage(System.AttributeTargets.Struct)] | ||||
|               public class {{AttributeName}}: System.Attribute | ||||
|               { | ||||
|                   public required bool {{IsLittleEndianProperty}} { get; init; } | ||||
|               } | ||||
|           } | ||||
|           """;
 | ||||
| 
 | ||||
|     public void Initialize(GeneratorInitializationContext context) | ||||
|     { | ||||
|         // Register the attribute source | ||||
|         context.RegisterForPostInitialization(i => i.AddSource($"{AttributeName}.g.cs", AttributeSourceCode)); | ||||
|     } | ||||
| 
 | ||||
|     public void Execute(GeneratorExecutionContext context) | ||||
|     { | ||||
|         var treesWithStructsWithAttributes = context.Compilation.SyntaxTrees | ||||
|             .Where(st => st.GetRoot().DescendantNodes() | ||||
|                 .OfType<StructDeclarationSyntax>() | ||||
|                 .Any(p => p.DescendantNodes() | ||||
|                     .OfType<AttributeSyntax>() | ||||
|                     .Any())) | ||||
|             .ToList(); | ||||
| 
 | ||||
|         foreach (var tree in treesWithStructsWithAttributes) | ||||
|         { | ||||
|             var semanticModel = context.Compilation.GetSemanticModel(tree); | ||||
| 
 | ||||
|             var structsWithAttributes = tree.GetRoot().DescendantNodes() | ||||
|                 .OfType<StructDeclarationSyntax>() | ||||
|                 .Where(cd => cd.DescendantNodes() | ||||
|                     .OfType<AttributeSyntax>() | ||||
|                     .Any()); | ||||
| 
 | ||||
|             foreach (var structDeclaration in structsWithAttributes) | ||||
|             { | ||||
|                 var foundAttribute = GetEndiannessAttribute(structDeclaration, semanticModel); | ||||
|                 if (foundAttribute == null) | ||||
|                     continue; // not my type | ||||
| 
 | ||||
|                 var structIsLittleEndian = GetStructIsLittleEndian(foundAttribute); | ||||
|                 HandleStruct(context, structDeclaration, semanticModel, structIsLittleEndian); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static void HandleStruct(GeneratorExecutionContext context, TypeDeclarationSyntax structDeclaration, | ||||
|         SemanticModel semanticModel, bool structIsLittleEndian) | ||||
|     { | ||||
|         var isPartial = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); | ||||
|         if (!isPartial) | ||||
|             throw new InvalidUsageException("struct is not marked partial"); | ||||
| 
 | ||||
|         var accessibilityModifier = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword)) | ||||
|             ? Token(SyntaxKind.InternalKeyword) | ||||
|             : Token(SyntaxKind.PublicKeyword); | ||||
| 
 | ||||
|         var structType = semanticModel.GetDeclaredSymbol(structDeclaration); | ||||
|         if (structType == null) | ||||
|             throw new DebugMeException("struct type info is null"); | ||||
| 
 | ||||
|         var structNamespace = structType.ContainingNamespace?.ToDisplayString(); | ||||
|         if (structNamespace == null) | ||||
|             throw new InvalidUsageException("struct has to be contained in a namespace"); | ||||
| 
 | ||||
|         if (structDeclaration.Members.Any(m => m.IsKind(SyntaxKind.PropertyDeclaration))) | ||||
|             throw new InvalidUsageException("struct cannot have properties"); | ||||
| 
 | ||||
|         var fieldDeclarations = structDeclaration.Members | ||||
|             .Where(m => m.IsKind(SyntaxKind.FieldDeclaration)).OfType<FieldDeclarationSyntax>(); | ||||
| 
 | ||||
|         var generatedCode = CompilationUnit() | ||||
|             .WithUsings(List<UsingDirectiveSyntax>([ | ||||
|                 UsingDirective(IdentifierName("System")), | ||||
|                 UsingDirective(IdentifierName("System.Buffers.Binary")) | ||||
|             ])) | ||||
|             .WithMembers(List<MemberDeclarationSyntax>([ | ||||
|                 FileScopedNamespaceDeclaration(IdentifierName(structNamespace)), | ||||
|                 StructDeclaration(structType.Name) | ||||
|                     .WithModifiers(TokenList([accessibilityModifier, Token(SyntaxKind.PartialKeyword)])) | ||||
|                     .WithMembers(GenerateStructProperties(fieldDeclarations, semanticModel, structIsLittleEndian)) | ||||
|             ])) | ||||
|             .NormalizeWhitespace() | ||||
|             .ToFullString(); | ||||
| 
 | ||||
|         context.AddSource( | ||||
|             $"{structNamespace}.{structType.Name}.g.cs", | ||||
|             SourceText.From(generatedCode, Encoding.UTF8) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private static SyntaxList<MemberDeclarationSyntax> GenerateStructProperties( | ||||
|         IEnumerable<FieldDeclarationSyntax> fieldDeclarations, SemanticModel semanticModel, bool structIsLittleEndian) | ||||
|     { | ||||
|         var result = new List<MemberDeclarationSyntax>(); | ||||
|         foreach (var field in fieldDeclarations) | ||||
|         { | ||||
|             if (!field.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword))) | ||||
|                 throw new InvalidUsageException("fields have to be private"); | ||||
| 
 | ||||
|             var variableDeclaration = field.DescendantNodes() | ||||
|                 .OfType<VariableDeclarationSyntax>() | ||||
|                 .FirstOrDefault(); | ||||
|             if (variableDeclaration == null) | ||||
|                 throw new DebugMeException("variable declaration of field declaration null"); | ||||
| 
 | ||||
|             var variableTypeInfo = semanticModel.GetTypeInfo(variableDeclaration.Type).Type; | ||||
|             if (variableTypeInfo == null) | ||||
|                 throw new DebugMeException("variable type info of field declaration null"); | ||||
| 
 | ||||
|             var typeName = variableTypeInfo.ToDisplayString(); | ||||
|             var fieldName = variableDeclaration.Variables.First().Identifier.ToString(); | ||||
| 
 | ||||
|             result.Add(GenerateProperty(typeName, structIsLittleEndian, fieldName)); | ||||
|         } | ||||
| 
 | ||||
|         return new SyntaxList<MemberDeclarationSyntax>(result); | ||||
|     } | ||||
| 
 | ||||
|     private static PropertyDeclarationSyntax GenerateProperty(string typeName, | ||||
|         bool structIsLittleEndian, string fieldName) | ||||
|     { | ||||
|         var propertyName = GeneratePropertyName(fieldName); | ||||
|         var fieldIdentifier = IdentifierName(fieldName); | ||||
| 
 | ||||
|         ExpressionSyntax condition = MemberAccessExpression( | ||||
|             kind: SyntaxKind.SimpleMemberAccessExpression, | ||||
|             expression: IdentifierName("BitConverter"), | ||||
|             name: IdentifierName("IsLittleEndian") | ||||
|         ); | ||||
| 
 | ||||
|         if (!structIsLittleEndian) | ||||
|             condition = PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, condition); | ||||
| 
 | ||||
|         var reverseEndiannessMethod = MemberAccessExpression( | ||||
|             kind: SyntaxKind.SimpleMemberAccessExpression, | ||||
|             expression: IdentifierName("BinaryPrimitives"), | ||||
|             name: IdentifierName("ReverseEndianness") | ||||
|         ); | ||||
| 
 | ||||
|         var valueIdentifier = IdentifierName("value"); | ||||
| 
 | ||||
|         return PropertyDeclaration(ParseTypeName(typeName), propertyName) | ||||
|             .WithModifiers(TokenList([Token(SyntaxKind.PublicKeyword)])) | ||||
|             .WithAccessorList(AccessorList(List<AccessorDeclarationSyntax>([ | ||||
|                     AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) | ||||
|                         .WithExpressionBody(ArrowExpressionClause(ConditionalExpression( | ||||
|                             condition: condition, | ||||
|                             whenTrue: fieldIdentifier, | ||||
|                             whenFalse: InvocationExpression( | ||||
|                                 expression: reverseEndiannessMethod, | ||||
|                                 argumentList: ArgumentList(SingletonSeparatedList( | ||||
|                                     Argument(fieldIdentifier) | ||||
|                                 )) | ||||
|                             ) | ||||
|                         ))) | ||||
|                         .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), | ||||
|                     AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) | ||||
|                         .WithExpressionBody(ArrowExpressionClause(AssignmentExpression( | ||||
|                             kind: SyntaxKind.SimpleAssignmentExpression, | ||||
|                             left: fieldIdentifier, | ||||
|                             right: ConditionalExpression( | ||||
|                                 condition: condition, | ||||
|                                 whenTrue: valueIdentifier, | ||||
|                                 whenFalse: InvocationExpression( | ||||
|                                     expression: reverseEndiannessMethod, | ||||
|                                     argumentList: ArgumentList(SingletonSeparatedList( | ||||
|                                         Argument(valueIdentifier) | ||||
|                                     )) | ||||
|                                 ) | ||||
|                             ) | ||||
|                         ))) | ||||
|                         .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) | ||||
|                 ])) | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     private static SyntaxToken GeneratePropertyName(string fieldName) | ||||
|     { | ||||
|         var propertyName = fieldName; | ||||
|         if (propertyName.StartsWith("_")) | ||||
|             propertyName = propertyName.Substring(1); | ||||
|         if (!char.IsLetter(propertyName, 0) || char.IsUpper(propertyName, 0)) | ||||
|             throw new InvalidUsageException("field names have to start with a lower case letter"); | ||||
|         propertyName = propertyName.Substring(0, 1).ToUpperInvariant() | ||||
|                        + propertyName.Substring(1); | ||||
|         return Identifier(propertyName); | ||||
|     } | ||||
| 
 | ||||
|     private static AttributeSyntax? GetEndiannessAttribute(SyntaxNode structDeclaration, SemanticModel semanticModel) | ||||
|     { | ||||
|         AttributeSyntax? foundAttribute = null; | ||||
|         foreach (var attributeSyntax in structDeclaration.DescendantNodes().OfType<AttributeSyntax>()) | ||||
|         { | ||||
|             var attributeTypeInfo = semanticModel.GetTypeInfo(attributeSyntax).Type; | ||||
|             if (attributeTypeInfo == null) | ||||
|                 throw new DebugMeException("attribute type info is null"); | ||||
| 
 | ||||
|             if (attributeTypeInfo.ContainingNamespace?.Name != Namespace) | ||||
|                 continue; | ||||
|             if (attributeTypeInfo.Name != AttributeName) | ||||
|                 continue; | ||||
| 
 | ||||
|             foundAttribute = attributeSyntax; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         return foundAttribute; | ||||
|     } | ||||
| 
 | ||||
|     private static bool GetStructIsLittleEndian(AttributeSyntax foundAttribute) | ||||
|     { | ||||
|         var endiannessArguments = foundAttribute.ArgumentList; | ||||
|         if (endiannessArguments == null) | ||||
|             throw new InvalidUsageException("endianness attribute has no arguments"); | ||||
| 
 | ||||
|         var isLittleEndianArgumentSyntax = endiannessArguments.Arguments | ||||
|             .FirstOrDefault(argumentSyntax => | ||||
|                 argumentSyntax.NameEquals?.Name.Identifier.ToString() == IsLittleEndianProperty); | ||||
|         if (isLittleEndianArgumentSyntax == null) | ||||
|             throw new InvalidUsageException("endianness attribute argument not found"); | ||||
| 
 | ||||
|         bool? structIsLittleEndian = isLittleEndianArgumentSyntax.Expression.Kind() switch | ||||
|         { | ||||
|             SyntaxKind.FalseLiteralExpression => false, | ||||
|             SyntaxKind.TrueLiteralExpression => true, | ||||
|             SyntaxKind.DefaultLiteralExpression => false, | ||||
|             _ => throw new InvalidUsageException($"{IsLittleEndianProperty} has to be set with a literal") | ||||
|         }; | ||||
|         return structIsLittleEndian.Value; | ||||
|     } | ||||
| } | ||||
|  | @ -1,25 +0,0 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
| 
 | ||||
|     <Import Project="../shared.props" /> | ||||
| 
 | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>netstandard2.0</TargetFramework> | ||||
|         <IsPackable>false</IsPackable> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <LangVersion>latest</LangVersion> | ||||
| 
 | ||||
|         <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||||
|         <IsRoslynComponent>true</IsRoslynComponent> | ||||
|         <PackageId>EndiannessSourceGenerator</PackageId> | ||||
|         <IsAotCompatible>false</IsAotCompatible> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"> | ||||
|             <PrivateAssets>all</PrivateAssets> | ||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" /> | ||||
|     </ItemGroup> | ||||
| 
 | ||||
| </Project> | ||||
|  | @ -1,9 +0,0 @@ | |||
| { | ||||
|     "$schema": "http://json.schemastore.org/launchsettings.json", | ||||
|     "profiles": { | ||||
|         "Generators": { | ||||
|             "commandName": "DebugRoslynComponent", | ||||
|             "targetProject": "../DisplayCommands/DisplayCommands.csproj" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,5 +0,0 @@ | |||
| # Endianness Source Generator | ||||
| 
 | ||||
| When annotating a struct with the `StructEndianness` attribute, this code generator will generate properties for the declared fields. | ||||
| 
 | ||||
| Each time a property is read or written, the endianness is converted from runtime endianness to struct endianness or vice-versa. | ||||
|  | @ -2,10 +2,6 @@ | |||
| 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 | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EndiannessSourceGenerator", "EndiannessSourceGenerator\EndiannessSourceGenerator.csproj", "{D77FE880-F2B8-43B6-8B33-B6FA089CC337}" | ||||
| EndProject | ||||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{12DB7D48-1BB2-488B-B4D9-4126087D2F8C}" | ||||
| 	ProjectSection(SolutionItems) = preProject | ||||
| 		global.json = global.json | ||||
|  | @ -13,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{12DB7D | |||
| 		Dockerfile = Dockerfile | ||||
| 	EndProjectSection | ||||
| EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint2", "..\..\servicepoint2\servicepoint2-binding-cs\ServicePoint2\ServicePoint2.csproj", "{BC27CED4-82FE-4CD4-B784-B7596586801D}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
|  | @ -23,13 +21,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 | ||||
| 		{D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{BC27CED4-82FE-4CD4-B784-B7596586801D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{BC27CED4-82FE-4CD4-B784-B7596586801D}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{BC27CED4-82FE-4CD4-B784-B7596586801D}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{BC27CED4-82FE-4CD4-B784-B7596586801D}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| EndGlobal | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| using System.IO; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using DisplayCommands; | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.AspNetCore.Diagnostics.HealthChecks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Http.HttpResults; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Diagnostics.HealthChecks; | ||||
| using ServicePoint2; | ||||
| using TanksServer.GameLogic; | ||||
| using TanksServer.Interactivity; | ||||
| 
 | ||||
|  | @ -19,7 +19,7 @@ internal sealed class Endpoints( | |||
|     ControlsServer controlsServer, | ||||
|     MapService mapService, | ||||
|     ChangeToRequestedMap changeToRequestedMap, | ||||
|     IDisplayConnection displayConnection | ||||
|     Connection displayConnection | ||||
| ) | ||||
| { | ||||
|     public void Map(WebApplication app) | ||||
|  | @ -31,7 +31,7 @@ internal sealed class Endpoints( | |||
|         app.Map("/controls", ConnectControlsAsync); | ||||
|         app.MapGet("/map", () => mapService.MapNames); | ||||
|         app.MapPost("/map", PostMap); | ||||
|         app.MapPost("/resetDisplay", displayConnection.SendHardResetAsync); | ||||
|         app.MapPost("/resetDisplay", () => displayConnection.Send(Command.HardReset())); | ||||
|         app.MapGet("/map/{name}", GetMapByName); | ||||
| 
 | ||||
|         app.MapHealthChecks("/health", new HealthCheckOptions | ||||
|  | @ -117,7 +117,7 @@ internal sealed class Endpoints( | |||
|         if (!mapService.TryGetPreview(name, out var preview)) | ||||
|             return TypedResults.NotFound(); | ||||
| 
 | ||||
|         var mapInfo = new MapInfo(prototype.Name, prototype.GetType().Name, preview.Data); | ||||
|         var mapInfo = new MapInfo(prototype.Name, prototype.GetType().Name, preview.Data.ToArray()); | ||||
|         return TypedResults.Ok(mapInfo); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| using System.Diagnostics; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.IO; | ||||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| using TanksServer.Graphics; | ||||
| 
 | ||||
| namespace TanksServer.GameLogic; | ||||
|  | @ -42,7 +42,7 @@ internal sealed class MapService | |||
|         if (!_mapPrototypes.TryGetValue(name, out var prototype)) | ||||
|             return false; // name not found | ||||
| 
 | ||||
|         pixelGrid = new PixelGrid(PixelsPerRow, PixelsPerColumn); | ||||
|         pixelGrid = PixelGrid.New(PixelsPerRow, PixelsPerColumn); | ||||
|         DrawMapStep.Draw(pixelGrid, prototype.CreateInstance()); | ||||
| 
 | ||||
|         _mapPreviews.TryAdd(name, pixelGrid); // another thread may have added the map already | ||||
|  |  | |||
|  | @ -1,5 +1,3 @@ | |||
| using System.Diagnostics; | ||||
| 
 | ||||
| namespace TanksServer.GameLogic; | ||||
| 
 | ||||
| internal sealed class SpawnPowerUp( | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| using TanksServer.GameLogic; | ||||
| 
 | ||||
| namespace TanksServer.Graphics; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| using TanksServer.GameLogic; | ||||
| using TanksServer.Interactivity; | ||||
| 
 | ||||
|  | @ -10,9 +10,9 @@ internal sealed class GeneratePixelsTickStep( | |||
| ) : ITickStep | ||||
| { | ||||
|     private GamePixelGrid _lastGamePixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn); | ||||
|     private PixelGrid _lastObserverPixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn); | ||||
|     private PixelGrid _lastObserverPixelGrid = PixelGrid.New(MapService.PixelsPerRow, MapService.PixelsPerColumn); | ||||
|     private GamePixelGrid _gamePixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn); | ||||
|     private PixelGrid _observerPixelGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn); | ||||
|     private PixelGrid _observerPixelGrid = PixelGrid.New(MapService.PixelsPerRow, MapService.PixelsPerColumn); | ||||
| 
 | ||||
|     private readonly List<IDrawStep> _drawSteps = drawSteps.ToList(); | ||||
|     private readonly List<IFrameConsumer> _consumers = consumers.ToList(); | ||||
|  | @ -20,7 +20,7 @@ internal sealed class GeneratePixelsTickStep( | |||
|     public async ValueTask TickAsync(TimeSpan _) | ||||
|     { | ||||
|         Draw(_gamePixelGrid, _observerPixelGrid); | ||||
|         if (_observerPixelGrid.Data.Span.SequenceEqual(_lastObserverPixelGrid.Data.Span)) | ||||
|         if (_observerPixelGrid.Data.SequenceEqual(_lastObserverPixelGrid.Data)) | ||||
|             return; | ||||
| 
 | ||||
|         await _consumers.Select(c => c.OnFrameDoneAsync(_gamePixelGrid, _observerPixelGrid)) | ||||
|  | @ -36,7 +36,7 @@ internal sealed class GeneratePixelsTickStep( | |||
|         foreach (var step in _drawSteps) | ||||
|             step.Draw(gamePixelGrid); | ||||
| 
 | ||||
|         observerPixelGrid.Clear(); | ||||
|         observerPixelGrid.Fill(false); | ||||
|         for (var y = 0; y < MapService.PixelsPerColumn; y++) | ||||
|         for (var x = 0; x < MapService.PixelsPerRow; x++) | ||||
|         { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| 
 | ||||
| namespace TanksServer.Graphics; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| using System.Net.WebSockets; | ||||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| using TanksServer.Graphics; | ||||
| 
 | ||||
| namespace TanksServer.Interactivity; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| using System.Buffers; | ||||
| using System.Net.WebSockets; | ||||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| using TanksServer.Graphics; | ||||
| 
 | ||||
| namespace TanksServer.Interactivity; | ||||
|  | @ -36,8 +36,9 @@ internal sealed class ClientScreenServerConnection | |||
| 
 | ||||
|     private Package BuildNextPackage(PixelGrid pixels, GamePixelGrid gamePixelGrid) | ||||
|     { | ||||
|         var nextPixels = _bufferPool.Rent(pixels.Data.Length); | ||||
|         pixels.Data.CopyTo(nextPixels.Memory); | ||||
|         var pixelsData = pixels.Data; | ||||
|         var nextPixels = _bufferPool.Rent(pixelsData.Length); | ||||
|         pixelsData.CopyTo(nextPixels.Memory.Span); | ||||
| 
 | ||||
|         if (_playerDataBuilder == null) | ||||
|             return new Package(nextPixels, null); | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ internal sealed class PlayerInfoConnection | |||
|     private readonly MapEntityManager _entityManager; | ||||
|     private readonly BufferPool _bufferPool; | ||||
|     private readonly MemoryStream _tempStream = new(); | ||||
|     private IMemoryOwner<byte>? _lastMessage = null; | ||||
|     private IMemoryOwner<byte>? _lastMessage; | ||||
| 
 | ||||
|     public PlayerInfoConnection( | ||||
|         Player player, | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| using System.Diagnostics; | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| using TanksServer.GameLogic; | ||||
| using TanksServer.Graphics; | ||||
| 
 | ||||
|  | @ -12,11 +13,11 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer | |||
|     private const int ScoresHeight = 20; | ||||
|     private const int ScoresPlayerRows = ScoresHeight - 6; | ||||
| 
 | ||||
|     private readonly IDisplayConnection _displayConnection; | ||||
|     private readonly Connection _displayConnection; | ||||
|     private readonly MapService _mapService; | ||||
|     private readonly ILogger<SendToServicePointDisplay> _logger; | ||||
|     private readonly PlayerServer _players; | ||||
|     private readonly Cp437Grid _scoresBuffer; | ||||
|     private readonly ByteGrid _scoresBuffer; | ||||
|     private readonly TimeSpan _minFrameTime; | ||||
|     private readonly IOptionsMonitor<HostConfiguration> _options; | ||||
| 
 | ||||
|  | @ -26,11 +27,11 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer | |||
|     public SendToServicePointDisplay( | ||||
|         PlayerServer players, | ||||
|         ILogger<SendToServicePointDisplay> logger, | ||||
|         IDisplayConnection displayConnection, | ||||
|         Connection displayConnection, | ||||
|         IOptions<HostConfiguration> hostOptions, | ||||
|         MapService mapService, | ||||
|         IOptionsMonitor<HostConfiguration> options | ||||
|     ) | ||||
|         IOptionsMonitor<HostConfiguration> options, | ||||
|         IOptions<DisplayConfiguration> displayConfig) | ||||
|     { | ||||
|         _players = players; | ||||
|         _logger = logger; | ||||
|  | @ -39,16 +40,15 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer | |||
|         _minFrameTime = TimeSpan.FromMilliseconds(hostOptions.Value.ServicePointDisplayMinFrameTimeMs); | ||||
|         _options = options; | ||||
| 
 | ||||
|         var localIp = _displayConnection.GetLocalIPv4().Split('.'); | ||||
|         var localIp = GetLocalIPv4(displayConfig.Value).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..]) | ||||
|         }; | ||||
|         _scoresBuffer = ByteGrid.New(12, 20); | ||||
| 
 | ||||
|         _scoresBuffer[00] = "== TANKS! =="; | ||||
|         _scoresBuffer[01] = "-- scores --"; | ||||
|         _scoresBuffer[17] = "--  join  --"; | ||||
|         _scoresBuffer[18] = string.Join('.', localIp[..2]); | ||||
|         _scoresBuffer[19] = string.Join('.', localIp[2..]); | ||||
|     } | ||||
| 
 | ||||
|     public async Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels) | ||||
|  | @ -66,8 +66,8 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer | |||
| 
 | ||||
|         try | ||||
|         { | ||||
|             await _displayConnection.SendBitmapLinearWindowAsync(0, 0, observerPixels); | ||||
|             await _displayConnection.SendCp437DataAsync(MapService.TilesPerRow, 0, _scoresBuffer); | ||||
|             _displayConnection.Send(Command.BitmapLinearWin(0, 0, observerPixels.Clone())); | ||||
|             _displayConnection.Send(Command.Cp437Data(MapService.TilesPerRow, 0, _scoresBuffer.Clone())); | ||||
|         } | ||||
|         catch (SocketException ex) | ||||
|         { | ||||
|  | @ -103,4 +103,12 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer | |||
| 
 | ||||
|         _scoresBuffer[16] = _mapService.Current.Name[..(Math.Min(ScoresWidth, _mapService.Current.Name.Length) - 1)]; | ||||
|     } | ||||
| 
 | ||||
|     private static string GetLocalIPv4(DisplayConfiguration configuration) | ||||
|     { | ||||
|         using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0); | ||||
|         socket.Connect(configuration.Hostname, configuration.Port); | ||||
|         var endPoint = socket.LocalEndPoint as IPEndPoint ?? throw new NotSupportedException(); | ||||
|         return endPoint.Address.ToString(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| namespace DisplayCommands; | ||||
| namespace TanksServer.Models; | ||||
| 
 | ||||
| public class DisplayConfiguration | ||||
| { | ||||
|     public string Hostname { get; set; } = "172.23.42.29"; | ||||
| 
 | ||||
|     public int Port { get; set; } = 2342; | ||||
| } | ||||
| } | ||||
|  | @ -1,8 +1,9 @@ | |||
| using System.IO; | ||||
| using DisplayCommands; | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.FileProviders; | ||||
| using ServicePoint2; | ||||
| using SixLabors.ImageSharp; | ||||
| using TanksServer.GameLogic; | ||||
| using TanksServer.Graphics; | ||||
| using TanksServer.Interactivity; | ||||
|  | @ -94,9 +95,14 @@ public static class Program | |||
| 
 | ||||
|         builder.Services.Configure<GameRules>(builder.Configuration.GetSection("GameRules")); | ||||
|         builder.Services.Configure<HostConfiguration>(builder.Configuration.GetSection("Host")); | ||||
|         builder.Services.Configure<DisplayConfiguration>(builder.Configuration.GetSection("ServicePointDisplay")); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<IFrameConsumer, SendToServicePointDisplay>(); | ||||
|         builder.Services.AddDisplay(builder.Configuration.GetSection("ServicePointDisplay")); | ||||
|         builder.Services.AddSingleton<Connection>(sp => | ||||
|         { | ||||
|             var config = sp.GetRequiredService<IOptions<DisplayConfiguration>>().Value; | ||||
|             return Connection.Open($"{config.Hostname}:{config.Port}"); | ||||
|         }); | ||||
| 
 | ||||
|         var app = builder.Build(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,11 +10,14 @@ | |||
|         <PackageReference Include="DotNext.Threading" Version="5.3.0" /> | ||||
|         <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" /> | ||||
|         <PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" /> | ||||
|         <ProjectReference Include="../DisplayCommands/DisplayCommands.csproj" /> | ||||
|     </ItemGroup> | ||||
| 
 | ||||
|     <ItemGroup> | ||||
|         <None Include="./assets/**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Always"/> | ||||
|     </ItemGroup> | ||||
| 
 | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\..\..\servicepoint2\servicepoint2-binding-cs\ServicePoint2\ServicePoint2.csproj" /> | ||||
|     </ItemGroup> | ||||
| 
 | ||||
| </Project> | ||||
|  |  | |||
|  | @ -16,7 +16,8 @@ | |||
|         } | ||||
|     }, | ||||
|     "ServicePointDisplay": { | ||||
|         "Hostname": "172.23.42.29", | ||||
|         //"Hostname": "172.23.42.29", | ||||
|         "Hostname": "127.0.0.1", | ||||
|         "Port": 2342 | ||||
|     }, | ||||
|     "GameRules": { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter