301 lines
9.0 KiB
C++
301 lines
9.0 KiB
C++
#include "testHarness.h"
|
|
#include "vf/WorldSim.h"
|
|
#include "vf/RandomTick.h"
|
|
#include "vf/ChunkSync.h"
|
|
#include "vf/Farming.h"
|
|
#include "vf/Weather.h"
|
|
#include "vf/TimeOfDay.h"
|
|
#include "vf/Mob.h"
|
|
#include "vf/Minecart.h"
|
|
|
|
#include <cmath>
|
|
|
|
using namespace vf;
|
|
|
|
namespace {
|
|
|
|
void stoneFloor(World& world, int x0, int x1, int z0, int z1, int y = 0) {
|
|
for (int x = x0; x <= x1; ++x)
|
|
for (int z = z0; z <= z1; ++z)
|
|
world.setBlock(x, y, z, Block::Stone);
|
|
}
|
|
|
|
float horizontalDistance(const Vec3& a, const Vec3& b) {
|
|
float dx = a.x - b.x, dz = a.z - b.z;
|
|
return std::sqrt(dx * dx + dz * dz);
|
|
}
|
|
|
|
void buildRedstoneSandScene(World& world) {
|
|
world.setBlock(3, 0, 3, Block::Stone);
|
|
world.setBlock(3, 8, 3, Block::Sand);
|
|
world.setBlock(5, 2, 5, Block::Lever);
|
|
world.setBlock(6, 2, 5, Block::RedstoneLamp);
|
|
}
|
|
|
|
WorldSimConfig daylightCropConfig() {
|
|
WorldSimConfig config;
|
|
config.worldSeed = 7u;
|
|
config.randomTicksPerChunk = 64;
|
|
// Freeze the clock at noon so crop sky light stays at the daytime maximum.
|
|
config.time.ticksPerDay = 100000000;
|
|
config.startTick = 50000000;
|
|
return config;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
VF_TEST(clockAdvancesOneTickPerSimTick) {
|
|
World world;
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16});
|
|
sim.step(100);
|
|
CHECK_EQ(sim.currentTick(), 100);
|
|
CHECK_EQ(sim.time().absoluteTick(), static_cast<WorldTick>(100));
|
|
}
|
|
|
|
VF_TEST(weatherEntersRainAndIsDeterministic) {
|
|
WorldSimConfig config;
|
|
config.worldSeed = 44u;
|
|
config.weather.clearMin = 1;
|
|
config.weather.clearMax = 1;
|
|
|
|
World worldA;
|
|
World worldB;
|
|
WorldSim a(worldA, {0, 0, 0}, {16, 16, 16}, config);
|
|
WorldSim b(worldB, {0, 0, 0}, {16, 16, 16}, config);
|
|
a.step(4);
|
|
b.step(4);
|
|
|
|
CHECK(a.weather().isRaining());
|
|
CHECK_EQ(a.weather().currentTick(), static_cast<long long>(4));
|
|
CHECK(a.weather().state() == b.weather().state());
|
|
CHECK_EQ(a.weather().currentTick(), b.weather().currentTick());
|
|
CHECK_NEAR(a.weather().rainGradient(), b.weather().rainGradient(), 1e-12);
|
|
}
|
|
|
|
VF_TEST(cropMaturesThroughDispatchedRandomTicks) {
|
|
World world;
|
|
world.setBlock(0, 63, 0, Block::Farmland);
|
|
world.setBlock(2, 63, 0, Block::Water);
|
|
|
|
WorldSim sim(world, {0, 64, 0}, {16, 16, 16}, daylightCropConfig());
|
|
Vec3i crop{0, 64, 0};
|
|
sim.farming().plant(world, crop, CropType::Wheat);
|
|
CHECK_EQ(sim.farming().stateAt(crop)->stage, 0);
|
|
CHECK_EQ(sim.time().skyLightLevel(), 15);
|
|
|
|
int steps = 0;
|
|
for (; steps < 40000 && !sim.farming().stateAt(crop)->mature(); ++steps) sim.step(1);
|
|
|
|
CHECK(sim.farming().stateAt(crop)->mature());
|
|
CHECK_EQ(sim.farming().stateAt(crop)->stage, 7);
|
|
CHECK(world.blockAt(0, 64, 0) == Block::Wheat);
|
|
CHECK(steps > 1);
|
|
}
|
|
|
|
VF_TEST(fallingSandIsDrivenByTheBlockTickScheduler) {
|
|
World world;
|
|
world.setBlock(3, 0, 3, Block::Stone);
|
|
world.setBlock(3, 6, 3, Block::Sand);
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16});
|
|
|
|
sim.step(6);
|
|
CHECK(world.blockAt(3, 1, 3) == Block::Sand);
|
|
CHECK(world.blockAt(3, 6, 3) == Block::Air);
|
|
}
|
|
|
|
VF_TEST(exposedDirtGrowsGrassThroughTheBlockTickHook) {
|
|
World world;
|
|
world.setBlock(5, 5, 5, Block::Dirt);
|
|
WorldSimConfig config;
|
|
config.blockRandomTicksPerChunk = 200;
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16}, config);
|
|
|
|
bool grew = false;
|
|
for (int i = 0; i < 300 && !grew; ++i) {
|
|
sim.step(1);
|
|
if (world.blockAt(5, 5, 5) == Block::Grass) grew = true;
|
|
}
|
|
CHECK(grew);
|
|
}
|
|
|
|
VF_TEST(redstoneLampFollowsTheLeverEachStep) {
|
|
World world;
|
|
world.setBlock(5, 2, 5, Block::Lever);
|
|
world.setBlock(6, 2, 5, Block::RedstoneLamp);
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16});
|
|
|
|
sim.circuit().setLever({5, 2, 5}, true);
|
|
sim.step(2);
|
|
CHECK(world.blockAt(6, 2, 5) == Block::RedstoneLampLit);
|
|
|
|
sim.circuit().setLever({5, 2, 5}, false);
|
|
sim.step(2);
|
|
CHECK(world.blockAt(6, 2, 5) == Block::RedstoneLamp);
|
|
}
|
|
|
|
VF_TEST(hopperDrainsSourceIntoTargetOverManyTicks) {
|
|
World world;
|
|
Vec3i pos{4, 4, 4};
|
|
world.setBlock(pos.x, pos.y, pos.z, Block::Hopper);
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16});
|
|
|
|
sim.containers().ensure(pos + Vec3i{0, 1, 0}, 5).insert(ItemId::Stone, 5);
|
|
sim.containers().ensure(pos + Vec3i{0, -1, 0}, 5);
|
|
|
|
sim.step(120);
|
|
CHECK_EQ(sim.containers().at(pos + Vec3i{0, 1, 0})->totalItems(), 0);
|
|
CHECK_EQ(sim.containers().at(pos + Vec3i{0, -1, 0})->totalItems(), 5);
|
|
}
|
|
|
|
VF_TEST(dispenserFiresOnRedstoneEdgeThroughLogistics) {
|
|
World world;
|
|
Vec3i disp{5, 2, 5};
|
|
world.setBlock(disp.x, disp.y, disp.z, Block::Dispenser);
|
|
world.setBlock(4, 2, 5, Block::Lever);
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16});
|
|
sim.containers().ensure(disp, dispenserSlots).insert(ItemId::Stone, 1);
|
|
|
|
sim.step(1);
|
|
CHECK(world.blockAt(6, 2, 5) == Block::Air);
|
|
|
|
sim.circuit().setLever({4, 2, 5}, true);
|
|
sim.step(2);
|
|
CHECK(world.blockAt(6, 2, 5) == Block::Stone);
|
|
CHECK_EQ(sim.containers().at(disp)->totalItems(), 0);
|
|
}
|
|
|
|
VF_TEST(minecartRollsAlongTrackWhenStepped) {
|
|
World world;
|
|
for (int x = 0; x <= 15; ++x) world.setBlock(x, 0, 0, Block::Rail);
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16});
|
|
|
|
Minecart cart = makeMinecart({2, 0, 0}, RailDir::East, CartKind::Empty);
|
|
cart.speed = 0.3f;
|
|
sim.addMinecart(cart);
|
|
|
|
sim.step(20);
|
|
CHECK(sim.minecarts()[0].railPos.x > 2);
|
|
CHECK(!sim.minecarts()[0].derailed);
|
|
}
|
|
|
|
VF_TEST(hostileCreatureChasesThePlayerAndClosesDistance) {
|
|
World world;
|
|
stoneFloor(world, -2, 40, -2, 40);
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16});
|
|
|
|
sim.addCreature(spawnCreature(Species::Zombie, {5.0f, 1.0f, 5.0f}, 5u));
|
|
Vec3 playerPos{11.0f, 1.0f, 5.0f};
|
|
sim.setPlayer(playerPos);
|
|
|
|
float startDist = horizontalDistance(sim.creatures()[0].body.position, playerPos);
|
|
bool sawChase = false;
|
|
for (int i = 0; i < 150; ++i) {
|
|
sim.step(1);
|
|
if (sim.creatures()[0].state == CreatureState::Chase) sawChase = true;
|
|
}
|
|
float endDist = horizontalDistance(sim.creatures()[0].body.position, playerPos);
|
|
|
|
CHECK(sawChase);
|
|
CHECK(endDist < startDist);
|
|
}
|
|
|
|
VF_TEST(climateLaysSnowUnderSnowfall) {
|
|
World world;
|
|
world.setBlock(0, 30, 0, Block::Stone);
|
|
|
|
WorldSimConfig config;
|
|
config.climateIntervalTicks = 1;
|
|
config.weather.clearMin = 1;
|
|
config.weather.clearMax = 1;
|
|
config.weather.rainMin = 1000000;
|
|
config.weather.rainMax = 1000000;
|
|
config.weather.snowTemperature = 1000.0f;
|
|
config.climate.freezeTemperature = 1000.0f;
|
|
config.climate.meltTemperature = 2000.0f;
|
|
|
|
WorldSim sim(world, {-1, 30, -1}, {3, 5, 3}, config);
|
|
sim.step(10);
|
|
|
|
CHECK(world.blockAt(0, 31, 0) == Block::SnowLayer);
|
|
CHECK(sim.snowDepthAt({0, 31, 0}) >= 1);
|
|
CHECK(sim.currentPrecipitation() == Precipitation::Snow);
|
|
}
|
|
|
|
VF_TEST(dirtyFeedReplicatesChangedBlocksThroughChunkSync) {
|
|
World server;
|
|
server.setBlock(5, 2, 5, Block::Lever);
|
|
server.setBlock(6, 2, 5, Block::RedstoneLamp);
|
|
|
|
Vec3i coord{0, 0, 0};
|
|
World client;
|
|
client.ensureChunk(coord).raw() = server.chunkAt(coord)->raw();
|
|
|
|
WorldSim sim(server, {0, 0, 0}, {16, 16, 16});
|
|
sim.circuit().setLever({5, 2, 5}, true);
|
|
sim.step(2);
|
|
|
|
const std::vector<Vec3i>& dirty = sim.dirtyBlocks();
|
|
CHECK(!dirty.empty());
|
|
bool foundLamp = false;
|
|
for (const Vec3i& cell : dirty)
|
|
if (cell == Vec3i{6, 2, 5}) foundLamp = true;
|
|
CHECK(foundLamp);
|
|
|
|
ServerChunkSync sync;
|
|
for (const Vec3i& cell : dirty) {
|
|
Vec3i chunkCoord{floorDiv(cell.x, chunkSize), floorDiv(cell.y, chunkSize),
|
|
floorDiv(cell.z, chunkSize)};
|
|
sync.markDirty(chunkCoord, floorMod(cell.x, chunkSize), floorMod(cell.y, chunkSize),
|
|
floorMod(cell.z, chunkSize));
|
|
}
|
|
|
|
MultiBlockChange delta;
|
|
CHECK(sync.buildDelta(server, coord, delta));
|
|
ClientChunkSync clientSync;
|
|
clientSync.applyDelta(delta, client);
|
|
|
|
CHECK(client.chunkAt(coord)->raw() == server.chunkAt(coord)->raw());
|
|
}
|
|
|
|
VF_TEST(clearDirtyRebasesTheDirtyFeed) {
|
|
World world;
|
|
world.setBlock(6, 2, 5, Block::RedstoneLamp);
|
|
world.setBlock(5, 2, 5, Block::Lever);
|
|
WorldSim sim(world, {0, 0, 0}, {16, 16, 16});
|
|
|
|
sim.circuit().setLever({5, 2, 5}, true);
|
|
sim.step(2);
|
|
CHECK(!sim.dirtyBlocks().empty());
|
|
|
|
sim.clearDirty();
|
|
CHECK(sim.dirtyBlocks().empty());
|
|
|
|
sim.step(2);
|
|
CHECK(sim.dirtyBlocks().empty());
|
|
}
|
|
|
|
VF_TEST(sameSeedYieldsByteIdenticalWorldState) {
|
|
WorldSimConfig config;
|
|
config.worldSeed = 2718u;
|
|
config.weather.clearMin = 1;
|
|
config.weather.clearMax = 1;
|
|
|
|
World worldA;
|
|
World worldB;
|
|
buildRedstoneSandScene(worldA);
|
|
buildRedstoneSandScene(worldB);
|
|
|
|
WorldSim a(worldA, {0, 0, 0}, {16, 16, 16}, config);
|
|
WorldSim b(worldB, {0, 0, 0}, {16, 16, 16}, config);
|
|
a.circuit().setLever({5, 2, 5}, true);
|
|
b.circuit().setLever({5, 2, 5}, true);
|
|
|
|
a.step(60);
|
|
b.step(60);
|
|
|
|
CHECK_EQ(a.regionHash(), b.regionHash());
|
|
CHECK_EQ(a.currentTick(), b.currentTick());
|
|
CHECK(a.weather().state() == b.weather().state());
|
|
CHECK(worldA.chunkAt({0, 0, 0})->raw() == worldB.chunkAt({0, 0, 0})->raw());
|
|
}
|