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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 06:18:29 -05:00
parent 13a3d18789
commit 96d73c30a4
3 changed files with 35 additions and 4 deletions

View File

@@ -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<uint32_t>(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<size_t>(obj.renderTechniqueCount) * (64 + 4 + 1 + 64), '\0');
file.read(techniques.data(), static_cast<std::streamsize>(techniques.size()));
}
}
if (file.fail()) throw std::runtime_error("Failed to read from istream.");
for (const auto& token : GeneralUtils::SplitString(GeneralUtils::UTF16ToWTF8(ldfString), '\n')) {

View File

@@ -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;
};

View File

@@ -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<uint16_t>(30).Put<uint16_t>(0);
w.data.append(48 + 12, '\0');
w.Put<uint32_t>(0).Put<uint32_t>(2);
// With two render techniques
w.Put<int64_t>(10).Put<int32_t>(1).Point(1, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(1.0f).Put<uint32_t>(0);
w.Put<uint32_t>(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<uint16_t>(30).Put<uint16_t>(0);