mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-06 20:07:13 +00:00
Chunk bounds span (width - 1) * scale; the nearest scene map cell is used and the far edge is kept one cell inside. Chunks are indexed by grid cell when they form a grid. Skipping reads instead of seeking, so a cut-off file can't seek past the end. Terrain mesh indices follow the vertex order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
612 lines
21 KiB
C++
612 lines
21 KiB
C++
#include "Raw.h"
|
|
#include "BinaryIO.h"
|
|
#include "Logger.h"
|
|
#include "SceneColor.h"
|
|
#include <fstream>
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
constexpr uint32_t kMaxResolution = 4096;
|
|
constexpr size_t kMaxBlobBytes = 64ULL * 1024 * 1024; // 64 MiB
|
|
constexpr uint32_t kMaxChunks = 1024;
|
|
} // namespace
|
|
|
|
namespace Raw {
|
|
|
|
bool Chunk::IsValidForSceneLookup() const {
|
|
return !sceneMap.empty() && colorMapResolution > 0 && !heightMap.empty()
|
|
&& scaleFactor > 0.0f && width > 1 && height > 1;
|
|
}
|
|
|
|
uint8_t Chunk::GetSceneIDAtLocal(float localX, float localZ) const {
|
|
// The nearest scene map cell
|
|
const auto toCell = [this](float cell) {
|
|
return static_cast<uint32_t>(std::clamp(static_cast<int32_t>(std::floor(cell + 0.5f)), 0, static_cast<int32_t>(colorMapResolution) - 1));
|
|
};
|
|
const uint32_t sceneIndex = toCell(sceneCellsPerUnitX * localX) * colorMapResolution + toCell(sceneCellsPerUnitZ * localZ);
|
|
if (sceneIndex >= sceneMap.size()) return 0;
|
|
return sceneMap[sceneIndex];
|
|
}
|
|
|
|
uint8_t Chunk::GetSceneIDAtGrid(uint32_t i, uint32_t j) const {
|
|
return GetSceneIDAtLocal(static_cast<float>(i) * scaleFactor, static_cast<float>(j) * scaleFactor);
|
|
}
|
|
|
|
NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const {
|
|
const uint32_t heightIndex = i * height + j;
|
|
const float y = heightIndex < heightMap.size() ? heightMap[heightIndex] : 0.0f;
|
|
return NiPoint3(offsetX + static_cast<float>(i) * scaleFactor, y, offsetZ + static_cast<float>(j) * scaleFactor);
|
|
}
|
|
|
|
/**
|
|
* @brief Skip bytes, such as the byte version 30 and older files have in place of a scene map
|
|
*/
|
|
static bool Skip(std::istream& stream, size_t bytes) {
|
|
if (bytes > kMaxBlobBytes) {
|
|
LOG("Raw block of %zu bytes exceeds maximum %zu bytes", bytes, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
// ignore, not seekg: the asset streams don't stop a seek at the end of the data
|
|
stream.ignore(static_cast<std::streamsize>(bytes));
|
|
return static_cast<size_t>(stream.gcount()) == bytes && !stream.fail();
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
for (uint16_t i = 0; i < meshTri.meshTriListSize; ++i) {
|
|
BinaryIO::BinaryRead(stream, meshTri.meshTriList[i]);
|
|
}
|
|
return true;
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Read a chunk from stream
|
|
*/
|
|
static bool ReadChunkData(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 == 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;
|
|
}
|
|
|
|
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 == 0 || totalBytes > kMaxBlobBytes) {
|
|
LOG("Chunk %u heightmap total size invalid: bytes=%zu (max %zu)", chunk.id, totalBytes, kMaxBlobBytes);
|
|
return false;
|
|
}
|
|
|
|
chunk.heightMap.resize(heightMapSize);
|
|
for (size_t i = 0; i < heightMapSize; ++i) {
|
|
BinaryIO::BinaryRead(stream, chunk.heightMap[i]);
|
|
}
|
|
|
|
if (stream.fail()) {
|
|
return false;
|
|
}
|
|
|
|
// ColorMap
|
|
if (version >= 32) {
|
|
BinaryIO::BinaryRead(stream, chunk.colorMapResolution);
|
|
} else {
|
|
chunk.colorMapResolution = chunk.width - 1;
|
|
}
|
|
|
|
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 {
|
|
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));
|
|
}
|
|
|
|
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));
|
|
|
|
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 = static_cast<size_t>(chunk.colorMapResolution + 1) * (chunk.colorMapResolution + 1);
|
|
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 if (!Skip(stream, 1)) {
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
for (uint32_t i = 0; i < chunk.vertSize; ++i) {
|
|
BinaryIO::BinaryRead(stream, chunk.meshVertUsage[i]);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Read a chunk and work out its scene map cell sizes
|
|
*/
|
|
static bool ReadChunk(std::istream& stream, Chunk& chunk, uint16_t version) {
|
|
if (!ReadChunkData(stream, chunk, version)) return false;
|
|
|
|
if (chunk.IsValidForSceneLookup()) {
|
|
chunk.sceneCellSizeX = static_cast<float>(chunk.width - 1) * chunk.scaleFactor / static_cast<float>(chunk.colorMapResolution);
|
|
chunk.sceneCellSizeZ = static_cast<float>(chunk.height - 1) * chunk.scaleFactor / static_cast<float>(chunk.colorMapResolution);
|
|
chunk.sceneCellsPerUnitX = 1.0f / chunk.sceneCellSizeX;
|
|
chunk.sceneCellsPerUnitZ = 1.0f / chunk.sceneCellSizeZ;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Index the chunks by grid cell when they form a regular grid of equal chunks
|
|
*/
|
|
static void BuildChunkGrid(Raw& raw) {
|
|
raw.chunkGrid.clear();
|
|
const auto& first = raw.chunks.front();
|
|
const float spanX = first.GetMaxX() - first.offsetX;
|
|
const float spanZ = first.GetMaxZ() - first.offsetZ;
|
|
if (spanX <= 0.0f || spanZ <= 0.0f) return;
|
|
|
|
const auto countX = static_cast<uint32_t>(std::lround((raw.maxBoundsX - raw.minBoundsX) / spanX));
|
|
const auto countZ = static_cast<uint32_t>(std::lround((raw.maxBoundsZ - raw.minBoundsZ) / spanZ));
|
|
if (countX == 0 || countZ == 0 || static_cast<size_t>(countX) * countZ > kMaxChunks) return;
|
|
|
|
std::vector<int32_t> grid(static_cast<size_t>(countX) * countZ, -1);
|
|
for (size_t i = 0; i < raw.chunks.size(); ++i) {
|
|
const auto& chunk = raw.chunks[i];
|
|
const float cellX = (chunk.offsetX - raw.minBoundsX) / spanX;
|
|
const float cellZ = (chunk.offsetZ - raw.minBoundsZ) / spanZ;
|
|
const long x = std::lround(cellX);
|
|
const long z = std::lround(cellZ);
|
|
const bool sameSize = chunk.width == first.width && chunk.height == first.height && chunk.scaleFactor == first.scaleFactor;
|
|
if (!sameSize || std::abs(cellX - x) > 0.01f || std::abs(cellZ - z) > 0.01f || x < 0 || z < 0
|
|
|| x >= static_cast<long>(countX) || z >= static_cast<long>(countZ)) {
|
|
LOG("Raw chunks are not a regular grid (chunk %u); scene lookups check every chunk", chunk.id);
|
|
return;
|
|
}
|
|
auto& cell = grid[static_cast<size_t>(x) * countZ + z];
|
|
if (cell == -1) cell = static_cast<int32_t>(i); // the client takes the first chunk that matches
|
|
}
|
|
|
|
raw.chunkGrid = std::move(grid);
|
|
raw.chunkGridCountX = countX;
|
|
raw.chunkGridCountZ = countZ;
|
|
raw.chunkSpanX = spanX;
|
|
raw.chunkSpanZ = spanZ;
|
|
}
|
|
|
|
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) {
|
|
outRaw.minBoundsX = std::min(outRaw.minBoundsX, chunk.offsetX);
|
|
outRaw.minBoundsZ = std::min(outRaw.minBoundsZ, chunk.offsetZ);
|
|
outRaw.maxBoundsX = std::max(outRaw.maxBoundsX, chunk.GetMaxX());
|
|
outRaw.maxBoundsZ = std::max(outRaw.maxBoundsZ, chunk.GetMaxZ());
|
|
}
|
|
LOG_DEBUG("Raw terrain bounds: X[%.2f, %.2f], Z[%.2f, %.2f]",
|
|
outRaw.minBoundsX, outRaw.maxBoundsX, outRaw.minBoundsZ, outRaw.maxBoundsZ);
|
|
BuildChunkGrid(outRaw);
|
|
}
|
|
}
|
|
return true;
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The client's TerrainManager::GetSceneAtPos: clamp to the terrain bounds, keep one scene cell inside the far edge,
|
|
// find the chunk containing the position (edges moved in by 0.001) and take its nearest scene map cell
|
|
std::optional<uint8_t> Raw::GetSceneIDAt(float x, float z) const {
|
|
if (chunks.empty()) return std::nullopt;
|
|
|
|
constexpr float kEdgeEpsilon = 0.001f;
|
|
const float posX = std::clamp(x, minBoundsX, maxBoundsX);
|
|
const float posZ = std::clamp(z, minBoundsZ, maxBoundsZ);
|
|
|
|
const auto sceneInChunk = [&](const Chunk& chunk) -> std::optional<uint8_t> {
|
|
if (!chunk.IsValidForSceneLookup()) return std::nullopt;
|
|
const float chunkX = std::min(posX, maxBoundsX - chunk.sceneCellSizeX);
|
|
const float chunkZ = std::min(posZ, maxBoundsZ - chunk.sceneCellSizeZ);
|
|
if (chunk.offsetX - kEdgeEpsilon <= chunkX && chunkX < chunk.GetMaxX() - kEdgeEpsilon &&
|
|
chunk.offsetZ - kEdgeEpsilon <= chunkZ && chunkZ < chunk.GetMaxZ() - kEdgeEpsilon) {
|
|
return chunk.GetSceneIDAtLocal(chunkX - chunk.offsetX, chunkZ - chunk.offsetZ);
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
|
|
if (chunkGrid.empty()) {
|
|
for (const auto& chunk : chunks) {
|
|
if (const auto sceneID = sceneInChunk(chunk)) return sceneID;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// The grid cell the position falls in, checked with the client's test; for a position on a float edge the
|
|
// neighbouring cells are checked too
|
|
const auto toCell = [](float pos, float min, float span, uint32_t count) {
|
|
const auto cell = static_cast<int32_t>(std::floor((pos + kEdgeEpsilon - min) / span));
|
|
return std::clamp(cell, 0, static_cast<int32_t>(count) - 1);
|
|
};
|
|
const int32_t cellX = toCell(posX, minBoundsX, chunkSpanX, chunkGridCountX);
|
|
const int32_t cellZ = toCell(posZ, minBoundsZ, chunkSpanZ, chunkGridCountZ);
|
|
for (const int32_t offsetX : { 0, -1, 1 }) {
|
|
for (const int32_t offsetZ : { 0, -1, 1 }) {
|
|
const int32_t x = cellX + offsetX;
|
|
const int32_t z = cellZ + offsetZ;
|
|
if (x < 0 || z < 0 || x >= static_cast<int32_t>(chunkGridCountX) || z >= static_cast<int32_t>(chunkGridCountZ)) continue;
|
|
const int32_t index = chunkGrid[static_cast<size_t>(x) * chunkGridCountZ + z];
|
|
if (index < 0) continue;
|
|
if (const auto sceneID = sceneInChunk(chunks[index])) return sceneID;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
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;
|
|
|
|
// One vertex per heightmap point, in [i * height + j] order
|
|
for (uint32_t i = 0; i < chunk.width; ++i) {
|
|
for (uint32_t j = 0; j < chunk.height; ++j) {
|
|
outMesh.vertices.emplace_back(chunk.GridToWorldPos(i, j), chunk.GetSceneIDAtGrid(i, j));
|
|
if (i > 0 && j > 0) {
|
|
const uint32_t currentVert = vertexOffset + chunk.height * i + j;
|
|
const uint32_t leftVert = currentVert - 1;
|
|
const uint32_t bottomLeftVert = vertexOffset + chunk.height * (i - 1) + j - 1;
|
|
const uint32_t bottomVert = vertexOffset + chunk.height * (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
|