get player input
This commit is contained in:
parent
abdfdf2ec0
commit
8f281d65b2
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.Net.WebSockets;
|
||||||
using System.Threading;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,103 @@
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace TanksServer;
|
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.Net.WebSockets;
|
||||||
using System.Threading;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace TanksServer;
|
namespace TanksServer;
|
||||||
|
@ -34,8 +33,6 @@ internal abstract class EasyWebSocket
|
||||||
|
|
||||||
await ReceiveAsync(_buffer[..response.Count]);
|
await ReceiveAsync(_buffer[..response.Count]);
|
||||||
} while (_socket.State == WebSocketState.Open);
|
} while (_socket.State == WebSocketState.Open);
|
||||||
|
|
||||||
await CloseAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract Task ReceiveAsync(ArraySegment<byte> buffer);
|
protected abstract Task ReceiveAsync(ArraySegment<byte> buffer);
|
||||||
|
@ -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)
|
if (Interlocked.Exchange(ref _closed, 1) == 1)
|
||||||
return;
|
return;
|
||||||
_logger.LogDebug("closing socket");
|
_logger.LogDebug("closing socket");
|
||||||
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
|
await _socket.CloseAsync(status, description, CancellationToken.None);
|
||||||
await _readLoop;
|
await _readLoop;
|
||||||
await ClosingAsync();
|
await ClosingAsync();
|
||||||
_completionSource.SetResult();
|
_completionSource.SetResult();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using System.Threading;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace TanksServer;
|
namespace TanksServer;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
global using System;
|
global using System;
|
||||||
global using System.Collections.Generic;
|
global using System.Collections.Generic;
|
||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
|
global using System.Threading;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace TanksServer;
|
namespace TanksServer;
|
||||||
|
@ -8,11 +9,36 @@ internal sealed class PlayerService(ILogger<PlayerService> logger)
|
||||||
private readonly ConcurrentDictionary<string, Player> _players = new();
|
private readonly ConcurrentDictionary<string, Player> _players = new();
|
||||||
|
|
||||||
public Player GetOrAdd(string name) => _players.GetOrAdd(name, _ => new Player(name));
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class Player(string name)
|
foundPlayer = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class Player(string name)
|
||||||
{
|
{
|
||||||
public string Name => 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.IO;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@ -19,6 +18,7 @@ internal static class Program
|
||||||
|
|
||||||
var clientScreenServer = app.Services.GetRequiredService<ClientScreenServer>();
|
var clientScreenServer = app.Services.GetRequiredService<ClientScreenServer>();
|
||||||
var playerService = app.Services.GetRequiredService<PlayerService>();
|
var playerService = app.Services.GetRequiredService<PlayerService>();
|
||||||
|
var controlsServer = app.Services.GetRequiredService<ControlsServer>();
|
||||||
|
|
||||||
var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client"));
|
var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client"));
|
||||||
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider });
|
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider });
|
||||||
|
@ -38,17 +38,17 @@ internal static class Program
|
||||||
await clientScreenServer.HandleClient(ws);
|
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)
|
if (!context.WebSockets.IsWebSocketRequest)
|
||||||
{
|
return Results.BadRequest();
|
||||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
||||||
return;
|
if (!playerService.TryGet(playerId, out var player))
|
||||||
}
|
return Results.NotFound();
|
||||||
|
|
||||||
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
await clientScreenServer.HandleClient(ws);
|
await controlsServer.HandleClient(ws, player);
|
||||||
|
return Results.Empty;
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
|
@ -80,6 +80,8 @@ internal static class Program
|
||||||
builder.Services.AddHostedService<ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>());
|
builder.Services.AddHostedService<ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>());
|
||||||
builder.Services.AddSingleton<ITickStep, ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>());
|
builder.Services.AddSingleton<ITickStep, ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>());
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<ControlsServer>();
|
||||||
|
|
||||||
builder.Services.AddHostedService<GameTickService>();
|
builder.Services.AddHostedService<GameTickService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<PlayerService>();
|
builder.Services.AddSingleton<PlayerService>();
|
||||||
|
@ -87,6 +89,3 @@ internal static class Program
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonSerializable(typeof(Player))]
|
|
||||||
internal partial class AppSerializerContext: JsonSerializerContext;
|
|
||||||
|
|
|
@ -22,16 +22,18 @@ export default function Controls({playerId}: {
|
||||||
if (event.defaultPrevented)
|
if (event.defaultPrevented)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const typeCode = type === 'input-on' ? 0x01 : 0x02;
|
||||||
|
|
||||||
const controls = {
|
const controls = {
|
||||||
'ArrowLeft': 'left',
|
'ArrowLeft': 0x03,
|
||||||
'ArrowUp': 'up',
|
'ArrowUp': 0x01,
|
||||||
'ArrowRight': 'right',
|
'ArrowRight': 0x04,
|
||||||
'ArrowDown': 'down',
|
'ArrowDown': 0x02,
|
||||||
'Space': 'shoot',
|
'Space': 0x05,
|
||||||
'KeyW': 'up',
|
'KeyW': 0x01,
|
||||||
'KeyA': 'left',
|
'KeyA': 0x03,
|
||||||
'KeyS': 'down',
|
'KeyS': 0x02,
|
||||||
'KeyD': 'right',
|
'KeyD': 0x04,
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -39,7 +41,9 @@ export default function Controls({playerId}: {
|
||||||
if (!value)
|
if (!value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
sendMessage(JSON.stringify({type, value}));
|
const message = new Uint8Array([typeCode, value]);
|
||||||
|
console.log('input', message);
|
||||||
|
sendMessage(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -51,7 +55,7 @@ export default function Controls({playerId}: {
|
||||||
window.onkeydown = null;
|
window.onkeydown = null;
|
||||||
window.onkeyup = null;
|
window.onkeyup = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [sendMessage]);
|
||||||
|
|
||||||
return <div className="controls">
|
return <div className="controls">
|
||||||
<div className="control">
|
<div className="control">
|
||||||
|
|
Loading…
Reference in a new issue