diff --git a/dCommon/LevelFile.cpp b/dCommon/LevelFile.cpp index a0c5c91d1..835f82603 100644 --- a/dCommon/LevelFile.cpp +++ b/dCommon/LevelFile.cpp @@ -71,6 +71,8 @@ void LevelFile::Read(std::istream& file) { if (fileInfo.fileInfo.particleChunkStart != 0) { const auto header = ReadChunkHeader(file, fileInfo.fileInfo.particleChunkStart); chunkHeaders.insert({ header.id, header }); + file.seekg(header.startPosition); + ReadParticles(file); } return; } @@ -98,6 +100,8 @@ void LevelFile::Read(std::istream& file) { ReadSkydomeInfo(file); ReadEditorSettings(file); ReadSceneObjectDataChunk(file); + ReadSounds(file); + ReadParticles(file); } LevelFile::ChunkHeader LevelFile::ReadChunkHeader(std::istream& file, uint32_t start) { @@ -247,3 +251,52 @@ void LevelFile::ReadSceneObjectDataChunk(std::istream& file) { objects.push_back(std::move(obj)); } } + +void LevelFile::ReadSounds(std::istream& file) { + // SceneLoader::LoadSounds (0x0103c110), only in files from before chunks + if (version < 5) return; + uint32_t count = 0; + BinaryIO::BinaryRead(file, count); + for (uint32_t i = 0; i < count; ++i) { + LevelSound sound; + BinaryIO::BinaryRead(file, sound.position); + if (version < 35) { + // A 260 character name, up to its first null + std::u16string name(260, u'\0'); + file.read(reinterpret_cast(name.data()), static_cast(name.size() * sizeof(char16_t))); + name.resize(std::min(name.find(u'\0'), name.size())); + sound.config.Insert(u"soundName", name); + } else { + for (const auto& token : GeneralUtils::SplitString(ReadWideString32(file), '\n')) sound.config.ParseInsert(token); + } + if (file.fail()) throw std::runtime_error("Failed to read from istream."); + sounds.push_back(std::move(sound)); + } +} + +void LevelFile::ReadParticles(std::istream& file) { + // SceneLoader::LoadParticles (0x01048320); before version 44 a file may end before them + if (version < 5) return; + if (version < 44 && file.peek() == std::char_traits::eof()) { + file.clear(); + return; + } + + uint32_t count = 0; + BinaryIO::BinaryRead(file, count); + std::string config; + for (uint32_t i = 0; i < count; ++i) { + LevelParticle particle; + if (version >= 43) BinaryIO::BinaryRead(file, particle.priority); + BinaryIO::BinaryRead(file, particle.position); + BinaryIO::BinaryRead(file, particle.rotation); + particle.effectNames = ReadWideString32(file); + if (version < 46) BinaryIO::BinaryRead(file, particle.unknown); + else config = ReadWideString32(file); + particle.config = config; + if (file.fail()) throw std::runtime_error("Failed to read from istream."); + // The client stops at a particle with no effect + if (particle.effectNames.empty()) break; + particles.push_back(std::move(particle)); + } +} diff --git a/dCommon/LevelFile.h b/dCommon/LevelFile.h index c83573ba6..6a85f21cf 100644 --- a/dCommon/LevelFile.h +++ b/dCommon/LevelFile.h @@ -79,9 +79,26 @@ struct LevelSkydome { std::array extraFilenames; }; +// A sound placed in the scene (SceneLoader::LoadSounds, version 5 on). Only the level editor makes anything of them +struct LevelSound { + NiPoint3 position; + LwoNameValue config; // before version 35 only a 260 character soundName +}; + +// A particle effect placed in the scene (SceneLoader::LoadParticles, version 5 on) +struct LevelParticle { + int16_t priority{}; // version 43 on + NiPoint3 position; + NiQuaternion rotation = QuatUtils::IDENTITY; + std::string effectNames; // separated by ';' + uint16_t unknown{}; // before version 46 + // Version 46 on. The client keeps the last one it read for a particle without one + std::string config; +}; + /** * A scene file (.lvl), read as the client reads it (SceneLoader::LoadLvlFile): its chunk headers, environment, every - * object in it with the object's settings parsed. Only reads; which objects the world + * object in it with the object's settings parsed, its sounds and its particles. Only reads; which objects the world * spawns (feature gating, client-only objects, spawners) stays in dZoneManager, so the dashboard reads scenes with the * same code. */ @@ -131,6 +148,8 @@ struct LevelFile { // Version 37 on: the editor's saved colours (the client keeps at most 32) std::vector editorColors; std::vector objects; + std::vector sounds; + std::vector particles; /** * Reads the whole file. Throws std::runtime_error (BinaryIO) if it ends early; what was read before that stays @@ -145,4 +164,6 @@ private: void ReadSkydomeInfo(std::istream& file); void ReadEditorSettings(std::istream& file); void ReadSceneObjectDataChunk(std::istream& file); + void ReadSounds(std::istream& file); + void ReadParticles(std::istream& file); }; diff --git a/tests/dGameTests/dZoneManagerTests/LevelTests.cpp b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp index 3f9d2c937..bc6ffa92d 100644 --- a/tests/dGameTests/dZoneManagerTests/LevelTests.cpp +++ b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp @@ -257,6 +257,13 @@ TEST(LevelTests, ReadsEveryPartOfAChunkedFile) { EXPECT_EQ(technique.renderTechniques[1].valueCount, 4); EXPECT_EQ(technique.renderTechniques[1].isColor, 1); EXPECT_FLOAT_EQ(technique.renderTechniques[1].values[15], 15.0f); + + ASSERT_EQ(level.particles.size(), 2); // the client stops at the one without an effect + EXPECT_EQ(level.particles[0].priority, -2); + EXPECT_EQ(level.particles[0].effectNames, "fx1.psb;fx2.psb"); + EXPECT_EQ(level.particles[0].config, "fxconfig=0:1"); + EXPECT_FLOAT_EQ(level.particles[1].position.y, 5.0f); + EXPECT_TRUE(level.sounds.empty()); } TEST(LevelTests, ReadsAFileFromBeforeChunks) { @@ -273,10 +280,17 @@ TEST(LevelTests, ReadsAFileFromBeforeChunks) { EXPECT_EQ(level.editorColors.size(), 2); ASSERT_EQ(level.objects.size(), 1); + + ASSERT_EQ(level.sounds.size(), 2); + EXPECT_FLOAT_EQ(level.sounds[1].position.z, 13.0f); + EXPECT_EQ(level.sounds[0].config.find(u"soundName")->second->GetValueAsString(), "birds"); + ASSERT_EQ(level.particles.size(), 1); + EXPECT_EQ(level.particles[0].priority, 0); // only from version 43 + EXPECT_EQ(level.particles[0].unknown, 9); } // Older lighting: fog distances only before 39, no directional colour before 36, and before 33 a fixed spawn point -TEST(LevelTests, ReadsOlderLighting) { +TEST(LevelTests, ReadsOlderLightingAndSounds) { const auto v32 = Read(WriteFlat(32)); EXPECT_EQ(v32.revision, 0); EXPECT_FLOAT_EQ(v32.lighting.minDrawDistances.fogFar, 60.0f); @@ -287,16 +301,25 @@ TEST(LevelTests, ReadsOlderLighting) { EXPECT_FLOAT_EQ(v32.lighting.spawnPosition.x, 347.0f); EXPECT_TRUE(v32.skydome.extraFilenames[0].empty()); EXPECT_TRUE(v32.editorColors.empty()); + ASSERT_EQ(v32.sounds.size(), 2); + EXPECT_EQ(v32.sounds[0].config.find(u"soundName")->second->GetValueAsString(), "birds"); ASSERT_EQ(v32.objects.size(), 1); const auto v30 = Read(WriteFlat(30)); EXPECT_FLOAT_EQ(v30.lighting.fogColor.m_Red, 0.75f); // the client's default EXPECT_FLOAT_EQ(v30.lighting.spawnRotation.y, 0.23979919f); EXPECT_EQ(v30.objects.size(), 1); + EXPECT_EQ(v30.particles.size(), 1); } -// A file older than 3 has no objects, a newer one than 48 isn't read +// Before version 44 a file may end before its particles; a file older than 3 has no objects, a newer one than 48 +// isn't read TEST(LevelTests, ReadsWhatTheClientReadsOfOtherFiles) { + const auto noParticles = Read(WriteFlat(43, false)); + EXPECT_EQ(noParticles.objects.size(), 1); + EXPECT_EQ(noParticles.sounds.size(), 2); + EXPECT_TRUE(noParticles.particles.empty()); + const auto old = Read(WriteFlat(2)); EXPECT_TRUE(old.hasLighting); EXPECT_TRUE(old.objects.empty());