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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 07:44:06 -05:00
parent 272fafc886
commit f73f4da0e4
2 changed files with 31 additions and 2 deletions

View File

@@ -1,5 +1,6 @@
#include "LevelFile.h" #include "LevelFile.h"
#include <algorithm>
#include <istream> #include <istream>
#include <stdexcept> #include <stdexcept>
@@ -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."); if (file.fail()) throw std::runtime_error("Failed to read from istream.");
for (const auto& token : GeneralUtils::SplitString(GeneralUtils::UTF16ToWTF8(ldfString), '\n')) { // LDF_FROM_STRING (0x010fa220): entries are split at every comma and line break, empty ones and ones without a
obj.settings.ParseInsert(token); // '=' 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); ApplyClientConfigFixups(obj, version);

View File

@@ -1,9 +1,11 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <algorithm>
#include <cstring> #include <cstring>
#include <sstream> #include <sstream>
#include "Game.h" #include "Game.h"
#include "GeneralUtils.h"
#include "LevelFile.h" #include "LevelFile.h"
#include "Logger.h" #include "Logger.h"
#include "ZoneFile.h" #include "ZoneFile.h"
@@ -535,6 +537,25 @@ TEST(LevelFileTests, FixesUpConfigAsTheClient) {
EXPECT_EQ(value(3, u"DisableModelBehaviors"), "DisableModelBehaviors=7:0"); 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<uint16_t>(30).Put<uint16_t>(0);
w.data.append(48 + 12, '\0');
w.Put<uint32_t>(0).Put<uint32_t>(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<std::string> keys;
for (const auto& [key, value] : settings) keys.push_back(GeneralUtils::UTF16ToWTF8(key));
std::ranges::sort(keys);
EXPECT_EQ(keys, (std::vector<std::string>{ "a", "b", "c" }));
}
TEST(LevelFileTests, DamagedFilesKeepWhatWasRead) { TEST(LevelFileTests, DamagedFilesKeepWhatWasRead) {
ZoneBytes w; ZoneBytes w;
w.Put<uint16_t>(30).Put<uint16_t>(0); w.Put<uint16_t>(30).Put<uint16_t>(0);