From 5c104046b42ef42c8c00b9b73554dd0b65120662 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 13:00:58 +0200 Subject: [PATCH] test: cover Lcg and buildTerrain --- tests/testNetServer.cpp | 160 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/testNetServer.cpp diff --git a/tests/testNetServer.cpp b/tests/testNetServer.cpp new file mode 100644 index 0000000..a1ee6c8 --- /dev/null +++ b/tests/testNetServer.cpp @@ -0,0 +1,160 @@ +#include "testHarness.h" +#include "vf/NetServer.h" + +#include +#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)); } +}; + +void buildTerrain(World& world, int chunkMin, int chunkMax) { + Lcg rng(0x1a2b3c4d); + const Block palette[4] = {Block::Stone, Block::Dirt, Block::Grass, Block::Sand}; + for (int cx = chunkMin; cx <= chunkMax; ++cx) { + for (int cz = chunkMin; cz <= chunkMax; ++cz) { + Chunk& chunk = world.ensureChunk(Vec3i{cx, 0, cz}); + for (int x = 0; x < chunkSize; ++x) + for (int z = 0; z < chunkSize; ++z) + for (int y = 0; y < 6; ++y) chunk.set(x, y, z, palette[rng.range(4)]); + } + } +} + +// The far end of one slot: an independent transport and session on endpoint 1 +// of that slot's link, which is exactly what a real remote player would be. +struct RemoteClient { + World world; + std::unique_ptr transport; + std::unique_ptr session; + + RemoteClient(NetServer& server, int slot, const TransportConfig& transportConfig, + const SessionConfig& sessionConfig, const std::string& name) { + transport.reset(new Transport(*server.linkFor(slot), 1, transportConfig)); + session.reset(new NetSession(*transport, NetSession::Role::Client, sessionConfig)); + session->attachWorld(world); + session->beginHandshake(name, 0x1000 + static_cast(slot), 0x2000); + } + + void step(uint64_t tick) { + session->receive(tick); + session->send(tick); + } +}; + +SessionConfig clientConfigFor(const ServerConfig& config) { + SessionConfig sessionConfig; + sessionConfig.protocolVersion = config.protocolVersion; + sessionConfig.viewRadius = config.viewRadius; + sessionConfig.keepAliveIntervalTicks = config.keepAliveIntervalTicks; + sessionConfig.keepAliveTimeoutTicks = config.keepAliveTimeoutTicks; + return sessionConfig; +} + +ServerConfig quietServerConfig() { + ServerConfig config; + config.maxClients = 4; + config.viewRadius = 1; + config.chunksPerClientPerTick = 4; + config.keepAliveIntervalTicks = 10; + config.keepAliveTimeoutTicks = 1000000; + config.link.latencyTicks = 1; + return config; +} + +// Seeds a slot's view before login; the first snapshot burst then goes out +// around this spot instead of around the origin. +void placeAt(NetServer& server, int slot, float x, float y, float z) { + server.sessionFor(slot)->view().position = Vec3{x, y, z}; +} + +} // namespace + +VF_TEST(twoClientsStandingApartReceiveDisjointSetsOfChunks) { + ServerConfig config = quietServerConfig(); + World world; + NetServer server(world, config); + buildTerrain(world, -6, 6); + + int slotNear = server.acceptClient(); + int slotFar = server.acceptClient(); + CHECK_EQ(slotNear, 0); + CHECK_EQ(slotFar, 1); + placeAt(server, slotNear, 8.0f, 8.0f, 8.0f); + placeAt(server, slotFar, 72.0f, 8.0f, 72.0f); + + TransportConfig transportConfig = config.transport; + SessionConfig sessionConfig = clientConfigFor(config); + RemoteClient near(server, slotNear, transportConfig, sessionConfig, "near"); + RemoteClient far(server, slotFar, transportConfig, sessionConfig, "far"); + + for (uint64_t tick = 0; tick < 60; ++tick) { + server.update(tick); + near.step(tick); + far.step(tick); + } + + CHECK(near.session->phase() == SessionPhase::Play); + CHECK(far.session->phase() == SessionPhase::Play); + + const ClientView& nearView = server.sessionFor(slotNear)->view(); + const ClientView& farView = server.sessionFor(slotFar)->view(); + CHECK_EQ(nearView.sent.size(), static_cast(5)); + CHECK_EQ(farView.sent.size(), static_cast(5)); + + std::size_t shared = 0; + for (const Vec3i& coord : nearView.sent) + if (farView.sent.count(coord) != 0) ++shared; + CHECK_EQ(shared, static_cast(0)); + + CHECK(nearView.hasChunk(Vec3i{0, 0, 0})); + CHECK(farView.hasChunk(Vec3i{4, 0, 4})); + CHECK(!nearView.hasChunk(Vec3i{4, 0, 4})); + CHECK(!farView.hasChunk(Vec3i{0, 0, 0})); + + CHECK_EQ(near.world.chunkCount(), static_cast(5)); + CHECK_EQ(far.world.chunkCount(), static_cast(5)); + CHECK(near.world.chunkAt(Vec3i{4, 0, 4}) == nullptr); + CHECK(far.world.chunkAt(Vec3i{0, 0, 0}) == nullptr); + CHECK_EQ(server.stats().chunksSent, static_cast(10)); +} + +VF_TEST(aBlockChangeInsideBothViewsReachesEachClientExactlyOnce) { + ServerConfig config = quietServerConfig(); + config.viewRadius = 2; + World world; + NetServer server(world, config); + buildTerrain(world, -4, 4); + + int slotLeft = server.acceptClient(); + int slotRight = server.acceptClient(); + // Both stand two chunks apart, so chunk (1,0,0) is inside both spheres. + placeAt(server, slotLeft, 8.0f, 8.0f, 8.0f); + placeAt(server, slotRight, 40.0f, 8.0f, 8.0f); + + TransportConfig transportConfig = config.transport; + SessionConfig sessionConfig = clientConfigFor(config); + sessionConfig.viewRadius = 2; + RemoteClient left(server, slotLeft, transportConfig, sessionConfig, "left"); + RemoteClient right(server, slotRight, transportConfig, sessionConfig, "right"); + + for (uint64_t tick = 0; tick < 80; ++tick) { + server.update(tick); + left.step(tick); + right.step(tick); + } + CHECK(server.sessionFor(slotLeft)->view().hasChunk(Vec3i{1, 0, 0})); + CHECK(server.sessionFor(slotRight)->view().hasChunk(Vec3i{1, 0, 0})); +