From f8a99c45a582e33dfa3b99bd6c2dadc76d3300e9 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Mon, 26 Oct 2026 20:33:41 -0500 Subject: [PATCH] fix: keep the zone boundary lines and read the luz scene count as the client does Each boundary line's destination is read as a map ID and an instance ID into a LWOZONEID. The scene count is a byte before version 37 and a u32 from it. Co-Authored-By: Claude Opus 5.5 --- dZoneManager/Zone.cpp | 24 ++++++----- dZoneManager/Zone.h | 11 +++++ .../dZoneManagerTests/TerrainTestWriter.h | 29 ++++++++++---- .../dZoneManagerTests/ZoneSceneTests.cpp | 40 +++++++++++++++++-- 4 files changed, 83 insertions(+), 21 deletions(-) diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index a5c8a1885..5dc92b525 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -70,7 +70,8 @@ void Zone::LoadZoneIntoMemory() { BinaryIO::BinaryRead(file, m_SpawnpointRotation); } - if (m_FileFormatVersion <= Zone::FileFormatVersion::LateAlpha) { + // A byte before version 37 (LateAlpha), a u32 from it + if (m_FileFormatVersion < Zone::FileFormatVersion::LateAlpha) { uint8_t sceneCount; BinaryIO::BinaryRead(file, sceneCount); m_SceneCount = sceneCount; @@ -80,17 +81,20 @@ void Zone::LoadZoneIntoMemory() { LoadScene(file); } - // Zone boundary lines — skip past them for correct file positioning + // Zone boundary lines: a count byte, then the lines uint8_t numBoundaries = 0; BinaryIO::BinaryRead(file, numBoundaries); - for (uint8_t i = 0; i < numBoundaries; ++i) { - NiPoint3 normal, point, spawnLocation; - uint32_t packed, destSceneID; - BinaryIO::BinaryRead(file, normal); - BinaryIO::BinaryRead(file, point); - BinaryIO::BinaryRead(file, packed); - BinaryIO::BinaryRead(file, destSceneID); - BinaryIO::BinaryRead(file, spawnLocation); + m_Boundaries.resize(numBoundaries); + for (auto& boundary : m_Boundaries) { + LWOMAPID destMapID = 0; + LWOINSTANCEID destInstanceID = 0; + BinaryIO::BinaryRead(file, boundary.normal); + BinaryIO::BinaryRead(file, boundary.point); + BinaryIO::BinaryRead(file, destMapID); + BinaryIO::BinaryRead(file, destInstanceID); + boundary.destinationZone = LWOZONEID(destMapID, destInstanceID, 0); + BinaryIO::BinaryRead(file, boundary.destinationSceneID); + BinaryIO::BinaryRead(file, boundary.spawnLocation); } BinaryIO::ReadString(file, m_ZoneRawPath, BinaryIO::ReadType::String); diff --git a/dZoneManager/Zone.h b/dZoneManager/Zone.h index 9dd5b1629..e469a5d63 100644 --- a/dZoneManager/Zone.h +++ b/dZoneManager/Zone.h @@ -45,6 +45,15 @@ struct SceneTransitionInfo { NiPoint3 position; }; +// A zone boundary line from the .luz +struct ZoneBoundary { + NiPoint3 normal; + NiPoint3 point; + LWOZONEID destinationZone; // the clone ID is always 0 + uint32_t destinationSceneID{}; + NiPoint3 spawnLocation; +}; + struct SceneTransition { std::string name; std::vector points; @@ -236,6 +245,7 @@ public: const Raw::Raw& GetZoneRaw() const { return m_Raw; } const SceneRef* GetScene(LWOSCENEID sceneID) const; const std::vector& GetSceneTransitions() const { return m_SceneTransitions; } + const std::vector& GetBoundaries() const { return m_Boundaries; } const std::map& GetScenes() const { return m_Scenes; } private: @@ -258,6 +268,7 @@ private: std::map m_Scenes; std::vector m_SceneTransitions; + std::vector m_Boundaries; uint32_t m_PathDataLength; uint32_t m_PathChunkVersion; diff --git a/tests/dGameTests/dZoneManagerTests/TerrainTestWriter.h b/tests/dGameTests/dZoneManagerTests/TerrainTestWriter.h index ee56a0e5c..ac7c2d8a2 100644 --- a/tests/dGameTests/dZoneManagerTests/TerrainTestWriter.h +++ b/tests/dGameTests/dZoneManagerTests/TerrainTestWriter.h @@ -132,16 +132,20 @@ namespace TerrainTestWriter { NiPoint3 position; }; - // A Latest (41) version .luz with no paths + // A .luz (version 36 to 41) with no paths inline std::string WriteLuz(uint32_t worldID, const std::vector& scenes, uint8_t boundaryCount, const std::string& rawName, - const std::vector>& transitions) { + const std::vector>& transitions, uint32_t version = 41) { std::ostringstream stream; - Write(stream, 41); // version + Write(stream, version); Write(stream, 5); // revision Write(stream, worldID); - Write(stream, NiPoint3(1.0f, 2.0f, 3.0f)); // spawn position - for (const float value : { 1.0f, 0.0f, 0.0f, 0.0f }) Write(stream, value); // spawn rotation - Write(stream, static_cast(scenes.size())); + if (version >= 38) { + Write(stream, NiPoint3(1.0f, 2.0f, 3.0f)); // spawn position + for (const float value : { 1.0f, 0.0f, 0.0f, 0.0f }) Write(stream, value); // spawn rotation + } + // the scene count is a byte before version 37 + if (version >= 37) Write(stream, static_cast(scenes.size())); + else Write(stream, static_cast(scenes.size())); for (const auto& scene : scenes) { WriteString8(stream, scene.filename); Write(stream, scene.id); @@ -157,7 +161,8 @@ namespace TerrainTestWriter { for (uint8_t i = 0; i < boundaryCount; ++i) { Write(stream, NiPoint3(0.0f, 0.0f, 1.0f)); Write(stream, NiPoint3(10.0f, 0.0f, static_cast(i))); - Write(stream, 1100); + Write(stream, 1100); // map + Write(stream, i + 1); // instance Write(stream, 2); Write(stream, NiPoint3(5.0f, 6.0f, 7.0f)); } @@ -168,7 +173,15 @@ namespace TerrainTestWriter { Write(stream, static_cast(transitions.size())); for (const auto& [from, to] : transitions) { - for (const auto& point : { from, to }) { + // before version 40 a transition has a name and a width + if (version < 40) { + WriteString8(stream, "transition"); + Write(stream, 2.5f); + } + // five points from version 34 to 38 (the last three repeat the second here), two otherwise + const size_t pointCount = version >= 34 && version < 39 ? 5 : 2; + for (size_t i = 0; i < pointCount; ++i) { + const auto& point = i == 0 ? from : to; Write(stream, point.sceneID); Write(stream, point.layerID); Write(stream, point.position); diff --git a/tests/dGameTests/dZoneManagerTests/ZoneSceneTests.cpp b/tests/dGameTests/dZoneManagerTests/ZoneSceneTests.cpp index e11f6fb05..c39f004ec 100644 --- a/tests/dGameTests/dZoneManagerTests/ZoneSceneTests.cpp +++ b/tests/dGameTests/dZoneManagerTests/ZoneSceneTests.cpp @@ -54,7 +54,7 @@ protected: } // Scenes 0 (global), 1, 2 and 3 (general) and 5 (audio, layer 1); transitions 1 <-> 2 and 2 <-> 3 - void WriteZone(const std::string& rawName = "test.raw", uint8_t boundaryCount = 2) const { + void WriteZone(const std::string& rawName = "test.raw", uint8_t boundaryCount = 2, uint32_t version = 41) const { const std::vector scenes = { { "global.lvl", 0, 0, "Global", 10, 20, 30 }, { "one.lvl", 1, 0, "Scene One", 255, 0, 0 }, @@ -65,7 +65,7 @@ protected: WriteFile("test.luz", WriteLuz(TEST_MAP, scenes, boundaryCount, rawName, { { { 1, 0, NiPoint3(1.0f, 0.0f, 0.0f) }, { 2, 0, NiPoint3(2.0f, 0.0f, 0.0f) } }, { { 2, 0, NiPoint3(3.0f, 0.0f, 0.0f) }, { 3, 0, NiPoint3(4.0f, 0.0f, 0.0f) } }, - })); + }, version)); } // One 3x3 point chunk from (0, 0) to (4, 4) whose 2x2 scene map holds scenes 1 2 / 3 1 @@ -99,9 +99,20 @@ TEST_F(ZoneSceneTest, ReadsScenesBoundariesAndTransitions) { EXPECT_EQ(zone->GetWorldID(), TEST_MAP); EXPECT_EQ(zone->GetZoneRawPath(), "test.raw"); - EXPECT_EQ(zone->GetZoneName(), "Test Zone"); // read after the skipped boundary lines + EXPECT_EQ(zone->GetZoneName(), "Test Zone"); // read after the boundary lines EXPECT_EQ(zone->GetZonePath(), "maps/testzone/"); + const auto& boundaries = zone->GetBoundaries(); + ASSERT_EQ(boundaries.size(), 2); + EXPECT_FLOAT_EQ(boundaries[1].normal.z, 1.0f); + EXPECT_FLOAT_EQ(boundaries[1].point.x, 10.0f); + EXPECT_FLOAT_EQ(boundaries[1].point.z, 1.0f); + EXPECT_EQ(boundaries[1].destinationZone.GetMapID(), 1100); + EXPECT_EQ(boundaries[1].destinationZone.GetInstanceID(), 2); + EXPECT_EQ(boundaries[1].destinationZone.GetCloneID(), 0); + EXPECT_EQ(boundaries[1].destinationSceneID, 2); + EXPECT_FLOAT_EQ(boundaries[1].spawnLocation.y, 6.0f); + ASSERT_EQ(zone->GetScenes().size(), 5); const SceneRef* one = zone->GetScene(LWOSCENEID(1, 0)); ASSERT_NE(one, nullptr); @@ -132,9 +143,32 @@ TEST_F(ZoneSceneTest, ZoneWithoutBoundaryLines) { WriteFile("test.raw", MakeRaw()); const Zone* zone = LoadZone(); EXPECT_EQ(zone->GetZoneName(), "Test Zone"); + EXPECT_TRUE(zone->GetBoundaries().empty()); EXPECT_EQ(zone->GetSceneTransitions().size(), 2); } +// Version 37 has a u32 scene count (a byte before it) and no spawn point; 36 and 37 have named five point transitions +TEST_F(ZoneSceneTest, ReadsOlderZoneVersions) { + for (const uint32_t version : { 36u, 37u, 38u, 39u, 40u }) { + WriteZone("test.raw", 2, version); + WriteFile("test.raw", MakeRaw()); + const Zone* zone = LoadZone(); + EXPECT_EQ(zone->GetScenes().size(), 5) << version; + EXPECT_EQ(zone->GetBoundaries().size(), 2) << version; + EXPECT_EQ(zone->GetZoneRawPath(), "test.raw") << version; + EXPECT_EQ(zone->GetZoneName(), "Test Zone") << version; + ASSERT_EQ(zone->GetSceneTransitions().size(), 2) << version; + const auto& transition = zone->GetSceneTransitions()[1]; + EXPECT_EQ(transition.points.size(), version < 39 ? 5 : 2) << version; + EXPECT_EQ(transition.points[1].sceneID, LWOSCENEID(3, 0)) << version; + if (version < 40) { + EXPECT_EQ(transition.name, "transition") << version; + EXPECT_FLOAT_EQ(transition.width, 2.5f) << version; + } + EXPECT_FALSE(zone->GetZoneRaw().chunks.empty()) << version; + } +} + TEST_F(ZoneSceneTest, SceneAtPosition) { WriteZone(); WriteFile("test.raw", MakeRaw());