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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 06:26:11 -05:00
parent 96d73c30a4
commit 458fb601c9
6 changed files with 134 additions and 93 deletions

View File

@@ -416,6 +416,7 @@ TEST(LevelFileTests, ReadsChunkedObjects) {
w.Put<uint32_t>(41).Put<uint32_t>(9).Put<uint32_t>(0).Put<uint32_t>(0).Put<uint32_t>(0);
end(info);
const auto objects = chunk(LevelFile::SceneObjectData);
w.At<uint32_t>(info + 20 + 12, static_cast<uint32_t>(objects)); // the file info gives where the objects start
w.Put<uint32_t>(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<uint16_t>(37).Put<uint16_t>(0).Put<uint8_t>(0).Put<uint32_t>(4); // version, type, important, revision
w.data.append(48 + 8 + 12 + 12 + 12 + 16, '\0'); // lighting
w.Put<uint32_t>(0); // skydome
for (int i = 0; i < 5; i++) w.Put<uint32_t>(0);
w.Put<uint32_t>(7);
w.data.append(7, '\x55'); // editor settings
w.Put<uint32_t>(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<uint16_t>(2).Put<uint16_t>(0);
old.data.append(48 + 12, '\0');
old.Put<uint32_t>(0).Put<uint32_t>(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<uint16_t>(30).Put<uint16_t>(0);

View File

@@ -32,6 +32,7 @@ namespace {
w.Put<uint32_t>(41).Put<uint32_t>(1).Put<uint32_t>(0).Put<uint32_t>(0).Put<uint32_t>(0);
w.End(info);
auto objects = w.Chunk(2001);
w.At<uint32_t>(info + 32, static_cast<uint32_t>(objects)); // the file info chunk says where the objects start, as in real files
w.Put<uint32_t>(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

View File

@@ -255,6 +255,7 @@ TEST(LevelGatingTests, CountsGatedObjects) {
w.Put<uint32_t>(41).Put<uint32_t>(1).Put<uint32_t>(0).Put<uint32_t>(0).Put<uint32_t>(0);
w.End(info);
auto objects = w.Chunk(2001);
w.At<uint32_t>(info + 32, static_cast<uint32_t>(objects)); // the file info chunk says where the objects start, as in real files
w.Put<uint32_t>(5);
w.Object(1000, "gatingOnFeature=13:oct2011content");
w.Object(1001, "custom_config_names=0:\ngatingOnFeature=13:oct2011content\r");

View File

@@ -33,6 +33,7 @@ namespace {
w.Put<uint32_t>(41).Put<uint32_t>(1).Put<uint32_t>(0).Put<uint32_t>(0).Put<uint32_t>(0);
w.End(info);
auto objects = w.Chunk(2001);
w.At<uint32_t>(info + 32, static_cast<uint32_t>(objects)); // the file info chunk says where the objects start, as in real files
w.Put<uint32_t>(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<uint32_t>(41).Put<uint32_t>(1).Put<uint32_t>(0).Put<uint32_t>(0).Put<uint32_t>(0);
w.End(info);
auto objects = w.Chunk(2001);
w.At<uint32_t>(info + 32, static_cast<uint32_t>(objects)); // the file info chunk says where the objects start, as in real files
w.Put<uint32_t>(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