get player input
This commit is contained in:
		
							parent
							
								
									abdfdf2ec0
								
							
						
					
					
						commit
						8f281d65b2
					
				
					 9 changed files with 165 additions and 34 deletions
				
			
		
							
								
								
									
										6
									
								
								TanksServer/AppSerializerContext.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								TanksServer/AppSerializerContext.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| namespace TanksServer; | ||||
| 
 | ||||
| [JsonSerializable(typeof(Player))] | ||||
| internal partial class AppSerializerContext: JsonSerializerContext; | ||||
|  | @ -1,5 +1,4 @@ | |||
| using System.Net.WebSockets; | ||||
| using System.Threading; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<ControlsServer> logger, ILoggerFactory loggerFactory) | ||||
|     : IHostedLifecycleService | ||||
| { | ||||
|      | ||||
|     private readonly List<ControlsServerConnection> _connections = new(); | ||||
| 
 | ||||
|     public Task HandleClient(WebSocket ws, Player player) | ||||
|     { | ||||
|         logger.LogDebug("control client connected {}", player.Id); | ||||
|         var clientLogger = loggerFactory.CreateLogger<ControlsServerConnection>(); | ||||
|         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<byte> 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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<byte> 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(); | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| using System.Threading; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| 
 | ||||
| namespace TanksServer; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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<PlayerService> logger) | |||
|     private readonly ConcurrentDictionary<string, Player> _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; } | ||||
| } | ||||
|  |  | |||
|  | @ -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<ClientScreenServer>(); | ||||
|         var playerService = app.Services.GetRequiredService<PlayerService>(); | ||||
|         var controlsServer = app.Services.GetRequiredService<ControlsServer>(); | ||||
| 
 | ||||
|         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<ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>()); | ||||
|         builder.Services.AddSingleton<ITickStep, ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<ControlsServer>(); | ||||
| 
 | ||||
|         builder.Services.AddHostedService<GameTickService>(); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<PlayerService>(); | ||||
|  | @ -87,6 +89,3 @@ internal static class Program | |||
|         return builder.Build(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| [JsonSerializable(typeof(Player))] | ||||
| internal partial class AppSerializerContext: JsonSerializerContext; | ||||
|  |  | |||
|  | @ -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 <div className="controls"> | ||||
|         <div className="control"> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter