#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())); Schematic backAgain = original; backAgain.rotate(Rotation::Cw90); backAgain.rotate(Rotation::Cw270); CHECK(backAgain.buffer().sameContent(original.buffer())); } VF_TEST(aQuarterTurnSwapsTheHorizontalDimensionsAndMovesTheMarker) { ClipboardBuffer buffer({4, 1, 2}, {0, 0, 0}); for (int z = 0; z < 2; ++z) for (int x = 0; x < 4; ++x) buffer.put(x, 0, z, Block::Stone); buffer.put(0, 0, 0, Block::Glowstone); ClipboardBuffer turned = buffer.rotatedY(Rotation::Cw90); CHECK(turned.dims() == Vec3i(2, 1, 4)); CHECK_EQ(turned.filledCount(), 8u); // (0,0,0) maps to (dimZ - 1, 0, 0) under a clockwise quarter turn. CHECK(turned.at(1, 0, 0) == Block::Glowstone); CHECK(turned.at(0, 0, 0) == Block::Stone); } VF_TEST(mirroringTwiceOnTheSameAxisReturnsTheOriginal) { ClipboardBuffer buffer({3, 2, 4}, {1, 0, 2}); for (int y = 0; y < 2; ++y) for (int z = 0; z < 4; ++z) for (int x = 0; x < 3; ++x) buffer.put(x, y, z, Block::Planks); buffer.put(0, 0, 0, Block::Chest); ClipboardBuffer flipped = buffer.mirrored(MirrorAxis::X); CHECK(flipped.at(2, 0, 0) == Block::Chest); CHECK(flipped.at(0, 0, 0) == Block::Planks); CHECK(flipped.anchor() == Vec3i(1, 0, 2)); CHECK(flipped.mirrored(MirrorAxis::X).sameContent(buffer)); CHECK(buffer.mirrored(MirrorAxis::Y).mirrored(MirrorAxis::Y).sameContent(buffer)); CHECK(buffer.mirrored(MirrorAxis::Z).mirrored(MirrorAxis::Z).sameContent(buffer)); ClipboardBuffer alongZ = buffer.mirrored(MirrorAxis::Z); CHECK(alongZ.at(0, 0, 3) == Block::Chest); CHECK(alongZ.anchor() == Vec3i(1, 0, 1)); } VF_TEST(trimmingDropsEmptyMarginsAndKeepsTheAnchorOnTheSameBlock) { ClipboardBuffer padded({8, 8, 8}, {5, 5, 5}); for (int y = 4; y <= 5; ++y) for (int z = 4; z <= 6; ++z) for (int x = 5; x <= 6; ++x) padded.put(x, y, z, Block::Bricks); ClipboardBuffer tight = padded.trimmed(); CHECK(tight.dims() == Vec3i(2, 2, 3)); CHECK_EQ(tight.filledCount(), 12u); CHECK(tight.anchor() == Vec3i(0, 1, 1)); CHECK(tight.at(0, 0, 0) == Block::Bricks); World empty; Schematic schematic = Schematic::fromBuffer(padded, "pad", "foksiu", 3u); schematic.trim(); CHECK(schematic.header().dims == Vec3i(2, 2, 3)); CHECK_EQ(schematic.pasteInto(empty, {0, 0, 0}, ClipboardBuffer::PasteMode::Overwrite), 12u); } VF_TEST(versionOneDataIsMigratedToVersionTwoWhenLoaded) { World source = terrainWorld(6, 6); Schematic saved = Schematic::capture(source, Selection::box({0, 0, 0}, {5, 4, 5}), "legacy", "foksiu", 777u); ByteBuffer oldBytes = saved.encode(SchematicVersion::V1); ByteBuffer newBytes = saved.encode(SchematicVersion::V2); CHECK(oldBytes.size() < newBytes.size()); SchematicError error = SchematicError::BadMagic; Schematic loaded = decodedCopy(oldBytes, error); CHECK(error == SchematicError::None); CHECK(loaded.migratedFromV1()); CHECK(loaded.header().version == SchematicVersion::V2); CHECK(loaded.header().name.empty()); CHECK(loaded.header().author.empty()); CHECK_EQ(loaded.header().tick, 0u); CHECK(loaded.buffer().sameContent(saved.buffer())); // Re-saving the migrated schematic yields a v2 buffer that carries metadata. loaded.setName("legacy"); loaded.setAuthor("foksiu"); loaded.setTick(777u); SchematicError second = SchematicError::BadMagic; Schematic again = decodedCopy(loaded.encode(), second); CHECK(second == SchematicError::None); CHECK(!again.migratedFromV1()); CHECK(again.header().name == std::string("legacy")); } VF_TEST(theVoidMaskKeepsHolesInVersionTwoAndIsLostInVersionOne) { World stoneBlock; for (int y = 0; y < 9; ++y) for (int z = 0; z < 9; ++z) for (int x = 0; x < 9; ++x) stoneBlock.setBlock(x, y, z, Block::Stone); Selection ball = Selection::sphere({4, 4, 4}, 4); Schematic saved = Schematic::capture(stoneBlock, ball, "ball", "foksiu", 5u); CHECK(saved.header().hasVoidMask); CHECK(saved.buffer().filledCount() < saved.buffer().volume()); SchematicError error = SchematicError::BadMagic; Schematic loaded = decodedCopy(saved.encode(), error); CHECK(error == SchematicError::None); CHECK(loaded.header().hasVoidMask); CHECK_EQ(loaded.buffer().filledCount(), saved.buffer().filledCount()); CHECK(!loaded.buffer().filledAt(0, 0, 0)); SchematicError legacyError = SchematicError::BadMagic; Schematic legacy = decodedCopy(saved.encode(SchematicVersion::V1), legacyError); CHECK(legacyError == SchematicError::None); CHECK(!legacy.header().hasVoidMask); CHECK_EQ(legacy.buffer().filledCount(), legacy.buffer().volume()); CHECK(legacy.buffer().at(0, 0, 0) == Block::Air); World target; loaded.pasteInto(target, {0, 0, 0}, ClipboardBuffer::PasteMode::Overwrite); CHECK(target.blockAt(4, 4, 4) == Block::Stone); CHECK(target.blockAt(0, 0, 0) == Block::Air); CHECK(target.blockAt(8, 8, 8) == Block::Air); } VF_TEST(aBufferWithTheWrongMagicNumberIsRejected) { World source = terrainWorld(3, 3); ByteBuffer bytes = Schematic::capture(source, Selection::box({0, 0, 0}, {2, 2, 2}), "m", "f", 1u).encode(); bytes[0] = 0x00; Schematic loaded; CHECK(Schematic::decode(bytes.data(), bytes.size(), loaded) == SchematicError::BadMagic); ByteBuffer garbage = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; CHECK(Schematic::decode(garbage.data(), garbage.size(), loaded) == SchematicError::BadMagic); } VF_TEST(anUnknownFormatVersionIsRejected) { World source = terrainWorld(3, 3); ByteBuffer bytes = Schematic::capture(source, Selection::box({0, 0, 0}, {2, 2, 2}), "v", "f", 1u).encode(); writeU32(bytes, 4, 7u); Schematic loaded; CHECK(Schematic::decode(bytes.data(), bytes.size(), loaded) == SchematicError::UnsupportedVersion); writeU32(bytes, 4, 0u); CHECK(Schematic::decode(bytes.data(), bytes.size(), loaded) == SchematicError::UnsupportedVersion); } VF_TEST(aTruncatedBufferIsRejectedAtEveryCutPoint) { World source = terrainWorld(5, 5); ByteBuffer bytes = Schematic::capture(source, Selection::box({0, 0, 0}, {4, 4, 4}), "cut", "f", 1u).encode(); CHECK(bytes.size() > 40u);