#include #include #include #include "LevelFile.h" #include "ZoneFile.h" namespace { // Little-endian file bytes, as the client's files are laid out struct ZoneBytes { std::string data; template ZoneBytes& Put(T value) { data.append(reinterpret_cast(&value), sizeof(T)); return *this; } template void At(size_t pos, T value) { std::memcpy(data.data() + pos, &value, sizeof(T)); } ZoneBytes& Text(const std::string& text) { Put(static_cast(text.size())); data += text; return *this; } ZoneBytes& Wide(const std::string& text) { Put(static_cast(text.size())); for (char c : text) Put(static_cast(c)); return *this; } ZoneBytes& Point(float x, float y, float z) { return Put(x).Put(y).Put(z); } void Object(uint32_t version, uint32_t lot, float x, const std::string& settings) { Put(lot * 10).Put(static_cast(lot)); if (version >= 38) Put(0); // node type if (version >= 32) Put(0); // glom ID Point(x, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(1.0f); // position, rotation, scale Put(static_cast(settings.size())); for (char c : settings) Put(static_cast(c)); Put(0); } }; std::string SampleZone() { ZoneBytes w; w.Put(41).Put(3).Put(1150); // version, revision, world w.Point(1, 2, 3).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f); // spawn point and rotation w.Put(1); w.Text("scene.lvl").Put(7).Put(2).Text("Global").Put(1).Put(2).Put(3); w.Put(0).Text("zone.raw").Text("Name").Text("Description"); // no zone boundaries w.Put(1); for (int i = 0; i < 2; i++) w.Put(1).Point(0, 0, 0); w.Put(0).Put(1).Put(3); // Movement: its config is waypoint commands (spaces dropped from the name) w.Put(18).Wide("Patrol").Put(0).Put(0).Put(0); w.Put(1).Point(5, 0, 5).Put(1).Wide("de lay").Wide(" 2 "); // Spawner: its config is LDF w.Put(18).Wide("Spawner").Put(4).Put(0).Put(0); w.Put(6010).Put(10).Put(1).Put(1).Put(123).Put(1); w.Put(1).Point(1, 1, 1).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(1).Wide("spawner_node_id").Wide("1:4"); // Property w.Put(8).Wide("PropertyPath").Put(2).Put(0).Put(0); w.Put(0).Put(1000).Put(0).Put(1100); w.Wide("Block Yard"); w.Put(4); for (char c : std::string("Desc")) w.Put(static_cast(c)); w.Put(0).Put(0).Put(1.0f).Put(0).Put(0).Point(0, 0, 0).Put(128.0f); w.Put(3).Point(0, 0, 0).Point(10, 0, 0).Point(10, 0, 10); return w.data; } } TEST(ZoneFileTests, ReadsScenesAndPaths) { std::istringstream stream(SampleZone()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); EXPECT_EQ(zone.fileFormatVersion, ZoneFile::FileFormatVersion::Latest); EXPECT_EQ(zone.worldID, 1150u); EXPECT_EQ(zone.spawnpoint, NiPoint3(1, 2, 3)); ASSERT_EQ(zone.scenes.size(), 1u); EXPECT_EQ(zone.scenes[0].filename, "scene.lvl"); EXPECT_EQ(zone.scenes[0].id, 7u); EXPECT_EQ(zone.scenes[0].sceneType, 2u); EXPECT_EQ(zone.zoneRawPath, "zone.raw"); ASSERT_EQ(zone.sceneTransitions.size(), 1u); EXPECT_EQ(zone.sceneTransitions[0].points.size(), 2u); ASSERT_EQ(zone.paths.size(), 3u); const auto& patrol = zone.paths[0].pathWaypoints.at(0); ASSERT_EQ(patrol.commands.size(), 1u); EXPECT_EQ(patrol.commands[0].command, eWaypointCommandType::DELAY); EXPECT_EQ(patrol.commands[0].data, "2"); const auto& spawner = zone.paths[1]; EXPECT_EQ(spawner.spawner.spawnedLOT, 6010); EXPECT_EQ(spawner.spawner.spawnerObjID, 123); EXPECT_EQ(spawner.pathWaypoints.at(0).config.find(u"spawner_node_id")->second->GetValueAsString(), "4"); const auto& property = zone.paths[2]; EXPECT_EQ(property.pathType, PathType::Property); EXPECT_EQ(property.property.displayName, "Block Yard"); EXPECT_EQ(property.property.displayDesc, "Desc"); EXPECT_EQ(property.property.maxBuildHeight, 128.0f); EXPECT_EQ(property.pathWaypoints.size(), 3u); } // Boundary lines sit between the scenes and the terrain file's name TEST(ZoneFileTests, ReadsZoneBoundaries) { ZoneBytes w; w.Put(41).Put(3).Put(1150); w.Point(0, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f); w.Put(1); w.Text("scene.lvl").Put(0).Put(0).Text("Global").Put(0).Put(0).Put(0); w.Put(2); w.Point(1, 0, 0).Point(10, 20, 30).Put(1100).Put(7).Put(4).Point(5, 6, 7); w.Point(0, 0, -1).Point(-1, -2, -3).Put(1200).Put(0).Put(0).Point(0, 0, 0); w.Text("zone.raw").Text("Name").Text("Description"); w.Put(0).Put(0).Put(1).Put(0); // no transitions, no paths std::istringstream stream(w.data); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); ASSERT_EQ(zone.zoneBoundaries.size(), 2u); const auto& boundary = zone.zoneBoundaries[0]; EXPECT_EQ(boundary.normal, NiPoint3(1, 0, 0)); EXPECT_EQ(boundary.point, NiPoint3(10, 20, 30)); EXPECT_EQ(boundary.destZoneID, LWOZONEID(1100, 7, 0)); EXPECT_EQ(boundary.destSceneID, 4u); EXPECT_EQ(boundary.spawnLocation, NiPoint3(5, 6, 7)); EXPECT_EQ(zone.zoneBoundaries[1].destZoneID, LWOZONEID(1200, 0, 0)); EXPECT_EQ(zone.zoneRawPath, "zone.raw"); EXPECT_EQ(zone.zoneName, "Name"); EXPECT_EQ(zone.zoneDesc, "Description"); } // From LateAlpha (37) on the scene count is a u32 (some LUP zones are 37) TEST(ZoneFileTests, LateAlphaSceneCountIsAU32) { ZoneBytes w; w.Put(37).Put(1).Put(20022); // version, revision, world; no spawn point before 38 w.Put(1); w.Text("scene.lvl").Put(5).Put(0).Text("Global Scene").Put(0).Put(0).Put(0); w.Put(0).Text("zone.raw").Text("Name").Text("Description"); w.Put(0).Put(8).Put(1).Put(0); // no transitions, no paths std::istringstream stream(w.data); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); ASSERT_EQ(zone.scenes.size(), 1u); EXPECT_EQ(zone.scenes[0].filename, "scene.lvl"); EXPECT_EQ(zone.scenes[0].id, 5u); EXPECT_EQ(zone.zoneRawPath, "zone.raw"); } // Before version 30 a scene is only an ID, with no file to load TEST(ZoneFileTests, VersionsBeforePrePreAlphaThrow) { ZoneBytes w; w.Put(20).Put(53).Put(1).Put(53).Put(0).Text(".raw"); std::istringstream stream(w.data); ZoneFile zone; EXPECT_THROW(zone.Read(stream), std::runtime_error); EXPECT_TRUE(zone.scenes.empty()); } // A PrePreAlpha (30) file ends at its terrain file's name: no zone name, description, transitions or paths TEST(ZoneFileTests, PrePreAlphaHasNoZoneName) { ZoneBytes w; w.Put(30).Put(72).Put(1).Text("scale.lvl"); // version, world, one scene: only its file w.Put(0).Text("scale.raw"); std::istringstream stream(w.data); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); ASSERT_EQ(zone.scenes.size(), 1u); EXPECT_EQ(zone.scenes[0].filename, "scale.lvl"); EXPECT_EQ(zone.zoneRawPath, "scale.raw"); EXPECT_TRUE(zone.zoneName.empty()); EXPECT_TRUE(zone.zoneDesc.empty()); } // Before path version 3 the type is a name and every waypoint has a platform's data and name/value pairs TEST(ZoneFileTests, ReadsLegacyPaths) { ZoneBytes w; w.Put(35).Put(137).Put(1); // version, world, scene count w.Text("lup.lvl").Put(0).Put(0).Text("Global Scene").Put(0).Put(0).Put(0); w.Put(0).Text("lup.raw").Text("Name").Text("Description"); w.Put(0); // no transitions w.Put(0).Put(1).Put(2); w.Put(2).Wide("LavaPath").Wide("npc").Put(1).Put(0); w.Put(1).Point(1, 2, 3).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(0).Put(3.0f).Put(0.5f); w.Put(1).Wide("delay").Wide("2"); w.Put(2).Wide("Mower").Wide("platform").Put(0).Put(2); w.Put(1).Point(4, 5, 6).Put(0.0f).Put(1.0f).Put(0.0f).Put(0.0f).Put(1).Put(7.0f).Put(1.5f).Put(0); std::istringstream stream(w.data); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); EXPECT_EQ(stream.peek(), std::char_traits::eof()); ASSERT_EQ(zone.paths.size(), 2u); const auto& npc = zone.paths[0]; EXPECT_EQ(npc.pathType, PathType::Movement); EXPECT_EQ(npc.flags, 1u); ASSERT_EQ(npc.pathWaypoints.size(), 1u); EXPECT_EQ(npc.pathWaypoints[0].position, NiPoint3(1, 2, 3)); EXPECT_EQ(npc.pathWaypoints[0].speed, 3.0f); ASSERT_EQ(npc.pathWaypoints[0].commands.size(), 1u); EXPECT_EQ(npc.pathWaypoints[0].commands[0].command, eWaypointCommandType::DELAY); const auto& platform = zone.paths[1]; EXPECT_EQ(platform.pathType, PathType::MovingPlatform); EXPECT_EQ(platform.pathBehavior, PathBehavior::Once); ASSERT_EQ(platform.pathWaypoints.size(), 1u); EXPECT_EQ(platform.pathWaypoints[0].rotation.x, 1.0f); EXPECT_EQ(platform.pathWaypoints[0].movingPlatform.lockPlayer, 1); EXPECT_EQ(platform.pathWaypoints[0].speed, 7.0f); EXPECT_EQ(platform.pathWaypoints[0].movingPlatform.wait, 1.5f); } TEST(ZoneFileTests, ShortFilesThrowOrFail) { const auto zone = SampleZone(); for (const size_t length : { size_t{ 3 }, size_t{ 40 }, zone.size() / 2, zone.size() - 1 }) { std::istringstream stream(zone.substr(0, length)); ZoneFile file; bool threw = false; try { file.Read(stream); } catch (const std::runtime_error&) { threw = true; } EXPECT_TRUE(threw || stream.fail()) << length; } } // Chunked files lay their objects out by the file info chunk's version TEST(LevelFileTests, ReadsChunkedObjects) { ZoneBytes w; const auto chunk = [&w](uint32_t id) { const auto start = w.data.size(); w.Put(0x4B4E4843).Put(id).Put(1).Put(0).Put(0).Put(0); w.At(start + 16, static_cast(w.data.size())); return start; }; const auto end = [&w](size_t start) { w.At(start + 12, static_cast(w.data.size() - start)); }; const auto info = chunk(LevelFile::FileInfo); w.Put(41).Put(9).Put(0).Put(0).Put(0); end(info); const auto objects = chunk(LevelFile::SceneObjectData); w.Put(2); w.Object(41, 4945, 10, "respawnname=0:NS_LW_Portal\nloadOnClientOnly=7:1"); w.Object(41, 176, -4, "spawntemplate=1:6010"); end(objects); std::istringstream stream(w.data); LevelFile level; level.Read(stream); EXPECT_EQ(level.chunkHeaders.at(LevelFile::FileInfo).fileInfo.revision, 9u); ASSERT_EQ(level.objects.size(), 2u); EXPECT_EQ(level.objects[0].lot, 4945); EXPECT_EQ(level.objects[0].position.x, 10.0f); EXPECT_EQ(level.objects[0].settings.find(u"respawnname")->second->GetValueAsString(), "NS_LW_Portal"); EXPECT_EQ(level.objects[1].id, 1760); EXPECT_EQ(level.objects[1].settings.find(u"spawntemplate")->second->GetValueAsString(), "6010"); } // Some live scenes (e.g. Nimbus Station's Gnarled Forest launch pad) are from before chunks TEST(LevelFileTests, ReadsFilesWithoutChunks) { ZoneBytes w; w.Put(30).Put(0).Put(1); // version, type, important w.data.append(48 + 12, '\0'); // version 30 settings the world skips w.Put(0); // no skydome w.Put(1); w.Object(30, 4945, 3, "respawnname=0:NS_GF"); std::istringstream stream(w.data); LevelFile level; level.Read(stream); ASSERT_EQ(level.objects.size(), 1u); EXPECT_EQ(level.objects[0].position.x, 3.0f); EXPECT_EQ(level.objects[0].settings.find(u"respawnname")->second->GetValueAsString(), "NS_GF"); EXPECT_EQ(level.chunkHeaders.count(LevelFile::SceneObjectData), 1u); } TEST(LevelFileTests, DamagedFilesKeepWhatWasRead) { ZoneBytes w; w.Put(30).Put(0).Put(1); w.data.append(48 + 12, '\0'); w.Put(0).Put(2); w.Object(30, 1, 0, "a=0:b"); w.Object(30, 2, 0, "c=0:d"); w.data.resize(w.data.size() - 2); // cut into the last object std::istringstream stream(w.data); LevelFile level; EXPECT_THROW(level.Read(stream), std::runtime_error); EXPECT_EQ(level.objects.size(), 1u); }