diff --git a/tests/testNetServer.cpp b/tests/testNetServer.cpp index ed3a364..a8ff1bb 100644 --- a/tests/testNetServer.cpp +++ b/tests/testNetServer.cpp @@ -320,3 +320,192 @@ VF_TEST(anUnbudgetedServerSendsTheSameChunksInFarFewerTicks) { CHECK(ticksNeeded[1] < 20u); } +VF_TEST(theServerRefusesConnectionsOnceEverySlotIsTaken) { + ServerConfig config = quietServerConfig(); + config.maxClients = 2; + World world; + NetServer server(world, config); + + CHECK_EQ(server.slotCount(), static_cast(2)); + CHECK_EQ(server.acceptClient(), 0); + CHECK_EQ(server.acceptClient(), 1); + CHECK_EQ(server.acceptClient(), -1); + CHECK_EQ(server.acceptClient(), -1); + CHECK_EQ(server.stats().rejectedConnections, static_cast(2)); + CHECK_EQ(server.stats().acceptedConnections, static_cast(2)); + CHECK_EQ(server.activeClients(), 2u); + CHECK_EQ(server.linkFor(2), nullptr); + CHECK_EQ(server.sessionFor(-1), nullptr); + + server.dropClient(0); + CHECK_EQ(server.activeClients(), 1u); + CHECK_EQ(server.acceptClient(), 0); + CHECK_EQ(server.activeClients(), 2u); +} + +VF_TEST(aSilentClientTimesOutAndItsSlotComesBackForReuse) { + ServerConfig config = quietServerConfig(); + config.maxClients = 1; + config.keepAliveIntervalTicks = 5; + config.keepAliveTimeoutTicks = 40; + World world; + NetServer server(world, config); + buildTerrain(world, -2, 2); + + int slot = server.acceptClient(); + TransportConfig transportConfig = config.transport; + { + RemoteClient leaving(server, slot, transportConfig, clientConfigFor(config), "leaving"); + for (uint64_t tick = 0; tick < 30; ++tick) { + server.update(tick); + leaving.step(tick); + } + CHECK(server.sessionFor(slot) != nullptr); + CHECK(server.sessionFor(slot)->phase() == SessionPhase::Play); + } + + for (uint64_t tick = 30; tick < 200; ++tick) server.update(tick); + + CHECK_EQ(server.activeClients(), 0u); + CHECK_EQ(server.stats().timeouts, static_cast(1)); + CHECK_EQ(server.stats().connectedClients, 0u); + CHECK_EQ(server.acceptClient(), 0); + CHECK_EQ(server.activeClients(), 1u); +} + +VF_TEST(entityMovesGoOnlyToTheClientsWhoseViewHoldsTheEntity) { + ServerConfig config = quietServerConfig(); + World world; + NetServer server(world, config); + buildTerrain(world, -6, 6); + + int slotNear = server.acceptClient(); + int slotFar = server.acceptClient(); + 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 < 40; ++tick) { + server.update(tick); + near.step(tick); + far.step(tick); + } + + for (uint64_t tick = 40; tick < 70; ++tick) { + server.moveEntity(31, Vec3{9.0f, 5.0f, 9.0f}, 1.0f, 0.0f, true); + server.update(tick); + near.step(tick); + far.step(tick); + } + + EntityMove seen; + CHECK(near.session->entityMove(31, seen)); + CHECK_NEAR(seen.x, 9.0f, 0.001f); + CHECK_EQ(far.session->trackedEntityCount(), static_cast(0)); + CHECK(far.session->stats().entityMovesApplied == 0u); +} + +VF_TEST(serverStatsAccountForClientsBytesChunksAndTheOutgoingQueue) { + ServerConfig config = quietServerConfig(); + config.viewRadius = 1; + config.chunksPerClientPerTick = 1; + World world; + NetServer server(world, config); + buildTerrain(world, -2, 2); + + CHECK_EQ(server.stats().connectedClients, 0u); + int slot = server.acceptClient(); + placeAt(server, slot, 8.0f, 8.0f, 8.0f); + TransportConfig transportConfig = config.transport; + RemoteClient only(server, slot, transportConfig, clientConfigFor(config), "counted"); + + uint64_t bytesSeenInAnyTick = 0; + for (uint64_t tick = 0; tick < 40; ++tick) { + server.update(tick); + only.step(tick); + bytesSeenInAnyTick += server.stats().bytesSentThisTick; + } + + CHECK_EQ(server.stats().connectedClients, 1u); + CHECK_EQ(server.stats().chunksSent, static_cast(5)); + CHECK_EQ(server.stats().totalBytesSent, bytesSeenInAnyTick); + CHECK(server.stats().totalBytesSent > 500u); + CHECK_EQ(server.stats().queuedPackets, static_cast(0)); + + server.setBlock(1, 2, 3, Block::Ice); + server.setBlock(2, 2, 3, Block::Ice); + for (uint64_t tick = 40; tick < 60; ++tick) { + server.update(tick); + only.step(tick); + } + CHECK_EQ(server.stats().blockUpdatesSent, static_cast(2)); + CHECK_EQ(server.stats().editsApplied, static_cast(2)); + CHECK(only.world.blockAt(1, 2, 3) == Block::Ice); +} + +VF_TEST(fourClientsSpreadAcrossTheWorldStayConsistentOverAnUnreliableLink) { + ServerConfig config = quietServerConfig(); + config.maxClients = 4; + config.viewRadius = 1; + config.chunksPerClientPerTick = 2; + config.link.lossPercent = 25; + config.link.duplicatePercent = 10; + config.link.reorderPercent = 20; + config.link.latencyTicks = 2; + config.link.seed = 0xc0ffee1234ull; + config.transport.retransmitTimeoutTicks = 8; + World world; + NetServer server(world, config); + buildTerrain(world, -8, 8); + + const float spots[4][2] = {{8.0f, 8.0f}, {72.0f, 8.0f}, {8.0f, 72.0f}, {72.0f, 72.0f}}; + std::vector> clients; + TransportConfig transportConfig = config.transport; + SessionConfig sessionConfig = clientConfigFor(config); + for (int i = 0; i < 4; ++i) { + int slot = server.acceptClient(); + CHECK_EQ(slot, i); + placeAt(server, slot, spots[i][0], 8.0f, spots[i][1]); + clients.push_back(std::unique_ptr( + new RemoteClient(server, slot, transportConfig, sessionConfig, "player"))); + } + + Lcg rng(0x9911aabb); + for (uint64_t tick = 0; tick < 500; ++tick) { + if (tick > 100 && tick % 5 == 0) { + int corner = rng.range(4); + int baseX = static_cast(spots[corner][0]); + int baseZ = static_cast(spots[corner][1]); + server.setBlock(baseX + rng.range(8) - 4, rng.range(6), baseZ + rng.range(8) - 4, + Block::Glowstone); + } + server.update(tick); + for (auto& client : clients) client->step(tick); + } + for (uint64_t tick = 500; tick < 800; ++tick) { + server.update(tick); + for (auto& client : clients) client->step(tick); + } + + CHECK_EQ(server.activeClients(), 4u); + for (int i = 0; i < 4; ++i) { + NetSession* session = server.sessionFor(i); + CHECK(session->phase() == SessionPhase::Play); + const ClientView& view = session->view(); + CHECK_EQ(view.sent.size(), static_cast(5)); + for (const Vec3i& coord : view.sent) { + const Chunk* authoritative = world.chunkAt(coord); + const Chunk* mirror = clients[static_cast(i)]->world.chunkAt(coord); + CHECK(mirror != nullptr); + if (mirror == nullptr) continue; + CHECK(authoritative->raw() == mirror->raw()); + } + // Each client only ever holds the chunks it was told about. + CHECK_EQ(clients[static_cast(i)]->world.chunkCount(), + static_cast(5)); + } + CHECK(server.stats().editsApplied > 50u); +}