From 0fce9668ffb5503967e6c7314f1c62856bce81d1 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 14:52:14 +0200 Subject: [PATCH] test: extend coverage of Lcg and buildTerrain --- tests/testNetSession.cpp | 155 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 tests/testNetSession.cpp diff --git a/tests/testNetSession.cpp b/tests/testNetSession.cpp new file mode 100644 index 0000000..698f738 --- /dev/null +++ b/tests/testNetSession.cpp @@ -0,0 +1,155 @@ +#include "testHarness.h" +#include "vf/NetSession.h" + +#include +#include +#include + +using namespace vf; + +namespace { + +struct Lcg { + uint64_t state; + explicit Lcg(uint64_t seed) : state(seed | 1ull) {} + uint32_t next() { + state = state * 6364136223846793005ull + 1442695040888963407ull; + return static_cast(state >> 32); + } + int range(int n) { return static_cast(next() % static_cast(n)); } +}; + +// Deterministic terrain in the chunk plane y = 0, so both ends have something +// substantial to disagree about if replication is wrong. +void buildTerrain(World& world, int columnsRadius) { + Lcg rng(0x51ed270b); + const Block palette[5] = {Block::Stone, Block::Dirt, Block::Grass, Block::Sand, Block::Gravel}; + for (int cx = -columnsRadius; cx <= columnsRadius; ++cx) { + for (int cz = -columnsRadius; cz <= columnsRadius; ++cz) { + Chunk& chunk = world.ensureChunk(Vec3i{cx, 0, cz}); + for (int x = 0; x < chunkSize; ++x) { + for (int z = 0; z < chunkSize; ++z) { + int height = 4 + rng.range(9); + for (int y = 0; y < height; ++y) + chunk.set(x, y, z, palette[rng.range(5)]); + } + } + } + } +} + +// Canonical over the whole world: chunk coordinates are sorted before hashing, +// so the digest does not inherit the hash map's iteration order. +uint64_t worldDigest(const World& world, const std::vector& coords) { + ByteWriter stream; + for (const Vec3i& coord : coords) { + const Chunk* chunk = world.chunkAt(coord); + if (!chunk) continue; + stream.putU32(static_cast(coord.x)); + stream.putU32(static_cast(coord.y)); + stream.putU32(static_cast(coord.z)); + for (Block block : chunk->raw()) stream.putU8(static_cast(block)); + } + return fnv1a64(stream.bytes().data(), stream.bytes().size()); +} + +std::vector sortedCoords(const std::unordered_set& set) { + std::vector coords(set.begin(), set.end()); + for (std::size_t i = 1; i < coords.size(); ++i) { + Vec3i key = coords[i]; + std::size_t j = i; + while (j > 0 && (coords[j - 1].y > key.y || + (coords[j - 1].y == key.y && + (coords[j - 1].z > key.z || + (coords[j - 1].z == key.z && coords[j - 1].x > key.x))))) { + coords[j] = coords[j - 1]; + --j; + } + coords[j] = key; + } + return coords; +} + +// Both ends of one connection, wired through a single loopback link. +struct Pair { + LoopbackLink link; + Transport serverTransport; + Transport clientTransport; + NetSession server; + NetSession client; + World serverWorld; + World clientWorld; + ServerChunkSync sync; + + Pair(const LinkQuality& quality, const TransportConfig& transportConfig, + const SessionConfig& sessionConfig) + : link(quality), + serverTransport(link, 0, transportConfig), + clientTransport(link, 1, transportConfig), + server(serverTransport, NetSession::Role::Server, sessionConfig), + client(clientTransport, NetSession::Role::Client, sessionConfig) { + server.attachWorld(serverWorld); + server.attachChunkSync(sync); + server.setEntityId(7); + client.attachWorld(clientWorld); + } + + void step(uint64_t tick) { + server.receive(tick); + server.send(tick); + client.receive(tick); + client.send(tick); + } + + // Server edit: authoritative world, dirty feed, and this session's own + // per-chunk work list all move together. + void edit(int gx, int gy, int gz, Block block) { + serverWorld.setBlock(gx, gy, gz, block); + Vec3i coord{floorDiv(gx, chunkSize), floorDiv(gy, chunkSize), floorDiv(gz, chunkSize)}; + sync.markDirty(coord, floorMod(gx, chunkSize), floorMod(gy, chunkSize), + floorMod(gz, chunkSize)); + server.markChunkDirty(coord); + } + + void clearDirty() { sync.clearAll(); } +}; + +SessionConfig quietConfig() { + SessionConfig config; + config.viewRadius = 2; + config.chunksPerTick = 3; + config.keepAliveIntervalTicks = 20; + config.keepAliveTimeoutTicks = 1000000; + return config; +} + +} // namespace + +VF_TEST(handshakeAndLoginCarryBothEndsOfTheSessionIntoThePlayPhase) { + LinkQuality quality; + quality.latencyTicks = 1; + TransportConfig transportConfig; + Pair pair(quality, transportConfig, quietConfig()); + + CHECK(pair.server.phase() == SessionPhase::Handshake); + CHECK(pair.client.phase() == SessionPhase::Handshake); + + pair.client.beginHandshake("vulp", 0x1111222233334444ull, 0x5555666677778888ull); + CHECK(pair.client.phase() == SessionPhase::Login); + + for (uint64_t tick = 0; tick < 12; ++tick) pair.step(tick); + + CHECK(pair.server.phase() == SessionPhase::Play); + CHECK(pair.client.phase() == SessionPhase::Play); + CHECK(pair.server.playerName() == "vulp"); + CHECK_EQ(pair.server.uuidHigh(), 0x1111222233334444ull); + CHECK_EQ(pair.client.entityId(), 7); + CHECK(pair.server.disconnectReason().empty()); +} + +VF_TEST(aPlayPacketArrivingBeforeLoginEndsTheSessionWithAReasonInsteadOfCrashing) { + LinkQuality quality; + quality.latencyTicks = 1; + TransportConfig transportConfig; + Pair pair(quality, transportConfig, quietConfig()); +