From 93c7b1948fd5a307af7164bdd217b25ae3030dc5 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 06:43:29 -0500 Subject: [PATCH] fix(terrain): read terrain files before version 32 as the client does The client reads a terrain chunk of a file older than version 32 (1.10.64): - its color map as width x width BGRA pixels, keeping the (width - 1) x (width - 1) before the last row and column, as RGBA (RAWReadColorandLightMaps); the reader kept all the pixels as written; - its texture blend map's pixels as BGRA, kept as RGBA (0x0103aaf0); - before version 31 no scene map, only a byte: the client's scene map is all scene 0 (RAWReadSceneMap); the reader had none. A chunk of width or height 0, which the client reads (no heights, no color map), no longer fails the whole file. Live terrain files are version 32, so they read as before. Check in game: nothing to check (no live terrain changes). Co-Authored-By: Claude Opus 5.5 --- dCommon/Raw.cpp | 32 +++++++++++++++++++---------- tests/dWebTests/RawTerrainTests.cpp | 30 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/dCommon/Raw.cpp b/dCommon/Raw.cpp index 19c356a27..54f39e9c7 100644 --- a/dCommon/Raw.cpp +++ b/dCommon/Raw.cpp @@ -123,11 +123,6 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { const size_t width = static_cast(chunk.width); const size_t height = static_cast(chunk.height); - if (width == 0 || height == 0) { - LOG("Chunk %u has invalid heightmap dimensions: width=%zu, height=%zu", chunk.id, width, height); - return false; - } - if (width > kMaxResolution || height > kMaxResolution) { LOG("Chunk %u heightmap dimensions exceed maximum resolution %u: width=%zu, height=%zu", chunk.id, kMaxResolution, width, height); return false; @@ -147,7 +142,7 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { } const size_t totalBytes = heightMapSize * elementSize; - if (totalBytes == 0 || totalBytes > kMaxBlobBytes) { + if (totalBytes > kMaxBlobBytes) { LOG("Chunk %u heightmap total size invalid: bytes=%zu (max %zu)", chunk.id, totalBytes, kMaxBlobBytes); return false; } @@ -165,7 +160,7 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { if (version >= 32) { BinaryIO::BinaryRead(stream, chunk.colorMapResolution); } else { - chunk.colorMapResolution = chunk.width - 1; + chunk.colorMapResolution = chunk.width > 0 ? chunk.width - 1 : 0; // no color map for a width of 0 } if (chunk.colorMapResolution > kMaxResolution) { @@ -181,14 +176,23 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { } chunk.colorMap.resize(colorMapPixelCount); stream.read(reinterpret_cast(chunk.colorMap.data()), static_cast(colorMapPixelCount)); - } else { + } else if (chunk.width > 0) { + // width x width BGRA pixels, of which the client keeps the (width - 1) x (width - 1) before the last row and + // column, as RGBA (RAWReadColorandLightMaps) const size_t legacyColorBytes = static_cast(chunk.width) * chunk.width * 4; if (legacyColorBytes > kMaxBlobBytes) { LOG("Chunk legacy colorMap size %zu exceeds maximum %zu bytes", legacyColorBytes, kMaxBlobBytes); return false; } - chunk.colorMap.resize(legacyColorBytes); - stream.read(reinterpret_cast(chunk.colorMap.data()), static_cast(legacyColorBytes)); + std::vector pixels(legacyColorBytes); + stream.read(reinterpret_cast(pixels.data()), static_cast(legacyColorBytes)); + chunk.colorMap.reserve(static_cast(chunk.colorMapResolution) * chunk.colorMapResolution * 4); + for (uint32_t y = 0; y < chunk.colorMapResolution; ++y) { + for (uint32_t x = 0; x < chunk.colorMapResolution; ++x) { + const auto* bgra = &pixels[(static_cast(y) * chunk.width + x) * 4]; + chunk.colorMap.insert(chunk.colorMap.end(), { bgra[2], bgra[1], bgra[0], bgra[3] }); + } + } } if (stream.fail()) { @@ -226,6 +230,10 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { } chunk.textureMap.resize(textureMapPixelCount); stream.read(reinterpret_cast(chunk.textureMap.data()), static_cast(textureMapPixelCount)); + // Before version 32 the pixels are BGRA; the client keeps them as RGBA (0x0103aaf0) + if (version < 32) { + for (size_t i = 0; i + 3 < chunk.textureMap.size(); i += 4) std::swap(chunk.textureMap[i], chunk.textureMap[i + 2]); + } if (stream.fail()) { return false; @@ -281,7 +289,7 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { chunk.sceneMap.resize(sceneMapSize); stream.read(reinterpret_cast(chunk.sceneMap.data()), static_cast(sceneMapSize)); } else if (version == 31) { - const size_t sceneMapCells = static_cast(chunk.colorMapResolution + 1) * (chunk.colorMapResolution + 1); + const size_t sceneMapCells = chunk.width > 0 ? static_cast(chunk.colorMapResolution + 1) * (chunk.colorMapResolution + 1) : 0; if (sceneMapCells > kMaxBlobBytes) { LOG("Chunk v31 sceneMap size %zu exceeds maximum %zu bytes", sceneMapCells, kMaxBlobBytes); return false; @@ -295,7 +303,9 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { } } } else { + // Before version 31 a chunk has no scene map, only a byte; the client's is all scene 0 (RAWReadSceneMap) stream.seekg(1, std::ios::cur); + chunk.sceneMap.assign(static_cast(chunk.colorMapResolution) * chunk.colorMapResolution, 0); } if (stream.fail()) { diff --git a/tests/dWebTests/RawTerrainTests.cpp b/tests/dWebTests/RawTerrainTests.cpp index 79303d7a3..818695d5a 100644 --- a/tests/dWebTests/RawTerrainTests.cpp +++ b/tests/dWebTests/RawTerrainTests.cpp @@ -84,6 +84,36 @@ TEST(RawTerrainTests, ReadsEveryLayer) { EXPECT_FLOAT_EQ(raw.maxBoundsX, 6.0f); } +// Before version 32: a shader ID, a width x width BGRA color map the client keeps (width - 1) x (width - 1) of as RGBA, +// a BGRA texture map, no light or blend maps, a (width x width) scene map in version 31 and none (all scene 0) before +TEST(RawTerrainTests, ReadsOlderVersions) { + for (const uint16_t version : { 30, 31 }) { + RawWriter w; + w.Put(version).Put(0).Put(1).Put(1).Put(1); + w.Put(0).Put(3).Put(3).Put(0.0f).Put(0.0f); + w.Put(7); + for (uint32_t texture : { 10u, 11u, 12u, 13u }) w.Put(texture); + w.Put(1.0f); + for (int i = 0; i < 9; i++) w.Put(0.0f); + for (int i = 0; i < 9; i++) w.Put(1).Put(2).Put(3).Put(static_cast(i)); // BGRA + w.Put(1).Put(1).Put(2).Put(3).Put(4); + w.Put(0); // no flairs + if (version == 31) for (int i = 0; i < 9; i++) w.Put(static_cast(i)); + else w.Put(0); + + Raw::Raw raw; + ASSERT_TRUE(Read(w.data, raw)) << version; + const auto& chunk = raw.chunks.at(0); + EXPECT_EQ(chunk.shaderId, 7u); + EXPECT_EQ(chunk.colorMapResolution, 2u); + // Rows 0-1, columns 0-1 of the 3 x 3, as RGBA + EXPECT_EQ(chunk.colorMap, (std::vector{ 3, 2, 1, 0, 3, 2, 1, 1, 3, 2, 1, 3, 3, 2, 1, 4 })); + EXPECT_EQ(chunk.textureMap, (std::vector{ 3, 2, 1, 4 })); + if (version == 31) EXPECT_EQ(chunk.sceneMap, (std::vector{ 0, 1, 3, 4 })); + else EXPECT_EQ(chunk.sceneMap, (std::vector(4, 0))); + } +} + TEST(RawTerrainTests, RejectsDamagedFiles) { const auto raw = SampleRaw(); for (const size_t length : { size_t{ 0 }, size_t{ 2 }, size_t{ 20 }, raw.size() / 2, raw.size() - 1 }) {