Net: heartbeat liveness, protocol version, message cap, replication reset
CI / build-test (push) Successful in 1m12s
CI / build-test (push) Successful in 1m12s
Robustness pass over MrGameEng.Net: - WebSocketServer heartbeats each connection (configurable interval/timeout) and drops peers idle past the timeout — detects half-open TCP that vanished without a close frame. Tracks last-activity per connection. - Reassembled messages capped (server and client) so a peer cannot exhaust memory with an oversized fragmented message. - Replication snapshots carry a protocol-version byte; a client receiving a mismatched version drops the message instead of decoding garbage, and a truncated snapshot is ignored without throwing. - ReplicationClient.Clear() deletes all replicated entities, so clients can wipe stale state before reconnecting. Tests: heartbeat healthy-survives / silent-peer-dropped, protocol-version mismatch, truncated snapshot, replication clear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d360093be1
commit
f382fc98ea
@@ -103,8 +103,8 @@ public class ReplicationTests
|
||||
server.Send([connection]);
|
||||
|
||||
Assert.True(connection.Sent.TryDequeue(out var delta));
|
||||
// Запись: type(1) + count(4) + netId(4) + op(1) + mask(4) + TestPosition(8) — без TestHealth.
|
||||
Assert.Equal(22, delta!.Length);
|
||||
// Запись: type(1) + version(1) + count(4) + netId(4) + op(1) + mask(4) + TestPosition(8) — без TestHealth.
|
||||
Assert.Equal(23, delta!.Length);
|
||||
|
||||
client.Apply(delta);
|
||||
var replicated = FindByNetId(clientStore, 1);
|
||||
@@ -185,6 +185,81 @@ public class ReplicationTests
|
||||
Assert.Equal(1, spawns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_DeletesEveryReplicatedEntity()
|
||||
{
|
||||
var serverStore = new EntityStore();
|
||||
var clientStore = new EntityStore();
|
||||
var server = new ReplicationServer(MakeSchema(), serverStore);
|
||||
var client = new ReplicationClient(MakeSchema(), clientStore);
|
||||
var connection = new FakeConnection();
|
||||
serverStore.CreateEntity(
|
||||
new NetId { Value = server.NextNetId() },
|
||||
new TestPosition { X = 1f, Y = 2f }
|
||||
);
|
||||
serverStore.CreateEntity(
|
||||
new NetId { Value = server.NextNetId() },
|
||||
new TestPosition { X = 3f, Y = 4f }
|
||||
);
|
||||
server.Send([connection]);
|
||||
client.Pump(connection);
|
||||
Assert.Equal(2, client.EntityCount);
|
||||
|
||||
client.Clear();
|
||||
|
||||
Assert.Equal(0, client.EntityCount);
|
||||
foreach (var entity in clientStore.Entities)
|
||||
{
|
||||
Assert.False(entity.HasComponent<NetId>());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MismatchedProtocolVersion_IsDropped()
|
||||
{
|
||||
var serverStore = new EntityStore();
|
||||
var clientStore = new EntityStore();
|
||||
var server = new ReplicationServer(MakeSchema(), serverStore);
|
||||
var client = new ReplicationClient(MakeSchema(), clientStore);
|
||||
var connection = new FakeConnection();
|
||||
serverStore.CreateEntity(
|
||||
new NetId { Value = server.NextNetId() },
|
||||
new TestPosition { X = 1f, Y = 2f }
|
||||
);
|
||||
server.Send([connection]);
|
||||
Assert.True(connection.Sent.TryDequeue(out var snapshot));
|
||||
|
||||
// Портим байт версии (индекс 1: type=0, version=1) — клиент обязан отбросить снапшот целиком.
|
||||
snapshot![1] = 0xFF;
|
||||
client.Apply(snapshot);
|
||||
|
||||
Assert.Equal(0, client.EntityCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TruncatedSnapshot_IsIgnoredWithoutThrowing()
|
||||
{
|
||||
var serverStore = new EntityStore();
|
||||
var clientStore = new EntityStore();
|
||||
var server = new ReplicationServer(MakeSchema(), serverStore);
|
||||
var client = new ReplicationClient(MakeSchema(), clientStore);
|
||||
var connection = new FakeConnection();
|
||||
serverStore.CreateEntity(
|
||||
new NetId { Value = server.NextNetId() },
|
||||
new TestPosition { X = 5f, Y = 6f },
|
||||
new TestHealth { Value = 9 }
|
||||
);
|
||||
server.Send([connection]);
|
||||
Assert.True(connection.Sent.TryDequeue(out var snapshot));
|
||||
|
||||
// Режем хвост: заголовок и счётчик целы, но данные компонентов оборваны.
|
||||
var truncated = snapshot!.AsSpan(0, snapshot.Length - 6).ToArray();
|
||||
client.Apply(truncated); // не должно бросить
|
||||
|
||||
// Записи могли частично примениться, но клиент остался живым и консистентным.
|
||||
Assert.True(client.EntityCount <= 1);
|
||||
}
|
||||
|
||||
private static Entity FindByNetId(EntityStore store, int netId)
|
||||
{
|
||||
foreach (var entity in store.Entities)
|
||||
|
||||
Reference in New Issue
Block a user