From 96d73c30a477e83c1ad0636c4dfd22657eef901c Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 06:18:29 -0500 Subject: [PATCH] fix(level): read scene objects as the client does ReadLvlObjectData (1.10.64, 0x0103ba20) reads a scene object's last field, from version 7 on, as a render technique count, and when it is not 0 a 64 byte block and 133 bytes per technique (a 64 byte name, a u32, a u8 and 16 floats) follow. The reader took the count for an unknown u32 and read the next object from inside the techniques, and read the u32 for every version. It also left the node type and glom ID uninitialized when a file has none (or, for the node type, one outside 0-10); the client uses 1 for both. The count is now SceneObject::renderTechniqueCount, the techniques are skipped, and the defaults are the client's. Every scene file on disk has no render techniques, so what they read is unchanged. Check in game: every world spawns its objects as before. Co-Authored-By: Claude Opus 5.5 --- dCommon/LevelFile.cpp | 10 +++++++++- dCommon/LevelFile.h | 6 +++--- tests/dCommonTests/ZoneFileTests.cpp | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/dCommon/LevelFile.cpp b/dCommon/LevelFile.cpp index ddf13bc9e..7a2b0819a 100644 --- a/dCommon/LevelFile.cpp +++ b/dCommon/LevelFile.cpp @@ -146,7 +146,15 @@ void LevelFile::ReadSceneObjectDataChunk(std::istream& file, uint32_t version) { BinaryIO::BinaryRead(file, obj.rotation); BinaryIO::BinaryRead(file, obj.scale); BinaryIO::ReadString(file, ldfString); - BinaryIO::BinaryRead(file, obj.value3); + // From version 7 the object's render techniques: a count, and when there are any a 64 byte header and 133 bytes + // each (64 byte name, u32, u8, 16 floats) (ReadLvlObjectData) + if (version > 6) { + BinaryIO::BinaryRead(file, obj.renderTechniqueCount); + if (obj.renderTechniqueCount != 0) { + std::string techniques(64 + static_cast(obj.renderTechniqueCount) * (64 + 4 + 1 + 64), '\0'); + file.read(techniques.data(), static_cast(techniques.size())); + } + } if (file.fail()) throw std::runtime_error("Failed to read from istream."); for (const auto& token : GeneralUtils::SplitString(GeneralUtils::UTF16ToWTF8(ldfString), '\n')) { diff --git a/dCommon/LevelFile.h b/dCommon/LevelFile.h index daaad1c9d..c0631df6d 100644 --- a/dCommon/LevelFile.h +++ b/dCommon/LevelFile.h @@ -13,12 +13,12 @@ struct SceneObject { LWOOBJID id; LOT lot; - uint32_t nodeType; - uint32_t glomId; + uint32_t nodeType = 1; // the client's default when the file has none, or one outside 0-10 + uint32_t glomId = 1; NiPoint3 position; NiQuaternion rotation = QuatUtils::IDENTITY; float scale = 1.0f; - uint32_t value3; + uint32_t renderTechniqueCount{}; LwoNameValue settings; }; diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index 6161162c5..98fa0db05 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -451,6 +451,29 @@ TEST(LevelFileTests, ReadsFilesWithoutChunks) { EXPECT_EQ(level.chunkHeaders.count(LevelFile::SceneObjectData), 1u); } +// An object's render techniques (from version 7) are skipped; the fields a file has no room for get the client's defaults +TEST(LevelFileTests, ReadsObjectsAsTheClient) { + ZoneBytes w; + w.Put(30).Put(0); + w.data.append(48 + 12, '\0'); + w.Put(0).Put(2); + // With two render techniques + w.Put(10).Put(1).Point(1, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(1.0f).Put(0); + w.Put(2); + w.data.append(64 + 2 * 133, '\x7f'); + w.Object(30, 2, 5, "a=0:b"); + + std::istringstream stream(w.Done()); + LevelFile level; + level.Read(stream); + ASSERT_EQ(level.objects.size(), 2u); + EXPECT_EQ(level.objects[0].renderTechniqueCount, 2u); + EXPECT_EQ(level.objects[0].nodeType, 1u); + EXPECT_EQ(level.objects[0].glomId, 1u); + EXPECT_EQ(level.objects[1].lot, 2); + EXPECT_EQ(level.objects[1].position.x, 5.0f); +} + TEST(LevelFileTests, DamagedFilesKeepWhatWasRead) { ZoneBytes w; w.Put(30).Put(0);