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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 04:34:20 -05:00
parent 0d2fcc7dff
commit e7ec11759e
3 changed files with 29 additions and 2 deletions

View File

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

View File

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

View File

@@ -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<uint32_t>(41).Put<uint32_t>(3).Put<uint32_t>(1150);
w.Point(0, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f);
w.Put<uint32_t>(1);
w.Text("scene.lvl").Put<uint32_t>(0).Put<uint32_t>(0).Text("Global").Put<uint8_t>(0).Put<uint8_t>(0).Put<uint8_t>(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>(2);
for (const uint32_t version : { 8u, 9u }) {
w.Put<uint32_t>(version).Wide("Spawner").Put<uint32_t>(4).Put<uint32_t>(0).Put<uint32_t>(0);
w.Put<int32_t>(6010).Put<uint32_t>(10).Put<int32_t>(1).Put<uint32_t>(1).Put<int64_t>(123);
if (version >= 9) w.Put<uint8_t>(0);
w.Put<uint32_t>(1).Point(1, 1, 1).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put<uint32_t>(0);
}
std::istringstream stream(w.data);
ZoneFile zone;
zone.Read(stream);
EXPECT_FALSE(stream.fail());
EXPECT_EQ(stream.peek(), std::char_traits<char>::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 }) {