161 lines
5.7 KiB
C++
161 lines
5.7 KiB
C++
#include "testHarness.h"
|
|
#include "vf/NetServer.h"
|
|
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<uint32_t>(state >> 32);
|
|
}
|
|
int range(int n) { return static_cast<int>(next() % static_cast<uint32_t>(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> transport;
|
|
std::unique_ptr<NetSession> 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<uint64_t>(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<std::size_t>(5));
|
|
CHECK_EQ(farView.sent.size(), static_cast<std::size_t>(5));
|
|
|
|
std::size_t shared = 0;
|
|
for (const Vec3i& coord : nearView.sent)
|
|
if (farView.sent.count(coord) != 0) ++shared;
|
|
CHECK_EQ(shared, static_cast<std::size_t>(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<std::size_t>(5));
|
|
CHECK_EQ(far.world.chunkCount(), static_cast<std::size_t>(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<uint64_t>(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}));
|
|
|