diff --git a/tests/testSchematic.cpp b/tests/testSchematic.cpp new file mode 100644 index 0000000..7e0568f --- /dev/null +++ b/tests/testSchematic.cpp @@ -0,0 +1,161 @@ +#include "testHarness.h" +#include "vf/Codec.h" +#include "vf/EditHistory.h" +#include "vf/Schematic.h" +#include "vf/Selection.h" + +#include +#include + +using namespace vf; + +namespace { + +// Byte offsets into an encoded schematic. The frame is magic, version, body +// length, body, checksum - all big-endian, all fixed width, which is what lets +// these tests damage one field at a time. +constexpr std::size_t bodyOffset = 12; +constexpr std::size_t paletteOffsetInBody = 26; + +uint32_t readU32(const ByteBuffer& bytes, std::size_t at) { + return (static_cast(bytes[at]) << 24) | (static_cast(bytes[at + 1]) << 16) | + (static_cast(bytes[at + 2]) << 8) | static_cast(bytes[at + 3]); +} + +void writeU32(ByteBuffer& bytes, std::size_t at, uint32_t value) { + bytes[at] = static_cast(value >> 24); + bytes[at + 1] = static_cast(value >> 16); + bytes[at + 2] = static_cast(value >> 8); + bytes[at + 3] = static_cast(value); +} + +void refreshChecksum(ByteBuffer& bytes) { + uint32_t bodyLength = readU32(bytes, 8); + writeU32(bytes, bodyOffset + bodyLength, fnv1a32(bytes.data() + bodyOffset, bodyLength)); +} + +// Rewrites one body byte and repairs the checksum, so the decoder has to reject +// the buffer on its own merits rather than on a broken hash. +void patchBody(ByteBuffer& bytes, std::size_t offsetInBody, uint8_t value) { + bytes[bodyOffset + offsetInBody] = value; + refreshChecksum(bytes); +} + +World terrainWorld(int spanX, int spanZ) { + World world; + for (int z = 0; z < spanZ; ++z) + for (int x = 0; x < spanX; ++x) { + int height = 2 + ((x * 3 + z * 5) % 3); + for (int y = 0; y <= height; ++y) { + Block block = Block::Stone; + if (y == height) block = Block::Grass; + else if (y > height - 2) block = Block::Dirt; + world.setBlock(x, y, z, block); + } + } + return world; +} + +bool sameRegion(const World& left, const World& right, const Vec3i& lo, const Vec3i& hi) { + for (int y = lo.y; y <= hi.y; ++y) + for (int z = lo.z; z <= hi.z; ++z) + for (int x = lo.x; x <= hi.x; ++x) + if (left.blockAt(x, y, z) != right.blockAt(x, y, z)) return false; + return true; +} + +Schematic decodedCopy(const ByteBuffer& bytes, SchematicError& error) { + Schematic loaded; + error = Schematic::decode(bytes.data(), bytes.size(), loaded); + return loaded; +} + +} // namespace + +VF_TEST(aSchematicRoundTripRebuildsTheExactSameBlocks) { + World source = terrainWorld(9, 9); + Selection region = Selection::box({0, 0, 0}, {8, 5, 8}); + Schematic saved = Schematic::capture(source, region, "ridge", "foksiu", 900u); + + CHECK(saved.header().dims == Vec3i(9, 6, 9)); + CHECK_EQ(saved.buffer().filledCount(), 9u * 6u * 9u); + + ByteBuffer bytes = saved.encode(); + SchematicError error = SchematicError::BadMagic; + Schematic loaded = decodedCopy(bytes, error); + CHECK(error == SchematicError::None); + CHECK(loaded.buffer().sameContent(saved.buffer())); + + World rebuilt; + std::size_t written = loaded.pasteInto(rebuilt, {0, 0, 0}, ClipboardBuffer::PasteMode::Overwrite); + CHECK(written > 0u); + CHECK(sameRegion(source, rebuilt, {0, 0, 0}, {8, 5, 8})); +} + +VF_TEST(aSchematicCarriesItsMetadataThroughTheRoundTrip) { + World source = terrainWorld(4, 4); + Schematic saved = Schematic::capture(source, Selection::box({0, 0, 0}, {3, 3, 3}), "watchtower", + "vulpes", 1234567890123ull); + saved.setAnchor({1, 2, 3}); + + ByteBuffer bytes = saved.encode(); + SchematicError error = SchematicError::BadMagic; + Schematic loaded = decodedCopy(bytes, error); + CHECK(error == SchematicError::None); + CHECK(loaded.header().name == std::string("watchtower")); + CHECK(loaded.header().author == std::string("vulpes")); + CHECK_EQ(loaded.header().tick, 1234567890123ull); + CHECK(loaded.header().anchor == Vec3i(1, 2, 3)); + CHECK(loaded.header().version == SchematicVersion::V2); + CHECK(!loaded.migratedFromV1()); +} + +VF_TEST(theIndexWidthShrinksWithTheNumberOfDistinctBlocks) { + CHECK_EQ(Schematic::bitsForPalette(1u), 1u); + CHECK_EQ(Schematic::bitsForPalette(2u), 1u); + CHECK_EQ(Schematic::bitsForPalette(3u), 2u); + CHECK_EQ(Schematic::bitsForPalette(4u), 2u); + CHECK_EQ(Schematic::bitsForPalette(5u), 4u); + CHECK_EQ(Schematic::bitsForPalette(16u), 4u); + CHECK_EQ(Schematic::bitsForPalette(17u), 8u); + + ClipboardBuffer twoTone({8, 8, 8}, {0, 0, 0}); + for (int y = 0; y < 8; ++y) + for (int z = 0; z < 8; ++z) + for (int x = 0; x < 8; ++x) twoTone.put(x, y, z, Block::Stone); + Schematic plain = Schematic::fromBuffer(twoTone, "plain", "foksiu", 1u); + CHECK_EQ(plain.header().paletteSize, 2u); + CHECK_EQ(plain.header().indexBits, 1u); + + ClipboardBuffer manyTone = twoTone; + for (int i = 0; i < 20; ++i) manyTone.put(i % 8, i / 8, 0, static_cast(20 + i)); + Schematic mixed = Schematic::fromBuffer(manyTone, "mixed", "foksiu", 1u); + CHECK_EQ(mixed.header().paletteSize, 22u); + CHECK_EQ(mixed.header().indexBits, 8u); + + SchematicError error = SchematicError::BadMagic; + Schematic reloaded = decodedCopy(mixed.encode(), error); + CHECK(error == SchematicError::None); + CHECK(reloaded.buffer().sameContent(manyTone)); + CHECK_EQ(reloaded.header().indexBits, 8u); +} + +VF_TEST(rotatingFourTimesByNinetyDegreesReturnsTheOriginal) { + World source = terrainWorld(7, 5); + Schematic original = Schematic::capture(source, Selection::box({0, 0, 0}, {6, 3, 4}), "spin", + "foksiu", 10u); + original.setAnchor({2, 1, 3}); + + Schematic turning = original; + for (int step = 0; step < 4; ++step) turning.rotate(Rotation::Cw90); + CHECK(turning.buffer().sameContent(original.buffer())); + CHECK(turning.header().dims == original.header().dims); + CHECK(turning.header().anchor == original.header().anchor); + + Schematic half = original; + half.rotate(Rotation::Cw180); + Schematic twoQuarters = original; + twoQuarters.rotate(Rotation::Cw90); + twoQuarters.rotate(Rotation::Cw90); + CHECK(half.buffer().sameContent(twoQuarters.buffer())); +