#include "testHarness.h" #include "vf/Nbt.h" #include using namespace vf; namespace { bool roundTrip(const std::string& name, const NbtTag& tag, std::string& outName, NbtTag& outTag) { std::vector bytes = serializeNbt(name, tag); return deserializeNbt(bytes.data(), bytes.size(), outName, outTag); } } // namespace VF_TEST(nbtScalarRoundTripPreservesValueAndType) { std::string name; NbtTag tag; CHECK(roundTrip("byteField", NbtTag::makeByte(-42), name, tag)); CHECK_EQ(name, std::string("byteField")); CHECK(tag.kind() == NbtType::Byte); CHECK_EQ(tag.asByte(), static_cast(-42)); CHECK(roundTrip("shortField", NbtTag::makeShort(-12345), name, tag)); CHECK(tag.kind() == NbtType::Short); CHECK_EQ(tag.asShort(), static_cast(-12345)); CHECK(roundTrip("intField", NbtTag::makeInt(-2000000000), name, tag)); CHECK(tag.kind() == NbtType::Int); CHECK_EQ(tag.asInt(), -2000000000); CHECK(roundTrip("longField", NbtTag::makeLong(-9000000000000000000LL), name, tag)); CHECK(tag.kind() == NbtType::Long); CHECK_EQ(tag.asLong(), -9000000000000000000LL); CHECK(roundTrip("floatField", NbtTag::makeFloat(3.14159f), name, tag)); CHECK(tag.kind() == NbtType::Float); CHECK_NEAR(tag.asFloat(), 3.14159f, 1e-6f); CHECK(roundTrip("doubleField", NbtTag::makeDouble(-2.718281828), name, tag)); CHECK(tag.kind() == NbtType::Double); CHECK_NEAR(tag.asDouble(), -2.718281828, 1e-12); CHECK(roundTrip("stringField", NbtTag::makeString("voxelforge"), name, tag)); CHECK(tag.kind() == NbtType::String); CHECK_EQ(tag.asString(), std::string("voxelforge")); } VF_TEST(nbtEmptyStringRoundTrips) { std::string name; NbtTag tag; CHECK(roundTrip("empty", NbtTag::makeString(""), name, tag)); CHECK_EQ(tag.asString(), std::string("")); } VF_TEST(nbtByteArrayRoundTripsExactBytes) { std::vector values = {1, -1, 0, 127, -128, 64, -64}; std::string name; NbtTag tag; CHECK(roundTrip("bytes", NbtTag::makeByteArray(values), name, tag)); CHECK(tag.kind() == NbtType::ByteArray); const std::vector& out = tag.asByteArray(); CHECK_EQ(out.size(), values.size()); for (std::size_t i = 0; i < values.size(); ++i) CHECK_EQ(out[i], values[i]); } VF_TEST(nbtIntArrayRoundTripsExactValues) { std::vector values = {0, 1, -1, 2147483647, -2147483647 - 1, 12345}; std::string name; NbtTag tag; CHECK(roundTrip("ints", NbtTag::makeIntArray(values), name, tag)); CHECK(tag.kind() == NbtType::IntArray); const std::vector& out = tag.asIntArray(); CHECK_EQ(out.size(), values.size()); for (std::size_t i = 0; i < values.size(); ++i) CHECK_EQ(out[i], values[i]); } VF_TEST(nbtListOfIntRoundTrips) { NbtTag list = NbtTag::makeList(NbtType::Int); for (int32_t v : {10, -20, 30, -40, 50}) list.listAppend(NbtTag::makeInt(v)); std::string name; NbtTag tag; CHECK(roundTrip("intList", list, name, tag)); CHECK(tag.kind() == NbtType::List); CHECK(tag.listElementType() == NbtType::Int); CHECK_EQ(tag.asList().size(), static_cast(5)); CHECK_EQ(tag.asList()[0].asInt(), 10); CHECK_EQ(tag.asList()[3].asInt(), -40); } VF_TEST(nbtListOfStringRoundTrips) { NbtTag list = NbtTag::makeList(NbtType::String); list.listAppend(NbtTag::makeString("oak")); list.listAppend(NbtTag::makeString("spruce")); list.listAppend(NbtTag::makeString("jungle")); std::string name; NbtTag tag; CHECK(roundTrip("trees", list, name, tag)); CHECK_EQ(tag.asList().size(), static_cast(3)); CHECK_EQ(tag.asList()[1].asString(), std::string("spruce")); } VF_TEST(nbtEmptyListRoundTrips) { NbtTag list = NbtTag::makeList(NbtType::Compound); std::string name; NbtTag tag; CHECK(roundTrip("nothing", list, name, tag)); CHECK(tag.kind() == NbtType::List); CHECK(tag.asList().empty()); } VF_TEST(nbtNestedCompoundRoundTrips) { NbtTag position = NbtTag::makeCompound(); position.put("x", NbtTag::makeInt(120)); position.put("y", NbtTag::makeInt(64)); position.put("z", NbtTag::makeInt(-90)); NbtTag inventory = NbtTag::makeList(NbtType::Compound); NbtTag item = NbtTag::makeCompound(); item.put("id", NbtTag::makeShort(9)); item.put("count", NbtTag::makeByte(32)); inventory.listAppend(item); NbtTag root = NbtTag::makeCompound(); root.put("position", position); root.put("inventory", inventory); root.put("health", NbtTag::makeFloat(18.5f)); std::string name; NbtTag out; CHECK(roundTrip("player", root, name, out)); CHECK_EQ(name, std::string("player")); CHECK(out.kind() == NbtType::Compound); const NbtTag* outPosition = out.find("position"); CHECK(outPosition != nullptr); CHECK_EQ(outPosition->find("x")->asInt(), 120); CHECK_EQ(outPosition->find("y")->asInt(), 64); CHECK_EQ(outPosition->find("z")->asInt(), -90); const NbtTag* outInventory = out.find("inventory"); CHECK(outInventory != nullptr); CHECK_EQ(outInventory->asList().size(), static_cast(1)); CHECK_EQ(outInventory->asList()[0].find("id")->asShort(), static_cast(9)); CHECK_EQ(outInventory->asList()[0].find("count")->asByte(), static_cast(32)); CHECK_NEAR(out.find("health")->asFloat(), 18.5f, 1e-6f); CHECK(out.find("missing") == nullptr); } VF_TEST(nbtPutOverwritesExistingKeyInsteadOfAppending) { NbtTag compound = NbtTag::makeCompound(); compound.put("score", NbtTag::makeInt(10)); compound.put("score", NbtTag::makeInt(20)); CHECK_EQ(compound.asCompound().size(), static_cast(1)); CHECK_EQ(compound.find("score")->asInt(), 20); } VF_TEST(nbtFindReturnsNullptrForMissingKey) { NbtTag compound = NbtTag::makeCompound(); compound.put("present", NbtTag::makeByte(1)); CHECK(compound.find("present") != nullptr); CHECK(compound.find("absent") == nullptr); } VF_TEST(nbtDeserializeRejectsTruncatedData) { NbtTag tag = NbtTag::makeInt(999); std::vector bytes = serializeNbt("value", tag); CHECK(bytes.size() > 2); std::string name; NbtTag out; for (std::size_t cut = 0; cut < bytes.size(); ++cut) { bool ok = deserializeNbt(bytes.data(), cut, name, out); CHECK(!ok); } CHECK(deserializeNbt(bytes.data(), bytes.size(), name, out)); } VF_TEST(nbtPaletteRoundTripsUniformSection) { std::array blocks; blocks.fill(Block::Stone); NbtPaletteSection section = encodeSectionPalette(blocks); CHECK_EQ(section.palette.size(), static_cast(1)); CHECK_EQ(section.bitsPerIndex, 1); std::array decoded; decoded.fill(Block::Air); decodeSectionPalette(section, decoded); CHECK(decoded == blocks); } VF_TEST(nbtPaletteRoundTripsMixedSection) { std::array blocks; for (std::size_t i = 0; i < blocks.size(); ++i) { switch (i % 5) { case 0: blocks[i] = Block::Air; break; case 1: blocks[i] = Block::Stone; break; case 2: blocks[i] = Block::Dirt; break; case 3: blocks[i] = Block::Grass; break; default: blocks[i] = Block::Water; break; } } NbtPaletteSection section = encodeSectionPalette(blocks); CHECK_EQ(section.palette.size(), static_cast(5)); CHECK_EQ(section.bitsPerIndex, 3); std::array decoded; decodeSectionPalette(section, decoded); CHECK(decoded == blocks); } VF_TEST(nbtPaletteBitsPerIndexScalesWithPaletteSize) { auto bitsForPaletteSize = [](int distinctBlocks) { std::array blocks; for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = static_cast(static_cast(i) % distinctBlocks); NbtPaletteSection section = encodeSectionPalette(blocks); return section.bitsPerIndex; }; CHECK_EQ(bitsForPaletteSize(1), 1); CHECK_EQ(bitsForPaletteSize(2), 1); CHECK_EQ(bitsForPaletteSize(3), 2); CHECK_EQ(bitsForPaletteSize(4), 2); CHECK_EQ(bitsForPaletteSize(5), 3); CHECK_EQ(bitsForPaletteSize(9), 4); CHECK_EQ(bitsForPaletteSize(20), 5); } VF_TEST(nbtPalettePackedIndicesSizeMatchesExpectedByteCount) { std::array blocks; for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = static_cast(static_cast(i) % 9); NbtPaletteSection section = encodeSectionPalette(blocks); std::size_t expectedBits = static_cast(section.bitsPerIndex) * blocks.size(); std::size_t expectedBytes = (expectedBits + 7) / 8; CHECK_EQ(section.packedIndices.size(), expectedBytes); } VF_TEST(nbtPalettePreservesFirstSeenOrder) { std::array blocks; blocks.fill(Block::Stone); blocks[0] = Block::Water; blocks[1] = Block::Grass; blocks[2] = Block::Stone; NbtPaletteSection section = encodeSectionPalette(blocks); CHECK_EQ(section.palette.size(), static_cast(3)); CHECK(section.palette[0] == Block::Water); CHECK(section.palette[1] == Block::Grass); CHECK(section.palette[2] == Block::Stone); }