#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());