diff --git a/tests/testSaveV3.cpp b/tests/testSaveV3.cpp new file mode 100644 index 0000000..39343e0 --- /dev/null +++ b/tests/testSaveV3.cpp @@ -0,0 +1,121 @@ +#include "testHarness.h" +#include "vf/SaveV3.h" +#include "vf/Serialize.h" +#include "vf/Fluid.h" +#include "vf/LightEngine.h" + +using namespace vf; + +namespace { + +GridSnapshot snapshotFluid(const FluidSim& sim) { + return {sim.regionOrigin(), sim.regionDims(), sim.levels(), sim.sourceFlags()}; +} + +GridSnapshot snapshotLight(const LightEngine& engine) { + return {engine.regionOrigin(), engine.regionDims(), engine.sunField(), engine.blockField()}; +} + +} // namespace + +VF_TEST(saveV3RoundTripsWorldMobsFluidLight) { + SaveV3 save; + for (int x = 0; x < 8; ++x) + for (int z = 0; z < 8; ++z) + save.world.setBlock(x, 0, z, Block::Stone); + save.world.setBlock(3, 4, 5, Block::Glowstone); + + FluidSim sim(save.world, {0, 0, 0}, {8, 4, 8}); + sim.addSource(4, 1, 4); + sim.settle(40); + save.hasFluid = true; + save.fluid = snapshotFluid(sim); + + LightEngine engine(save.world, {0, 0, 0}, {8, 8, 8}); + engine.rebuild(); + save.hasLight = true; + save.light = snapshotLight(engine); + + Mob a; + a.position = {1.5f, 1.0f, 2.5f}; + a.velocity = {0.1f, -0.2f, 0.3f}; + a.state = MobState::Chase; + a.rng = 0xdeadbeefu; + a.onGround = true; + Mob b; + b.position = {6.5f, 3.0f, 6.5f}; + b.state = MobState::Wander; + save.mobs = {a, b}; + + std::vector bytes = serializeSaveV3(save); + SaveV3 loaded; + CHECK(deserializeSaveV3(bytes.data(), bytes.size(), loaded)); + + CHECK(loaded.world.blockAt(3, 4, 5) == Block::Glowstone); + CHECK(loaded.world.blockAt(0, 0, 0) == Block::Stone); + CHECK_EQ(loaded.mobs.size(), static_cast(2)); + CHECK_NEAR(loaded.mobs[0].position.x, 1.5f, 1e-5f); + CHECK_NEAR(loaded.mobs[0].velocity.z, 0.3f, 1e-5f); + CHECK(loaded.mobs[0].state == MobState::Chase); + CHECK(loaded.mobs[0].onGround); + CHECK_EQ(loaded.mobs[0].rng, 0xdeadbeefu); + CHECK(loaded.mobs[1].state == MobState::Wander); + + CHECK(loaded.hasFluid); + CHECK(loaded.fluid.channelA == save.fluid.channelA); + CHECK(loaded.fluid.channelB == save.fluid.channelB); + CHECK(loaded.hasLight); + CHECK(loaded.light.channelA == save.light.channelA); + + // The restored snapshots feed live systems again. + FluidSim restoredFluid(loaded.world, loaded.fluid.origin, loaded.fluid.dims); + restoredFluid.loadRaw(loaded.fluid.channelA, loaded.fluid.channelB); + CHECK_EQ(static_cast(restoredFluid.level(4, 1, 4)), static_cast(sim.level(4, 1, 4))); + + LightEngine restoredLight(loaded.world, loaded.light.origin, loaded.light.dims); + restoredLight.loadRaw(loaded.light.channelA, loaded.light.channelB); + CHECK_EQ(static_cast(restoredLight.blockAt(3, 4, 5)), + static_cast(engine.blockAt(3, 4, 5))); +} + +VF_TEST(saveV3LoadsLegacyV1File) { + World world; + world.setBlock(0, 0, 0, Block::Stone); + world.setBlock(-5, 40, 17, Block::Water); + std::vector legacy = serializeWorld(world); // v1 format + + SaveV3 loaded; + CHECK(deserializeSaveV3(legacy.data(), legacy.size(), loaded)); + CHECK(loaded.world.blockAt(0, 0, 0) == Block::Stone); + CHECK(loaded.world.blockAt(-5, 40, 17) == Block::Water); + CHECK(loaded.mobs.empty()); + CHECK(!loaded.hasFluid); + CHECK(!loaded.hasLight); +} + +VF_TEST(saveV3RejectsBadMagic) { + std::vector junk = {0x00, 0x11, 0x22, 0x33, 0x44}; + SaveV3 out; + CHECK(!deserializeSaveV3(junk.data(), junk.size(), out)); +} + +VF_TEST(saveV3RejectsTruncatedStream) { + SaveV3 save; + save.world.setBlock(0, 0, 0, Block::Stone); + Mob m; + save.mobs = {m}; + std::vector bytes = serializeSaveV3(save); + bytes.resize(bytes.size() - 6); // cut into the mob section + SaveV3 out; + CHECK(!deserializeSaveV3(bytes.data(), bytes.size(), out)); +} + +VF_TEST(saveV3EmptyRoundTrips) { + SaveV3 save; + std::vector bytes = serializeSaveV3(save); + SaveV3 loaded; + CHECK(deserializeSaveV3(bytes.data(), bytes.size(), loaded)); + CHECK_EQ(loaded.world.chunkCount(), static_cast(0)); + CHECK(loaded.mobs.empty()); + CHECK(!loaded.hasFluid); +}