From 97a1453bf747574b3b3eb9854a722066c606a4b9 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 04:22:34 -0500 Subject: [PATCH] feat(zone): read the zone boundary lines of .luz files Between the scenes and the terrain file's name a zone file has its boundary lines: a u8 count, then per line a normal, a point, the destination zone, the destination scene ID and a spawn location. The reader took that spot for a u8-length "zone path" string, which is the same bytes only when there are no boundaries; a zone with boundaries would have misread everything after it. The destination is one u32 in the client (LuzReader::ReadZoneBoundaryLines, 0x01018490 in 1.10.64: map ID in bits 0-15, instance ID in bits 16-31, clone 0), read here as two u16s into an LWOZONEID with clone 0. The boundaries are kept on ZoneFile::zoneBoundaries. No zone of the 1.10.64 client (or any other client on disk) has boundary lines, so what is read from them is unchanged. Check in game: every world loads and zone transfers (launch pads, rockets, portals) work as before. Co-Authored-By: Claude Opus 5.5 --- dCommon/ZoneFile.cpp | 23 +++++++++++++++++++- dCommon/ZoneFile.h | 13 ++++++++++- tests/dCommonTests/ZoneFileTests.cpp | 32 +++++++++++++++++++++++++++- tests/dWebTests/ZonePathsTests.cpp | 2 +- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/dCommon/ZoneFile.cpp b/dCommon/ZoneFile.cpp index 7adf82869..cfa61b665 100644 --- a/dCommon/ZoneFile.cpp +++ b/dCommon/ZoneFile.cpp @@ -31,8 +31,9 @@ void ZoneFile::ReadHeader(std::istream& file) { ReadScene(file); } + ReadZoneBoundaries(file); + //Read generic zone info: - BinaryIO::ReadString(file, zonePath, BinaryIO::ReadType::String); BinaryIO::ReadString(file, zoneRawPath, BinaryIO::ReadType::String); BinaryIO::ReadString(file, zoneName, BinaryIO::ReadType::String); BinaryIO::ReadString(file, zoneDesc, BinaryIO::ReadType::String); @@ -89,6 +90,26 @@ void ZoneFile::ReadScene(std::istream& file) { scenes.push_back(std::move(scene)); } +void ZoneFile::ReadZoneBoundaries(std::istream& file) { + uint8_t count = 0; + BinaryIO::BinaryRead(file, count); + zoneBoundaries.reserve(count); + for (uint8_t i = 0; i < count; ++i) { + ZoneBoundary boundary; + BinaryIO::BinaryRead(file, boundary.normal); + BinaryIO::BinaryRead(file, boundary.point); + // The client reads one u32 of map ID (low 16 bits) and instance ID (high 16 bits) + LWOMAPID mapID = 0; + LWOINSTANCEID instanceID = 0; + BinaryIO::BinaryRead(file, mapID); + BinaryIO::BinaryRead(file, instanceID); + boundary.destZoneID = LWOZONEID(mapID, instanceID, 0); + BinaryIO::BinaryRead(file, boundary.destSceneID); + BinaryIO::BinaryRead(file, boundary.spawnLocation); + zoneBoundaries.push_back(boundary); + } +} + void ZoneFile::ReadSceneTransition(std::istream& file) { SceneTransition sceneTrans; if (fileFormatVersion < FileFormatVersion::Auramar) { diff --git a/dCommon/ZoneFile.h b/dCommon/ZoneFile.h index 72d8224b2..425811e83 100644 --- a/dCommon/ZoneFile.h +++ b/dCommon/ZoneFile.h @@ -35,6 +35,15 @@ struct ZoneScene { uint8_t color_b{}; }; +// A line the player crosses to leave the zone (the client's LuzReader::ReadZoneBoundaryLines) +struct ZoneBoundary { + NiPoint3 normal; + NiPoint3 point; + LWOZONEID destZoneID; // clone 0 + uint32_t destSceneID{}; + NiPoint3 spawnLocation; +}; + struct SceneTransitionInfo { uint64_t sceneID{}; //id of the scene being transitioned to. NiPoint3 position; @@ -209,7 +218,8 @@ struct ZoneFile { std::vector scenes; - std::string zonePath; //Path to the .luz's folder as written in the file + std::vector zoneBoundaries; + std::string zoneRawPath; //Path to the .raw file of this zone. std::string zoneName; //Name given to the zone by a level designer std::string zoneDesc; //Description of the zone by a level designer @@ -234,6 +244,7 @@ struct ZoneFile { private: void ReadScene(std::istream& file); + void ReadZoneBoundaries(std::istream& file); void ReadSceneTransition(std::istream& file); SceneTransitionInfo ReadSceneTransitionInfo(std::istream& file); void ReadPath(std::istream& file); diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index aa8e2b0d6..ee5b34600 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -32,7 +32,7 @@ namespace { 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("zone").Text("zone.raw").Text("Name").Text("Description"); + 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); w.Put(0).Put(1).Put(3); @@ -89,6 +89,36 @@ TEST(ZoneFileTests, ReadsScenesAndPaths) { EXPECT_EQ(property.pathWaypoints.size(), 3u); } +// Boundary lines sit between the scenes and the terrain file's name +TEST(ZoneFileTests, ReadsZoneBoundaries) { + 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(2); + w.Point(1, 0, 0).Point(10, 20, 30).Put(1100).Put(7).Put(4).Point(5, 6, 7); + w.Point(0, 0, -1).Point(-1, -2, -3).Put(1200).Put(0).Put(0).Point(0, 0, 0); + w.Text("zone.raw").Text("Name").Text("Description"); + w.Put(0).Put(0).Put(1).Put(0); // no transitions, no paths + + std::istringstream stream(w.data); + ZoneFile zone; + zone.Read(stream); + EXPECT_FALSE(stream.fail()); + ASSERT_EQ(zone.zoneBoundaries.size(), 2u); + const auto& boundary = zone.zoneBoundaries[0]; + EXPECT_EQ(boundary.normal, NiPoint3(1, 0, 0)); + EXPECT_EQ(boundary.point, NiPoint3(10, 20, 30)); + EXPECT_EQ(boundary.destZoneID, LWOZONEID(1100, 7, 0)); + EXPECT_EQ(boundary.destSceneID, 4u); + EXPECT_EQ(boundary.spawnLocation, NiPoint3(5, 6, 7)); + EXPECT_EQ(zone.zoneBoundaries[1].destZoneID, LWOZONEID(1200, 0, 0)); + EXPECT_EQ(zone.zoneRawPath, "zone.raw"); + EXPECT_EQ(zone.zoneName, "Name"); + EXPECT_EQ(zone.zoneDesc, "Description"); +} + TEST(ZoneFileTests, ShortFilesThrowOrFail) { const auto zone = SampleZone(); for (const size_t length : { size_t{ 3 }, size_t{ 40 }, zone.size() / 2, zone.size() - 1 }) { diff --git a/tests/dWebTests/ZonePathsTests.cpp b/tests/dWebTests/ZonePathsTests.cpp index 2cfe66e61..907e55a2e 100644 --- a/tests/dWebTests/ZonePathsTests.cpp +++ b/tests/dWebTests/ZonePathsTests.cpp @@ -18,7 +18,7 @@ namespace { w.Point(1, 2, 3).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f); // spawn point and rotation w.Put(1); // one scene w.Text("scene.lvl").Put(1).Put(0).Text("Global").Put(1).Put(2).Put(3); - w.Text("zone").Text("zone.raw").Text("Name").Text("Description"); + w.Put(0).Text("zone.raw").Text("Name").Text("Description"); // no zone boundaries w.Put(1); // one transition: 2 points in this version for (int i = 0; i < 2; i++) w.Put(1).Point(0, 0, 0); w.Put(0).Put(1).Put(3); // path data length, chunk version, 3 paths