From 83c2136e9a0bb68e9eeb8dd463b73e2e817a8490 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 06:09:04 -0500 Subject: [PATCH] fix(zone): spawner and camera paths have their fields only from version 4 The client reads a spawner path's fields (LOT, respawn time, counts, object ID) and a camera path's next path and rotate-player flag only when the path is newer than version 3 (LevelPath::FromBuffer, 1.10.64: pathVersion > 3). The reader read them for every version, which would misread an older spawner or camera path from there on. No zone file on disk has a spawner or camera path older than version 4, so what they read is unchanged. Check in game: nothing to check (no live zone changes). Co-Authored-By: Claude Opus 5.5 --- dCommon/ZoneFile.cpp | 4 ++-- tests/dCommonTests/ZoneFileTests.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/dCommon/ZoneFile.cpp b/dCommon/ZoneFile.cpp index 7b4fc7fd4..b381e6fc3 100644 --- a/dCommon/ZoneFile.cpp +++ b/dCommon/ZoneFile.cpp @@ -287,13 +287,13 @@ bool ZoneFile::ReadPath(std::istream& file) { BinaryIO::BinaryRead(file, path.property.playerZoneCoords); BinaryIO::BinaryRead(file, path.property.maxBuildHeight); } - } else if (path.pathType == PathType::Camera) { + } else if (path.pathType == PathType::Camera && path.pathVersion > 3) { BinaryIO::ReadString(file, path.camera.nextPath, BinaryIO::ReadType::WideString); if (path.pathVersion >= 14) { BinaryIO::BinaryRead(file, path.camera.rotatePlayer); } - } else if (path.pathType == PathType::Spawner) { + } else if (path.pathType == PathType::Spawner && path.pathVersion > 3) { BinaryIO::BinaryRead(file, path.spawner.spawnedLOT); BinaryIO::BinaryRead(file, path.spawner.respawnTime); BinaryIO::BinaryRead(file, path.spawner.maxToSpawn); diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index fd368383a..4599c0306 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -304,6 +304,31 @@ TEST(ZoneFileTests, SpawnerNetActiveFromVersion9) { EXPECT_EQ(zone.paths[1].spawner.spawnerNetActive, 0); } +// Spawner and camera paths have their own fields only from version 4 +TEST(ZoneFileTests, SpawnerAndCameraFieldsFromVersion4) { + ZoneBytes w; + w.Put(41).Put(3).Put(1150); + w.Point(0, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f); + w.Put(1); + w.Text("scene.lvl").Put(0).Put(0).Text("Global").Put(0).Put(0).Put(0); + w.Put(0).Text("zone.raw").Text("Name").Text("Description"); + w.Put(0).Paths().Put(1).Put(2); + w.Put(3).Wide("Spawner").Put(4).Put(0).Put(0); + w.Put(1).Point(1, 1, 1).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(0); + w.Put(3).Wide("Camera").Put(3).Put(0).Put(0); + w.Put(1).Point(2, 2, 2).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(1.0f).Put(0.5f).Put(0.0f).Put(0.0f).Put(0.0f); + + std::istringstream stream(w.Done()); + ZoneFile zone; + zone.Read(stream); + EXPECT_FALSE(stream.fail()); + ASSERT_EQ(zone.paths.size(), 2u); + EXPECT_EQ(zone.paths[0].spawner.spawnedLOT, 0); + EXPECT_EQ(zone.paths[0].pathWaypoints.at(0).position, NiPoint3(1, 1, 1)); + EXPECT_TRUE(zone.paths[1].camera.nextPath.empty()); + EXPECT_EQ(zone.paths[1].pathWaypoints.at(0).position, NiPoint3(2, 2, 2)); +} + // The paths are a chunk the client reads on its own: a path it refuses (here version 19) leaves the zone with no paths, // and the zone file goes on after the chunk TEST(ZoneFileTests, RefusedPathsLeaveNoPaths) {