feat: Raw reading on zone load and scene tools (#1910)

* feat: raw terrain parsing for scene data

Replace old dNavigation/dTerrain raw parser with new Raw module in
dZoneManager. Parse heightmaps, color maps, and scene maps from .raw
files to determine which scene a position belongs to. Build scene
adjacency graph from terrain data and scene transitions.

Adds NiColor type, SceneColor lookup table, eSceneType enum, terrain
mesh generation with OBJ export, and debug slash commands for scene
visualization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: deduplicate terrain grid lookup logic, remove unnecessary cast

Add IsValidForSceneLookup(), GetSceneIDAtGrid(), and GridToWorldPos()
to Raw::Chunk. Use them in GenerateTerrainMesh, GetSceneIDFromPosition,
SpawnScenePoints, and SpawnAllScenePoints instead of duplicated inline
math. Remove unnecessary static_cast in LoadSceneTransitionInfo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add version < 32 support to raw terrain parser

The parser was only correct for v32 files. For v<32:
- colorMapResolution is width-1, not width
- Color map reads width*width*4 bytes (BGRA per-pixel), not colorMapRes^2*4
- No DDS lightmap
- No texture settings byte or blend DDS
- Scene map: v31 reads (colorMapRes+1)^2 cells with border skip,
  v<31 skips 1 byte
- No mesh data

Verified against all 309 .raw files across multiple client versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: log and reject legacy raw terrain format, clean up ReadRaw API

Log a clear message and throw if the zone uses a legacy raw format
(LUZ version < 30) since no known maps use it. Remove ReadLegacyRaw
and the mapVersion parameter from ReadRaw.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: revert v33 scene field renames to unknown1/unknown2

These fields are skipped by the client in every known build (ptr += 12
and ptr += 4). No client ever reads them into a struct. Keep the
original unknown1/unknown2 names from main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: load the zone without scene lookups when its raw terrain is missing or unreadable

Log at debug when it parses; build the terrain mesh only for the OBJ export; read the zone name and description only for version 31 and later, as the client does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: look up the scene at a position as the client does, in constant time

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>

* feat: scene point commands take a step and use brick LOTs from the BrickIDTable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: rename NiColor::ToXYZ to Luminance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: raw terrain reading, scene lookup, scene graph and scene commands

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: close the OBJ file before removing it so the test passes on Windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* fix: name the fourth flair colour byte padding

The client never sets or reads it, so it holds leftover memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep the scene map of version 31 and older raws

These files have a (resolution + 1)^2 scene map, kept in sceneMapLegacy. Version 31 uses its first resolution rows and columns; older versions are all scene 0, as in the client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: simplify the raw heightmap size check

The dimensions are capped at kMaxResolution, so the product can't overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: remove the scene point spawn commands and send /getadjacentscenes as one message

The spawn commands were only for testing. /getscene and /getadjacentscenes stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-27 09:22:28 -05:00
committed by GitHub
parent fc6a7fbd52
commit 9bf2ff4d44
29 changed files with 2102 additions and 315 deletions

View File

@@ -1,11 +1,5 @@
set(DNAVIGATION_SOURCES "dNavMesh.cpp")
add_subdirectory(dTerrain)
foreach(file ${DNAVIGATIONS_DTERRAIN_SOURCES})
set(DNAVIGATION_SOURCES ${DNAVIGATION_SOURCES} "dTerrain/${file}")
endforeach()
add_library(dNavigation OBJECT ${DNAVIGATION_SOURCES})
target_include_directories(dNavigation PUBLIC "."
PRIVATE

View File

@@ -1,6 +1,5 @@
#include "dNavMesh.h"
#include "RawFile.h"
#include "Game.h"
#include "Logger.h"

View File

@@ -1,3 +0,0 @@
set(DNAVIGATIONS_DTERRAIN_SOURCES "RawFile.cpp"
"RawChunk.cpp"
"RawHeightMap.cpp" PARENT_SCOPE)

View File

@@ -1,93 +0,0 @@
#include "RawChunk.h"
#include "BinaryIO.h"
#include "RawMesh.h"
#include "RawHeightMap.h"
RawChunk::RawChunk(std::ifstream& stream) {
// Read the chunk index and info
BinaryIO::BinaryRead(stream, m_ChunkIndex);
BinaryIO::BinaryRead(stream, m_Width);
BinaryIO::BinaryRead(stream, m_Height);
BinaryIO::BinaryRead(stream, m_X);
BinaryIO::BinaryRead(stream, m_Z);
m_HeightMap = new RawHeightMap(stream, m_Height, m_Width);
// We can just skip the rest of the data so we can read the next chunks, we don't need anymore data
// Possible overflow here? TODO make reasonable upper bound or confirm big numbers arent necessary to have
uint32_t colorMapSize;
BinaryIO::BinaryRead(stream, colorMapSize);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (colorMapSize * colorMapSize * 4));
uint32_t lightmapSize;
BinaryIO::BinaryRead(stream, lightmapSize);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (lightmapSize));
uint32_t colorMapSize2;
BinaryIO::BinaryRead(stream, colorMapSize2);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (colorMapSize2 * colorMapSize2 * 4));
uint8_t unknown;
BinaryIO::BinaryRead(stream, unknown);
uint32_t blendmapSize;
BinaryIO::BinaryRead(stream, blendmapSize);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (blendmapSize));
uint32_t pointSize;
BinaryIO::BinaryRead(stream, pointSize);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (pointSize * 9 * 4));
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (colorMapSize * colorMapSize));
uint32_t endCounter;
BinaryIO::BinaryRead(stream, endCounter);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (endCounter * 2));
if (endCounter != 0) {
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (32));
for (int i = 0; i < 0x10; i++) {
uint16_t finalCountdown;
BinaryIO::BinaryRead(stream, finalCountdown);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (finalCountdown * 2));
}
}
// Generate our mesh/geo data for this chunk
this->GenerateMesh();
}
RawChunk::~RawChunk() {
if (m_Mesh) delete m_Mesh;
if (m_HeightMap) delete m_HeightMap;
}
void RawChunk::GenerateMesh() {
RawMesh* meshData = new RawMesh();
for (int i = 0; i < m_Width; ++i) {
for (int j = 0; j < m_Height; ++j) {
float y = *std::next(m_HeightMap->m_FloatMap.begin(), m_Width * i + j);
meshData->m_Vertices.push_back(NiPoint3(i, y, j));
if (i == 0 || j == 0) continue;
meshData->m_Triangles.push_back(m_Width * i + j);
meshData->m_Triangles.push_back(m_Width * i + j - 1);
meshData->m_Triangles.push_back(m_Width * (i - 1) + j - 1);
meshData->m_Triangles.push_back(m_Width * (i - 1) + j - 1);
meshData->m_Triangles.push_back(m_Width * (i - 1) + j);
meshData->m_Triangles.push_back(m_Width * i + j);
}
}
m_Mesh = meshData;
}

View File

@@ -1,24 +0,0 @@
#pragma once
#include <cstdint>
#include <fstream>
struct RawMesh;
class RawHeightMap;
class RawChunk {
public:
RawChunk(std::ifstream& stream);
~RawChunk();
void GenerateMesh();
uint32_t m_ChunkIndex;
uint32_t m_Width;
uint32_t m_Height;
float m_X;
float m_Z;
RawHeightMap* m_HeightMap;
RawMesh* m_Mesh;
};

View File

@@ -1,84 +0,0 @@
#include "RawFile.h"
#include "BinaryIO.h"
#include "RawChunk.h"
#include "RawMesh.h"
#include "RawHeightMap.h"
RawFile::RawFile(std::string fileName) {
if (!BinaryIO::DoesFileExist(fileName)) return;
std::ifstream file(fileName, std::ios::binary);
// Read header
BinaryIO::BinaryRead(file, m_Version);
BinaryIO::BinaryRead(file, m_Padding);
BinaryIO::BinaryRead(file, m_ChunkCount);
BinaryIO::BinaryRead(file, m_Width);
BinaryIO::BinaryRead(file, m_Height);
if (m_Version < 0x20) {
return; // Version is too old to be supported
}
// Read in chunks
m_Chunks = {};
for (uint32_t i = 0; i < m_ChunkCount; i++) {
RawChunk* chunk = new RawChunk(file);
m_Chunks.push_back(chunk);
}
m_FinalMesh = new RawMesh();
this->GenerateFinalMeshFromChunks();
}
RawFile::~RawFile() {
if (m_FinalMesh) delete m_FinalMesh;
for (const auto* item : m_Chunks) {
if (item) delete item;
}
}
void RawFile::GenerateFinalMeshFromChunks() {
uint32_t lenOfLastChunk = 0; // index of last vert set in the last chunk
for (const auto& chunk : m_Chunks) {
for (const auto& vert : chunk->m_Mesh->m_Vertices) {
auto tempVert = vert;
// Scale X and Z by the chunk's position in the world
// Scale Y by the chunk's heightmap scale factor
tempVert.SetX(tempVert.GetX() + (chunk->m_X / chunk->m_HeightMap->m_ScaleFactor));
tempVert.SetY(tempVert.GetY() / chunk->m_HeightMap->m_ScaleFactor);
tempVert.SetZ(tempVert.GetZ() + (chunk->m_Z / chunk->m_HeightMap->m_ScaleFactor));
// Then scale it again for some reason
tempVert *= chunk->m_HeightMap->m_ScaleFactor;
m_FinalMesh->m_Vertices.push_back(tempVert);
}
for (const auto& tri : chunk->m_Mesh->m_Triangles) {
m_FinalMesh->m_Triangles.push_back(tri + lenOfLastChunk);
}
lenOfLastChunk += chunk->m_Mesh->m_Vertices.size();
}
}
void RawFile::WriteFinalMeshToOBJ(std::string path) {
std::ofstream file(path);
for (const auto& v : m_FinalMesh->m_Vertices) {
file << "v " << v.x << ' ' << v.y << ' ' << v.z << '\n';
}
for (int i = 0; i < m_FinalMesh->m_Triangles.size(); i += 3) {
file << "f " << *std::next(m_FinalMesh->m_Triangles.begin(), i) + 1 << ' ' << *std::next(m_FinalMesh->m_Triangles.begin(), i + 1) + 1 << ' ' << *std::next(m_FinalMesh->m_Triangles.begin(), i + 2) + 1 << '\n';
}
}

View File

@@ -1,28 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
class RawChunk;
struct RawMesh;
class RawFile {
public:
RawFile(std::string filePath);
~RawFile();
private:
void GenerateFinalMeshFromChunks();
void WriteFinalMeshToOBJ(std::string path);
uint8_t m_Version;
uint16_t m_Padding;
uint32_t m_ChunkCount;
uint32_t m_Width;
uint32_t m_Height;
std::vector<RawChunk*> m_Chunks;
RawMesh* m_FinalMesh = nullptr;
};

View File

@@ -1,27 +0,0 @@
#include "RawHeightMap.h"
#include "BinaryIO.h"
RawHeightMap::RawHeightMap() {}
RawHeightMap::RawHeightMap(std::ifstream& stream, float height, float width) {
// Read in height map data header and scale
BinaryIO::BinaryRead(stream, m_Unknown1);
BinaryIO::BinaryRead(stream, m_Unknown2);
BinaryIO::BinaryRead(stream, m_Unknown3);
BinaryIO::BinaryRead(stream, m_Unknown4);
BinaryIO::BinaryRead(stream, m_ScaleFactor);
// read all vertices in
for (uint64_t i = 0; i < width * height; i++) {
float value;
BinaryIO::BinaryRead(stream, value);
m_FloatMap.push_back(value);
}
}
RawHeightMap::~RawHeightMap() {
}

View File

@@ -1,21 +0,0 @@
#pragma once
#include <cstdint>
#include <vector>
#include <fstream>
class RawHeightMap {
public:
RawHeightMap();
RawHeightMap(std::ifstream& stream, float height, float width);
~RawHeightMap();
uint32_t m_Unknown1;
uint32_t m_Unknown2;
uint32_t m_Unknown3;
uint32_t m_Unknown4;
float m_ScaleFactor;
std::vector<float> m_FloatMap = {};
};

View File

@@ -1,10 +0,0 @@
#pragma once
#include <vector>
#include "NiPoint3.h"
struct RawMesh {
std::vector<NiPoint3> m_Vertices;
std::vector<uint32_t> m_Triangles;
};