diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index adf985f0f..d0443ae40 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -199,6 +199,18 @@ namespace { if (file.fail()) throw std::runtime_error("Failed to read from istream."); return GeneralUtils::UTF16ToWTF8(value); } + + // LwoNameValue::FromString (0x010fa220): entries are split at every comma and line break, empty ones and ones + // without a '=' are left out + void ParseConfig(LwoNameValue& config, const std::string& text) { + size_t start = 0; + while (start <= text.size()) { + const auto end = std::min(text.find_first_of(",\n", start), text.size()); + const auto entry = text.substr(start, end - start); + if (entry.find('=') != std::string::npos) config.ParseInsert(entry); + start = end + 1; + } + } } void Level::ReadChunks(std::istream& file) { @@ -418,12 +430,49 @@ void Level::ReadSceneObjectDataChunk(std::istream& file) { } if (file.fail()) throw std::runtime_error("Failed to read from istream."); - for (const auto& token : GeneralUtils::SplitString(config, '\n')) obj.settings.ParseInsert(token); + ParseConfig(obj.settings, config); + ApplyClientConfigFixups(obj); m_Objects.push_back(std::move(obj)); } } +void Level::ApplyClientConfigFixups(SceneObject& obj) const { + // What ReadLvlObjectData changes in an object's config as it loads it + auto& settings = obj.settings; + const auto typeOf = [&settings](const std::u16string& key) { + const auto it = settings.find(key); + return it == settings.end() || !it->second ? LDF_TYPE_UNKNOWN : it->second->GetValueType(); + }; + const auto valueOf = [&settings](const std::u16string& key) { + return static_cast*>(settings.find(key)->second.get())->GetValue(); + }; + + // Before version 47 a respawn time was in milliseconds: a float over 100, or any u32, becomes seconds (a float) + if (m_Version < 47) { + if (typeOf(u"respawn") == LDF_TYPE_FLOAT) { + const auto respawn = valueOf.operator()(u"respawn"); + if (respawn > 100.0f) settings.Insert(u"respawn", respawn * 0.001f); + } else if (typeOf(u"respawn") == LDF_TYPE_U32) { + settings.Insert(u"respawn", static_cast(valueOf.operator()(u"respawn") * 0.001)); + } + } + + // A model spawner (LOT 176 spawning template 14): its blueprint is its subkey unless it has one, its behaviors are + // off unless it says otherwise, and it never wraps its render + if (obj.lot == 176 && typeOf(u"spawntemplate") == LDF_TYPE_S32 && valueOf.operator()(u"spawntemplate") == 14) { + if (typeOf(u"blueprintid") == LDF_TYPE_UNKNOWN) { + const LWOOBJID subkey = typeOf(u"subkey") == LDF_TYPE_OBJID ? valueOf.operator()(u"subkey") : LWOOBJID_EMPTY; + settings.Insert(u"blueprintid", subkey); + } + if (typeOf(u"DisableModelBehaviors") == LDF_TYPE_UNKNOWN) settings.Insert(u"DisableModelBehaviors", true); + settings.Insert(u"preventRenderWrapping", true); + } + + // Before version 44 an object that overrides its scene goes to layer 0 of the scene it names + if (m_Version < 44 && typeOf(u"sceneIDOverrideEnabled") != LDF_TYPE_UNKNOWN) settings.Insert(u"sceneLayerIDOverride", 0); +} + void Level::ReadSounds(std::istream& file) { // SceneLoader::LoadSounds (0x0103c110), only in files from before chunks if (m_Version < 5) return; @@ -439,7 +488,7 @@ void Level::ReadSounds(std::istream& file) { 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); + ParseConfig(sound.config, ReadWideString32(file)); } if (file.fail()) throw std::runtime_error("Failed to read from istream."); m_Sounds.push_back(std::move(sound)); diff --git a/dZoneManager/Level.h b/dZoneManager/Level.h index 5aef7d8d8..f91ac6266 100644 --- a/dZoneManager/Level.h +++ b/dZoneManager/Level.h @@ -155,6 +155,7 @@ private: void ReadSkydomeInfo(std::istream& file); void ReadEditorSettings(std::istream& file); void ReadSceneObjectDataChunk(std::istream& file); + void ApplyClientConfigFixups(SceneObject& obj) const; 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 04d83ac26..aff6604c9 100644 --- a/tests/dGameTests/dZoneManagerTests/LevelTests.cpp +++ b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp @@ -271,6 +271,32 @@ TEST_F(LevelTests, ReadsEveryPartOfAChunkedFile) { EXPECT_TRUE(level.GetSounds().empty()); } +// LwoNameValue::FromString splits at commas and line breaks and leaves out entries without a '='; ReadLvlObjectData +// adjusts some values as it loads the object +TEST_F(LevelTests, ObjectConfigIsReadAndAdjustedAsTheClientDoes) { + const auto level = Read(WriteChunked(43)); + ASSERT_EQ(level.GetObjects().size(), 3); + + const auto& spawner = level.GetObjects()[0].settings; + ASSERT_NE(spawner.find(u"blueprintid"), spawner.end()); + EXPECT_EQ(spawner.find(u"blueprintid")->second->GetValueAsString(), "55"); + EXPECT_EQ(spawner.find(u"DisableModelBehaviors")->second->GetValueAsString(), "1"); + EXPECT_EQ(spawner.find(u"preventRenderWrapping")->second->GetValueAsString(), "1"); + // A float respawn over 100 was milliseconds + EXPECT_EQ(spawner.find(u"respawn")->second->GetValueType(), LDF_TYPE_FLOAT); + EXPECT_FLOAT_EQ(static_cast*>(spawner.find(u"respawn")->second.get())->GetValue(), 250.0f * 0.001f); + + const auto& overridden = level.GetObjects()[2].settings; + EXPECT_EQ(overridden.values.size(), 2); // the entry without a '=' is left out + ASSERT_NE(overridden.find(u"sceneLayerIDOverride"), overridden.end()); + EXPECT_EQ(overridden.find(u"sceneLayerIDOverride")->second->GetValueAsString(), "0"); + + // From version 47 a respawn is in seconds already, and from 44 the scene override stays + const auto newer = Read(WriteChunked(47)); + EXPECT_FLOAT_EQ(static_cast*>(newer.GetObjects()[0].settings.find(u"respawn")->second.get())->GetValue(), 250.0f); + EXPECT_EQ(newer.GetObjects()[2].settings.find(u"sceneLayerIDOverride"), newer.GetObjects()[2].settings.end()); +} + TEST_F(LevelTests, ReadsAFileFromBeforeChunks) { const auto level = Read(WriteFlat(40)); EXPECT_EQ(level.GetVersion(), 40); @@ -285,6 +311,10 @@ TEST_F(LevelTests, ReadsAFileFromBeforeChunks) { EXPECT_EQ(level.GetEditorColors().size(), 2); ASSERT_EQ(level.GetObjects().size(), 1); + // Before version 47 a u32 respawn is milliseconds; it becomes seconds + const auto& respawn = level.GetObjects()[0].settings.find(u"respawn")->second; + EXPECT_EQ(respawn->GetValueType(), LDF_TYPE_FLOAT); + EXPECT_FLOAT_EQ(static_cast*>(respawn.get())->GetValue(), 20.0f); ASSERT_EQ(level.GetSounds().size(), 2); EXPECT_FLOAT_EQ(level.GetSounds()[1].position.z, 13.0f);