Add MrGameEng.Net: WebSocket transport + delta component replication
CI / build-test (push) Successful in 1m16s
CI / build-test (push) Successful in 1m16s
Browsers can't speak UDP, so WebSocket is the engine's one transport. The server side is a dependency-free RFC 6455 implementation over TcpListener (handshake, frame codec with masking and fragmentation, ping/pong — unit-tested against the RFC example vectors); the client wraps ClientWebSocket, which works on desktop and maps to the browser WebSocket in Blazor WASM. Both sit behind the poll-based INetConnection so simulation systems drain messages from their own thread; client sends are chained fire-and-forget (no blocking — wasm-safe). Replication is server-authoritative: games register unmanaged component types in a ReplicationSchema (same order both sides, up to 32 types), ReplicationServer snapshots entities carrying NetId once per send and ships each connection only the components that changed since its last snapshot — a reliable ordered transport needs no acks for deltas. New connections receive the full state through the same path; despawns are tracked by set difference. ReplicationClient applies snapshots to a local EntityStore and raises EntitySpawned so the game can decorate replicated entities with presentation components. Covered by 16 tests including a real loopback exchange between WebSocketClient and WebSocketServer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
10898b08a0
commit
3438ed77f6
@@ -0,0 +1,93 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using MrGameEng.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Net.Tests;
|
||||
|
||||
public class WebSocketLoopbackTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ClientAndServer_ExchangeBinaryMessages_OverLoopback()
|
||||
{
|
||||
var port = FreePort();
|
||||
using var server = new WebSocketServer(port);
|
||||
server.Start();
|
||||
|
||||
using var client = await WebSocketClient.ConnectAsync(
|
||||
new Uri($"ws://localhost:{port}/"),
|
||||
new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token
|
||||
);
|
||||
|
||||
var connection = await WaitFor(
|
||||
() => server.TryAcceptConnection(out var c) ? c : null,
|
||||
"server accept"
|
||||
);
|
||||
|
||||
// Сервер → клиент.
|
||||
connection.Send([1, 2, 3, 250]);
|
||||
var received = await WaitFor(
|
||||
() => client.TryReceive(out var m) ? m : null,
|
||||
"client receive"
|
||||
);
|
||||
Assert.Equal(new byte[] { 1, 2, 3, 250 }, received);
|
||||
|
||||
// Клиент → сервер (ClientWebSocket маскирует кадры — сервер обязан размаскировать).
|
||||
client.Send([9, 8, 7]);
|
||||
var echoed = await WaitFor(
|
||||
() => connection.TryReceive(out var m) ? m : null,
|
||||
"server receive"
|
||||
);
|
||||
Assert.Equal(new byte[] { 9, 8, 7 }, echoed);
|
||||
|
||||
Assert.True(connection.IsOpen);
|
||||
Assert.True(client.IsOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClosingTheClient_ClosesTheServerConnection()
|
||||
{
|
||||
var port = FreePort();
|
||||
using var server = new WebSocketServer(port);
|
||||
server.Start();
|
||||
|
||||
var client = await WebSocketClient.ConnectAsync(
|
||||
new Uri($"ws://localhost:{port}/"),
|
||||
new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token
|
||||
);
|
||||
var connection = await WaitFor(
|
||||
() => server.TryAcceptConnection(out var c) ? c : null,
|
||||
"server accept"
|
||||
);
|
||||
|
||||
client.Close();
|
||||
|
||||
await WaitFor(() => connection.IsOpen ? null : "closed", "server-side close");
|
||||
Assert.False(connection.IsOpen);
|
||||
}
|
||||
|
||||
private static async Task<T> WaitFor<T>(Func<T?> poll, string what)
|
||||
where T : class
|
||||
{
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
if (poll() is { } result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
await Task.Delay(25);
|
||||
}
|
||||
|
||||
throw new TimeoutException($"Timed out waiting for {what}.");
|
||||
}
|
||||
|
||||
private static int FreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user