From 6a8e8ece0442c4f9945eafc9d8d313f40aeca8f4 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 05:54:29 -0500 Subject: [PATCH] fix(zone): keep the scenes of a zone file the client loads LuzReader::ReadScenes (1.10.64) keeps a zone's scenes by scene ID and layer, so a later scene with the same ID and layer replaces an earlier one. ZoneLoader::ReadZoneFile then goes through them in scene ID order and - leaves out every layer of a scene ID that has no layer 0 (General) scene; - before version 41 loads only the layer 0 scene of each ID; from 41 on the other layers (Audio, FX) are loaded with it. ZoneFile now keeps the scenes the same way, so the world loads the same scenes as the client. No zone file on disk has a duplicate, a scene without a General layer, or a non-General layer before version 41, and their scenes are already in ID order: what every file reads is unchanged. Check in game: nothing to check (no live zone changes). Co-Authored-By: Claude Opus 5.5 --- dCommon/ZoneFile.cpp | 16 ++++++++++++++ dCommon/ZoneFile.h | 1 + tests/dCommonTests/ZoneFileTests.cpp | 33 ++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/dCommon/ZoneFile.cpp b/dCommon/ZoneFile.cpp index 12825e3dc..4e9b9ee34 100644 --- a/dCommon/ZoneFile.cpp +++ b/dCommon/ZoneFile.cpp @@ -35,6 +35,7 @@ void ZoneFile::ReadHeader(std::istream& file) { for (uint32_t i = 0; i < sceneCount; ++i) { ReadScene(file, i); } + KeepLoadedScenes(); ReadZoneBoundaries(file); @@ -106,6 +107,21 @@ void ZoneFile::ReadScene(std::istream& file, uint32_t index) { scenes.push_back(std::move(scene)); } +void ZoneFile::KeepLoadedScenes() { + if (fileFormatVersion < FileFormatVersion::PrePreAlpha) return; // ResolveSceneTable does this for those + // LuzReader::ReadScenes keeps the scenes by scene ID and layer, a later one replacing an earlier one. + // ZoneLoader::ReadZoneFile then goes through them by scene ID: a scene ID with no General layer is left out, and + // before Latest only the General layer is loaded + std::map, ZoneScene> byID; + for (auto& scene : scenes) byID.insert_or_assign({ scene.id, scene.sceneType }, std::move(scene)); + scenes.clear(); + for (auto& [key, scene] : byID) { + if (!byID.contains({ key.first, eSceneType::General })) continue; + if (key.second != eSceneType::General && fileFormatVersion < FileFormatVersion::Latest) continue; + scenes.push_back(std::move(scene)); + } +} + void ZoneFile::ResolveSceneTable(const std::function(uint32_t)>& sceneName) { if (fileFormatVersion >= FileFormatVersion::PrePreAlpha) return; // ZoneLoader::ReadZoneFile: the scenes go by their SceneTable ID; each one with a row there gets the next index diff --git a/dCommon/ZoneFile.h b/dCommon/ZoneFile.h index e560a93f4..341501f8f 100644 --- a/dCommon/ZoneFile.h +++ b/dCommon/ZoneFile.h @@ -256,6 +256,7 @@ struct ZoneFile { private: void ReadScene(std::istream& file, uint32_t index); + void KeepLoadedScenes(); void ReadZoneBoundaries(std::istream& file); void ReadSceneTransition(std::istream& file); SceneTransitionInfo ReadSceneTransitionInfo(std::istream& file); diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index 646741034..3a2df7596 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -31,7 +31,7 @@ namespace { w.Put(41).Put(3).Put(1150); // version, revision, world w.Point(1, 2, 3).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f); // spawn point and rotation w.Put(1); - w.Text("scene.lvl").Put(7).Put(2).Text("Global").Put(1).Put(2).Put(3); + w.Text("scene.lvl").Put(7).Put(0).Text("Global").Put(1).Put(2).Put(3); w.Put(0).Text("zone.raw").Text("Name").Text("Description"); // no zone boundaries w.Put(1); for (int i = 0; i < 2; i++) w.Put(1).Point(0, 0, 0); @@ -65,7 +65,7 @@ TEST(ZoneFileTests, ReadsScenesAndPaths) { ASSERT_EQ(zone.scenes.size(), 1u); EXPECT_EQ(zone.scenes[0].filename, "scene.lvl"); EXPECT_EQ(zone.scenes[0].id, 7u); - EXPECT_EQ(zone.scenes[0].sceneType, eSceneType::FX); + EXPECT_EQ(zone.scenes[0].sceneType, eSceneType::General); EXPECT_EQ(zone.zoneRawPath, "zone.raw"); ASSERT_EQ(zone.sceneTransitions.size(), 1u); EXPECT_EQ(zone.sceneTransitions[0].points.size(), 2u); @@ -183,6 +183,35 @@ TEST(ZoneFileTests, EarlyScenesAreNumberedInOrder) { EXPECT_EQ(zone.scenes[2].filename, "c.lvl"); } +// The scenes the client loads: by scene ID and layer, a later one replacing an earlier one; none without a General +// layer; before version 41 only the General layer +TEST(ZoneFileTests, KeepsTheScenesTheClientLoads) { + for (const uint32_t version : { 40u, 41u }) { + ZoneBytes w; + w.Put(version).Put(1).Put(1100); + w.Point(0, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f); + w.Put(5); + const auto scene = [&w](const std::string& file, uint32_t id, uint32_t layer) { + w.Text(file).Put(id).Put(layer).Text("n").Put(0).Put(0).Put(0); + }; + scene("b.lvl", 2, 0); + scene("b_audio.lvl", 2, 1); + scene("orphan_audio.lvl", 3, 1); + scene("a.lvl", 1, 0); + scene("b2.lvl", 2, 0); + w.Put(0).Text("zone.raw").Text("Name").Text("Description"); + w.Put(0).Put(0).Put(1).Put(0); + std::istringstream stream(w.data); + ZoneFile zone; + zone.Read(stream); + EXPECT_FALSE(stream.fail()); + std::vector files; + for (const auto& read : zone.scenes) files.push_back(read.filename); + if (version == 41) EXPECT_EQ(files, (std::vector{ "a.lvl", "b2.lvl", "b_audio.lvl" })); + else EXPECT_EQ(files, (std::vector{ "a.lvl", "b2.lvl" })); + } +} + // A PrePreAlpha (30) file ends at its terrain file's name: no zone name, description, transitions or paths TEST(ZoneFileTests, PrePreAlphaHasNoZoneName) { ZoneBytes w;