diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..75a838a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tanks-backend/servicepoint"] + path = tanks-backend/servicepoint + url = https://github.com/kaesaecracker/servicepoint.git diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fa1abcd..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "cccb-tanks-cs", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/tank-frontend/src/PlayerInfo.tsx b/tank-frontend/src/PlayerInfo.tsx index 4ab04bc..37c479e 100644 --- a/tank-frontend/src/PlayerInfo.tsx +++ b/tank-frontend/src/PlayerInfo.tsx @@ -48,27 +48,31 @@ export default function PlayerInfo({player}: { player: string }) { return

- Playing as {lastJsonMessage.name} + Playing as {lastJsonMessage.player.name}

- + {lastJsonMessage.tank && <> + + + + + + + + + } + - - - + + + + + + + - - - - - - - - - - - +
; diff --git a/tank-frontend/src/serverCalls.tsx b/tank-frontend/src/serverCalls.tsx index 83e4f2d..36d2e74 100644 --- a/tank-frontend/src/serverCalls.tsx +++ b/tank-frontend/src/serverCalls.tsx @@ -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(url: string, options: Options = {}) { return useWebSocket(url, { shouldReconnect: () => true, diff --git a/tanks-backend/.dockerignore b/tanks-backend/.dockerignore index d041927..5aba7dc 100644 --- a/tanks-backend/.dockerignore +++ b/tanks-backend/.dockerignore @@ -11,3 +11,5 @@ **/bin **/Dockerfile* **/obj +**/target +**/examples diff --git a/tanks-backend/DisplayCommands/ByteGrid.cs b/tanks-backend/DisplayCommands/ByteGrid.cs deleted file mode 100644 index f7e990f..0000000 --- a/tanks-backend/DisplayCommands/ByteGrid.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Diagnostics; - -namespace DisplayCommands; - -public sealed class ByteGrid(ushort width, ushort height) : IEquatable -{ - public ushort Height { get; } = height; - - public ushort Width { get; } = width; - - internal Memory Data { get; } = new byte[width * height].AsMemory(); - - public byte this[ushort x, ushort y] - { - get => Data.Span[GetIndex(x, y)]; - set => Data.Span[GetIndex(x, y)] = value; - } - - 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); -} diff --git a/tanks-backend/DisplayCommands/Cp437Grid.cs b/tanks-backend/DisplayCommands/Cp437Grid.cs deleted file mode 100644 index 19afe2f..0000000 --- a/tanks-backend/DisplayCommands/Cp437Grid.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Diagnostics; -using System.Text; - -namespace DisplayCommands; - -public sealed class Cp437Grid(ushort width, ushort height) : IEquatable -{ - 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 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 valuesStr = stackalloc char[] { c }; - Span convertedStr = stackalloc byte[1]; - var consumed = _encoding.GetBytes(valuesStr, convertedStr); - Debug.Assert(consumed == 1); - return convertedStr[0]; - } - - private char ByteToChar(byte b) - { - ReadOnlySpan valueBytes = stackalloc byte[] { b }; - Span resultStr = stackalloc char[1]; - 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); -} diff --git a/tanks-backend/DisplayCommands/DisplayCommands.csproj b/tanks-backend/DisplayCommands/DisplayCommands.csproj deleted file mode 100644 index ca146b4..0000000 --- a/tanks-backend/DisplayCommands/DisplayCommands.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - true - true - - - - - - - - - - - - - - diff --git a/tanks-backend/DisplayCommands/DisplayExtensions.cs b/tanks-backend/DisplayCommands/DisplayExtensions.cs deleted file mode 100644 index 5c94d8d..0000000 --- a/tanks-backend/DisplayCommands/DisplayExtensions.cs +++ /dev/null @@ -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(); - if (configurationSection != null) - services.Configure(configurationSection); - return services; - } -} \ No newline at end of file diff --git a/tanks-backend/DisplayCommands/GlobalUsings.cs b/tanks-backend/DisplayCommands/GlobalUsings.cs deleted file mode 100644 index b124d17..0000000 --- a/tanks-backend/DisplayCommands/GlobalUsings.cs +++ /dev/null @@ -1,2 +0,0 @@ -global using System; -global using System.Threading.Tasks; diff --git a/tanks-backend/DisplayCommands/IDisplayConnection.cs b/tanks-backend/DisplayCommands/IDisplayConnection.cs deleted file mode 100644 index 950e75b..0000000 --- a/tanks-backend/DisplayCommands/IDisplayConnection.cs +++ /dev/null @@ -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); - - /// - /// Returns the IPv4 address that is associated with the interface with which the display is reachable. - /// - /// IPv4 as text - public string GetLocalIPv4(); -} diff --git a/tanks-backend/DisplayCommands/Internals/DisplayCommand.cs b/tanks-backend/DisplayCommands/Internals/DisplayCommand.cs deleted file mode 100644 index abe94e1..0000000 --- a/tanks-backend/DisplayCommands/Internals/DisplayCommand.cs +++ /dev/null @@ -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 -} diff --git a/tanks-backend/DisplayCommands/Internals/DisplayConnection.cs b/tanks-backend/DisplayCommands/Internals/DisplayConnection.cs deleted file mode 100644 index 972a5db..0000000 --- a/tanks-backend/DisplayCommands/Internals/DisplayConnection.cs +++ /dev/null @@ -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 options) : IDisplayConnection, IDisposable -{ - private readonly ArrayPool _arrayPool = ArrayPool.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.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.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 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(); -} diff --git a/tanks-backend/DisplayCommands/Internals/DisplaySubCommand.cs b/tanks-backend/DisplayCommands/Internals/DisplaySubCommand.cs deleted file mode 100644 index bf5427f..0000000 --- a/tanks-backend/DisplayCommands/Internals/DisplaySubCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DisplayCommands.Internals; - -internal enum DisplaySubCommand : ushort -{ - BitmapNormal = 0x0, - BitmapCompressZ = 0x677a, - BitmapCompressBz = 0x627a, - BitmapCompressLz = 0x6c7a, - BitmapCompressZs = 0x7a73 -} diff --git a/tanks-backend/DisplayCommands/Internals/HeaderBitmap.cs b/tanks-backend/DisplayCommands/Internals/HeaderBitmap.cs deleted file mode 100644 index b733995..0000000 --- a/tanks-backend/DisplayCommands/Internals/HeaderBitmap.cs +++ /dev/null @@ -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; -} diff --git a/tanks-backend/DisplayCommands/Internals/HeaderWindow.cs b/tanks-backend/DisplayCommands/Internals/HeaderWindow.cs deleted file mode 100644 index 2be4916..0000000 --- a/tanks-backend/DisplayCommands/Internals/HeaderWindow.cs +++ /dev/null @@ -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; -} diff --git a/tanks-backend/DisplayCommands/PixelGrid.cs b/tanks-backend/DisplayCommands/PixelGrid.cs deleted file mode 100644 index 5006ddb..0000000 --- a/tanks-backend/DisplayCommands/PixelGrid.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics; - -namespace DisplayCommands; - -public sealed class PixelGrid(ushort width, ushort height) : IEquatable -{ - private readonly ByteGrid _byteGrid = new((ushort)(width / 8u), height); - - public ushort Width { get; } = width; - - public ushort Height { get; } = height; - - public Memory 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); - } -} diff --git a/tanks-backend/Dockerfile b/tanks-backend/Dockerfile index 4fcda7d..af7acd4 100644 --- a/tanks-backend/Dockerfile +++ b/tanks-backend/Dockerfile @@ -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 diff --git a/tanks-backend/EndiannessSourceGenerator/EndiannessGenerator.cs b/tanks-backend/EndiannessSourceGenerator/EndiannessGenerator.cs deleted file mode 100644 index eb772e5..0000000 --- a/tanks-backend/EndiannessSourceGenerator/EndiannessGenerator.cs +++ /dev/null @@ -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 = - $$""" - // - 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() - .Any(p => p.DescendantNodes() - .OfType() - .Any())) - .ToList(); - - foreach (var tree in treesWithStructsWithAttributes) - { - var semanticModel = context.Compilation.GetSemanticModel(tree); - - var structsWithAttributes = tree.GetRoot().DescendantNodes() - .OfType() - .Where(cd => cd.DescendantNodes() - .OfType() - .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(); - - var generatedCode = CompilationUnit() - .WithUsings(List([ - UsingDirective(IdentifierName("System")), - UsingDirective(IdentifierName("System.Buffers.Binary")) - ])) - .WithMembers(List([ - 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 GenerateStructProperties( - IEnumerable fieldDeclarations, SemanticModel semanticModel, bool structIsLittleEndian) - { - var result = new List(); - 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() - .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(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([ - 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()) - { - 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; - } -} diff --git a/tanks-backend/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj b/tanks-backend/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj deleted file mode 100644 index ae25c62..0000000 --- a/tanks-backend/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - netstandard2.0 - false - enable - latest - - true - true - EndiannessSourceGenerator - false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - diff --git a/tanks-backend/EndiannessSourceGenerator/Properties/launchSettings.json b/tanks-backend/EndiannessSourceGenerator/Properties/launchSettings.json deleted file mode 100644 index b8b1379..0000000 --- a/tanks-backend/EndiannessSourceGenerator/Properties/launchSettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "Generators": { - "commandName": "DebugRoslynComponent", - "targetProject": "../DisplayCommands/DisplayCommands.csproj" - } - } -} diff --git a/tanks-backend/EndiannessSourceGenerator/Readme.md b/tanks-backend/EndiannessSourceGenerator/Readme.md deleted file mode 100644 index a69efd1..0000000 --- a/tanks-backend/EndiannessSourceGenerator/Readme.md +++ /dev/null @@ -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. diff --git a/tanks-backend/TanksServer.sln b/tanks-backend/TanksServer.sln index fc3a074..f26467a 100644 --- a/tanks-backend/TanksServer.sln +++ b/tanks-backend/TanksServer.sln @@ -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 diff --git a/tanks-backend/TanksServer/Endpoints.cs b/tanks-backend/TanksServer/Endpoints.cs index e0e0252..baf476b 100644 --- a/tanks-backend/TanksServer/Endpoints.cs +++ b/tanks-backend/TanksServer/Endpoints.cs @@ -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); } diff --git a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs index 3b3571f..938f2f5 100644 --- a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs @@ -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 _collectPredicate = b => TryCollect(b, entityManager.Tanks); + private readonly Predicate _collectPredicate; + private readonly GameRules _rules; + private readonly MapEntityManager _entityManager; + + public CollectPowerUp(MapEntityManager entityManager, + IOptions 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 tanks) + private bool TryCollect(PowerUp powerUp, IEnumerable 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}"); } } } diff --git a/tanks-backend/TanksServer/GameLogic/CollideBullets.cs b/tanks-backend/TanksServer/GameLogic/CollideBullets.cs index 4be0dca..c75fe6a 100644 --- a/tanks-backend/TanksServer/GameLogic/CollideBullets.cs +++ b/tanks-backend/TanksServer/GameLogic/CollideBullets.cs @@ -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; } diff --git a/tanks-backend/TanksServer/GameLogic/GameRules.cs b/tanks-backend/TanksServer/GameLogic/GameRules.cs index 2284464..1ebf548 100644 --- a/tanks-backend/TanksServer/GameLogic/GameRules.cs +++ b/tanks-backend/TanksServer/GameLogic/GameRules.cs @@ -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; } diff --git a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs index e7b376d..dd674a1 100644 --- a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs +++ b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs @@ -15,19 +15,17 @@ internal sealed class MapEntityManager( public IEnumerable Tanks => _playerTanks.Values; public IEnumerable 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); } diff --git a/tanks-backend/TanksServer/GameLogic/MapService.cs b/tanks-backend/TanksServer/GameLogic/MapService.cs index 28a5aef..753d007 100644 --- a/tanks-backend/TanksServer/GameLogic/MapService.cs +++ b/tanks-backend/TanksServer/GameLogic/MapService.cs @@ -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 diff --git a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs index 96bde60..60bd5b8 100644 --- a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs +++ b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs @@ -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( diff --git a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs index 9df8a64..3014c45 100644 --- a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs +++ b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs @@ -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); } } diff --git a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs index 72b0592..b79abb0 100644 --- a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs @@ -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().Max()); var position = emptyTileFinder.ChooseEmptyTile().GetCenter().ToFloatPosition(); - entityManager.SpawnPowerUp(position, type, magazineType); + entityManager.SpawnPowerUp(position, type); return ValueTask.CompletedTask; } } diff --git a/tanks-backend/TanksServer/Graphics/DrawMapStep.cs b/tanks-backend/TanksServer/Graphics/DrawMapStep.cs index 3be6a37..3fc59b5 100644 --- a/tanks-backend/TanksServer/Graphics/DrawMapStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawMapStep.cs @@ -1,4 +1,4 @@ -using DisplayCommands; +using ServicePoint2; using TanksServer.GameLogic; namespace TanksServer.Graphics; diff --git a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs index 6d47060..c6125ec 100644 --- a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs @@ -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 }; diff --git a/tanks-backend/TanksServer/Graphics/GeneratePixelsTickStep.cs b/tanks-backend/TanksServer/Graphics/GeneratePixelsTickStep.cs index 7edf1a0..de83cbb 100644 --- a/tanks-backend/TanksServer/Graphics/GeneratePixelsTickStep.cs +++ b/tanks-backend/TanksServer/Graphics/GeneratePixelsTickStep.cs @@ -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 _drawSteps = drawSteps.ToList(); private readonly List _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++) { diff --git a/tanks-backend/TanksServer/Graphics/IFrameConsumer.cs b/tanks-backend/TanksServer/Graphics/IFrameConsumer.cs index 5a83a86..9ee8d3e 100644 --- a/tanks-backend/TanksServer/Graphics/IFrameConsumer.cs +++ b/tanks-backend/TanksServer/Graphics/IFrameConsumer.cs @@ -1,4 +1,4 @@ -using DisplayCommands; +using ServicePoint2; namespace TanksServer.Graphics; diff --git a/tanks-backend/TanksServer/Interactivity/ClientScreenServer.cs b/tanks-backend/TanksServer/Interactivity/ClientScreenServer.cs index 6955c4d..2832fba 100644 --- a/tanks-backend/TanksServer/Interactivity/ClientScreenServer.cs +++ b/tanks-backend/TanksServer/Interactivity/ClientScreenServer.cs @@ -1,5 +1,5 @@ using System.Net.WebSockets; -using DisplayCommands; +using ServicePoint2; using TanksServer.Graphics; namespace TanksServer.Interactivity; diff --git a/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs b/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs index 6208cbf..483176a 100644 --- a/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/ClientScreenServerConnection.cs @@ -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); diff --git a/tanks-backend/TanksServer/Interactivity/DroppablePackageRequestConnection.cs b/tanks-backend/TanksServer/Interactivity/DroppablePackageRequestConnection.cs index 19956a9..49dccc2 100644 --- a/tanks-backend/TanksServer/Interactivity/DroppablePackageRequestConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/DroppablePackageRequestConnection.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using DotNext.Threading; namespace TanksServer.Interactivity; @@ -10,27 +9,15 @@ internal abstract class DroppablePackageRequestConnection( where TPackage : class, IDisposable { private readonly AsyncAutoResetEvent _nextPackageEvent = new(false, 1); - private int _runningMessageHandlers = 0; private TPackage? _next; - protected override ValueTask HandleMessageAsync(Memory _) + protected override async ValueTask HandleMessageAsync(Memory _) { - 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) diff --git a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs index 04661c3..6340fbc 100644 --- a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs @@ -13,7 +13,7 @@ internal sealed class PlayerInfoConnection private readonly MapEntityManager _entityManager; private readonly BufferPool _bufferPool; private readonly MemoryStream _tempStream = new(); - private IMemoryOwner? _lastMessage = null; + private IMemoryOwner? _lastMessage; public PlayerInfoConnection( Player player, @@ -47,20 +47,7 @@ internal sealed class PlayerInfoConnection private async ValueTask?> 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 +); diff --git a/tanks-backend/TanksServer/Interactivity/SendToServicePointDisplay.cs b/tanks-backend/TanksServer/Interactivity/SendToServicePointDisplay.cs index 5625566..3effe4d 100644 --- a/tanks-backend/TanksServer/Interactivity/SendToServicePointDisplay.cs +++ b/tanks-backend/TanksServer/Interactivity/SendToServicePointDisplay.cs @@ -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 _logger; private readonly PlayerServer _players; - private readonly Cp437Grid _scoresBuffer; + private readonly ByteGrid _scoresBuffer; private readonly TimeSpan _minFrameTime; + private readonly IOptionsMonitor _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 logger, - IDisplayConnection displayConnection, + Connection displayConnection, IOptions hostOptions, - MapService mapService - ) + MapService mapService, + IOptionsMonitor options, + IOptions 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(); + } } diff --git a/tanks-backend/TanksServer/Models/Bullet.cs b/tanks-backend/TanksServer/Models/Bullet.cs index 9e17066..aed3ddf 100644 --- a/tanks-backend/TanksServer/Models/Bullet.cs +++ b/tanks-backend/TanksServer/Models/Bullet.cs @@ -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; } } diff --git a/tanks-backend/DisplayCommands/DisplayConfiguration.cs b/tanks-backend/TanksServer/Models/DisplayConfiguration.cs similarity index 81% rename from tanks-backend/DisplayCommands/DisplayConfiguration.cs rename to tanks-backend/TanksServer/Models/DisplayConfiguration.cs index ff3cf05..fc9e941 100644 --- a/tanks-backend/DisplayCommands/DisplayConfiguration.cs +++ b/tanks-backend/TanksServer/Models/DisplayConfiguration.cs @@ -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; -} \ No newline at end of file +} diff --git a/tanks-backend/TanksServer/Models/Magazine.cs b/tanks-backend/TanksServer/Models/Magazine.cs deleted file mode 100644 index febd5b8..0000000 --- a/tanks-backend/TanksServer/Models/Magazine.cs +++ /dev/null @@ -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(); - } -} diff --git a/tanks-backend/TanksServer/Models/PlayerInfo.cs b/tanks-backend/TanksServer/Models/PlayerInfo.cs deleted file mode 100644 index a8545f3..0000000 --- a/tanks-backend/TanksServer/Models/PlayerInfo.cs +++ /dev/null @@ -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 -); diff --git a/tanks-backend/TanksServer/Models/PowerUp.cs b/tanks-backend/TanksServer/Models/PowerUp.cs index a8adb25..318a3a4 100644 --- a/tanks-backend/TanksServer/Models/PowerUp.cs +++ b/tanks-backend/TanksServer/Models/PowerUp.cs @@ -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; } } diff --git a/tanks-backend/TanksServer/Models/Tank.cs b/tanks-backend/TanksServer/Models/Tank.cs index e9de961..7147349 100644 --- a/tanks-backend/TanksServer/Models/Tank.cs +++ b/tanks-backend/TanksServer/Models/Tank.cs @@ -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); diff --git a/tanks-backend/TanksServer/Program.cs b/tanks-backend/TanksServer/Program.cs index 91c14fe..89c71ae 100644 --- a/tanks-backend/TanksServer/Program.cs +++ b/tanks-backend/TanksServer/Program.cs @@ -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("updates check"); - builder.Services.Configure(builder.Configuration.GetSection("Host")); - var hostConfiguration = builder.Configuration.GetSection("Host").Get(); - if (hostConfiguration == null) - throw new InvalidOperationException("'Host' configuration missing"); - builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -99,12 +94,15 @@ public static class Program sp.GetRequiredService()); builder.Services.Configure(builder.Configuration.GetSection("GameRules")); + builder.Services.Configure(builder.Configuration.GetSection("Host")); + builder.Services.Configure(builder.Configuration.GetSection("ServicePointDisplay")); - if (hostConfiguration.EnableServicePointDisplay) + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => { - builder.Services.AddSingleton(); - builder.Services.AddDisplay(builder.Configuration.GetSection("ServicePointDisplay")); - } + var config = sp.GetRequiredService>().Value; + return Connection.Open($"{config.Hostname}:{config.Port}"); + }); var app = builder.Build(); diff --git a/tanks-backend/TanksServer/TanksServer.csproj b/tanks-backend/TanksServer/TanksServer.csproj index 4cce028..aefb03e 100644 --- a/tanks-backend/TanksServer/TanksServer.csproj +++ b/tanks-backend/TanksServer/TanksServer.csproj @@ -1,20 +1,35 @@ - + + net8.0 + disable + enable + true + true + true + + + + Recommended + true + CA1805,CA1848 - + + + + diff --git a/tanks-backend/TanksServer/appsettings.json b/tanks-backend/TanksServer/appsettings.json index 37f8c7b..4555c28 100644 --- a/tanks-backend/TanksServer/appsettings.json +++ b/tanks-backend/TanksServer/appsettings.json @@ -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 } diff --git a/tanks-backend/global.json b/tanks-backend/global.json deleted file mode 100644 index 8eb62be..0000000 --- a/tanks-backend/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "8.0.0", - "rollForward": "latestMajor" - } -} diff --git a/tanks-backend/servicepoint b/tanks-backend/servicepoint new file mode 160000 index 0000000..eab2d58 --- /dev/null +++ b/tanks-backend/servicepoint @@ -0,0 +1 @@ +Subproject commit eab2d58945ebf68a4a6e8cf69cf113875fe6168d diff --git a/tanks-backend/shared.props b/tanks-backend/shared.props deleted file mode 100644 index 2417333..0000000 --- a/tanks-backend/shared.props +++ /dev/null @@ -1,20 +0,0 @@ - - - - net8.0 - disable - enable - - - - true - true - - - - Recommended - true - CA1805,CA1848 - - -