diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index f9f6b679f..adf985f0f 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -243,6 +243,8 @@ void Level::ReadChunks(std::istream& file) { if (fileInfo.fileInfo.particleChunkStart != 0) { const auto header = ReadChunkHeader(file, fileInfo.fileInfo.particleChunkStart); m_ChunkHeaders.insert({ header.id, header }); + file.seekg(header.startPosition); + ReadParticles(file); } return; } @@ -270,6 +272,8 @@ void Level::ReadChunks(std::istream& file) { ReadSkydomeInfo(file); ReadEditorSettings(file); ReadSceneObjectDataChunk(file); + ReadSounds(file); + ReadParticles(file); } Level::Header Level::ReadChunkHeader(std::istream& file, uint32_t start) { @@ -419,3 +423,52 @@ void Level::ReadSceneObjectDataChunk(std::istream& file) { m_Objects.push_back(std::move(obj)); } } + +void Level::ReadSounds(std::istream& file) { + // SceneLoader::LoadSounds (0x0103c110), only in files from before chunks + if (m_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 (m_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."); + m_Sounds.push_back(std::move(sound)); + } +} + +void Level::ReadParticles(std::istream& file) { + // SceneLoader::LoadParticles (0x01048320); before version 44 a file may end before them + if (m_Version < 5) return; + if (m_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 (m_Version >= 43) BinaryIO::BinaryRead(file, particle.priority); + BinaryIO::BinaryRead(file, particle.position); + BinaryIO::BinaryRead(file, particle.rotation); + particle.effectNames = ReadWideString32(file); + if (m_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; + m_Particles.push_back(std::move(particle)); + } +} diff --git a/dZoneManager/Level.h b/dZoneManager/Level.h index 56a72485b..5aef7d8d8 100644 --- a/dZoneManager/Level.h +++ b/dZoneManager/Level.h @@ -50,6 +50,23 @@ 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) class Level { public: @@ -108,6 +125,8 @@ public: const LevelSkydome& GetSkydome() const { return m_Skydome; } const std::vector& GetEditorColors() const { return m_EditorColors; } const std::vector& GetObjects() const { return m_Objects; } + const std::vector& GetSounds() const { return m_Sounds; } + const std::vector& GetParticles() const { return m_Particles; } std::map m_ChunkHeaders; private: @@ -125,6 +144,8 @@ private: // Version 37 on: the editor's saved colours (the client keeps at most 32) std::vector m_EditorColors; std::vector m_Objects; + std::vector m_Sounds; + std::vector m_Particles; //private functions: void ReadChunks(std::istream& file); @@ -134,5 +155,7 @@ 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); void LoadSceneObjects(); }; diff --git a/tests/dGameTests/dZoneManagerTests/LevelTests.cpp b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp index 2bb1fb4a3..04d83ac26 100644 --- a/tests/dGameTests/dZoneManagerTests/LevelTests.cpp +++ b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp @@ -262,6 +262,13 @@ TEST_F(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.GetParticles().size(), 2); // the client stops at the one without an effect + EXPECT_EQ(level.GetParticles()[0].priority, -2); + EXPECT_EQ(level.GetParticles()[0].effectNames, "fx1.psb;fx2.psb"); + EXPECT_EQ(level.GetParticles()[0].config, "fxconfig=0:1"); + EXPECT_FLOAT_EQ(level.GetParticles()[1].position.y, 5.0f); + EXPECT_TRUE(level.GetSounds().empty()); } TEST_F(LevelTests, ReadsAFileFromBeforeChunks) { @@ -278,10 +285,17 @@ TEST_F(LevelTests, ReadsAFileFromBeforeChunks) { EXPECT_EQ(level.GetEditorColors().size(), 2); ASSERT_EQ(level.GetObjects().size(), 1); + + ASSERT_EQ(level.GetSounds().size(), 2); + EXPECT_FLOAT_EQ(level.GetSounds()[1].position.z, 13.0f); + EXPECT_EQ(level.GetSounds()[0].config.find(u"soundName")->second->GetValueAsString(), "birds"); + ASSERT_EQ(level.GetParticles().size(), 1); + EXPECT_EQ(level.GetParticles()[0].priority, 0); // only from version 43 + EXPECT_EQ(level.GetParticles()[0].unknown, 9); } // Older lighting: fog distances only before 39, no directional colour before 36, and before 33 a fixed spawn point -TEST_F(LevelTests, ReadsOlderLighting) { +TEST_F(LevelTests, ReadsOlderLightingAndSounds) { const auto v32 = Read(WriteFlat(32)); EXPECT_EQ(v32.GetRevision(), 0); EXPECT_FLOAT_EQ(v32.GetLighting().minDrawDistances.fogFar, 60.0f); @@ -292,16 +306,25 @@ TEST_F(LevelTests, ReadsOlderLighting) { EXPECT_FLOAT_EQ(v32.GetLighting().spawnPosition.x, 347.0f); EXPECT_TRUE(v32.GetSkydome().extraFilenames[0].empty()); EXPECT_TRUE(v32.GetEditorColors().empty()); + ASSERT_EQ(v32.GetSounds().size(), 2); + EXPECT_EQ(v32.GetSounds()[0].config.find(u"soundName")->second->GetValueAsString(), "birds"); ASSERT_EQ(v32.GetObjects().size(), 1); const auto v30 = Read(WriteFlat(30)); EXPECT_FLOAT_EQ(v30.GetLighting().fogColor.m_Red, 0.75f); // the client's default EXPECT_FLOAT_EQ(v30.GetLighting().spawnRotation.y, 0.23979919f); EXPECT_EQ(v30.GetObjects().size(), 1); + EXPECT_EQ(v30.GetParticles().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_F(LevelTests, ReadsWhatTheClientReadsOfOtherFiles) { + const auto noParticles = Read(WriteFlat(43, false)); + EXPECT_EQ(noParticles.GetObjects().size(), 1); + EXPECT_EQ(noParticles.GetSounds().size(), 2); + EXPECT_TRUE(noParticles.GetParticles().empty()); + const auto old = Read(WriteFlat(2)); EXPECT_TRUE(old.HasLighting()); EXPECT_TRUE(old.GetObjects().empty());