diff --git a/TanksServer/AppSerializerContext.cs b/TanksServer/AppSerializerContext.cs new file mode 100644 index 0000000..c02d085 --- /dev/null +++ b/TanksServer/AppSerializerContext.cs @@ -0,0 +1,6 @@ +using System.Text.Json.Serialization; + +namespace TanksServer; + +[JsonSerializable(typeof(Player))] +internal partial class AppSerializerContext: JsonSerializerContext; diff --git a/TanksServer/ClientScreenServer.cs b/TanksServer/ClientScreenServer.cs index 4b7fc44..5a741cd 100644 --- a/TanksServer/ClientScreenServer.cs +++ b/TanksServer/ClientScreenServer.cs @@ -1,5 +1,4 @@ using System.Net.WebSockets; -using System.Threading; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/TanksServer/ControlsServer.cs b/TanksServer/ControlsServer.cs index 356d3e6..5d38f83 100644 --- a/TanksServer/ControlsServer.cs +++ b/TanksServer/ControlsServer.cs @@ -1,6 +1,103 @@ +using System.Net.WebSockets; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + namespace TanksServer; -public class ControlsServer +internal sealed class ControlsServer(ILogger logger, ILoggerFactory loggerFactory) + : IHostedLifecycleService { - + private readonly List _connections = new(); + + public Task HandleClient(WebSocket ws, Player player) + { + logger.LogDebug("control client connected {}", player.Id); + var clientLogger = loggerFactory.CreateLogger(); + var sock = new ControlsServerConnection(ws, clientLogger, this, player); + _connections.Add(sock); + return sock.Done; + } + + public Task StoppingAsync(CancellationToken cancellationToken) + { + return Task.WhenAll(_connections.Select(c => c.CloseAsync())); + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private void Remove(ControlsServerConnection connection) + { + _connections.Remove(connection); + } + + private sealed class ControlsServerConnection(WebSocket socket, ILogger logger, ControlsServer server, + Player player) + : EasyWebSocket(socket, logger, new byte[2]) + { + private enum MessageType : byte + { + Enable = 0x01, + Disable = 0x02, + } + + private enum InputType : byte + { + Forward = 0x01, + Backward = 0x02, + Left = 0x03, + Right = 0x04, + Shoot = 0x05 + } + + protected override Task ReceiveAsync(ArraySegment buffer) + { + logger.LogDebug("player input {} {}", buffer[0], buffer[1]); + + bool isEnable; + switch ((MessageType)buffer[0]) + { + case MessageType.Enable: + isEnable = true; + break; + case MessageType.Disable: + isEnable = false; + break; + default: + return CloseAsync(WebSocketCloseStatus.InvalidPayloadData, "invalid state"); + } + + switch ((InputType)buffer[1]) + { + case InputType.Forward: + player.Controls.Forward = isEnable; + break; + case InputType.Backward: + player.Controls.Backward = isEnable; + break; + case InputType.Left: + player.Controls.TurnLeft = isEnable; + break; + case InputType.Right: + player.Controls.TurnRight = isEnable; + break; + case InputType.Shoot: + player.Controls.Shoot = isEnable; + break; + default: + return CloseAsync(WebSocketCloseStatus.InvalidPayloadData, "invalid control"); + } + + return Task.CompletedTask; + } + + protected override Task ClosingAsync() + { + server.Remove(this); + return Task.CompletedTask; + } + } } diff --git a/TanksServer/EasyWebSocket.cs b/TanksServer/EasyWebSocket.cs index d043fcb..2889317 100644 --- a/TanksServer/EasyWebSocket.cs +++ b/TanksServer/EasyWebSocket.cs @@ -1,5 +1,4 @@ using System.Net.WebSockets; -using System.Threading; using Microsoft.Extensions.Logging; namespace TanksServer; @@ -34,8 +33,6 @@ internal abstract class EasyWebSocket await ReceiveAsync(_buffer[..response.Count]); } while (_socket.State == WebSocketState.Open); - - await CloseAsync(); } protected abstract Task ReceiveAsync(ArraySegment buffer); @@ -45,7 +42,7 @@ internal abstract class EasyWebSocket { if (_socket.State != WebSocketState.Open) await CloseAsync(); - + _logger.LogTrace("sending {} bytes of data", _buffer.Count); try @@ -58,12 +55,15 @@ internal abstract class EasyWebSocket } } - public async Task CloseAsync() + public async Task CloseAsync( + WebSocketCloseStatus status = WebSocketCloseStatus.NormalClosure, + string? description = null + ) { if (Interlocked.Exchange(ref _closed, 1) == 1) return; _logger.LogDebug("closing socket"); - await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + await _socket.CloseAsync(status, description, CancellationToken.None); await _readLoop; await ClosingAsync(); _completionSource.SetResult(); diff --git a/TanksServer/GameTickService.cs b/TanksServer/GameTickService.cs index 728863c..0cd80cc 100644 --- a/TanksServer/GameTickService.cs +++ b/TanksServer/GameTickService.cs @@ -1,4 +1,3 @@ -using System.Threading; using Microsoft.Extensions.Hosting; namespace TanksServer; diff --git a/TanksServer/GlobalUsings.cs b/TanksServer/GlobalUsings.cs index 29ee885..3e07978 100644 --- a/TanksServer/GlobalUsings.cs +++ b/TanksServer/GlobalUsings.cs @@ -1,4 +1,5 @@ global using System; global using System.Collections.Generic; global using System.Linq; +global using System.Threading; global using System.Threading.Tasks; diff --git a/TanksServer/PlayerService.cs b/TanksServer/PlayerService.cs index 94097ae..7989004 100644 --- a/TanksServer/PlayerService.cs +++ b/TanksServer/PlayerService.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; namespace TanksServer; @@ -8,11 +9,36 @@ internal sealed class PlayerService(ILogger logger) private readonly ConcurrentDictionary _players = new(); public Player GetOrAdd(string name) => _players.GetOrAdd(name, _ => new Player(name)); + + public bool TryGet(Guid? playerId, [MaybeNullWhen(false)] out Player foundPlayer) + { + foreach (var player in _players.Values) + { + if (player.Id != playerId) + continue; + foundPlayer = player; + return true; + } + + foundPlayer = null; + return false; + } } -internal class Player(string name) +internal sealed class Player(string name) { public string Name => name; - public Guid Id => Guid.NewGuid(); + public Guid Id { get; } = Guid.NewGuid(); + + public PlayerControls Controls { get; } = new(); +} + +internal sealed class PlayerControls +{ + public bool Forward { get; set; } + public bool Backward { get; set; } + public bool TurnLeft { get; set; } + public bool TurnRight { get; set; } + public bool Shoot { get; set; } } diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index 5a0951e..2c1c66c 100644 --- a/TanksServer/Program.cs +++ b/TanksServer/Program.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Text.Json.Serialization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -19,6 +18,7 @@ internal static class Program var clientScreenServer = app.Services.GetRequiredService(); var playerService = app.Services.GetRequiredService(); + var controlsServer = app.Services.GetRequiredService(); var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client")); app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider }); @@ -38,17 +38,17 @@ internal static class Program await clientScreenServer.HandleClient(ws); }); - app.Map("/controls", async (HttpContext context, [FromQuery] string playerId) => + app.Map("/controls", async (HttpContext context, [FromQuery] Guid playerId) => { if (!context.WebSockets.IsWebSocketRequest) - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } + return Results.BadRequest(); + if (!playerService.TryGet(playerId, out var player)) + return Results.NotFound(); + using var ws = await context.WebSockets.AcceptWebSocketAsync(); - await clientScreenServer.HandleClient(ws); - + await controlsServer.HandleClient(ws, player); + return Results.Empty; }); await app.RunAsync(); @@ -80,6 +80,8 @@ internal static class Program builder.Services.AddHostedService(sp => sp.GetRequiredService()); builder.Services.AddSingleton(sp => sp.GetRequiredService()); + builder.Services.AddSingleton(); + builder.Services.AddHostedService(); builder.Services.AddSingleton(); @@ -87,6 +89,3 @@ internal static class Program return builder.Build(); } } - -[JsonSerializable(typeof(Player))] -internal partial class AppSerializerContext: JsonSerializerContext; diff --git a/tank-frontend/src/Controls.tsx b/tank-frontend/src/Controls.tsx index 147cecd..4eb3c91 100644 --- a/tank-frontend/src/Controls.tsx +++ b/tank-frontend/src/Controls.tsx @@ -22,16 +22,18 @@ export default function Controls({playerId}: { if (event.defaultPrevented) return; + const typeCode = type === 'input-on' ? 0x01 : 0x02; + const controls = { - 'ArrowLeft': 'left', - 'ArrowUp': 'up', - 'ArrowRight': 'right', - 'ArrowDown': 'down', - 'Space': 'shoot', - 'KeyW': 'up', - 'KeyA': 'left', - 'KeyS': 'down', - 'KeyD': 'right', + 'ArrowLeft': 0x03, + 'ArrowUp': 0x01, + 'ArrowRight': 0x04, + 'ArrowDown': 0x02, + 'Space': 0x05, + 'KeyW': 0x01, + 'KeyA': 0x03, + 'KeyS': 0x02, + 'KeyD': 0x04, }; // @ts-ignore @@ -39,7 +41,9 @@ export default function Controls({playerId}: { if (!value) return; - sendMessage(JSON.stringify({type, value})); + const message = new Uint8Array([typeCode, value]); + console.log('input', message); + sendMessage(message); }; useEffect(() => { @@ -51,7 +55,7 @@ export default function Controls({playerId}: { window.onkeydown = null; window.onkeyup = null; }; - }, []); + }, [sendMessage]); return