From f73f4da0e43f4f5abe5160715d20678334451f37 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 07:44:06 -0500 Subject: [PATCH] fix(level): split a scene object's config as the client does LDF_FROM_STRING (1.10.64, 0x010fa220) splits an object's config at every comma and line break, leaves out empty entries and entries with no '=', and sets each key=value it finds. The reader split at line breaks only and parsed every piece, so an object with no config got an empty entry of unknown type, and a value with a comma would have stayed whole. Live scene files are unchanged; objects in older client files with an empty config no longer get the empty entry. Check in game: every world spawns its objects as before. Co-Authored-By: Claude Opus 5.5 --- dCommon/LevelFile.cpp | 12 ++++++++++-- tests/dCommonTests/ZoneFileTests.cpp | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/dCommon/LevelFile.cpp b/dCommon/LevelFile.cpp index f3d7fa2ff..140c82b63 100644 --- a/dCommon/LevelFile.cpp +++ b/dCommon/LevelFile.cpp @@ -1,5 +1,6 @@ #include "LevelFile.h" +#include #include #include @@ -198,8 +199,15 @@ void LevelFile::ReadSceneObjectDataChunk(std::istream& file, uint32_t version) { } if (file.fail()) throw std::runtime_error("Failed to read from istream."); - for (const auto& token : GeneralUtils::SplitString(GeneralUtils::UTF16ToWTF8(ldfString), '\n')) { - obj.settings.ParseInsert(token); + // LDF_FROM_STRING (0x010fa220): entries are split at every comma and line break, empty ones and ones without a + // '=' are left out + const auto config = GeneralUtils::UTF16ToWTF8(ldfString); + size_t start = 0; + while (start <= config.size()) { + const auto end = std::min(config.find_first_of(",\n", start), config.size()); + const auto entry = config.substr(start, end - start); + if (entry.find('=') != std::string::npos) obj.settings.ParseInsert(entry); + start = end + 1; } ApplyClientConfigFixups(obj, version); diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index 67e05fe9e..6a53294a0 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -1,9 +1,11 @@ #include +#include #include #include #include "Game.h" +#include "GeneralUtils.h" #include "LevelFile.h" #include "Logger.h" #include "ZoneFile.h" @@ -535,6 +537,25 @@ TEST(LevelFileTests, FixesUpConfigAsTheClient) { EXPECT_EQ(value(3, u"DisableModelBehaviors"), "DisableModelBehaviors=7:0"); } +// An object's config splits at commas as well as line breaks; entries without a '=' are left out +TEST(LevelFileTests, SplitsConfigAsTheClient) { + ZoneBytes w; + w.Put(30).Put(0); + w.data.append(48 + 12, '\0'); + w.Put(0).Put(1); + w.Object(30, 2, 0, "a=0:x,b=1:4\n\nstray,c=0:y"); + + std::istringstream stream(w.Done()); + LevelFile level; + level.Read(stream); + ASSERT_EQ(level.objects.size(), 1u); + const auto& settings = level.objects[0].settings; + std::vector keys; + for (const auto& [key, value] : settings) keys.push_back(GeneralUtils::UTF16ToWTF8(key)); + std::ranges::sort(keys); + EXPECT_EQ(keys, (std::vector{ "a", "b", "c" })); +} + TEST(LevelFileTests, DamagedFilesKeepWhatWasRead) { ZoneBytes w; w.Put(30).Put(0);