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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 05:54:29 -05:00
parent 9f390958f5
commit 6a8e8ece04
3 changed files with 48 additions and 2 deletions

View File

@@ -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<std::pair<uint32_t, eSceneType>, 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<std::optional<std::string>(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

View File

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

View File

@@ -31,7 +31,7 @@ namespace {
w.Put<uint32_t>(41).Put<uint32_t>(3).Put<uint32_t>(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<uint32_t>(1);
w.Text("scene.lvl").Put<uint32_t>(7).Put<uint32_t>(2).Text("Global").Put<uint8_t>(1).Put<uint8_t>(2).Put<uint8_t>(3);
w.Text("scene.lvl").Put<uint32_t>(7).Put<uint32_t>(0).Text("Global").Put<uint8_t>(1).Put<uint8_t>(2).Put<uint8_t>(3);
w.Put<uint8_t>(0).Text("zone.raw").Text("Name").Text("Description"); // no zone boundaries
w.Put<uint32_t>(1);
for (int i = 0; i < 2; i++) w.Put<uint64_t>(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<uint32_t>(version).Put<uint32_t>(1).Put<uint32_t>(1100);
w.Point(0, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f);
w.Put<uint32_t>(5);
const auto scene = [&w](const std::string& file, uint32_t id, uint32_t layer) {
w.Text(file).Put<uint32_t>(id).Put<uint32_t>(layer).Text("n").Put<uint8_t>(0).Put<uint8_t>(0).Put<uint8_t>(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<uint8_t>(0).Text("zone.raw").Text("Name").Text("Description");
w.Put<uint32_t>(0).Put<uint32_t>(0).Put<uint32_t>(1).Put<uint32_t>(0);
std::istringstream stream(w.data);
ZoneFile zone;
zone.Read(stream);
EXPECT_FALSE(stream.fail());
std::vector<std::string> files;
for (const auto& read : zone.scenes) files.push_back(read.filename);
if (version == 41) EXPECT_EQ(files, (std::vector<std::string>{ "a.lvl", "b2.lvl", "b_audio.lvl" }));
else EXPECT_EQ(files, (std::vector<std::string>{ "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;