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,88 @@
|
||||
using MrGameEng.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Net.Tests;
|
||||
|
||||
public class WebSocketProtocolTests
|
||||
{
|
||||
[Fact]
|
||||
public void AcceptKey_MatchesRfc6455Example()
|
||||
{
|
||||
// Пример рукопожатия прямо из RFC 6455, раздел 1.3.
|
||||
Assert.Equal(
|
||||
"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
|
||||
WebSocketProtocol.AcceptKey("dGhlIHNhbXBsZSBub25jZQ==")
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(125)]
|
||||
[InlineData(126)]
|
||||
[InlineData(70_000)]
|
||||
public void Frame_Roundtrips_Unmasked(int payloadLength)
|
||||
{
|
||||
var payload = MakePayload(payloadLength);
|
||||
var frame = WebSocketProtocol.EncodeFrame(payload, WebSocketOpcode.Binary);
|
||||
|
||||
using var stream = new MemoryStream(frame);
|
||||
Assert.True(
|
||||
WebSocketProtocol.TryReadFrame(stream, out var opcode, out var fin, out var decoded)
|
||||
);
|
||||
Assert.Equal(WebSocketOpcode.Binary, opcode);
|
||||
Assert.True(fin);
|
||||
Assert.Equal(payload, decoded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Frame_Roundtrips_Masked()
|
||||
{
|
||||
var payload = MakePayload(1000);
|
||||
var frame = WebSocketProtocol.EncodeFrame(
|
||||
payload,
|
||||
WebSocketOpcode.Binary,
|
||||
maskKey: [0x12, 0x34, 0x56, 0x78]
|
||||
);
|
||||
|
||||
using var stream = new MemoryStream(frame);
|
||||
Assert.True(WebSocketProtocol.TryReadFrame(stream, out _, out _, out var decoded));
|
||||
Assert.Equal(payload, decoded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryReadHandshakeKey_ExtractsHeader()
|
||||
{
|
||||
var request =
|
||||
"GET /chat HTTP/1.1\r\n"
|
||||
+ "Host: localhost\r\n"
|
||||
+ "Upgrade: websocket\r\n"
|
||||
+ "Connection: Upgrade\r\n"
|
||||
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
+ "Sec-WebSocket-Version: 13\r\n"
|
||||
+ "\r\n";
|
||||
using var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(request));
|
||||
|
||||
Assert.True(WebSocketProtocol.TryReadHandshakeKey(stream, out var key));
|
||||
Assert.Equal("dGhlIHNhbXBsZSBub25jZQ==", key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryReadFrame_TruncatedStream_ReturnsFalse()
|
||||
{
|
||||
var frame = WebSocketProtocol.EncodeFrame(MakePayload(100), WebSocketOpcode.Binary);
|
||||
using var stream = new MemoryStream(frame, 0, frame.Length - 10);
|
||||
|
||||
Assert.False(WebSocketProtocol.TryReadFrame(stream, out _, out _, out _));
|
||||
}
|
||||
|
||||
private static byte[] MakePayload(int length)
|
||||
{
|
||||
var payload = new byte[length];
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
payload[i] = (byte)(i * 31);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user