Use native rust library for parsing packages (#21)
This commit is contained in:
commit
796c5d19c5
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,4 +1,4 @@
|
|||
namespace DisplayCommands;
|
||||
namespace TanksServer.Models;
|
||||
|
||||
public class DisplayConfiguration
|
||||
{
|
|
@ -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…
Reference in a new issue