fix: one raw terrain reader, in dZoneManager, after the merge

The dashboard's copy of the terrain reader in dCommon is gone; main's in dZoneManager reads for everyone, and keeps
what the copy had over it: colour and texture maps from before version 32 become RGBA, cut to the resolution the
client keeps. The scene map (ZoneScenes) and the dCommon tests compile against that reader.

A scene ID takes its layer as the zone file numbers it (0 General, 1 Audio) as well as the enum, so main's scene tools
and tests build; /getscene prints the layer as a number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 11:19:33 -06:00
parent 0f826bdc99
commit fcf8591749
7 changed files with 23 additions and 687 deletions

View File

@@ -35,7 +35,6 @@ set(DCOMMON_SOURCES
"VanityEvents.cpp"
"ZoneFile.cpp"
"LevelFile.cpp"
"Raw.cpp"
"ZoneScenes.cpp"
"Process.cpp"
"Permissions.cpp"
@@ -58,6 +57,7 @@ endforeach()
add_library(dCommon STATIC ${DCOMMON_SOURCES})
target_include_directories(dCommon
PUBLIC "." "dClient" "dEnums"
"${PROJECT_SOURCE_DIR}/dZoneManager" # Raw.h (ZoneScenes.h); its reader is dZoneManager's
PRIVATE
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"

View File

@@ -1,513 +0,0 @@
#include "Raw.h"
#include "BinaryIO.h"
#include "Logger.h"
#include "SceneColor.h"
#include <fstream>
#include <algorithm>
#include <limits>
#include <stdexcept>
namespace {
constexpr uint32_t kMaxResolution = 4096;
constexpr size_t kMaxBlobBytes = 64ULL * 1024 * 1024; // 64 MiB
constexpr uint32_t kMaxChunks = 1024;
} // namespace
namespace Raw {
// Little-endian values as the file has them, in one read (the heights of a chunk are millions of floats)
template<typename T>
static void ReadArray(std::istream& stream, std::vector<T>& values) {
stream.read(reinterpret_cast<char*>(values.data()), static_cast<std::streamsize>(values.size() * sizeof(T)));
if (!stream) throw std::runtime_error("Failed to read from istream.");
}
bool Chunk::IsValidForSceneLookup() const {
return !sceneMap.empty() && colorMapResolution > 0 && !heightMap.empty()
&& scaleFactor > 0.0f && width > 1 && height > 1;
}
uint8_t Chunk::GetSceneIDAtGrid(uint32_t i, uint32_t j) const {
const float sceneMapI = (static_cast<float>(i) / static_cast<float>(width - 1)) * static_cast<float>(colorMapResolution - 1);
const float sceneMapJ = (static_cast<float>(j) / static_cast<float>(height - 1)) * static_cast<float>(colorMapResolution - 1);
const uint32_t sceneI = std::min(static_cast<uint32_t>(sceneMapI), colorMapResolution - 1);
const uint32_t sceneJ = std::min(static_cast<uint32_t>(sceneMapJ), colorMapResolution - 1);
const uint32_t sceneIndex = sceneI * colorMapResolution + sceneJ;
if (sceneIndex >= sceneMap.size()) return 0;
return sceneMap[sceneIndex];
}
NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const {
const float y = (i * width + j < heightMap.size()) ? heightMap[i * width + j] : 0.0f;
return NiPoint3(
(static_cast<float>(i) + (offsetX / scaleFactor)) * scaleFactor,
y,
(static_cast<float>(j) + (offsetZ / scaleFactor)) * scaleFactor
);
}
/**
* @brief Read flair attributes from stream
*/
static bool ReadFlairAttributes(std::istream& stream, FlairAttributes& flair) {
try {
BinaryIO::BinaryRead(stream, flair.id);
BinaryIO::BinaryRead(stream, flair.scaleFactor);
BinaryIO::BinaryRead(stream, flair.position.x);
BinaryIO::BinaryRead(stream, flair.position.y);
BinaryIO::BinaryRead(stream, flair.position.z);
BinaryIO::BinaryRead(stream, flair.rotation.x);
BinaryIO::BinaryRead(stream, flair.rotation.y);
BinaryIO::BinaryRead(stream, flair.rotation.z);
BinaryIO::BinaryRead(stream, flair.colorR);
BinaryIO::BinaryRead(stream, flair.colorG);
BinaryIO::BinaryRead(stream, flair.colorB);
BinaryIO::BinaryRead(stream, flair.colorA);
return true;
} catch (const std::exception&) {
return false;
}
}
/**
* @brief Read mesh triangle data from stream
*/
static bool ReadMeshTri(std::istream& stream, MeshTri& meshTri) {
try {
BinaryIO::BinaryRead(stream, meshTri.meshTriListSize);
meshTri.meshTriList.resize(meshTri.meshTriListSize);
ReadArray(stream, meshTri.meshTriList);
return true;
} catch (const std::exception&) {
return false;
}
}
/**
* @brief Read a chunk from stream
*/
static bool ReadChunk(std::istream& stream, Chunk& chunk, uint16_t version) {
try {
// Read basic chunk info
BinaryIO::BinaryRead(stream, chunk.id);
if (stream.fail()) {
return false;
}
BinaryIO::BinaryRead(stream, chunk.width);
BinaryIO::BinaryRead(stream, chunk.height);
BinaryIO::BinaryRead(stream, chunk.offsetX);
BinaryIO::BinaryRead(stream, chunk.offsetZ);
if (stream.fail()) {
return false;
}
// For version < 32, shader ID comes before texture IDs
if (version < 32) {
BinaryIO::BinaryRead(stream, chunk.shaderId);
}
// Read texture IDs (4 textures)
chunk.textureIds.resize(4);
for (int i = 0; i < 4; ++i) {
BinaryIO::BinaryRead(stream, chunk.textureIds[i]);
}
if (stream.fail()) {
return false;
}
// Read scale factor
BinaryIO::BinaryRead(stream, chunk.scaleFactor);
if (stream.fail()) {
return false;
}
// Read heightmap
const size_t width = static_cast<size_t>(chunk.width);
const size_t height = static_cast<size_t>(chunk.height);
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;
}
if (height != 0 && width > std::numeric_limits<size_t>::max() / height) {
LOG("Chunk %u heightmap size multiplication overflows: width=%zu, height=%zu", chunk.id, width, height);
return false;
}
const size_t heightMapSize = width * height;
const size_t elementSize = sizeof(chunk.heightMap[0]);
if (elementSize != 0 && heightMapSize > std::numeric_limits<size_t>::max() / elementSize) {
LOG("Chunk %u heightmap byte size overflows: elements=%zu, elementSize=%zu", chunk.id, heightMapSize, elementSize);
return false;
}
const size_t totalBytes = heightMapSize * elementSize;
if (totalBytes > kMaxBlobBytes) {
LOG("Chunk %u heightmap total size invalid: bytes=%zu (max %zu)", chunk.id, totalBytes, kMaxBlobBytes);
return false;
}
chunk.heightMap.resize(heightMapSize);
ReadArray(stream, chunk.heightMap);
if (stream.fail()) {
return false;
}
// ColorMap
if (version >= 32) {
BinaryIO::BinaryRead(stream, chunk.colorMapResolution);
} else {
chunk.colorMapResolution = chunk.width > 0 ? chunk.width - 1 : 0; // no color map for a width of 0
}
if (chunk.colorMapResolution > kMaxResolution) {
LOG("Chunk colorMapResolution %u exceeds maximum %u", chunk.colorMapResolution, kMaxResolution);
return false;
}
if (version >= 32) {
const size_t colorMapPixelCount = static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution * 4;
if (colorMapPixelCount > kMaxBlobBytes) {
LOG("Chunk colorMap size %zu exceeds maximum %zu bytes", colorMapPixelCount, kMaxBlobBytes);
return false;
}
chunk.colorMap.resize(colorMapPixelCount);
stream.read(reinterpret_cast<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(colorMapPixelCount));
} 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;
}
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()) {
return false;
}
// LightMap/diffusemap.dds (v>=32 only)
if (version >= 32) {
uint32_t lightMapSize;
BinaryIO::BinaryRead(stream, lightMapSize);
if (lightMapSize > kMaxBlobBytes) {
LOG("Chunk lightMap size %u exceeds maximum %zu bytes", lightMapSize, kMaxBlobBytes);
return false;
}
chunk.lightMap.resize(lightMapSize);
stream.read(reinterpret_cast<char*>(chunk.lightMap.data()), static_cast<std::streamsize>(lightMapSize));
if (stream.fail()) {
return false;
}
}
// Blend/texture map
BinaryIO::BinaryRead(stream, chunk.textureMapResolution);
if (chunk.textureMapResolution > kMaxResolution) {
LOG("Chunk textureMapResolution %u exceeds maximum %u", chunk.textureMapResolution, kMaxResolution);
return false;
}
const size_t textureMapPixelCount = static_cast<size_t>(chunk.textureMapResolution) * chunk.textureMapResolution * 4;
if (textureMapPixelCount > kMaxBlobBytes) {
LOG("Chunk textureMap size %zu exceeds maximum %zu bytes", textureMapPixelCount, kMaxBlobBytes);
return false;
}
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;
}
// Texture settings + blend map DDS (v>=32 only)
if (version >= 32) {
BinaryIO::BinaryRead(stream, chunk.textureSettings);
uint32_t blendMapDDSSize;
BinaryIO::BinaryRead(stream, blendMapDDSSize);
if (blendMapDDSSize > kMaxBlobBytes) {
LOG("Chunk blendMap size %u exceeds maximum %zu bytes", blendMapDDSSize, kMaxBlobBytes);
return false;
}
chunk.blendMap.resize(blendMapDDSSize);
stream.read(reinterpret_cast<char*>(chunk.blendMap.data()), static_cast<std::streamsize>(blendMapDDSSize));
if (stream.fail()) {
return false;
}
}
// Read flairs
uint32_t numFlairs;
BinaryIO::BinaryRead(stream, numFlairs);
if (stream.fail()) {
return false;
}
const size_t flairBytes = static_cast<size_t>(numFlairs) * sizeof(FlairAttributes);
if (flairBytes > kMaxBlobBytes) {
LOG("Chunk %u flair count %u exceeds maximum (byte size %zu > %zu)", chunk.id, numFlairs, flairBytes, kMaxBlobBytes);
return false;
}
chunk.flairs.resize(numFlairs);
for (uint32_t i = 0; i < numFlairs; ++i) {
if (!ReadFlairAttributes(stream, chunk.flairs[i])) {
return false;
}
}
// Scene map
if (version >= 32) {
const size_t sceneMapSize = static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution;
if (sceneMapSize > kMaxBlobBytes) {
LOG("Chunk sceneMap size %zu exceeds maximum %zu bytes", sceneMapSize, kMaxBlobBytes);
return false;
}
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 = 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;
}
std::vector<uint8_t> rawSceneMap(sceneMapCells);
stream.read(reinterpret_cast<char*>(rawSceneMap.data()), static_cast<std::streamsize>(sceneMapCells));
chunk.sceneMap.resize(static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution);
for (uint32_t row = 0; row < chunk.colorMapResolution; ++row) {
for (uint32_t col = 0; col < chunk.colorMapResolution; ++col) {
chunk.sceneMap[row * chunk.colorMapResolution + col] = rawSceneMap[row * (chunk.colorMapResolution + 1) + col];
}
}
} 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()) {
return false;
}
// Mesh data (v>=32 only)
if (version < 32) {
return true;
}
BinaryIO::BinaryRead(stream, chunk.vertSize);
if (stream.fail()) {
return false;
}
if (chunk.vertSize == 0) {
return true;
}
const size_t vertBytes = static_cast<size_t>(chunk.vertSize) * sizeof(uint16_t);
if (vertBytes > kMaxBlobBytes) {
LOG("Chunk %u vertSize %u exceeds maximum (byte size %zu > %zu)", chunk.id, chunk.vertSize, vertBytes, kMaxBlobBytes);
return false;
}
chunk.meshVertUsage.resize(chunk.vertSize);
ReadArray(stream, chunk.meshVertUsage);
if (stream.fail()) {
return false;
}
chunk.meshVertSize.resize(16);
for (int i = 0; i < 16; ++i) {
BinaryIO::BinaryRead(stream, chunk.meshVertSize[i]);
}
if (stream.fail()) {
return false;
}
chunk.meshTri.resize(16);
for (int i = 0; i < 16; ++i) {
if (!ReadMeshTri(stream, chunk.meshTri[i])) {
return false;
}
}
return true;
} catch (const std::exception&) {
return false;
}
}
bool ReadRaw(std::istream& stream, Raw& outRaw) {
// Get stream size
stream.seekg(0, std::ios::end);
auto streamSize = stream.tellg();
stream.seekg(0, std::ios::beg);
if (streamSize <= 0) {
return false;
}
try {
// Read header
BinaryIO::BinaryRead(stream, outRaw.version);
if (stream.fail()) {
return false;
}
BinaryIO::BinaryRead(stream, outRaw.dev);
if (stream.fail()) {
return false;
}
// Only read chunks if dev == 0
if (outRaw.dev == 0) {
BinaryIO::BinaryRead(stream, outRaw.numChunks);
BinaryIO::BinaryRead(stream, outRaw.numChunksWidth);
BinaryIO::BinaryRead(stream, outRaw.numChunksHeight);
if (outRaw.numChunks > kMaxChunks) {
LOG("Raw numChunks %u exceeds maximum %u", outRaw.numChunks, kMaxChunks);
return false;
}
// Read all chunks
outRaw.chunks.resize(outRaw.numChunks);
for (uint32_t i = 0; i < outRaw.numChunks; ++i) {
if (!ReadChunk(stream, outRaw.chunks[i], outRaw.version)) {
return false;
}
}
// Calculate terrain bounds from all chunks
if (!outRaw.chunks.empty()) {
outRaw.minBoundsX = std::numeric_limits<float>::max();
outRaw.minBoundsZ = std::numeric_limits<float>::max();
outRaw.maxBoundsX = std::numeric_limits<float>::lowest();
outRaw.maxBoundsZ = std::numeric_limits<float>::lowest();
for (const auto& chunk : outRaw.chunks) {
const float chunkMinX = chunk.offsetX;
const float chunkMinZ = chunk.offsetZ;
const float chunkMaxX = chunkMinX + (chunk.width * chunk.scaleFactor);
const float chunkMaxZ = chunkMinZ + (chunk.height * chunk.scaleFactor);
outRaw.minBoundsX = std::min(outRaw.minBoundsX, chunkMinX);
outRaw.minBoundsZ = std::min(outRaw.minBoundsZ, chunkMinZ);
outRaw.maxBoundsX = std::max(outRaw.maxBoundsX, chunkMaxX);
outRaw.maxBoundsZ = std::max(outRaw.maxBoundsZ, chunkMaxZ);
}
LOG_DEBUG("Raw terrain bounds: X[%.2f, %.2f], Z[%.2f, %.2f]",
outRaw.minBoundsX, outRaw.maxBoundsX, outRaw.minBoundsZ, outRaw.maxBoundsZ);
}
}
return true;
} catch (const std::exception&) {
return false;
}
}
void GenerateTerrainMesh(const Raw& raw, TerrainMesh& outMesh) {
outMesh.vertices.clear();
outMesh.triangles.clear();
if (raw.chunks.empty() || raw.version < 32) {
return; // No scene data available
}
LOG_DEBUG("GenerateTerrainMesh: Processing %zu chunks", raw.chunks.size());
uint32_t vertexOffset = 0;
for (const auto& chunk : raw.chunks) {
if (!chunk.IsValidForSceneLookup()) continue;
for (uint32_t i = 0; i < chunk.width; ++i) {
for (uint32_t j = 0; j < chunk.height; ++j) {
const uint32_t heightIndex = chunk.width * i + j;
if (heightIndex >= chunk.heightMap.size()) continue;
outMesh.vertices.emplace_back(chunk.GridToWorldPos(i, j), chunk.GetSceneIDAtGrid(i, j));
if (i > 0 && j > 0) {
const uint32_t currentVert = vertexOffset + chunk.width * i + j;
const uint32_t leftVert = currentVert - 1;
const uint32_t bottomLeftVert = vertexOffset + chunk.width * (i - 1) + j - 1;
const uint32_t bottomVert = vertexOffset + chunk.width * (i - 1) + j;
// First triangle
outMesh.triangles.push_back(currentVert);
outMesh.triangles.push_back(leftVert);
outMesh.triangles.push_back(bottomLeftVert);
// Second triangle
outMesh.triangles.push_back(bottomLeftVert);
outMesh.triangles.push_back(bottomVert);
outMesh.triangles.push_back(currentVert);
}
}
}
vertexOffset += chunk.width * chunk.height;
}
}
bool WriteTerrainMeshToOBJ(const TerrainMesh& mesh, const std::string& path) {
try {
std::ofstream file(path);
if (!file.is_open()) {
LOG("Failed to open OBJ file for writing: %s", path.c_str());
return false;
}
for (const auto& v : mesh.vertices) {
const NiColor& color = SceneColor::Get(v.sceneID);
file << "v " << v.position.x << ' ' << v.position.y << ' ' << v.position.z
<< ' ' << color.m_Red << ' ' << color.m_Green << ' ' << color.m_Blue << '\n';
}
for (size_t i = 0; i < mesh.triangles.size(); i += 3) {
file << "f " << (mesh.triangles[i] + 1) << ' '
<< (mesh.triangles[i + 1] + 1) << ' '
<< (mesh.triangles[i + 2] + 1) << '\n';
}
file.close();
LOG("Successfully wrote terrain mesh to OBJ: %s (%zu vertices, %zu triangles)",
path.c_str(), mesh.vertices.size(), mesh.triangles.size() / 3);
return true;
} catch (const std::exception& e) {
LOG("Exception while writing OBJ file: %s", e.what());
return false;
}
}
} // namespace Raw

View File

@@ -1,168 +0,0 @@
#pragma once
#ifndef __RAW_H__
#define __RAW_H__
#include <cstdint>
#include <vector>
#include <string>
#include <istream>
#include "NiPoint3.h"
#include "dCommonVars.h"
/**
* A zone's terrain file (.raw), read whole: per chunk its heights, texture ids, color and texture blend maps, the
* scene each terrain cell belongs to, the flairs (grass, flowers and small rocks strewn over it) and the mesh data
* the client renders from. Guards against malformed files: sizes are capped before anything is allocated. Lives in
* dCommon so the dashboard's 3D views read terrain with the same code.
*/
namespace Raw {
/**
* @brief Flair attributes structure
* Represents decorative elements on the terrain: id is a FlairTable row (its asset is the model), rotation is in
* radians about x, y and z, and the color tints the model (the client's Flair.fx multiplies the lighting by it)
*/
struct FlairAttributes {
uint32_t id;
float scaleFactor;
NiPoint3 position;
NiPoint3 rotation;
uint8_t colorR;
uint8_t colorG;
uint8_t colorB;
uint8_t colorA;
};
/**
* @brief Mesh triangle structure
* Contains triangle indices for terrain mesh
*/
struct MeshTri {
uint16_t meshTriListSize;
std::vector<uint16_t> meshTriList;
};
/**
* @brief Vertex with scene ID
* Used for the generated terrain mesh to enable fast scene lookups
*/
struct SceneVertex {
NiPoint3 position;
uint8_t sceneID;
SceneVertex() : position(), sceneID(0) {}
SceneVertex(const NiPoint3& pos, uint8_t scene) : position(pos), sceneID(scene) {}
};
/**
* @brief Generated terrain mesh
* Contains vertices with scene IDs for fast scene lookups at arbitrary positions
*/
struct TerrainMesh {
std::vector<SceneVertex> vertices;
std::vector<uint32_t> triangles; // Indices into vertices array (groups of 3)
TerrainMesh() = default;
};
/**
* @brief Terrain chunk structure
* Represents a single chunk of terrain with heightmap, textures, and meshes
*/
struct Chunk {
uint32_t id;
uint32_t width;
uint32_t height;
float offsetX;
float offsetZ;
uint32_t shaderId;
// Texture IDs (4 textures per chunk)
std::vector<uint32_t> textureIds;
// Terrain scale factor
float scaleFactor;
// Heightmap data (width * height floats)
std::vector<float> heightMap;
// Version 32+ fields
uint32_t colorMapResolution = 0;
std::vector<uint8_t> colorMap; // RGBA pixels (colorMap * colorMap * 4)
std::vector<uint8_t> lightMap;
uint32_t textureMapResolution = 0;
std::vector<uint8_t> textureMap; // (textureMapResolution * textureMapResolution * 4)
uint8_t textureSettings = 0;
std::vector<uint8_t> blendMap;
// Flair data
std::vector<FlairAttributes> flairs;
// Scene map (version 32+)
std::vector<uint8_t> sceneMap;
// Mesh data
uint32_t vertSize = 0;
std::vector<uint16_t> meshVertUsage;
std::vector<uint16_t> meshVertSize;
std::vector<MeshTri> meshTri;
bool IsValidForSceneLookup() const;
uint8_t GetSceneIDAtGrid(uint32_t i, uint32_t j) const;
NiPoint3 GridToWorldPos(uint32_t i, uint32_t j) const;
};
/**
* @brief RAW terrain file structure
* Complete representation of a .raw terrain file
*/
struct Raw {
uint16_t version;
uint8_t dev;
uint32_t numChunks = 0;
uint32_t numChunksWidth = 0;
uint32_t numChunksHeight = 0;
std::vector<Chunk> chunks;
// Calculated bounds of the entire terrain
float minBoundsX = 0.0f;
float minBoundsZ = 0.0f;
float maxBoundsX = 0.0f;
float maxBoundsZ = 0.0f;
};
/**
* @brief Read a RAW terrain file from an input stream
*
* @param stream Input stream containing RAW file data
* @param outRaw Output RAW file structure
* @return true if successfully read, false otherwise
*/
bool ReadRaw(std::istream& stream, Raw& outRaw);
/**
* @brief Generate a terrain mesh from raw chunks
* Similar to dTerrain's GenerateFinalMeshFromChunks but creates a mesh with scene IDs
* per vertex for fast scene lookups at arbitrary positions.
*
* @param raw The RAW terrain data to generate mesh from
* @param outMesh Output terrain mesh with vertices and scene IDs
*/
void GenerateTerrainMesh(const Raw& raw, TerrainMesh& outMesh);
/**
* @brief Write terrain mesh to OBJ file for debugging/visualization
* Merged from dTerrain's WriteFinalMeshToOBJ functionality
* Vertices are colored based on their scene ID using a hash function
*
* @param mesh The terrain mesh to export
* @param path Output path for the OBJ file
* @return true if successfully written, false otherwise
*/
bool WriteTerrainMeshToOBJ(const TerrainMesh& mesh, const std::string& path);
} // namespace Raw
#endif // __RAW_H__

View File

@@ -2028,7 +2028,7 @@ namespace DEVGMCommands {
// Build the feedback message
std::ostringstream feedback;
feedback << "Scene ID: " << sceneID.GetSceneID();
feedback << " (Layer: " << sceneID.GetLayerID() << ")";
feedback << " (Layer: " << GeneralUtils::ToUnderlying(sceneID.GetLayerID()) << ")";
// Get the scene name
const auto* sceneRef = zone->GetScene(sceneID);
@@ -2071,7 +2071,7 @@ namespace DEVGMCommands {
} else {
feedback << "\nAdjacent Scenes (" << adjacentSceneIDs.size() << "):";
for (const auto& adjSceneID : adjacentSceneIDs) {
feedback << "\n - Scene ID: " << adjSceneID.GetSceneID() << " (Layer: " << adjSceneID.GetLayerID() << ")";
feedback << "\n - Scene ID: " << adjSceneID.GetSceneID() << " (Layer: " << GeneralUtils::ToUnderlying(adjSceneID.GetLayerID()) << ")";
// Get the scene name if available
const auto* sceneRef = zone->GetScene(adjSceneID);

View File

@@ -11,6 +11,8 @@ public:
constexpr LWOSCENEID() noexcept { m_sceneID = -1; m_layerID = eSceneType::General; }
constexpr LWOSCENEID(int32_t sceneID) noexcept { m_sceneID = sceneID; m_layerID = eSceneType::General; }
constexpr LWOSCENEID(int32_t sceneID, eSceneType layerID) noexcept { m_sceneID = sceneID; m_layerID = layerID; }
// The layer as the zone file and the terrain number it (0 General, 1 Audio)
constexpr LWOSCENEID(int32_t sceneID, uint32_t layerID) noexcept { m_sceneID = sceneID; m_layerID = static_cast<eSceneType>(layerID); }
constexpr LWOSCENEID& operator=(const LWOSCENEID& rhs) noexcept { m_sceneID = rhs.m_sceneID; m_layerID = rhs.m_layerID; return *this; }
constexpr LWOSCENEID& operator=(const int32_t rhs) noexcept { m_sceneID = rhs; m_layerID = eSceneType::General; return *this; }

View File

@@ -157,14 +157,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()) {
@@ -202,6 +211,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;

View File

@@ -60,6 +60,8 @@ endif()
target_link_libraries(dCommonTests ${COMMON_LIBRARIES} MD5 GTest::gtest_main)
# SpareBackoff.h (header only)
target_include_directories(dCommonTests PRIVATE "${PROJECT_SOURCE_DIR}/dMasterServer")
# The terrain reader (ClientZoneFilesTests reads every client .raw), without the rest of dZoneManager
target_sources(dCommonTests PRIVATE "${PROJECT_SOURCE_DIR}/dZoneManager/Raw.cpp")
# ChatFilterCore.h (header only)
target_include_directories(dCommonTests PRIVATE "${PROJECT_SOURCE_DIR}/dChatFilter")