test: cover testNetSession

This commit is contained in:
dualfroz committed 2026-09-12 15:08:59 +02:00
1 parent 0fce9668ff
commit 5a665a3dd9
1 file changed
+157
+157
View File
@@ -153,3 +153,160 @@ VF_TEST(aPlayPacketArrivingBeforeLoginEndsTheSessionWithAReasonInsteadOfCrashing
TransportConfig transportConfig;
Pair pair(quality, transportConfig, quietConfig());
// A well-formed PlayerState sent straight at a server still in handshake.
PlayerState early;
early.x = 12.0f;
early.y = 40.0f;
early.z = -3.0f;
early.health = 20.0f;
PacketWriter body;
early.encode(body);
ByteWriter stream;
writeFramed(stream, PacketId::PlayerState, body);
pair.clientTransport.sendReliable(stream.bytes());
for (uint64_t tick = 0; tick < 10; ++tick) pair.step(tick);
CHECK(pair.server.phase() == SessionPhase::Disconnected);
CHECK(pair.server.disconnectReason() == "packet out of phase");
CHECK_EQ(pair.server.stats().packetsRejected, static_cast<uint64_t>(1));
CHECK(!pair.server.timedOut());
CHECK_EQ(pair.server.view().position.x, 0.0f);
}
VF_TEST(aJoinGameArrivingAfterTheClientIsAlreadyPlayingIsRejected) {
LinkQuality quality;
quality.latencyTicks = 1;
TransportConfig transportConfig;
Pair pair(quality, transportConfig, quietConfig());
pair.client.beginHandshake("vulp", 1, 2);
for (uint64_t tick = 0; tick < 10; ++tick) pair.step(tick);
CHECK(pair.client.phase() == SessionPhase::Play);
JoinGame again;
again.entityId = 99;
again.levelType = "default";
PacketWriter body;
again.encode(body);
ByteWriter stream;
writeFramed(stream, PacketId::JoinGame, body);
pair.serverTransport.sendReliable(stream.bytes());
for (uint64_t tick = 10; tick < 20; ++tick) pair.step(tick);
CHECK(pair.client.phase() == SessionPhase::Disconnected);
CHECK(pair.client.disconnectReason() == "packet out of phase");
CHECK_EQ(pair.client.entityId(), 7);
}
VF_TEST(theServerDisconnectsAClientThatStopsAnsweringKeepAlive) {
LinkQuality quality;
quality.latencyTicks = 1;
TransportConfig transportConfig;
SessionConfig config = quietConfig();
config.keepAliveIntervalTicks = 5;
config.keepAliveTimeoutTicks = 30;
Pair pair(quality, transportConfig, config);
pair.client.beginHandshake("vulp", 1, 2);
for (uint64_t tick = 0; tick < 60; ++tick) pair.step(tick);
CHECK(pair.server.phase() == SessionPhase::Play);
CHECK(pair.server.stats().keepAlivesSent > 5u);
CHECK(!pair.server.timedOut());
// The client goes silent: the server keeps ticking on its own.
for (uint64_t tick = 60; tick < 130; ++tick) {
pair.server.receive(tick);
pair.server.send(tick);
}
CHECK(pair.server.phase() == SessionPhase::Disconnected);
CHECK(pair.server.timedOut());
CHECK(pair.server.disconnectReason() == "keep alive timeout");
}
VF_TEST(chunksAreStreamedNearestFirstAndNeverMoreThanTheTickBudget) {
LinkQuality quality;
quality.latencyTicks = 1;
TransportConfig transportConfig;
SessionConfig config = quietConfig();
config.chunksPerTick = 2;
Pair pair(quality, transportConfig, config);
buildTerrain(pair.serverWorld, 3);
pair.server.view().position = Vec3{8.0f, 8.0f, 8.0f};
pair.client.beginHandshake("vulp", 1, 2);
for (uint64_t tick = 0; tick < 6; ++tick) pair.step(tick);
CHECK(pair.server.phase() == SessionPhase::Play);
uint64_t afterFirstTicks = pair.server.stats().chunksSent;
CHECK(afterFirstTicks > 0u);
CHECK(afterFirstTicks <= 2u * 6u);
// The chunk the player stands in must be among the first handful sent.
CHECK(pair.server.view().hasChunk(Vec3i{0, 0, 0}));
CHECK(!pair.server.view().hasChunk(Vec3i{2, 0, 2}));
for (uint64_t tick = 6; tick < 60; ++tick) pair.step(tick);
CHECK_EQ(pair.server.view().pendingCount(), static_cast<std::size_t>(0));
// The sphere of radius 2 holds 33 chunk slots, only 13 of which exist in
// the y = 0 plane, so exactly those get snapshots.
CHECK_EQ(pair.server.stats().chunksSent, static_cast<uint64_t>(13));
CHECK_EQ(pair.client.stats().chunksApplied, static_cast<uint64_t>(13));
CHECK_EQ(pair.clientWorld.chunkCount(), static_cast<std::size_t>(13));
}
VF_TEST(manyChangesInOneChunkBecomeAMultiBlockChangeAndFewBecomeSingleBlockChanges) {
LinkQuality quality;
quality.latencyTicks = 1;
TransportConfig transportConfig;
SessionConfig config = quietConfig();
config.multiBlockThreshold = 4;
Pair pair(quality, transportConfig, config);
buildTerrain(pair.serverWorld, 1);
pair.client.beginHandshake("vulp", 1, 2);
for (uint64_t tick = 0; tick < 40; ++tick) pair.step(tick);
CHECK(pair.server.view().hasChunk(Vec3i{0, 0, 0}));
for (int i = 0; i < 3; ++i) pair.edit(i, 14, 0, Block::Glowstone);
pair.step(40);
pair.clearDirty();
for (uint64_t tick = 41; tick < 50; ++tick) pair.step(tick);
CHECK_EQ(pair.server.stats().blockChangesSent, static_cast<uint64_t>(3));
CHECK_EQ(pair.server.stats().multiBlockChangesSent, static_cast<uint64_t>(0));
CHECK_EQ(pair.client.stats().blockChangesApplied, static_cast<uint64_t>(3));
for (int i = 0; i < 9; ++i) pair.edit(i, 15, 0, Block::Obsidian);
pair.step(50);
pair.clearDirty();
for (uint64_t tick = 51; tick < 60; ++tick) pair.step(tick);
CHECK_EQ(pair.server.stats().blockChangesSent, static_cast<uint64_t>(3));
CHECK_EQ(pair.server.stats().multiBlockChangesSent, static_cast<uint64_t>(1));
CHECK_EQ(pair.client.stats().multiBlockChangesApplied, static_cast<uint64_t>(1));
for (int i = 0; i < 9; ++i) CHECK(pair.clientWorld.blockAt(i, 15, 0) == Block::Obsidian);
for (int i = 0; i < 3; ++i) CHECK(pair.clientWorld.blockAt(i, 14, 0) == Block::Glowstone);
}
VF_TEST(theClientWorldMirrorsTheServerByteForByteAcrossAnUnreliableLink) {
LinkQuality quality;
quality.lossPercent = 30;
quality.duplicatePercent = 10;
quality.reorderPercent = 25;
quality.reorderExtraTicks = 4;
quality.latencyTicks = 2;
quality.seed = 0xabcdef0123456789ull;
TransportConfig transportConfig;
transportConfig.mtu = 512;
transportConfig.retransmitTimeoutTicks = 10;
transportConfig.sendWindow = 24;
SessionConfig config = quietConfig();
config.chunksPerTick = 2;
config.multiBlockThreshold = 6;
Pair pair(quality, transportConfig, config);
buildTerrain(pair.serverWorld, 2);
pair.server.view().position = Vec3{8.0f, 8.0f, 8.0f};
pair.client.beginHandshake("vulp", 1, 2);