Files
voxelforge/tests/testLightEngine.cpp

134 lines
5.1 KiB
C++

#include "testHarness.h"
#include "vf/LightEngine.h"
using namespace vf;
namespace {
void fillBox(World& world, Vec3i lo, Vec3i hi, Block block) {
for (int x = lo.x; x <= hi.x; ++x)
for (int y = lo.y; y <= hi.y; ++y)
for (int z = lo.z; z <= hi.z; ++z)
world.setBlock(x, y, z, block);
}
// Compares an incrementally maintained engine against a full rebuild of the
// same world+box. Equality is the correctness oracle for incremental edits.
bool matchesRebuild(const LightEngine& incremental, World& world) {
Vec3i origin = incremental.regionOrigin();
Vec3i dims = incremental.regionDims();
LightEngine fresh(world, origin, dims);
fresh.rebuild();
for (int lx = 0; lx < dims.x; ++lx)
for (int ly = 0; ly < dims.y; ++ly)
for (int lz = 0; lz < dims.z; ++lz) {
int gx = origin.x + lx, gy = origin.y + ly, gz = origin.z + lz;
if (incremental.sunAt(gx, gy, gz) != fresh.sunAt(gx, gy, gz)) return false;
if (incremental.blockAt(gx, gy, gz) != fresh.blockAt(gx, gy, gz)) return false;
}
return true;
}
} // namespace
VF_TEST(lightEngineSkyFillsOpenColumnStraightDown) {
World world;
fillBox(world, {0, 0, 0}, {7, 0, 7}, Block::Stone);
LightEngine engine(world, {0, 0, 0}, {8, 8, 8});
engine.rebuild();
CHECK_EQ(static_cast<int>(engine.sunAt(3, 7, 3)), maxLightLevel);
CHECK_EQ(static_cast<int>(engine.sunAt(3, 1, 3)), maxLightLevel); // no vertical loss
}
VF_TEST(lightEngineBlockEmitterFallsOffRadially) {
World world;
world.setBlock(5, 5, 5, Block::Glowstone);
LightEngine engine(world, {0, 0, 0}, {11, 11, 11});
engine.rebuild();
CHECK_EQ(static_cast<int>(engine.blockAt(5, 5, 5)), maxLightLevel);
CHECK_EQ(static_cast<int>(engine.blockAt(6, 5, 5)), maxLightLevel - 1);
CHECK_EQ(static_cast<int>(engine.blockAt(8, 5, 5)), maxLightLevel - 3);
}
VF_TEST(lightEnginePlacingOpaqueDarkensColumnBelow) {
World world;
fillBox(world, {0, 0, 0}, {4, 11, 4}, Block::Stone); // solid box
for (int y = 1; y <= 11; ++y) world.setBlock(2, y, 2, Block::Air); // walled shaft
LightEngine engine(world, {0, 0, 0}, {5, 12, 5});
engine.rebuild();
CHECK_EQ(static_cast<int>(engine.sunAt(2, 1, 2)), maxLightLevel);
engine.setBlock(2, 6, 2, Block::Stone); // seal the shaft midway
CHECK_EQ(static_cast<int>(engine.sunAt(2, 5, 2)), 0); // shadow below the lid
CHECK_EQ(static_cast<int>(engine.sunAt(2, 7, 2)), maxLightLevel); // still lit above
CHECK(matchesRebuild(engine, world));
}
VF_TEST(lightEngineBreakingOpaqueRePropagates) {
World world;
fillBox(world, {0, 0, 0}, {4, 11, 4}, Block::Stone);
for (int y = 1; y <= 11; ++y) world.setBlock(2, y, 2, Block::Air);
world.setBlock(2, 6, 2, Block::Stone); // lid already in place
LightEngine engine(world, {0, 0, 0}, {5, 12, 5});
engine.rebuild();
CHECK_EQ(static_cast<int>(engine.sunAt(2, 5, 2)), 0); // in shadow
engine.setBlock(2, 6, 2, Block::Air); // reopen the shaft
CHECK_EQ(static_cast<int>(engine.sunAt(2, 5, 2)), maxLightLevel); // relit straight down
CHECK(matchesRebuild(engine, world));
}
VF_TEST(lightEngineRemovingEmitterDarkensRoom) {
World world;
fillBox(world, {0, 0, 0}, {8, 8, 8}, Block::Stone);
fillBox(world, {1, 1, 1}, {7, 7, 7}, Block::Air); // sealed room, no sky
world.setBlock(4, 4, 4, Block::Glowstone);
LightEngine engine(world, {0, 0, 0}, {9, 9, 9});
engine.rebuild();
CHECK(engine.blockAt(5, 4, 4) > 0);
engine.setBlock(4, 4, 4, Block::Air); // remove the emitter
CHECK_EQ(static_cast<int>(engine.blockAt(5, 4, 4)), 0);
CHECK_EQ(static_cast<int>(engine.blockAt(4, 4, 4)), 0);
CHECK(matchesRebuild(engine, world));
}
VF_TEST(lightEngineEditAcrossChunkBorderMatchesRebuild) {
World world;
// floor spanning four chunk columns so light crosses chunk borders in the box.
fillBox(world, {12, 0, 12}, {35, 0, 35}, Block::Stone);
LightEngine engine(world, {12, 0, 12}, {24, 20, 24}); // spans chunks x/z 0..2
engine.rebuild();
world.setBlock(0, 0, 0, Block::Air);
engine.setBlock(15, 5, 15, Block::Glowstone); // near a chunk boundary
engine.setBlock(16, 5, 16, Block::Stone);
engine.setBlock(31, 5, 31, Block::Glowstone); // in a different chunk
engine.setBlock(31, 5, 31, Block::Air);
CHECK(matchesRebuild(engine, world));
}
VF_TEST(lightEngineEditSequenceStaysConsistentWithRebuild) {
World world;
fillBox(world, {0, 0, 0}, {15, 0, 15}, Block::Stone);
LightEngine engine(world, {0, 0, 0}, {16, 16, 16});
engine.rebuild();
const Block palette[4] = {Block::Stone, Block::Air, Block::Glowstone, Block::Water};
uint32_t rng = 0x1234567u;
auto nextRng = [&rng]() {
rng ^= rng << 13;
rng ^= rng >> 17;
rng ^= rng << 5;
return rng;
};
for (int i = 0; i < 120; ++i) {
int x = static_cast<int>(nextRng() % 16);
int y = 1 + static_cast<int>(nextRng() % 12);
int z = static_cast<int>(nextRng() % 16);
Block b = palette[nextRng() % 4];
engine.setBlock(x, y, z, b);
}
CHECK(matchesRebuild(engine, world));
}