From e7ec11759e2b7d811799c06c2b9d7cbf54de285c Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 04:34:20 -0500 Subject: [PATCH] fix(zone): spawner paths have an activate-on-load byte only from version 9 The client reads a spawner path's activateOnLoad byte only when the path is newer than version 8 (LevelPath::FromBuffer, 1.10.64: pathVersion > 8) and otherwise keeps its default of 1 (Path::InitializeSpawnerInfo). The reader read the byte for every version, which would misread an older spawner path from there on, and its default was 0. The byte is now read from version 9, and an older path's spawner is active on load as in the client. No zone file on disk has a spawner path older than version 9, so what the world spawns is unchanged. Check in game: nothing to check (spawners on live worlds are unchanged). Co-Authored-By: Claude Opus 5.5 --- dCommon/ZoneFile.cpp | 2 +- dCommon/ZoneFile.h | 2 +- tests/dCommonTests/ZoneFileTests.cpp | 27 +++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/dCommon/ZoneFile.cpp b/dCommon/ZoneFile.cpp index 43d02bda0..c19147f3d 100644 --- a/dCommon/ZoneFile.cpp +++ b/dCommon/ZoneFile.cpp @@ -227,7 +227,7 @@ void ZoneFile::ReadPath(std::istream& file) { BinaryIO::BinaryRead(file, path.spawner.maxToSpawn); BinaryIO::BinaryRead(file, path.spawner.amountMaintained); BinaryIO::BinaryRead(file, path.spawner.spawnerObjID); - BinaryIO::BinaryRead(file, path.spawner.spawnerNetActive); + if (path.pathVersion >= 9) BinaryIO::BinaryRead(file, path.spawner.spawnerNetActive); } // Read waypoints diff --git a/dCommon/ZoneFile.h b/dCommon/ZoneFile.h index 621e6fade..1f3e09a76 100644 --- a/dCommon/ZoneFile.h +++ b/dCommon/ZoneFile.h @@ -178,7 +178,7 @@ struct SpawnerPath { int32_t maxToSpawn{}; uint32_t amountMaintained{}; LWOOBJID spawnerObjID; - uint8_t spawnerNetActive{}; + uint8_t spawnerNetActive = 1; // not in paths before version 9; the client then activates it on load }; diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index 6b38f3932..b63c8f6d6 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -204,6 +204,33 @@ TEST(ZoneFileTests, ReadsLegacyPaths) { EXPECT_EQ(platform.pathWaypoints[0].movingPlatform.wait, 1.5f); } +// Spawner paths before version 9 have no activate-on-load byte; the client then activates them on load +TEST(ZoneFileTests, SpawnerNetActiveFromVersion9) { + 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).Put(0).Put(1).Put(2); + for (const uint32_t version : { 8u, 9u }) { + w.Put(version).Wide("Spawner").Put(4).Put(0).Put(0); + w.Put(6010).Put(10).Put(1).Put(1).Put(123); + if (version >= 9) w.Put(0); + w.Put(1).Point(1, 1, 1).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(0); + } + + std::istringstream stream(w.data); + ZoneFile zone; + zone.Read(stream); + EXPECT_FALSE(stream.fail()); + EXPECT_EQ(stream.peek(), std::char_traits::eof()); + ASSERT_EQ(zone.paths.size(), 2u); + EXPECT_EQ(zone.paths[0].spawner.spawnerNetActive, 1); + EXPECT_EQ(zone.paths[0].pathWaypoints.at(0).position, NiPoint3(1, 1, 1)); + EXPECT_EQ(zone.paths[1].spawner.spawnerNetActive, 0); +} + TEST(ZoneFileTests, ShortFilesThrowOrFail) { const auto zone = SampleZone(); for (const size_t length : { size_t{ 3 }, size_t{ 40 }, zone.size() / 2, zone.size() - 1 }) {