diff --git a/dCommon/LevelFile.cpp b/dCommon/LevelFile.cpp index 1c36a4e62..0a5409c8e 100644 --- a/dCommon/LevelFile.cpp +++ b/dCommon/LevelFile.cpp @@ -27,6 +27,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 LevelFile::Read(std::istream& file) { @@ -246,12 +258,49 @@ void LevelFile::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); objects.push_back(std::move(obj)); } } +void LevelFile::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 (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 (version < 44 && typeOf(u"sceneIDOverrideEnabled") != LDF_TYPE_UNKNOWN) settings.Insert(u"sceneLayerIDOverride", 0); +} + void LevelFile::ReadSounds(std::istream& file) { // SceneLoader::LoadSounds (0x0103c110), only in files from before chunks if (version < 5) return; @@ -267,7 +316,7 @@ void LevelFile::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."); sounds.push_back(std::move(sound)); diff --git a/dCommon/LevelFile.h b/dCommon/LevelFile.h index be176f14c..42fb350c8 100644 --- a/dCommon/LevelFile.h +++ b/dCommon/LevelFile.h @@ -163,6 +163,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); }; diff --git a/tests/dGameTests/dZoneManagerTests/LevelTests.cpp b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp index b81a75c06..024e7078e 100644 --- a/tests/dGameTests/dZoneManagerTests/LevelTests.cpp +++ b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp @@ -266,6 +266,32 @@ TEST(LevelTests, ReadsEveryPartOfAChunkedFile) { EXPECT_TRUE(level.sounds.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(LevelTests, ObjectConfigIsReadAndAdjustedAsTheClientDoes) { + const auto level = Read(WriteChunked(43)); + ASSERT_EQ(level.objects.size(), 3); + + const auto& spawner = level.objects[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.objects[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.objects[0].settings.find(u"respawn")->second.get())->GetValue(), 250.0f); + EXPECT_EQ(newer.objects[2].settings.find(u"sceneLayerIDOverride"), newer.objects[2].settings.end()); +} + TEST(LevelTests, ReadsAFileFromBeforeChunks) { const auto level = Read(WriteFlat(40)); EXPECT_EQ(level.version, 40); @@ -280,6 +306,10 @@ TEST(LevelTests, ReadsAFileFromBeforeChunks) { EXPECT_EQ(level.editorColors.size(), 2); ASSERT_EQ(level.objects.size(), 1); + // Before version 47 a u32 respawn is milliseconds; it becomes seconds + const auto& respawn = level.objects[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.sounds.size(), 2); EXPECT_FLOAT_EQ(level.sounds[1].position.z, 13.0f);