From 5a665a3dd95f725dbd35c958c98d5f65824697b3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 15:08:59 +0200 Subject: [PATCH] test: cover testNetSession --- tests/testNetSession.cpp | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/tests/testNetSession.cpp b/tests/testNetSession.cpp index 698f738..85c1342 100644 --- a/tests/testNetSession.cpp +++ b/tests/testNetSession.cpp @@ -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(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(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(13)); + CHECK_EQ(pair.client.stats().chunksApplied, static_cast(13)); + CHECK_EQ(pair.clientWorld.chunkCount(), static_cast(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(3)); + CHECK_EQ(pair.server.stats().multiBlockChangesSent, static_cast(0)); + CHECK_EQ(pair.client.stats().blockChangesApplied, static_cast(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(3)); + CHECK_EQ(pair.server.stats().multiBlockChangesSent, static_cast(1)); + CHECK_EQ(pair.client.stats().multiBlockChangesApplied, static_cast(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); +