test: add tests for chunk dirty-tracking and network sync batching
This commit is contained in:
1 parent
52619dc3ec
commit
0bf884f15d
1 file changed
+208
@@ -0,0 +1,208 @@
|
|||||||
|
#include "testHarness.h"
|
||||||
|
#include "vf/ChunkSync.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace vf;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Lcg {
|
||||||
|
uint64_t state;
|
||||||
|
explicit Lcg(uint64_t seed) : state(seed) {}
|
||||||
|
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)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Server-side edit: mutate the authoritative world and flag the slot dirty.
|
||||||
|
void serverEdit(World& world, ServerChunkSync& sync, const Vec3i& chunkCoord, int x, int y, int z,
|
||||||
|
Block block) {
|
||||||
|
Chunk& chunk = world.ensureChunk(chunkCoord);
|
||||||
|
chunk.set(x, y, z, block);
|
||||||
|
sync.markDirty(chunkCoord, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool chunksEqual(const World& a, const World& b, const Vec3i& coord) {
|
||||||
|
const Chunk* ca = a.chunkAt(coord);
|
||||||
|
const Chunk* cb = b.chunkAt(coord);
|
||||||
|
if (!ca || !cb) return false;
|
||||||
|
return ca->raw() == cb->raw();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChunkData reframeChunkData(const ChunkData& packet) {
|
||||||
|
PacketWriter w;
|
||||||
|
packet.encode(w);
|
||||||
|
PacketReader r(w.bytes().data(), w.bytes().size());
|
||||||
|
ChunkData back;
|
||||||
|
CHECK(back.decode(r));
|
||||||
|
return back;
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiBlockChange reframeDelta(const MultiBlockChange& packet) {
|
||||||
|
PacketWriter w;
|
||||||
|
packet.encode(w);
|
||||||
|
PacketReader r(w.bytes().data(), w.bytes().size());
|
||||||
|
MultiBlockChange back;
|
||||||
|
CHECK(back.decode(r));
|
||||||
|
return back;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
VF_TEST(chunkSyncFullSnapshotReplicatesChunk) {
|
||||||
|
World server;
|
||||||
|
ServerChunkSync sync;
|
||||||
|
Vec3i coord{2, -1, 3};
|
||||||
|
for (int x = 0; x < chunkSize; ++x)
|
||||||
|
for (int z = 0; z < chunkSize; ++z) {
|
||||||
|
server.ensureChunk(coord).set(x, 0, z, Block::Bedrock);
|
||||||
|
for (int y = 1; y < 6; ++y) server.ensureChunk(coord).set(x, y, z, Block::Stone);
|
||||||
|
server.ensureChunk(coord).set(x, 6, z, Block::Grass);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChunkData full;
|
||||||
|
CHECK(sync.buildFull(server, coord, full));
|
||||||
|
ChunkData wire = reframeChunkData(full);
|
||||||
|
|
||||||
|
World client;
|
||||||
|
ClientChunkSync clientSync;
|
||||||
|
clientSync.applyFull(wire, client);
|
||||||
|
CHECK(chunksEqual(server, client, coord));
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_TEST(chunkSyncDeltaAppliesDirtyBlocks) {
|
||||||
|
World server;
|
||||||
|
ServerChunkSync sync;
|
||||||
|
Vec3i coord{0, 0, 0};
|
||||||
|
server.ensureChunk(coord);
|
||||||
|
|
||||||
|
World client;
|
||||||
|
ClientChunkSync clientSync;
|
||||||
|
ChunkData full;
|
||||||
|
CHECK(sync.buildFull(server, coord, full));
|
||||||
|
clientSync.applyFull(reframeChunkData(full), client);
|
||||||
|
CHECK(chunksEqual(server, client, coord));
|
||||||
|
|
||||||
|
serverEdit(server, sync, coord, 1, 1, 1, Block::Glowstone);
|
||||||
|
serverEdit(server, sync, coord, 5, 8, 2, Block::DiamondOre);
|
||||||
|
serverEdit(server, sync, coord, 15, 15, 15, Block::Obsidian);
|
||||||
|
CHECK_EQ(sync.dirtyCount(coord), static_cast<std::size_t>(3));
|
||||||
|
|
||||||
|
MultiBlockChange delta;
|
||||||
|
CHECK(sync.buildDelta(server, coord, delta));
|
||||||
|
clientSync.applyDelta(reframeDelta(delta), client);
|
||||||
|
CHECK(chunksEqual(server, client, coord));
|
||||||
|
|
||||||
|
sync.clear(coord);
|
||||||
|
CHECK(!sync.anyDirty());
|
||||||
|
MultiBlockChange empty;
|
||||||
|
CHECK(!sync.buildDelta(server, coord, empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_TEST(chunkSyncSingleBlockChange) {
|
||||||
|
World client;
|
||||||
|
ClientChunkSync clientSync;
|
||||||
|
BlockChange packet;
|
||||||
|
packet.x = -7;
|
||||||
|
packet.y = 70;
|
||||||
|
packet.z = 42;
|
||||||
|
packet.block = Block::RedstoneLampLit;
|
||||||
|
clientSync.applyBlockChange(packet, client);
|
||||||
|
CHECK(client.blockAt(-7, 70, 42) == Block::RedstoneLampLit);
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_TEST(chunkSyncResendThreshold) {
|
||||||
|
World server;
|
||||||
|
ServerChunkSync sync;
|
||||||
|
Vec3i coord{1, 0, 1};
|
||||||
|
server.ensureChunk(coord);
|
||||||
|
for (int i = 0; i < 40; ++i) serverEdit(server, sync, coord, i % 16, i / 16, 0, Block::Sand);
|
||||||
|
CHECK(sync.shouldResendFull(coord, 32));
|
||||||
|
CHECK(!sync.shouldResendFull(coord, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_TEST(chunkSyncOverlappingEditsCollapse) {
|
||||||
|
World server;
|
||||||
|
ServerChunkSync sync;
|
||||||
|
Vec3i coord{0, 0, 0};
|
||||||
|
server.ensureChunk(coord);
|
||||||
|
serverEdit(server, sync, coord, 3, 3, 3, Block::Dirt);
|
||||||
|
serverEdit(server, sync, coord, 3, 3, 3, Block::Stone);
|
||||||
|
serverEdit(server, sync, coord, 3, 3, 3, Block::Grass);
|
||||||
|
// The same slot touched three times is one dirty entry carrying the final block.
|
||||||
|
CHECK_EQ(sync.dirtyCount(coord), static_cast<std::size_t>(1));
|
||||||
|
|
||||||
|
MultiBlockChange delta;
|
||||||
|
CHECK(sync.buildDelta(server, coord, delta));
|
||||||
|
CHECK_EQ(delta.changes.size(), static_cast<std::size_t>(1));
|
||||||
|
CHECK(delta.changes[0].block == Block::Grass);
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_TEST(chunkSyncManyTicksConverge) {
|
||||||
|
World server;
|
||||||
|
World client;
|
||||||
|
ServerChunkSync sync;
|
||||||
|
ClientChunkSync clientSync;
|
||||||
|
Vec3i coord{4, 2, -6};
|
||||||
|
server.ensureChunk(coord);
|
||||||
|
|
||||||
|
ChunkData full;
|
||||||
|
CHECK(sync.buildFull(server, coord, full));
|
||||||
|
clientSync.applyFull(reframeChunkData(full), client);
|
||||||
|
|
||||||
|
Lcg rng(0xC0FFEE);
|
||||||
|
Block palette[6] = {Block::Air, Block::Stone, Block::Dirt,
|
||||||
|
Block::Water, Block::Glowstone, Block::Sand};
|
||||||
|
for (int tick = 0; tick < 30; ++tick) {
|
||||||
|
int edits = 1 + rng.range(20);
|
||||||
|
for (int e = 0; e < edits; ++e) {
|
||||||
|
int x = rng.range(chunkSize);
|
||||||
|
int y = rng.range(chunkSize);
|
||||||
|
int z = rng.range(chunkSize);
|
||||||
|
serverEdit(server, sync, coord, x, y, z, palette[rng.range(6)]);
|
||||||
|
}
|
||||||
|
MultiBlockChange delta;
|
||||||
|
if (sync.buildDelta(server, coord, delta)) {
|
||||||
|
clientSync.applyDelta(reframeDelta(delta), client);
|
||||||
|
sync.clear(coord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(chunksEqual(server, client, coord));
|
||||||
|
CHECK(!sync.anyDirty());
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_TEST(chunkSyncFramedEndToEnd) {
|
||||||
|
World server;
|
||||||
|
ServerChunkSync sync;
|
||||||
|
Vec3i coord{7, 0, 7};
|
||||||
|
Chunk& source = server.ensureChunk(coord);
|
||||||
|
for (int i = 0; i < chunkVolume; ++i)
|
||||||
|
source.raw()[static_cast<std::size_t>(i)] = static_cast<Block>(i % 5);
|
||||||
|
|
||||||
|
ChunkData full;
|
||||||
|
CHECK(sync.buildFull(server, coord, full));
|
||||||
|
|
||||||
|
ByteWriter stream;
|
||||||
|
PacketWriter body;
|
||||||
|
full.encode(body);
|
||||||
|
writeFramed(stream, PacketId::ChunkData, body);
|
||||||
|
|
||||||
|
std::vector<Frame> frames;
|
||||||
|
std::size_t consumed = 0;
|
||||||
|
splitFrames(stream.bytes().data(), stream.bytes().size(), frames, consumed);
|
||||||
|
CHECK_EQ(frames.size(), static_cast<std::size_t>(1));
|
||||||
|
CHECK(frames[0].id == PacketId::ChunkData);
|
||||||
|
|
||||||
|
PacketReader r(frames[0].payload.data(), frames[0].payload.size());
|
||||||
|
ChunkData back;
|
||||||
|
CHECK(back.decode(r));
|
||||||
|
|
||||||
|
World client;
|
||||||
|
ClientChunkSync clientSync;
|
||||||
|
clientSync.applyFull(back, client);
|
||||||
|
CHECK(chunksEqual(server, client, coord));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user