Use native rust library for parsing packages (#21)
This commit is contained in:
		
						commit
						796c5d19c5
					
				
					 53 changed files with 242 additions and 1048 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| [submodule "tanks-backend/servicepoint"] | ||||
| 	path = tanks-backend/servicepoint | ||||
| 	url = https://github.com/kaesaecracker/servicepoint.git | ||||
							
								
								
									
										6
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -1,6 +0,0 @@ | |||
| { | ||||
|   "name": "cccb-tanks-cs", | ||||
|   "lockfileVersion": 3, | ||||
|   "requires": true, | ||||
|   "packages": {} | ||||
| } | ||||
|  | @ -48,27 +48,31 @@ export default function PlayerInfo({player}: { player: string }) { | |||
| 
 | ||||
|     return <Column className="PlayerInfo"> | ||||
|         <h3> | ||||
|             Playing as {lastJsonMessage.name} | ||||
|             Playing as {lastJsonMessage.player.name} | ||||
|         </h3> | ||||
|         <table> | ||||
|             <tbody> | ||||
|             <ScoreRow name="magazine" value={lastJsonMessage.tank?.magazine}/> | ||||
|             {lastJsonMessage.tank && <> | ||||
|                 <ScoreRow name="magazine" value={`${lastJsonMessage.tank.usedBullets} / ${lastJsonMessage.tank.maxBullets}`}/> | ||||
|                 <ScoreRow name="position" value={`(${Math.round(lastJsonMessage.tank.pixelPosition.x)}|${Math.round(lastJsonMessage.tank.pixelPosition.y)})`}/> | ||||
|                 <ScoreRow name="orientation" value={lastJsonMessage.tank.orientation}/> | ||||
|                 <ScoreRow name="bullet speed" value={lastJsonMessage.tank.bulletStats.speed}/> | ||||
|                 <ScoreRow name="bullet acceleration" value={lastJsonMessage.tank.bulletStats.acceleration}/> | ||||
|                 <ScoreRow name="smart bullets" value={lastJsonMessage.tank.bulletStats.smart}/> | ||||
|                 <ScoreRow name="explosive bullets" value={lastJsonMessage.tank.bulletStats.explosive}/> | ||||
|                 <ScoreRow name="moving" value={lastJsonMessage.tank.moving}/> | ||||
|             </>} | ||||
| 
 | ||||
|             <ScoreRow name="controls" value={lastJsonMessage.controls}/> | ||||
|             <ScoreRow name="position" value={lastJsonMessage.tank?.position}/> | ||||
|             <ScoreRow name="orientation" value={lastJsonMessage.tank?.orientation}/> | ||||
|             <ScoreRow name="moving" value={lastJsonMessage.tank?.moving}/> | ||||
|             <ScoreRow name="kills" value={lastJsonMessage.player.scores.kills}/> | ||||
|             <ScoreRow name="deaths" value={lastJsonMessage.player.scores.deaths}/> | ||||
|             <ScoreRow name="walls destroyed" value={lastJsonMessage.player.scores.wallsDestroyed}/> | ||||
|             <ScoreRow name="bullets fired" value={lastJsonMessage.player.scores.shotsFired}/> | ||||
|             <ScoreRow name="power ups collected" value={lastJsonMessage.player.scores.powerUpsCollected}/> | ||||
|             <ScoreRow name="pixels moved" value={lastJsonMessage.player.scores.pixelsMoved}/> | ||||
|             <ScoreRow name="score" value={lastJsonMessage.player.scores.overallScore}/> | ||||
| 
 | ||||
|             <ScoreRow name="kills" value={lastJsonMessage.scores.kills}/> | ||||
|             <ScoreRow name="deaths" value={lastJsonMessage.scores.deaths}/> | ||||
| 
 | ||||
|             <ScoreRow name="walls destroyed" value={lastJsonMessage.scores.wallsDestroyed}/> | ||||
|             <ScoreRow name="bullets fired" value={lastJsonMessage.scores.shotsFired}/> | ||||
|             <ScoreRow name="power ups collected" value={lastJsonMessage.scores.powerUpsCollected}/> | ||||
|             <ScoreRow name="pixels moved" value={lastJsonMessage.scores.pixelsMoved}/> | ||||
| 
 | ||||
|             <ScoreRow name="score" value={lastJsonMessage.scores.overallScore}/> | ||||
| 
 | ||||
|             <ScoreRow name="connections" value={lastJsonMessage.openConnections}/> | ||||
|             <ScoreRow name="connections" value={lastJsonMessage.player.openConnections}/> | ||||
|             </tbody> | ||||
|         </table> | ||||
|     </Column>; | ||||
|  |  | |||
|  | @ -14,24 +14,28 @@ export type Scores = { | |||
|     readonly pixelsMoved: number; | ||||
| }; | ||||
| 
 | ||||
| type Tank = { | ||||
|     readonly pixelPosition: { x: number; y: number }; | ||||
|     readonly orientation: number; | ||||
|     readonly moving: boolean; | ||||
|     readonly bulletStats: BulletStats; | ||||
|     readonly reloadingUntil: string; | ||||
|     readonly nextShotAfter: string; | ||||
|     readonly usedBullets: number; | ||||
|     readonly maxBullets: number; | ||||
| } | ||||
| 
 | ||||
| export type Player = { | ||||
|     readonly name: string; | ||||
|     readonly scores: Scores; | ||||
| }; | ||||
| 
 | ||||
| type TankInfo = { | ||||
|     readonly magazine: string; | ||||
|     readonly position: { x: number; y: number }; | ||||
|     readonly orientation: number; | ||||
|     readonly moving: boolean; | ||||
|     readonly openConnections: number; | ||||
|     readonly lastInput: string; | ||||
| } | ||||
| 
 | ||||
| export type PlayerInfoMessage = { | ||||
|     readonly name: string; | ||||
|     readonly scores: Scores; | ||||
|     readonly player: Player; | ||||
|     readonly controls: string; | ||||
|     readonly tank?: TankInfo; | ||||
|     readonly openConnections: number; | ||||
|     readonly tank?: Tank; | ||||
| } | ||||
| 
 | ||||
| export type MapInfo = { | ||||
|  | @ -40,6 +44,13 @@ export type MapInfo = { | |||
|     readonly preview: string; | ||||
| } | ||||
| 
 | ||||
| export type BulletStats = { | ||||
|     speed: number; | ||||
|     acceleration: number, | ||||
|     explosive: boolean, | ||||
|     smart: boolean | ||||
| }; | ||||
| 
 | ||||
| export function useMyWebSocket<T = unknown>(url: string, options: Options = {}) { | ||||
|     return useWebSocket<T>(url, { | ||||
|         shouldReconnect: () => true, | ||||
|  |  | |||
|  | @ -11,3 +11,5 @@ | |||
| **/bin | ||||
| **/Dockerfile* | ||||
| **/obj | ||||
| **/target | ||||
| **/examples | ||||
|  |  | |||
|  | @ -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,18 +1,10 @@ | |||
| FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build-server | ||||
| 
 | ||||
| RUN apk add clang binutils musl-dev build-base zlib-static cmake openssl-dev openssl-libs-static openssl | ||||
| RUN apk add rust cargo | ||||
| 
 | ||||
| WORKDIR /src/tanks-server | ||||
| 
 | ||||
| # dependencies | ||||
| COPY ./shared.props . | ||||
| COPY ./TanksServer.sln . | ||||
| COPY ./EndiannessSourceGenerator/EndiannessSourceGenerator.csproj EndiannessSourceGenerator/EndiannessSourceGenerator.csproj | ||||
| COPY ./DisplayCommands/DisplayCommands.csproj DisplayCommands/DisplayCommands.csproj | ||||
| COPY ./TanksServer/TanksServer.csproj TanksServer/TanksServer.csproj | ||||
| RUN dotnet restore --runtime linux-musl-x64 TanksServer.sln | ||||
| 
 | ||||
| #build | ||||
| COPY . . | ||||
| RUN dotnet build TanksServer/TanksServer.csproj -c Release -r linux-musl-x64 -o /build | ||||
| RUN dotnet publish TanksServer/TanksServer.csproj -c Release -r linux-musl-x64 -o /app | ||||
|  |  | |||
|  | @ -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,17 +2,13 @@ | |||
| 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 | ||||
| 		shared.props = shared.props | ||||
| 		Dockerfile = Dockerfile | ||||
| 	EndProjectSection | ||||
| EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint2", "servicepoint\servicepoint2-binding-cs\src\ServicePoint2.csproj", "{DFCC69ED-E02B-4631-8A23-5D394BA01E03}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
|  | @ -23,13 +19,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 | ||||
| 		{DFCC69ED-E02B-4631-8A23-5D394BA01E03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{DFCC69ED-E02B-4631-8A23-5D394BA01E03}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{DFCC69ED-E02B-4631-8A23-5D394BA01E03}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{DFCC69ED-E02B-4631-8A23-5D394BA01E03}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| EndGlobal | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ 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; | ||||
| 
 | ||||
|  | @ -17,7 +18,8 @@ internal sealed class Endpoints( | |||
|     PlayerServer playerService, | ||||
|     ControlsServer controlsServer, | ||||
|     MapService mapService, | ||||
|     ChangeToRequestedMap changeToRequestedMap | ||||
|     ChangeToRequestedMap changeToRequestedMap, | ||||
|     Connection displayConnection | ||||
| ) | ||||
| { | ||||
|     public void Map(WebApplication app) | ||||
|  | @ -29,6 +31,7 @@ internal sealed class Endpoints( | |||
|         app.Map("/controls", ConnectControlsAsync); | ||||
|         app.MapGet("/map", () => mapService.MapNames); | ||||
|         app.MapPost("/map", PostMap); | ||||
|         app.MapPost("/resetDisplay", () => displayConnection.Send(Command.HardReset().IntoPacket())); | ||||
|         app.MapGet("/map/{name}", GetMapByName); | ||||
| 
 | ||||
|         app.MapHealthChecks("/health", new HealthCheckOptions | ||||
|  | @ -114,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,20 +1,26 @@ | |||
| using System.Diagnostics; | ||||
| 
 | ||||
| namespace TanksServer.GameLogic; | ||||
| 
 | ||||
| internal sealed class CollectPowerUp( | ||||
|     MapEntityManager entityManager | ||||
| ) : ITickStep | ||||
| internal sealed class CollectPowerUp : ITickStep | ||||
| { | ||||
|     private readonly Predicate<PowerUp> _collectPredicate = b => TryCollect(b, entityManager.Tanks); | ||||
|     private readonly Predicate<PowerUp> _collectPredicate; | ||||
|     private readonly GameRules _rules; | ||||
|     private readonly MapEntityManager _entityManager; | ||||
| 
 | ||||
|     public CollectPowerUp(MapEntityManager entityManager, | ||||
|         IOptions<GameRules> options) | ||||
|     { | ||||
|         _entityManager = entityManager; | ||||
|         _rules = options.Value; | ||||
|         _collectPredicate = b => TryCollect(b, entityManager.Tanks); | ||||
|     } | ||||
| 
 | ||||
|     public ValueTask TickAsync(TimeSpan delta) | ||||
|     { | ||||
|         entityManager.RemoveWhere(_collectPredicate); | ||||
|         _entityManager.RemoveWhere(_collectPredicate); | ||||
|         return ValueTask.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     private static bool TryCollect(PowerUp powerUp, IEnumerable<Tank> tanks) | ||||
|     private bool TryCollect(PowerUp powerUp, IEnumerable<Tank> tanks) | ||||
|     { | ||||
|         var position = powerUp.Position; | ||||
|         foreach (var tank in tanks) | ||||
|  | @ -34,32 +40,38 @@ internal sealed class CollectPowerUp( | |||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     private static void ApplyPowerUpEffect(PowerUp powerUp, Tank tank) | ||||
|     private void ApplyPowerUpEffect(PowerUp powerUp, Tank tank) | ||||
|     { | ||||
|         switch (powerUp.Type) | ||||
|         { | ||||
|             case PowerUpType.MagazineType: | ||||
|                 if (powerUp.MagazineType == null) | ||||
|                     throw new UnreachableException(); | ||||
| 
 | ||||
|                 tank.Magazine = tank.Magazine with | ||||
|                 { | ||||
|                     Type = tank.Magazine.Type | powerUp.MagazineType.Value, | ||||
|                     UsedBullets = 0 | ||||
|                 }; | ||||
| 
 | ||||
|                 if (tank.ReloadingUntil >= DateTime.Now) | ||||
|                     tank.ReloadingUntil = DateTime.Now; | ||||
| 
 | ||||
|                 break; | ||||
|             case PowerUpType.MagazineSize: | ||||
|                 tank.Magazine = tank.Magazine with | ||||
|                 tank.MaxBullets = int.Clamp(tank.MaxBullets + 1, 1, 32); | ||||
|                 break; | ||||
| 
 | ||||
|             case PowerUpType.BulletAcceleration: | ||||
|                 tank.BulletStats = tank.BulletStats with | ||||
|                 { | ||||
|                     MaxBullets = (byte)int.Clamp(tank.Magazine.MaxBullets + 1, 1, 32) | ||||
|                     Acceleration = tank.BulletStats.Acceleration + _rules.BulletAccelerationUpgradeStrength | ||||
|                 }; | ||||
|                 break; | ||||
| 
 | ||||
|             case PowerUpType.ExplosiveBullets: | ||||
|                 tank.BulletStats = tank.BulletStats with { Explosive = true }; | ||||
|                 break; | ||||
| 
 | ||||
|             case PowerUpType.SmartBullets: | ||||
|                 tank.BulletStats = tank.BulletStats with { Smart = true }; | ||||
|                 break; | ||||
| 
 | ||||
|             case PowerUpType.BulletSpeed: | ||||
|                 tank.BulletStats = tank.BulletStats with | ||||
|                 { | ||||
|                     Speed = tank.BulletStats.Speed + _rules.BulletSpeedUpgradeStrength | ||||
|                 }; | ||||
|                 break; | ||||
| 
 | ||||
|             default: | ||||
|                 throw new UnreachableException(); | ||||
|                 throw new NotImplementedException($"unknown type {powerUp.Type}"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ internal sealed class CollideBullets : ITickStep | |||
|         if (bullet.Timeout > DateTime.Now) | ||||
|             return false; | ||||
| 
 | ||||
|         ExplodeAt(bullet.Position.ToPixelPosition(), bullet.IsExplosive, bullet.Owner); | ||||
|         ExplodeAt(bullet.Position.ToPixelPosition(), bullet.Stats.Explosive, bullet.Owner); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|  | @ -45,7 +45,7 @@ internal sealed class CollideBullets : ITickStep | |||
|         if (!_map.Current.IsWall(pixel)) | ||||
|             return false; | ||||
| 
 | ||||
|         ExplodeAt(pixel, bullet.IsExplosive, bullet.Owner); | ||||
|         ExplodeAt(pixel, bullet.Stats.Explosive, bullet.Owner); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|  | @ -55,7 +55,7 @@ internal sealed class CollideBullets : ITickStep | |||
|         if (hitTank == null) | ||||
|             return false; | ||||
| 
 | ||||
|         ExplodeAt(bullet.Position.ToPixelPosition(), bullet.IsExplosive, bullet.Owner); | ||||
|         ExplodeAt(bullet.Position.ToPixelPosition(), bullet.Stats.Explosive, bullet.Owner); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ internal sealed class GameRules | |||
| 
 | ||||
|     public double ShootDelayMs { get; set; } | ||||
| 
 | ||||
|     public double BulletSpeed { get; set; } | ||||
|     public double BulletSpeed { get; set; } = 75; | ||||
| 
 | ||||
|     public int SpawnDelayMs { get; set; } | ||||
| 
 | ||||
|  | @ -27,4 +27,8 @@ internal sealed class GameRules | |||
|     public int ReloadDelayMs { get; set; } = 3000; | ||||
| 
 | ||||
|     public double SmartBulletInertia { get; set; } = 1; | ||||
| 
 | ||||
|     public double BulletAccelerationUpgradeStrength { get; set; } = 15; | ||||
| 
 | ||||
|     public double BulletSpeedUpgradeStrength { get; set; } = 5; | ||||
| } | ||||
|  |  | |||
|  | @ -15,19 +15,17 @@ internal sealed class MapEntityManager( | |||
|     public IEnumerable<Tank> Tanks => _playerTanks.Values; | ||||
|     public IEnumerable<PowerUp> PowerUps => _powerUps; | ||||
| 
 | ||||
|     public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, MagazineType type) | ||||
|     public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, BulletStats stats) | ||||
|     { | ||||
|         var speed = _rules.BulletSpeed * (type.HasFlag(MagazineType.Fast) ? 2 : 1); | ||||
|         _bullets.Add(new Bullet | ||||
|         { | ||||
|             Owner = tankOwner, | ||||
|             Position = position, | ||||
|             Rotation = rotation, | ||||
|             IsExplosive = type.HasFlag(MagazineType.Explosive), | ||||
|             Timeout = DateTime.Now + _bulletTimeout, | ||||
|             OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1), | ||||
|             Speed = speed, | ||||
|             IsSmart = type.HasFlag(MagazineType.Smart) | ||||
|             Speed = _rules.BulletSpeed, | ||||
|             Stats = stats | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | @ -35,24 +33,22 @@ internal sealed class MapEntityManager( | |||
| 
 | ||||
|     public void SpawnTank(Player player, FloatPosition position) | ||||
|     { | ||||
|         var tank = new Tank | ||||
|         var tank = new Tank(player, position) | ||||
|         { | ||||
|             Owner = player, | ||||
|             Position = position, | ||||
|             Rotation = Random.Shared.NextDouble(), | ||||
|             Magazine = new Magazine(MagazineType.Basic, 0, _rules.MagazineSize) | ||||
|             MaxBullets = _rules.MagazineSize, | ||||
|             BulletStats =new BulletStats(_rules.BulletSpeed, 0, false, false) | ||||
|         }; | ||||
|         _playerTanks[player] = tank; | ||||
|         logger.LogInformation("Tank added for player {}", player.Name); | ||||
|     } | ||||
| 
 | ||||
|     public void SpawnPowerUp(FloatPosition position, PowerUpType type, MagazineType? magazineType) | ||||
|     public void SpawnPowerUp(FloatPosition position, PowerUpType type) | ||||
|     { | ||||
|         var powerUp = new PowerUp | ||||
|         { | ||||
|             Position = position, | ||||
|             Type = type, | ||||
|             MagazineType = magazineType | ||||
|             Type = type | ||||
|         }; | ||||
|         _powerUps.Add(powerUp); | ||||
|     } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -17,13 +17,15 @@ internal sealed class MoveBullets( | |||
| 
 | ||||
|     private void MoveBullet(Bullet bullet, TimeSpan delta) | ||||
|     { | ||||
|         if (bullet.IsSmart && TryGetSmartRotation(bullet.Position, bullet.Owner, out var wantedRotation)) | ||||
|         if (bullet.Stats.Smart && TryGetSmartRotation(bullet.Position, bullet.Owner, out var wantedRotation)) | ||||
|         { | ||||
|             var inertiaFactor = _smartBulletInertia * delta.TotalSeconds; | ||||
|             var difference = wantedRotation - bullet.Rotation; | ||||
|             bullet.Rotation += difference * inertiaFactor; | ||||
|         } | ||||
| 
 | ||||
|         bullet.Speed += (bullet.Stats.Acceleration * delta.TotalSeconds); | ||||
| 
 | ||||
|         var speed = bullet.Speed * delta.TotalSeconds; | ||||
|         var angle = bullet.Rotation * 2 * Math.PI; | ||||
|         bullet.Position = new FloatPosition( | ||||
|  |  | |||
|  | @ -26,24 +26,17 @@ internal sealed class ShootFromTanks( | |||
|         if (tank.ReloadingUntil >= now) | ||||
|             return; | ||||
| 
 | ||||
|         if (tank.Magazine.Empty) | ||||
|         if (tank.UsedBullets >= tank.MaxBullets) | ||||
|         { | ||||
|             tank.ReloadingUntil = now.AddMilliseconds(_config.ReloadDelayMs); | ||||
|             tank.Magazine = tank.Magazine with | ||||
|             { | ||||
|                 UsedBullets = 0, | ||||
|                 Type = MagazineType.Basic | ||||
|             }; | ||||
|             tank.UsedBullets = 0; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         tank.NextShotAfter = now.AddMilliseconds(_config.ShootDelayMs); | ||||
|         tank.Magazine = tank.Magazine with | ||||
|         { | ||||
|             UsedBullets = (byte)(tank.Magazine.UsedBullets + 1) | ||||
|         }; | ||||
|         tank.UsedBullets++; | ||||
| 
 | ||||
|         tank.Owner.Scores.ShotsFired++; | ||||
|         entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, tank.Magazine.Type); | ||||
|         entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, tank.BulletStats); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,3 @@ | |||
| using System.Diagnostics; | ||||
| 
 | ||||
| namespace TanksServer.GameLogic; | ||||
| 
 | ||||
| internal sealed class SpawnPowerUp( | ||||
|  | @ -18,25 +16,9 @@ internal sealed class SpawnPowerUp( | |||
|         if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds) | ||||
|             return ValueTask.CompletedTask; | ||||
| 
 | ||||
| 
 | ||||
|         var type = Random.Shared.Next(4) == 0 | ||||
|             ? PowerUpType.MagazineSize | ||||
|             : PowerUpType.MagazineType; | ||||
| 
 | ||||
|         MagazineType? magazineType = type switch | ||||
|         { | ||||
|             PowerUpType.MagazineType => Random.Shared.Next(0, 3) switch | ||||
|             { | ||||
|                 0 => MagazineType.Fast, | ||||
|                 1 => MagazineType.Explosive, | ||||
|                 2 => MagazineType.Smart, | ||||
|                 _ => throw new UnreachableException() | ||||
|             }, | ||||
|             _ => null | ||||
|         }; | ||||
| 
 | ||||
|         var type = (PowerUpType)Random.Shared.Next((int)Enum.GetValues<PowerUpType>().Max()); | ||||
|         var position = emptyTileFinder.ChooseEmptyTile().GetCenter().ToFloatPosition(); | ||||
|         entityManager.SpawnPowerUp(position, type, magazineType); | ||||
|         entityManager.SpawnPowerUp(position, type); | ||||
|         return ValueTask.CompletedTask; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| using TanksServer.GameLogic; | ||||
| 
 | ||||
| namespace TanksServer.Graphics; | ||||
|  |  | |||
|  | @ -14,12 +14,12 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt | |||
|     { | ||||
|         foreach (var powerUp in entityManager.PowerUps) | ||||
|         { | ||||
|             var sprite = powerUp switch | ||||
|             var sprite = powerUp.Type switch | ||||
|             { | ||||
|                 { Type: PowerUpType.MagazineSize } => _magazineSprite, | ||||
|                 { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart } => _smartSprite, | ||||
|                 { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Explosive } => _explosiveSprite, | ||||
|                 { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Fast } => _fastSprite, | ||||
|                 PowerUpType.MagazineSize => _magazineSprite, | ||||
|                 PowerUpType.BulletAcceleration or PowerUpType.BulletSpeed => _fastSprite, | ||||
|                 PowerUpType.SmartBullets => _smartSprite, | ||||
|                 PowerUpType.ExplosiveBullets => _explosiveSprite, | ||||
|                 _ => _genericSprite | ||||
|             }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| using System.Diagnostics; | ||||
| using DotNext.Threading; | ||||
| 
 | ||||
| namespace TanksServer.Interactivity; | ||||
|  | @ -10,27 +9,15 @@ internal abstract class DroppablePackageRequestConnection<TPackage>( | |||
|     where TPackage : class, IDisposable | ||||
| { | ||||
|     private readonly AsyncAutoResetEvent _nextPackageEvent = new(false, 1); | ||||
|     private int _runningMessageHandlers = 0; | ||||
|     private TPackage? _next; | ||||
| 
 | ||||
|     protected override ValueTask HandleMessageAsync(Memory<byte> _) | ||||
|     protected override async ValueTask HandleMessageAsync(Memory<byte> _) | ||||
|     { | ||||
|         if (Interlocked.Increment(ref _runningMessageHandlers) == 1) | ||||
|             return Core(); | ||||
| 
 | ||||
|         // client has requested multiple frames, ignoring duplicate requests | ||||
|         Interlocked.Decrement(ref _runningMessageHandlers); | ||||
|         return ValueTask.CompletedTask; | ||||
| 
 | ||||
|         async ValueTask Core() | ||||
|         { | ||||
|             await _nextPackageEvent.WaitAsync(); | ||||
|             var package = Interlocked.Exchange(ref _next, null); | ||||
|             if (package == null) | ||||
|                 throw new UnreachableException("package should be set here"); | ||||
|             await SendPackageAsync(package); | ||||
|             Interlocked.Decrement(ref _runningMessageHandlers); | ||||
|         } | ||||
|         await _nextPackageEvent.WaitAsync(); | ||||
|         var package = Interlocked.Exchange(ref _next, null); | ||||
|         if (package == null) | ||||
|             return; | ||||
|         await SendPackageAsync(package); | ||||
|     } | ||||
| 
 | ||||
|     protected void SetNextPackage(TPackage next) | ||||
|  |  | |||
|  | @ -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, | ||||
|  | @ -47,20 +47,7 @@ internal sealed class PlayerInfoConnection | |||
|     private async ValueTask<IMemoryOwner<byte>?> GenerateMessageAsync() | ||||
|     { | ||||
|         var tank = _entityManager.GetCurrentTankOfPlayer(_player); | ||||
| 
 | ||||
|         TankInfo? tankInfo = null; | ||||
|         if (tank != null) | ||||
|         { | ||||
|             var magazine = tank.ReloadingUntil > DateTime.Now ? "[ RELOADING ]" : tank.Magazine.ToDisplayString(); | ||||
|             tankInfo = new TankInfo(tank.Orientation, magazine, tank.Position.ToPixelPosition(), tank.Moving); | ||||
|         } | ||||
| 
 | ||||
|         var info = new PlayerInfo( | ||||
|             _player.Name, | ||||
|             _player.Scores, | ||||
|             _player.Controls.ToDisplayString(), | ||||
|             tankInfo, | ||||
|             _player.OpenConnections); | ||||
|         var info = new PlayerInfo(_player, _player.Controls.ToDisplayString(), tank); | ||||
| 
 | ||||
|         _tempStream.Position = 0; | ||||
|         await JsonSerializer.SerializeAsync(_tempStream, info, AppSerializerContext.Default.PlayerInfo); | ||||
|  | @ -85,3 +72,9 @@ internal sealed class PlayerInfoConnection | |||
|         Interlocked.Exchange(ref _lastMessage, data)?.Dispose(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| internal record struct PlayerInfo( | ||||
|     Player Player, | ||||
|     string Controls, | ||||
|     Tank? Tank | ||||
| ); | ||||
|  |  | |||
|  | @ -1,8 +1,10 @@ | |||
| using System.Diagnostics; | ||||
| using System.Net; | ||||
| using System.Net.Sockets; | ||||
| using DisplayCommands; | ||||
| using ServicePoint2; | ||||
| using TanksServer.GameLogic; | ||||
| using TanksServer.Graphics; | ||||
| using CompressionCode = ServicePoint2.BindGen.CompressionCode; | ||||
| 
 | ||||
| namespace TanksServer.Interactivity; | ||||
| 
 | ||||
|  | @ -12,12 +14,13 @@ 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; | ||||
| 
 | ||||
|     private DateTime _nextFailLogAfter = DateTime.Now; | ||||
|     private DateTime _nextFrameAfter = DateTime.Now; | ||||
|  | @ -25,31 +28,35 @@ internal sealed class SendToServicePointDisplay : IFrameConsumer | |||
|     public SendToServicePointDisplay( | ||||
|         PlayerServer players, | ||||
|         ILogger<SendToServicePointDisplay> logger, | ||||
|         IDisplayConnection displayConnection, | ||||
|         Connection displayConnection, | ||||
|         IOptions<HostConfiguration> hostOptions, | ||||
|         MapService mapService | ||||
|     ) | ||||
|         MapService mapService, | ||||
|         IOptionsMonitor<HostConfiguration> options, | ||||
|         IOptions<DisplayConfiguration> displayConfig) | ||||
|     { | ||||
|         _players = players; | ||||
|         _logger = logger; | ||||
|         _displayConnection = displayConnection; | ||||
|         _mapService = mapService; | ||||
|         _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) | ||||
|     { | ||||
|         if (!_options.CurrentValue.EnableServicePointDisplay) | ||||
|             return; | ||||
| 
 | ||||
|         if (DateTime.Now < _nextFrameAfter) | ||||
|             return; | ||||
| 
 | ||||
|  | @ -60,8 +67,9 @@ 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(), CompressionCode.Lzma) | ||||
|                 .IntoPacket()); | ||||
|             _displayConnection.Send(Command.Cp437Data(MapService.TilesPerRow, 0, _scoresBuffer.Clone()).IntoPacket()); | ||||
|         } | ||||
|         catch (SocketException ex) | ||||
|         { | ||||
|  | @ -97,4 +105,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(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -8,15 +8,13 @@ internal sealed class Bullet : IMapEntity | |||
| 
 | ||||
|     public required FloatPosition Position { get; set; } | ||||
| 
 | ||||
|     public required bool IsExplosive { get; init; } | ||||
| 
 | ||||
|     public required DateTime Timeout { get; init; } | ||||
| 
 | ||||
|     public PixelBounds Bounds => new(Position.ToPixelPosition(), Position.ToPixelPosition()); | ||||
| 
 | ||||
|     internal required DateTime OwnerCollisionAfter { get; init; } | ||||
| 
 | ||||
|     public required double Speed { get; init; } | ||||
|     public required double Speed { get; set; } | ||||
| 
 | ||||
|     public required bool IsSmart { get; init; } | ||||
|     public required BulletStats Stats { get; init; } | ||||
| } | ||||
|  |  | |||
|  | @ -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,38 +0,0 @@ | |||
| using System.Text; | ||||
| 
 | ||||
| namespace TanksServer.Models; | ||||
| 
 | ||||
| [Flags] | ||||
| internal enum MagazineType | ||||
| { | ||||
|     Basic = 0, | ||||
|     Fast = 1 << 0, | ||||
|     Explosive = 1 << 1, | ||||
|     Smart = 1 << 2, | ||||
| } | ||||
| 
 | ||||
| internal readonly record struct Magazine(MagazineType Type, byte UsedBullets, byte MaxBullets) | ||||
| { | ||||
|     public bool Empty => UsedBullets >= MaxBullets; | ||||
| 
 | ||||
|     public string ToDisplayString() | ||||
|     { | ||||
|         var sb = new StringBuilder(); | ||||
| 
 | ||||
|         if (Type.HasFlag(MagazineType.Fast)) | ||||
|             sb.Append("» "); | ||||
|         if (Type.HasFlag(MagazineType.Explosive)) | ||||
|             sb.Append("* "); | ||||
|         if (Type.HasFlag(MagazineType.Smart)) | ||||
|             sb.Append("@ "); | ||||
| 
 | ||||
|         sb.Append("[ "); | ||||
|         for (var i = 0; i < UsedBullets; i++) | ||||
|             sb.Append("\u25cb "); | ||||
|         for (var i = UsedBullets; i < MaxBullets; i++) | ||||
|             sb.Append("• "); | ||||
|         sb.Append(']'); | ||||
| 
 | ||||
|         return sb.ToString(); | ||||
|     } | ||||
| } | ||||
|  | @ -1,16 +0,0 @@ | |||
| namespace TanksServer.Models; | ||||
| 
 | ||||
| internal record struct TankInfo( | ||||
|     int Orientation, | ||||
|     string Magazine, | ||||
|     PixelPosition Position, | ||||
|     bool Moving | ||||
| ); | ||||
| 
 | ||||
| internal record struct PlayerInfo( | ||||
|     string Name, | ||||
|     Scores Scores, | ||||
|     string Controls, | ||||
|     TankInfo? Tank, | ||||
|     int OpenConnections | ||||
| ); | ||||
|  | @ -4,8 +4,11 @@ namespace TanksServer.Models; | |||
| 
 | ||||
| internal enum PowerUpType | ||||
| { | ||||
|     MagazineType, | ||||
|     MagazineSize | ||||
|     MagazineSize, | ||||
|     BulletSpeed, | ||||
|     BulletAcceleration, | ||||
|     ExplosiveBullets, | ||||
|     SmartBullets, | ||||
| } | ||||
| 
 | ||||
| internal sealed class PowerUp: IMapEntity | ||||
|  | @ -15,6 +18,4 @@ internal sealed class PowerUp: IMapEntity | |||
|     public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize); | ||||
| 
 | ||||
|     public required PowerUpType Type { get; init; } | ||||
| 
 | ||||
|     public MagazineType? MagazineType { get; init; } | ||||
| } | ||||
|  |  | |||
|  | @ -1,15 +1,16 @@ | |||
| using System.Diagnostics; | ||||
| using System.Text.Json.Serialization; | ||||
| using TanksServer.GameLogic; | ||||
| 
 | ||||
| namespace TanksServer.Models; | ||||
| 
 | ||||
| internal sealed class Tank : IMapEntity | ||||
| internal sealed class Tank(Player owner, FloatPosition position) : IMapEntity | ||||
| { | ||||
|     private double _rotation; | ||||
| 
 | ||||
|     public required Player Owner { get; init; } | ||||
|     [JsonIgnore] public Player Owner { get; } = owner; | ||||
| 
 | ||||
|     public double Rotation | ||||
|     [JsonIgnore] public double Rotation | ||||
|     { | ||||
|         get => _rotation; | ||||
|         set | ||||
|  | @ -24,13 +25,21 @@ internal sealed class Tank : IMapEntity | |||
| 
 | ||||
|     public bool Moving { get; set; } | ||||
| 
 | ||||
|     public required FloatPosition Position { get; set; } | ||||
|     [JsonIgnore] public FloatPosition Position { get; set; } = position; | ||||
| 
 | ||||
|     public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize); | ||||
|     public PixelPosition PixelPosition => Position.ToPixelPosition(); | ||||
| 
 | ||||
|     [JsonIgnore] public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize); | ||||
| 
 | ||||
|     public int Orientation => (int)Math.Round(Rotation * 16) % 16; | ||||
| 
 | ||||
|     public required Magazine Magazine { get; set; } | ||||
|     public int UsedBullets { get; set; } | ||||
| 
 | ||||
|     public int MaxBullets { get; set; } | ||||
| 
 | ||||
|     public DateTime ReloadingUntil { get; set; } | ||||
| 
 | ||||
|     public required BulletStats BulletStats { get; set; } | ||||
| } | ||||
| 
 | ||||
| internal sealed record class BulletStats(double Speed, double Acceleration, bool Explosive, bool Smart); | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| using System.IO; | ||||
| using DisplayCommands; | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.FileProviders; | ||||
| using ServicePoint2; | ||||
| using SixLabors.ImageSharp; | ||||
| using TanksServer.GameLogic; | ||||
| using TanksServer.Graphics; | ||||
| using TanksServer.Interactivity; | ||||
|  | @ -54,11 +54,6 @@ public static class Program | |||
|         var healthCheckBuilder = builder.Services.AddHealthChecks(); | ||||
|         healthCheckBuilder.AddCheck<UpdatesPerSecondCounter>("updates check"); | ||||
| 
 | ||||
|         builder.Services.Configure<HostConfiguration>(builder.Configuration.GetSection("Host")); | ||||
|         var hostConfiguration = builder.Configuration.GetSection("Host").Get<HostConfiguration>(); | ||||
|         if (hostConfiguration == null) | ||||
|             throw new InvalidOperationException("'Host' configuration missing"); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<MapService>(); | ||||
|         builder.Services.AddSingleton<MapEntityManager>(); | ||||
|         builder.Services.AddSingleton<ControlsServer>(); | ||||
|  | @ -99,12 +94,15 @@ public static class Program | |||
|             sp.GetRequiredService<ClientScreenServer>()); | ||||
| 
 | ||||
|         builder.Services.Configure<GameRules>(builder.Configuration.GetSection("GameRules")); | ||||
|         builder.Services.Configure<HostConfiguration>(builder.Configuration.GetSection("Host")); | ||||
|         builder.Services.Configure<DisplayConfiguration>(builder.Configuration.GetSection("ServicePointDisplay")); | ||||
| 
 | ||||
|         if (hostConfiguration.EnableServicePointDisplay) | ||||
|         builder.Services.AddSingleton<IFrameConsumer, SendToServicePointDisplay>(); | ||||
|         builder.Services.AddSingleton<Connection>(sp => | ||||
|         { | ||||
|             builder.Services.AddSingleton<IFrameConsumer, SendToServicePointDisplay>(); | ||||
|             builder.Services.AddDisplay(builder.Configuration.GetSection("ServicePointDisplay")); | ||||
|         } | ||||
|             var config = sp.GetRequiredService<IOptions<DisplayConfiguration>>().Value; | ||||
|             return Connection.Open($"{config.Hostname}:{config.Port}"); | ||||
|         }); | ||||
| 
 | ||||
|         var app = builder.Build(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,20 +1,35 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
| 
 | ||||
|     <Import Project="../shared.props" /> | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net8.0</TargetFramework> | ||||
|         <ImplicitUsings>disable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
|     <PropertyGroup> | ||||
|         <PublishAot>true</PublishAot> | ||||
|         <IsAotCompatible>true</IsAotCompatible> | ||||
|         <InvariantGlobalization>true</InvariantGlobalization> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
|     <PropertyGroup> | ||||
|         <AnalysisMode>Recommended</AnalysisMode> | ||||
|         <TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||||
|         <NoWarn>CA1805,CA1848</NoWarn> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
|     <ItemGroup> | ||||
|         <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="..\servicepoint\servicepoint2-binding-cs\src\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": { | ||||
|  | @ -33,7 +34,7 @@ | |||
|         "SmartBulletHomingSpeed": 1.5 | ||||
|     }, | ||||
|     "Host": { | ||||
|         "EnableServicePointDisplay": false, | ||||
|         "EnableServicePointDisplay": true, | ||||
|         "ServicePointDisplayMinFrameTimeMs": 28, | ||||
|         "ClientScreenMinFrameTime": 5 | ||||
|     } | ||||
|  |  | |||
|  | @ -1,6 +0,0 @@ | |||
| { | ||||
|   "sdk": { | ||||
|     "version": "8.0.0", | ||||
|     "rollForward": "latestMajor" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										1
									
								
								tanks-backend/servicepoint
									
										
									
									
									
										Submodule
									
								
							
							
						
						
									
										1
									
								
								tanks-backend/servicepoint
									
										
									
									
									
										Submodule
									
								
							|  | @ -0,0 +1 @@ | |||
| Subproject commit eab2d58945ebf68a4a6e8cf69cf113875fe6168d | ||||
|  | @ -1,20 +0,0 @@ | |||
| <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||||
| 
 | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net8.0</TargetFramework> | ||||
|         <ImplicitUsings>disable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
|     <PropertyGroup> | ||||
|         <IsAotCompatible>true</IsAotCompatible> | ||||
|         <InvariantGlobalization>true</InvariantGlobalization> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
|     <PropertyGroup> | ||||
|         <AnalysisMode>Recommended</AnalysisMode> | ||||
|         <TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||||
|         <NoWarn>CA1805,CA1848</NoWarn> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
| </Project> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 RobbersDaughter
						RobbersDaughter