diff --git a/src/unittest/test_mapblock.cpp b/src/unittest/test_mapblock.cpp index 53075d7ac..6260e9687 100644 --- a/src/unittest/test_mapblock.cpp +++ b/src/unittest/test_mapblock.cpp @@ -24,7 +24,15 @@ public: testSaveLoad(gamedef, SER_FMT_VER_LOWEST_WRITE); } + void testSave29(IGameDef *gamedef); + void testLoad29(IGameDef *gamedef); + + // Tests loading a MapBlock from Minetest-c55 0.3 + void testLoad20(IGameDef *gamedef); + + // Tests loading a non-standard MapBlock + void testLoadNonStd(IGameDef *gamedef); }; static TestMapBlock g_test_instance; @@ -33,7 +41,10 @@ void TestMapBlock::runTests(IGameDef *gamedef) { TEST(testSaveLoad, gamedef, SER_FMT_VER_HIGHEST_WRITE); TEST(testSaveLoadLowest, gamedef); + TEST(testSave29, gamedef); TEST(testLoad29, gamedef); + TEST(testLoad20, gamedef); + TEST(testLoadNonStd, gamedef); } //////////////////////////////////////////////////////////////////////////////// @@ -83,6 +94,73 @@ void TestMapBlock::testSaveLoad(IGameDef *gamedef, const u8 version) } } +#define SS2_CHECK() UASSERT(!ss2.eof() || !ss2.bad()) + +void TestMapBlock::testSave29(IGameDef *gamedef) +{ + auto *ndef = gamedef->getNodeDefManager(); + std::stringstream ss; + + { + // Prepare test block + MapBlock block({}, gamedef); + for (size_t i = 0; i < MapBlock::nodecount; ++i) + block.getData()[i] = MapNode(CONTENT_AIR); + block.setNode({0, 0, 0}, MapNode(t_CONTENT_STONE)); + + block.serialize(ss, 29, true, -1); + } + + // Pick it apart a bit: + std::stringstream ss2; + decompressZstd(ss, ss2); // first zstd + + u8 flags = readU8(ss2); + SS2_CHECK(); + UASSERT(flags & 0x02); // !is_air == true + UASSERT(flags & 0x08); // !is_genereated == true + + ss2.seekg(2+4, std::ios_base::cur); // lighting_complete & timestamp + SS2_CHECK(); + + // nimap + u8 ver = readU8(ss2); + u16 count = readU16(ss2); + SS2_CHECK(); + UASSERTEQ(int, ver, 0); + // Invariant 1: map only contains nodes that are actually present in the data + // and no duplicates. + UASSERTEQ(int, count, 2); + std::string nn[2]; + content_t ii[2]; + for (int i = 0; i < 2; i++) { + ii[i] = readU16(ss2); + nn[i] = deSerializeString16(ss2); + } + // no particular order is guaranteed + if (nn[0] > nn[1]) + std::swap(nn[0], nn[1]); + UASSERTEQ(auto, nn[0], ndef->get(CONTENT_AIR).name); + UASSERTEQ(auto, nn[1], ndef->get(t_CONTENT_STONE).name); + // Invariant 2: IDs must start from zero + if (ii[0] > ii[1]) + std::swap(ii[0], ii[1]); + UASSERTEQ(int, ii[0], 0x0000); + UASSERTEQ(int, ii[1], 0x0001); + + /* + * Quick note: + * Minetest deals with MapBlocks that violates these invariants (mostly) fine, + * but we should still be careful about them as third-party tools may rely on them. + */ + + u8 content_width, params_width; + content_width = readU8(ss2); + params_width = readU8(ss2); + SS2_CHECK(); + UASSERT(content_width == 2 && params_width == 2); +} + // This was generated with: minetestmapper -i testworld --dumpblock 6,0,0 | // python -c 'import sys;d=bytes.fromhex(sys.stdin.read().strip());print(",".join("%d"%c for c in d))' @@ -146,3 +224,123 @@ void TestMapBlock::testLoad29(IGameDef *gamedef) UASSERTEQ(int, ilist->getSize(), 32); UASSERTEQ(auto, ilist->getItem(1).name, "default:stone"); } + +static const u8 coded_mapblock20[] = { + 20,2,120,156,237,150,91,114,131,48,12,69,197,63,30,88,2,75,242,138,24,47, + 189,230,145,196,186,184,22,170,12,161,33,183,51,105,78,41,182,142,95,48,142, + 33,132,113,250,152,126,17,45,95,95,159,143,43,75,22,30,199,132,89,144,173, + 57,186,253,49,209,29,39,125,98,190,4,254,1,252,3,212,179,97,185,247,242,253, + 156,195,134,197,246,229,36,109,210,234,255,98,169,127,99,125,106,95,105,184, + 132,254,202,161,217,159,115,218,98,128,122,180,220,199,217,54,206,183,106, + 248,149,33,240,39,240,15,224,179,139,203,93,234,252,181,203,79,153,156,191, + 218,23,248,215,206,254,114,176,73,203,223,22,2,255,0,62,74,206,20,91,222,43, + 21,20,44,137,229,208,72,172,126,240,3,37,129,5,127,235,98,189,124,222,241, + 40,187,82,132,13,126,194,139,141,34,31,63,27,66,46,119,26,157,156,204,163, + 250,94,67,112,119,255,111,238,154,206,181,212,186,206,77,233,58,162,149,187, + 132,219,233,219,146,137,95,52,51,11,178,53,105,95,57,174,209,197,236,55,91, + 57,226,190,11,59,135,252,188,149,184,239,150,121,185,206,109,216,9,220,150, + 175,203,122,98,92,58,187,220,143,192,71,207,45,175,183,181,50,140,31,178, + 109,60,8,234,111,26,206,120,93,203,77,220,89,41,187,29,236,56,151,135,7,215, + 151,46,4,245,82,3,140,215,247,48,175,191,117,86,238,74,108,240,143,119,178, + 250,43,113,218,62,219,254,18,175,62,42,182,36,174,45,94,191,153,29,172,93, + 231,138,92,255,96,215,101,94,172,201,140,17,247,137,103,121,203,86,247,14, + 230,139,19,185,230,236,217,99,63,154,213,71,121,253,71,153,33,219,71,201, + 209,220,21,249,220,28,242,98,241,143,114,185,211,232,228,76,47,154,151,90, + 144,103,103,123,26,223,203,255,155,187,134,104,232,135,248,49,101,121,227, + 137,188,254,129,200,251,244,202,131,137,18,62,52,105,95,57,174,210,197,170, + 155,240,179,23,240,247,224,239,193,95,63,26,3,167,161,200,195,134,213,253, + 101,42,40,245,201,253,188,151,184,102,223,57,223,226,191,31,177,58,82,35,15, + 190,34,199,159,158,181,214,35,15,69,94,118,34,99,134,194,112,213,72,226,227, + 193,47,203,186,214,117,243,47,249,30,224,47,250,2,239,110,248,47,7,155,180, + 252,235,198,131,159,204,197,226,50,115,201,217,92,176,45,139,74,234,3,167, + 123,47,50,11,236,213,237,94,62,118,246,222,158,119,60,202,174,20,97,131,159, + 240,98,163,200,199,207,134,144,203,157,70,39,7,253,233,110,67,112,119,255, + 111,238,154,31,244,135,233,2,120,156,99,96,100,96,100,206,102,16,96,232,242, + 201,44,46,81,72,43,77,205,81,48,228,114,205,45,40,169,228,114,205,75,241, + 204,43,75,205,43,201,47,170,4,201,114,129,149,20,23,37,19,80,145,2,196,38, + 48,21,152,36,186,30,100,1,46,3,5,3,5,0,187,133,49,88,0,0,0,0,0,0,210,62,6 +}; + +void TestMapBlock::testLoad20(IGameDef *gamedef) +{ + UASSERT(MAP_BLOCKSIZE == 16); + const std::string_view buf(reinterpret_cast(coded_mapblock20), sizeof(coded_mapblock20)); + + // Conversion of minerals does not work if these nodes are not already + // defined at load time. (Is this a bug? Does anyone even care?) + gamedef->allocateUnknownNodeId("default:stone_with_coal"); + gamedef->allocateUnknownNodeId("default:stone_with_iron"); + + std::istringstream iss; + iss.str(std::string(buf)); + u8 version = readU8(iss); + UASSERTEQ(int, version, 20); + MapBlock block({}, gamedef); + block.deSerialize(iss, version, true); + + auto *ndef = gamedef->getNodeDefManager(); + auto get_node = [&] (s16 x, s16 y, s16 z) -> std::string_view { + MapNode n = block.getNodeNoEx({x, y, z}); + return ndef->get(n).name; + }; + + // These names come from content_mapnode.cpp and are hardcoded in the engine. + UASSERTEQ(auto, get_node(11, 0, 3), "default:stone"); + UASSERTEQ(auto, get_node(11, 1, 3), "default:stone_with_coal"); + UASSERTEQ(auto, get_node(11, 2, 3), "default:dirt"); + UASSERTEQ(auto, get_node(10, 5, 4), "default:dirt_with_grass"); + UASSERTEQ(auto, get_node(10, 6, 4), "air"); + UASSERTEQ(auto, get_node(11, 6, 3), "default:furnace"); + + for (size_t i = 0; i < MapBlock::nodecount; ++i) + UASSERT(block.getData()[i].getContent() != CONTENT_IGNORE); +} + +static const u8 coded_mapblock_nonstd[] = { + 27,0,255,255,1,2,120,218,237,208,177,13,130,64,0,0,192,127,21,5,31,176,182, + 182,34,177,103,3,18,6,176,115,20,72,76,156,137,45,88,199,222,26,38,128,228, + 110,132,139,113,233,179,18,0,0,0,0,0,0,0,128,205,75,227,235,59,63,223,237, + 227,222,76,67,247,235,243,99,125,169,138,243,33,102,101,186,93,195,201,16,0, + 0,0,236,223,31,36,149,14,142,120,218,99,0,0,0,1,0,1,0,0,0,255,255,255,255,0, + 0,3,0,0,0,3,97,105,114,0,1,0,8,116,101,115,116,58,111,110,101,8,0,0,8,116, + 101,115,116,58,116,119,111,10,0,0 +}; + +void TestMapBlock::testLoadNonStd(IGameDef *gamedef) +{ + /* + * Node IDs were originally 8-bit, then some special format that allowed exactly + * 2176 IDs (but only the first 128 could use the full range of param2), and + * finally 16-bit like today. + * The content_width field in MapBlocks was used around that time during the + * transition to 16-bit: + * A content_width of 1 would normally never appear with a version > 24, but + * it is in fact perfectly possible to serialize a block in todays format with + * shortened IDs. + * + * This test checks that such a block is deserialized correctly. + * To future readers: I think it's worth not breaking this quirk (~sfan5) + */ + + UASSERT(MAP_BLOCKSIZE == 16); + const std::string_view buf(reinterpret_cast(coded_mapblock_nonstd), sizeof(coded_mapblock_nonstd)); + + std::istringstream iss; + iss.str(std::string(buf)); + u8 version = readU8(iss); + UASSERT(version > 24); + MapBlock block({}, gamedef); + block.deSerialize(iss, version, true); + + auto *ndef = gamedef->getNodeDefManager(); + UASSERTEQ(int, block.getNodeNoEx({0, 0, 0}).getContent(), ndef->getId("test:one")); + UASSERTEQ(int, block.getNodeNoEx({0, 1, 0}).getContent(), ndef->getId("test:two")); + + // Check that param2 was handeled correctly + const static u8 data_lo[] = {8, 3, 14, 7, 13, 9, 6, 2, 1, 5, 12, 11, 15, 10, 0, 4}; + const static u8 data_hi[] = {11, 125, 85, 131, 204, 44, 92, 55, 35, 25, 41, 181, 124, 70, 245, 73}; + for (s16 i = 0; i < 16; i++) + UASSERTEQ(int, block.getNodeNoEx({i, 0, 0}).param2, data_hi[i]); + for (s16 i = 0; i < 16; i++) + UASSERTEQ(int, block.getNodeNoEx({i, 1, 0}).param2, data_lo[i]); +}