diff --git a/dZoneManager/CMakeLists.txt b/dZoneManager/CMakeLists.txt index ba6eff68a..069a3b6a3 100644 --- a/dZoneManager/CMakeLists.txt +++ b/dZoneManager/CMakeLists.txt @@ -15,7 +15,7 @@ target_include_directories(dZoneManager PUBLIC "." "${PROJECT_SOURCE_DIR}/dGame" # Entity.h "${PROJECT_SOURCE_DIR}/dGame/dEntity" # EntityInfo.h PRIVATE - "${PROJECT_SOURCE_DIR}/dCommon/dClient" # SceneColors.h + "${PROJECT_SOURCE_DIR}/dCommon/dClient" # SceneColor.h "${PROJECT_SOURCE_DIR}/dGame/dComponents" #InventoryComponent.h "${PROJECT_SOURCE_DIR}/dGame/dInventory" #InventoryComponent.h (transitive) "${PROJECT_SOURCE_DIR}/dGame/dBehaviors" #BehaviorSlot.h diff --git a/dZoneManager/Raw.cpp b/dZoneManager/Raw.cpp index 6ac96a7cd..bc97a583e 100644 --- a/dZoneManager/Raw.cpp +++ b/dZoneManager/Raw.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace { constexpr uint32_t kMaxResolution = 4096; @@ -19,25 +20,39 @@ bool Chunk::IsValidForSceneLookup() const { && scaleFactor > 0.0f && width > 1 && height > 1; } -uint8_t Chunk::GetSceneIDAtGrid(uint32_t i, uint32_t j) const { - const float sceneMapI = (static_cast(i) / static_cast(width - 1)) * static_cast(colorMapResolution - 1); - const float sceneMapJ = (static_cast(j) / static_cast(height - 1)) * static_cast(colorMapResolution - 1); - const uint32_t sceneI = std::min(static_cast(sceneMapI), colorMapResolution - 1); - const uint32_t sceneJ = std::min(static_cast(sceneMapJ), colorMapResolution - 1); - const uint32_t sceneIndex = sceneI * colorMapResolution + sceneJ; +uint8_t Chunk::GetSceneIDAtLocal(float localX, float localZ) const { + // The nearest scene map cell + const auto toCell = [this](float cell) { + return static_cast(std::clamp(static_cast(std::floor(cell + 0.5f)), 0, static_cast(colorMapResolution) - 1)); + }; + const uint32_t sceneIndex = toCell(sceneCellsPerUnitX * localX) * colorMapResolution + toCell(sceneCellsPerUnitZ * localZ); 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(i) + (offsetX / scaleFactor)) * scaleFactor, - y, - (static_cast(j) + (offsetZ / scaleFactor)) * scaleFactor - ); +uint8_t Chunk::GetSceneIDAtGrid(uint32_t i, uint32_t j) const { + return GetSceneIDAtLocal(static_cast(i) * scaleFactor, static_cast(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(i) * scaleFactor, y, offsetZ + static_cast(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(bytes)); + return static_cast(stream.gcount()) == bytes && !stream.fail(); + } + /** * @brief Read flair attributes from stream */ @@ -80,7 +95,7 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { /** * @brief Read a chunk from stream */ - static bool ReadChunk(std::istream& stream, Chunk& chunk, uint16_t version) { + static bool ReadChunkData(std::istream& stream, Chunk& chunk, uint16_t version) { try { // Read basic chunk info BinaryIO::BinaryRead(stream, chunk.id); @@ -294,8 +309,8 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { chunk.sceneMap[row * chunk.colorMapResolution + col] = rawSceneMap[row * (chunk.colorMapResolution + 1) + col]; } } - } else { - stream.seekg(1, std::ios::cur); + } else if (!Skip(stream, 1)) { + return false; } if (stream.fail()) { @@ -353,6 +368,59 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { } } + /** + * @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(chunk.width - 1) * chunk.scaleFactor / static_cast(chunk.colorMapResolution); + chunk.sceneCellSizeZ = static_cast(chunk.height - 1) * chunk.scaleFactor / static_cast(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(std::lround((raw.maxBoundsX - raw.minBoundsX) / spanX)); + const auto countZ = static_cast(std::lround((raw.maxBoundsZ - raw.minBoundsZ) / spanZ)); + if (countX == 0 || countZ == 0 || static_cast(countX) * countZ > kMaxChunks) return; + + std::vector grid(static_cast(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(countX) || z >= static_cast(countZ)) { + LOG("Raw chunks are not a regular grid (chunk %u); scene lookups check every chunk", chunk.id); + return; + } + auto& cell = grid[static_cast(x) * countZ + z]; + if (cell == -1) cell = static_cast(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); @@ -404,18 +472,14 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { outRaw.maxBoundsZ = std::numeric_limits::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); + 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("Raw terrain bounds: X[%.2f, %.2f], Z[%.2f, %.2f]", + LOG_DEBUG("Raw terrain bounds: X[%.2f, %.2f], Z[%.2f, %.2f]", outRaw.minBoundsX, outRaw.maxBoundsX, outRaw.minBoundsZ, outRaw.maxBoundsZ); + BuildChunkGrid(outRaw); } } return true; @@ -424,6 +488,54 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { } } + // 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 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 { + 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(std::floor((pos + kEdgeEpsilon - min) / span)); + return std::clamp(cell, 0, static_cast(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(chunkGridCountX) || z >= static_cast(chunkGridCountZ)) continue; + const int32_t index = chunkGrid[static_cast(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(); @@ -432,24 +544,22 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const { return; // No scene data available } - LOG("GenerateTerrainMesh: Processing %zu chunks", raw.chunks.size()); + 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) { - 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 currentVert = vertexOffset + chunk.height * 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; + 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); diff --git a/dZoneManager/Raw.h b/dZoneManager/Raw.h index 61469633a..33de6fa83 100644 --- a/dZoneManager/Raw.h +++ b/dZoneManager/Raw.h @@ -4,6 +4,7 @@ #define __RAW_H__ #include +#include #include #include #include @@ -61,7 +62,7 @@ struct TerrainMesh { /** * @brief Terrain chunk structure - * Represents a single chunk of terrain with heightmap, textures, and meshes + * Represents a single chunk of terrain with heightmap, textures, flair, scene map and meshes */ struct Chunk { uint32_t id; @@ -69,40 +70,50 @@ struct Chunk { uint32_t height; float offsetX; float offsetZ; - uint32_t shaderId; - + uint32_t shaderId = 0; // version < 32 only + // Texture IDs (4 textures per chunk) std::vector textureIds; - - // Terrain scale factor + + // Terrain scale factor (world units between two heightmap points) float scaleFactor; - + // Heightmap data (width * height floats) std::vector heightMap; - - // Version 32+ fields + + // Color map: colorMapResolution^2 RGBA pixels (version < 32: width^2 pixels, resolution width - 1) uint32_t colorMapResolution = 0; - std::vector colorMap; // RGBA pixels (colorMap * colorMap * 4) - std::vector lightMap; - + std::vector colorMap; + std::vector lightMap; // DDS, version 32+ + uint32_t textureMapResolution = 0; - std::vector textureMap; // (textureMapResolution * textureMapResolution * 4) - uint8_t textureSettings = 0; - std::vector blendMap; - + std::vector textureMap; // textureMapResolution^2 RGBA pixels + uint8_t textureSettings = 0; // version 32+ + std::vector blendMap; // DDS, version 32+ + // Flair data std::vector flairs; - - // Scene map (version 32+) + + // The scene map is colorMapResolution * colorMapResolution scene IDs, indexed [x cell * resolution + z cell] std::vector sceneMap; - - // Mesh data + + // Mesh data (version 32+) uint32_t vertSize = 0; std::vector meshVertUsage; std::vector meshVertSize; std::vector meshTri; + // Scene map cells per world unit, and the size of one cell, along X and Z (set once the chunk is read) + float sceneCellsPerUnitX = 0.0f; + float sceneCellsPerUnitZ = 0.0f; + float sceneCellSizeX = 0.0f; + float sceneCellSizeZ = 0.0f; + bool IsValidForSceneLookup() const; + float GetMaxX() const { return offsetX + static_cast(width - 1) * scaleFactor; } + float GetMaxZ() const { return offsetZ + static_cast(height - 1) * scaleFactor; } + // The scene ID at a position relative to the chunk's offset, as the client looks it up + uint8_t GetSceneIDAtLocal(float localX, float localZ) const; uint8_t GetSceneIDAtGrid(uint32_t i, uint32_t j) const; NiPoint3 GridToWorldPos(uint32_t i, uint32_t j) const; }; @@ -112,8 +123,8 @@ struct Chunk { * Complete representation of a .raw terrain file */ struct Raw { - uint16_t version; - uint8_t dev; + uint16_t version = 0; + uint8_t dev = 0; uint32_t numChunks = 0; uint32_t numChunksWidth = 0; uint32_t numChunksHeight = 0; @@ -124,6 +135,23 @@ struct Raw { float minBoundsZ = 0.0f; float maxBoundsX = 0.0f; float maxBoundsZ = 0.0f; + + // Chunk lookup by position: the chunks of every client terrain form a regular grid, written X-major + // ([x * chunks along Z + z]). chunkGrid holds the index into chunks per grid cell (-1 for none); it is empty when + // the chunks don't form a grid, and lookups then check every chunk. + std::vector chunkGrid; + uint32_t chunkGridCountX = 0; + uint32_t chunkGridCountZ = 0; + float chunkSpanX = 0.0f; + float chunkSpanZ = 0.0f; + + /** + * @brief The scene ID at a world position, looked up as the client's TerrainManager::GetSceneAtPos does + * (height is ignored). Constant time on a chunk grid. + * + * @return The scene ID, or nullopt when there is no terrain with a scene map there + */ + std::optional GetSceneIDAt(float x, float z) const; }; /** diff --git a/dZoneManager/dZoneManager.cpp b/dZoneManager/dZoneManager.cpp index aeb6b4785..dee9e6d34 100644 --- a/dZoneManager/dZoneManager.cpp +++ b/dZoneManager/dZoneManager.cpp @@ -12,7 +12,6 @@ #include "CDZoneTableTable.h" #include #include -#include #include #include "eObjectBits.h" #include "AssetManager.h" @@ -307,36 +306,8 @@ void dZoneManager::LoadWorldConfig() { LWOSCENEID dZoneManager::GetSceneIDFromPosition(const NiPoint3& position) const { if (!m_pZone) return LWOSCENEID_INVALID; - const auto& raw = m_pZone->GetZoneRaw(); - - // If no chunks, no scene data available - if (raw.chunks.empty()) { - return LWOSCENEID_INVALID; - } - - // Convert 3D position to 2D (XZ plane) and clamp to terrain bounds - float posX = std::clamp(position.x, raw.minBoundsX, raw.maxBoundsX); - float posZ = std::clamp(position.z, raw.minBoundsZ, raw.maxBoundsZ); - - // Find the chunk containing this position - // Reverse the world position calculation from GenerateTerrainMesh - for (const auto& chunk : raw.chunks) { - if (!chunk.IsValidForSceneLookup()) continue; - - const float heightI = posX / chunk.scaleFactor - (chunk.offsetX / chunk.scaleFactor); - const float heightJ = posZ / chunk.scaleFactor - (chunk.offsetZ / chunk.scaleFactor); - - if (heightI >= 0.0f && heightI < static_cast(chunk.width) && - heightJ >= 0.0f && heightJ < static_cast(chunk.height)) { - - const uint32_t gridI = std::min(static_cast(heightI), chunk.width - 1); - const uint32_t gridJ = std::min(static_cast(heightJ), chunk.height - 1); - return LWOSCENEID(chunk.GetSceneIDAtGrid(gridI, gridJ), 0); - } - } - - // Position not found in any chunk - return LWOSCENEID_INVALID; + const auto sceneID = m_pZone->GetZoneRaw().GetSceneIDAt(position.x, position.z); + return sceneID ? LWOSCENEID(*sceneID, 0) : LWOSCENEID_INVALID; } void dZoneManager::BuildSceneGraph() { diff --git a/dZoneManager/dZoneManager.h b/dZoneManager/dZoneManager.h index b6164e246..394c3ade8 100644 --- a/dZoneManager/dZoneManager.h +++ b/dZoneManager/dZoneManager.h @@ -77,6 +77,11 @@ public: */ std::vector GetAdjacentScenes(LWOSCENEID sceneID) const; + /** + * @brief (Re)builds the scene graph adjacency list from the loaded zone's scene transitions. + */ + void BuildSceneGraph(); + // The world config should not be modified by a caller. const WorldConfig& GetWorldConfig() { if (!m_WorldConfig) LoadWorldConfig(); @@ -84,10 +89,6 @@ public: }; private: - /** - * Builds the scene graph adjacency list from scene transitions - */ - void BuildSceneGraph(); /** * The starting unique mission ID. */