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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 06:43:29 -05:00
parent 71b7f19ec8
commit 93c7b1948f
2 changed files with 51 additions and 11 deletions

View File

@@ -123,11 +123,6 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const {
const size_t width = static_cast<size_t>(chunk.width);
const size_t height = static_cast<size_t>(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<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(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<size_t>(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<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(legacyColorBytes));
std::vector<uint8_t> pixels(legacyColorBytes);
stream.read(reinterpret_cast<char*>(pixels.data()), static_cast<std::streamsize>(legacyColorBytes));
chunk.colorMap.reserve(static_cast<size_t>(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<size_t>(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<char*>(chunk.textureMap.data()), static_cast<std::streamsize>(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<char*>(chunk.sceneMap.data()), static_cast<std::streamsize>(sceneMapSize));
} else if (version == 31) {
const size_t sceneMapCells = static_cast<size_t>(chunk.colorMapResolution + 1) * (chunk.colorMapResolution + 1);
const size_t sceneMapCells = chunk.width > 0 ? static_cast<size_t>(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<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution, 0);
}
if (stream.fail()) {

View File

@@ -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<uint16_t>(version).Put<uint8_t>(0).Put<uint32_t>(1).Put<uint32_t>(1).Put<uint32_t>(1);
w.Put<uint32_t>(0).Put<uint32_t>(3).Put<uint32_t>(3).Put(0.0f).Put(0.0f);
w.Put<uint32_t>(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<uint8_t>(1).Put<uint8_t>(2).Put<uint8_t>(3).Put<uint8_t>(static_cast<uint8_t>(i)); // BGRA
w.Put<uint32_t>(1).Put<uint8_t>(1).Put<uint8_t>(2).Put<uint8_t>(3).Put<uint8_t>(4);
w.Put<uint32_t>(0); // no flairs
if (version == 31) for (int i = 0; i < 9; i++) w.Put<uint8_t>(static_cast<uint8_t>(i));
else w.Put<uint8_t>(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<uint8_t>{ 3, 2, 1, 0, 3, 2, 1, 1, 3, 2, 1, 3, 3, 2, 1, 4 }));
EXPECT_EQ(chunk.textureMap, (std::vector<uint8_t>{ 3, 2, 1, 4 }));
if (version == 31) EXPECT_EQ(chunk.sceneMap, (std::vector<uint8_t>{ 0, 1, 3, 4 }));
else EXPECT_EQ(chunk.sceneMap, (std::vector<uint8_t>(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 }) {