From 458fb601c93641a130cdb5c75c0b963d27e90d4b Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 06:26:11 -0500 Subject: [PATCH] fix(level): find a scene file's chunks and sections as the client does SceneLoader::ReadLvlFile (1.10.64): - chunked files: the file info chunk starts the file and gives where the environment, object and particle chunks start, 0 for none (DoLvlChunk, ReadLvlChunk1000); the reader walked the chunks one after another by their sizes instead; - older files: the editor settings are a u32 size and that many bytes (SceneLoader::ReadEditorSettings, skipped by size); the reader took them for a u32 and then a count of 12 byte points; - older files before version 3 have no objects for the client ("Level file is unsupported"). LevelFile now does the same. Every scene file on disk reads the same objects as before. Check in game: every world spawns its objects as before. Co-Authored-By: Claude Opus 5.5 --- dCommon/LevelFile.cpp | 191 +++++++++++----------- dCommon/LevelFile.h | 1 + tests/dCommonTests/ZoneFileTests.cpp | 31 ++++ tests/dWebTests/LevelObjectsTests.cpp | 1 + tests/dWebTests/ServerOperationsTests.cpp | 1 + tests/dWebTests/WorldViewTests.cpp | 2 + 6 files changed, 134 insertions(+), 93 deletions(-) diff --git a/dCommon/LevelFile.cpp b/dCommon/LevelFile.cpp index 7a2b0819a..326a15366 100644 --- a/dCommon/LevelFile.cpp +++ b/dCommon/LevelFile.cpp @@ -9,109 +9,114 @@ void LevelFile::Read(std::istream& file) { const uint32_t CHNK_HEADER = ('C' + ('H' << 8) + ('N' << 16) + ('K' << 24)); - while (!file.eof()) { - uint32_t initPos = uint32_t(file.tellg()); - uint32_t header = 0; - BinaryIO::BinaryRead(file, header); - if (header == CHNK_HEADER) { //Make sure we're reading a valid CHNK - ChunkHeader header; - BinaryIO::BinaryRead(file, header.id); - BinaryIO::BinaryRead(file, header.chunkVersion); - BinaryIO::BinaryRead(file, header.chunkType); - BinaryIO::BinaryRead(file, header.size); - BinaryIO::BinaryRead(file, header.startPosition); - - uint32_t target = initPos + header.size; + uint32_t magic = 0; + BinaryIO::BinaryRead(file, magic); + if (magic == CHNK_HEADER) { + // SceneLoader::ReadLvlFile / DoLvlChunk: the file info chunk starts the file and gives where the environment, + // object and particle chunks start (0 for none); only the objects are read here + ChunkHeader fileInfo = ReadChunkHeader(file, 0); + file.seekg(fileInfo.startPosition); + ReadFileInfoChunk(file, fileInfo); + chunkHeaders.insert({ fileInfo.id, fileInfo }); + for (const auto start : { fileInfo.fileInfo.enviromentChunkStart, fileInfo.fileInfo.objectChunkStart, fileInfo.fileInfo.particleChunkStart }) { + if (start == 0) continue; + const auto header = ReadChunkHeader(file, start); + chunkHeaders.insert({ header.id, header }); + if (start != fileInfo.fileInfo.objectChunkStart) continue; + // The objects are laid out by the file's version, from its file info chunk file.seekg(header.startPosition); + ReadSceneObjectDataChunk(file, fileInfo.fileInfo.version); + } + return; + } - //We're currently not loading env or particle data - if (header.id == ChunkTypeID::FileInfo) { - ReadFileInfoChunk(file, header); - } else if (header.id == ChunkTypeID::SceneObjectData) { - // The objects are laid out by the file's version, from its file info chunk - const auto fileInfo = chunkHeaders.find(ChunkTypeID::FileInfo); - ReadSceneObjectDataChunk(file, fileInfo == chunkHeaders.end() ? 0 : fileInfo->second.fileInfo.version); + // Scenes from before chunks (SceneLoader::ReadLvlSections): a header, then lighting, skydome and editor settings + // the world skips, then the objects + file.seekg(0); + ChunkHeader header; + header.id = ChunkTypeID::FileInfo; + BinaryIO::BinaryRead(file, header.chunkVersion); + BinaryIO::BinaryRead(file, header.chunkType); + // Only from version 32 (SceneLoader::ReadLvlHeader) + uint8_t important = 0; + if (header.chunkVersion >= 32) BinaryIO::BinaryRead(file, important); + if (header.chunkVersion > 36) { + BinaryIO::BinaryRead(file, header.fileInfo.revision); + } + // Lighting (SceneLoader::ReadLightingInfo) + if (header.chunkVersion >= 45) file.ignore(4); + file.ignore(4 * (4 * 3)); + + if (header.chunkVersion >= 31) { + if (header.chunkVersion >= 39) { + file.ignore(12 * 4); + + if (header.chunkVersion >= 40) { + uint32_t s = 0; + BinaryIO::BinaryRead(file, s); + for (uint32_t i = 0; i < s; ++i) { + file.ignore(4 * 3); //a uint and two floats + } } - - chunkHeaders.insert(std::make_pair(header.id, header)); - file.seekg(target); } else { - if (initPos == std::streamoff(0)) { //Really old chunk version - file.seekg(0); - ChunkHeader header; - header.id = ChunkTypeID::FileInfo; - BinaryIO::BinaryRead(file, header.chunkVersion); - BinaryIO::BinaryRead(file, header.chunkType); - // Only from version 32 (SceneLoader::ReadLvlHeader) - uint8_t important = 0; - if (header.chunkVersion >= 32) BinaryIO::BinaryRead(file, important); - if (header.chunkVersion > 36) { - BinaryIO::BinaryRead(file, header.fileInfo.revision); - } - // HARDCODED 3 - if (header.chunkVersion >= 45) file.ignore(4); - file.ignore(4 * (4 * 3)); + file.ignore(8); + } - if (header.chunkVersion >= 31) { - if (header.chunkVersion >= 39) { - file.ignore(12 * 4); + file.ignore(3 * 4); + } - if (header.chunkVersion >= 40) { - uint32_t s = 0; - BinaryIO::BinaryRead(file, s); - for (uint32_t i = 0; i < s; ++i) { - file.ignore(4 * 3); //a uint and two floats - } - } - } else { - file.ignore(8); - } + if (header.chunkVersion >= 36) { + file.ignore(3 * 4); + } - file.ignore(3 * 4); - } + if (header.chunkVersion < 42) { + file.ignore(3 * 4); - if (header.chunkVersion >= 36) { - file.ignore(3 * 4); - } - - if (header.chunkVersion < 42) { - file.ignore(3 * 4); - - if (header.chunkVersion >= 33) { - file.ignore(4 * 4); - } - } - - // skydome info - uint32_t count = 0; - BinaryIO::BinaryRead(file, count); - file.ignore(count); - - // Five more strings from version 34 (SceneLoader::ReadSkydomeInfo) - if (header.chunkVersion >= 34) { - for (uint32_t i = 0; i < 5; ++i) { - uint32_t count = 0; - BinaryIO::BinaryRead(file, count); - file.ignore(count); - } - } - // editor settings - if (!important && header.chunkVersion >= 37){ - file.ignore(4); - - uint32_t count = 0; - BinaryIO::BinaryRead(file, count); - file.ignore(count * 12); - - } - - header.id = ChunkTypeID::SceneObjectData; - header.fileInfo.version = header.chunkVersion; - ReadSceneObjectDataChunk(file, header.fileInfo.version); - chunkHeaders.insert(std::make_pair(header.id, header)); - } break; + if (header.chunkVersion >= 33) { + file.ignore(4 * 4); } } + + // skydome info; five more strings from version 34 (SceneLoader::ReadSkydomeInfo) + uint32_t count = 0; + BinaryIO::BinaryRead(file, count); + file.ignore(count); + + if (header.chunkVersion >= 34) { + for (uint32_t i = 0; i < 5; ++i) { + uint32_t count = 0; + BinaryIO::BinaryRead(file, count); + file.ignore(count); + } + } + // editor settings: their size and then that many bytes (SceneLoader::ReadEditorSettings) + if (!important && header.chunkVersion >= 37) { + uint32_t size = 0; + BinaryIO::BinaryRead(file, size); + file.ignore(size); + } + + // The client has no objects for a file older than version 3 ("Level file is unsupported") + header.fileInfo.version = header.chunkVersion; + if (header.chunkVersion >= 3) { + header.id = ChunkTypeID::SceneObjectData; + ReadSceneObjectDataChunk(file, header.fileInfo.version); + } + chunkHeaders.insert(std::make_pair(header.id, header)); +} + +LevelFile::ChunkHeader LevelFile::ReadChunkHeader(std::istream& file, uint32_t start) { + // No check of the magic, as in the client (0x01063a00) + file.seekg(start); + uint32_t magic = 0; + ChunkHeader header; + BinaryIO::BinaryRead(file, magic); + BinaryIO::BinaryRead(file, header.id); + BinaryIO::BinaryRead(file, header.chunkVersion); + BinaryIO::BinaryRead(file, header.chunkType); + BinaryIO::BinaryRead(file, header.size); + BinaryIO::BinaryRead(file, header.startPosition); + return header; } void LevelFile::ReadFileInfoChunk(std::istream& file, ChunkHeader& header) { diff --git a/dCommon/LevelFile.h b/dCommon/LevelFile.h index c0631df6d..881517a45 100644 --- a/dCommon/LevelFile.h +++ b/dCommon/LevelFile.h @@ -62,6 +62,7 @@ struct LevelFile { void Read(std::istream& file); private: + static ChunkHeader ReadChunkHeader(std::istream& file, uint32_t start); void ReadFileInfoChunk(std::istream& file, ChunkHeader& header); void ReadSceneObjectDataChunk(std::istream& file, uint32_t version); }; diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index 98fa0db05..ff77ea421 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -416,6 +416,7 @@ TEST(LevelFileTests, ReadsChunkedObjects) { w.Put(41).Put(9).Put(0).Put(0).Put(0); end(info); const auto objects = chunk(LevelFile::SceneObjectData); + w.At(info + 20 + 12, static_cast(objects)); // the file info gives where the objects start w.Put(2); w.Object(41, 4945, 10, "respawnname=0:NS_LW_Portal\nloadOnClientOnly=7:1"); w.Object(41, 176, -4, "spawntemplate=1:6010"); @@ -474,6 +475,36 @@ TEST(LevelFileTests, ReadsObjectsAsTheClient) { EXPECT_EQ(level.objects[1].position.x, 5.0f); } +// Editor settings are skipped by their size; a file older than version 3 has no objects for the client +TEST(LevelFileTests, SkipsEditorSettingsBySize) { + ZoneBytes w; + w.Put(37).Put(0).Put(0).Put(4); // version, type, important, revision + w.data.append(48 + 8 + 12 + 12 + 12 + 16, '\0'); // lighting + w.Put(0); // skydome + for (int i = 0; i < 5; i++) w.Put(0); + w.Put(7); + w.data.append(7, '\x55'); // editor settings + w.Put(1); + w.Object(37, 4945, 3, "a=0:b"); + + std::istringstream stream(w.Done()); + LevelFile level; + level.Read(stream); + ASSERT_EQ(level.objects.size(), 1u); + EXPECT_EQ(level.objects[0].lot, 4945); + EXPECT_EQ(level.chunkHeaders.begin()->second.fileInfo.revision, 4u); + + ZoneBytes old; + old.Put(2).Put(0); + old.data.append(48 + 12, '\0'); + old.Put(0).Put(1); + old.Object(2, 1, 0, "a=0:b"); + std::istringstream oldStream(old.Done()); + LevelFile oldLevel; + oldLevel.Read(oldStream); + EXPECT_TRUE(oldLevel.objects.empty()); +} + TEST(LevelFileTests, DamagedFilesKeepWhatWasRead) { ZoneBytes w; w.Put(30).Put(0); diff --git a/tests/dWebTests/LevelObjectsTests.cpp b/tests/dWebTests/LevelObjectsTests.cpp index 278f3b9fe..719462606 100644 --- a/tests/dWebTests/LevelObjectsTests.cpp +++ b/tests/dWebTests/LevelObjectsTests.cpp @@ -32,6 +32,7 @@ namespace { w.Put(41).Put(1).Put(0).Put(0).Put(0); w.End(info); auto objects = w.Chunk(2001); + w.At(info + 32, static_cast(objects)); // the file info chunk says where the objects start, as in real files w.Put(4); w.Object(4945, 10, 20, 30, "respawnname=0:NS_LW_Portal\ncustom_config_names=0:"); w.Object(6010, 1, 1, 1, "spawntemplate=1:6010"); // not a spawn point diff --git a/tests/dWebTests/ServerOperationsTests.cpp b/tests/dWebTests/ServerOperationsTests.cpp index 96d7a8041..4710440a1 100644 --- a/tests/dWebTests/ServerOperationsTests.cpp +++ b/tests/dWebTests/ServerOperationsTests.cpp @@ -255,6 +255,7 @@ TEST(LevelGatingTests, CountsGatedObjects) { w.Put(41).Put(1).Put(0).Put(0).Put(0); w.End(info); auto objects = w.Chunk(2001); + w.At(info + 32, static_cast(objects)); // the file info chunk says where the objects start, as in real files w.Put(5); w.Object(1000, "gatingOnFeature=13:oct2011content"); w.Object(1001, "custom_config_names=0:\ngatingOnFeature=13:oct2011content\r"); diff --git a/tests/dWebTests/WorldViewTests.cpp b/tests/dWebTests/WorldViewTests.cpp index 912c9b2fc..f1a0bab7c 100644 --- a/tests/dWebTests/WorldViewTests.cpp +++ b/tests/dWebTests/WorldViewTests.cpp @@ -33,6 +33,7 @@ namespace { w.Put(41).Put(1).Put(0).Put(0).Put(0); w.End(info); auto objects = w.Chunk(2001); + w.At(info + 32, static_cast(objects)); // the file info chunk says where the objects start, as in real files w.Put(3); w.Object(10, 4945, 1, 2, 3, "respawnname=0:NS_Portal\r"); w.Object(11, 176, -4, 0, 8, "spawntemplate=1:6010\nspawner_name=0:Crates"); @@ -121,6 +122,7 @@ TEST(WorldSceneTests, ClientDrawsWhatTheGameDraws) { w.Put(41).Put(1).Put(0).Put(0).Put(0); w.End(info); auto objects = w.Chunk(2001); + w.At(info + 32, static_cast(objects)); // the file info chunk says where the objects start, as in real files w.Put(6); w.Object(1, 4630, 0, 0, 0, "carver_only=7:1\ncreate_physics=7:1"); // an invisible trigger cube w.Object(2, 5651, 0, 0, 0, "carver_only=7:0"); // loaded, so drawn