From 12fd9d0a21c36da0420bb7b453e792a59ff60738 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 14 Oct 2025 22:44:56 -0500 Subject: [PATCH] Impl raw reading, and some slash commands to test with scenes --- dGame/dUtilities/SlashCommandHandler.cpp | 36 ++ .../SlashCommands/DEVGMCommands.cpp | 215 +++++++++++ .../dUtilities/SlashCommands/DEVGMCommands.h | 4 + dZoneManager/CMakeLists.txt | 1 + dZoneManager/Raw.cpp | 353 ++++++++++++++++++ dZoneManager/Raw.h | 143 +++++++ dZoneManager/Zone.cpp | 24 ++ dZoneManager/Zone.h | 9 + dZoneManager/dZoneManager.cpp | 110 ++++++ dZoneManager/dZoneManager.h | 33 ++ 10 files changed, 928 insertions(+) create mode 100644 dZoneManager/Raw.cpp create mode 100644 dZoneManager/Raw.h diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index 218ccfa8..08ea3403 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -817,6 +817,42 @@ void SlashCommandHandler::Startup() { }; RegisterCommand(ExecuteCommand); + Command GetSceneCommand{ + .help = "Get the current scene ID and name at your position", + .info = "Displays the scene ID and name at the player's current position. Scenes do not care about height.", + .aliases = { "getscene", "scene" }, + .handle = DEVGMCommands::GetScene, + .requiredLevel = eGameMasterLevel::DEVELOPER + }; + RegisterCommand(GetSceneCommand); + + Command GetAdjacentScenesCommand{ + .help = "Get all scenes adjacent to your current scene", + .info = "Displays all scenes that are directly connected to the player's current scene via scene transitions.", + .aliases = { "getadjacentscenes", "adjacentscenes" }, + .handle = DEVGMCommands::GetAdjacentScenes, + .requiredLevel = eGameMasterLevel::DEVELOPER + }; + RegisterCommand(GetAdjacentScenesCommand); + + Command SpawnScenePointsCommand{ + .help = "Spawn bricks at points across your current scene", + .info = "Spawns bricks at sampled points across the player's current scene using terrain scene map data.", + .aliases = { "spawnscenepoints" }, + .handle = DEVGMCommands::SpawnScenePoints, + .requiredLevel = eGameMasterLevel::DEVELOPER + }; + RegisterCommand(SpawnScenePointsCommand); + + Command SpawnAllScenePointsCommand{ + .help = "Spawn bricks at ALL vertices in ALL scenes (high density, many entities)", + .info = "Spawns bricks at every vertex in the terrain mesh for all scenes in the zone. WARNING: Creates a massive number of entities for maximum accuracy visualization.", + .aliases = { "spawnallscenepoints", "spawnallscenes" }, + .handle = DEVGMCommands::SpawnAllScenePoints, + .requiredLevel = eGameMasterLevel::DEVELOPER + }; + RegisterCommand(SpawnAllScenePointsCommand); + // Register Greater Than Zero Commands Command KickCommand{ diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index 0c5b7585..3ddd3ef4 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -1836,4 +1836,219 @@ namespace DEVGMCommands { } } } + + void GetScene(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + const auto position = entity->GetPosition(); + + // Get the scene ID from the zone manager + const auto sceneID = Game::zoneManager->GetSceneIDFromPosition(position); + + if (sceneID == LWOSCENEID_INVALID) { + ChatPackets::SendSystemMessage(sysAddr, u"No scene found at current position."); + return; + } + + // Get the scene reference from the zone to get the name + const auto* zone = Game::zoneManager->GetZone(); + if (!zone) { + ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded."); + return; + } + + // Build the feedback message + std::ostringstream feedback; + feedback << "Scene ID: " << sceneID.GetSceneID(); + feedback << " (Layer: " << sceneID.GetLayerID() << ")"; + + // Get the scene name + const auto* sceneRef = zone->GetScene(sceneID); + if (sceneRef && !sceneRef->name.empty()) { + feedback << " - Name: " << sceneRef->name; + } + + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str())); + } + + void GetAdjacentScenes(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + const auto position = entity->GetPosition(); + + // Get the scene ID from the zone manager + const auto sceneID = Game::zoneManager->GetSceneIDFromPosition(position); + + if (sceneID == LWOSCENEID_INVALID) { + ChatPackets::SendSystemMessage(sysAddr, u"No scene found at current position."); + return; + } + + // Get the zone reference + const auto* zone = Game::zoneManager->GetZone(); + if (!zone) { + ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded."); + return; + } + + // Get current scene info + const auto* currentScene = zone->GetScene(sceneID); + std::string currentSceneName = currentScene && !currentScene->name.empty() ? currentScene->name : "Unknown"; + + // Get adjacent scenes + const auto adjacentSceneIDs = Game::zoneManager->GetAdjacentScenes(sceneID); + + if (adjacentSceneIDs.empty()) { + std::ostringstream feedback; + feedback << "Current Scene: " << sceneID.GetSceneID() << " (" << currentSceneName << ")"; + feedback << " - No adjacent scenes found."; + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str())); + return; + } + + // Build the feedback message with current scene + std::ostringstream feedback; + feedback << "Current Scene: " << sceneID.GetSceneID() << " (" << currentSceneName << ")"; + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str())); + + // List all adjacent scenes + feedback.str(""); + feedback << "Adjacent Scenes (" << adjacentSceneIDs.size() << "):"; + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str())); + + for (const auto& adjSceneID : adjacentSceneIDs) { + feedback.str(""); + feedback << " - Scene ID: " << adjSceneID.GetSceneID(); + feedback << " (Layer: " << adjSceneID.GetLayerID() << ")"; + + // Get the scene name if available + const auto* sceneRef = zone->GetScene(adjSceneID); + if (sceneRef && !sceneRef->name.empty()) { + feedback << " - " << sceneRef->name; + } + + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str())); + } + } + + void SpawnScenePoints(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + // Hardcoded to use LOT 33 + const uint32_t lot = 33; + + // Get player's current position and scene + const auto position = entity->GetPosition(); + const auto currentSceneID = Game::zoneManager->GetSceneIDFromPosition(position); + if (currentSceneID == LWOSCENEID_INVALID) { + ChatPackets::SendSystemMessage(sysAddr, u"No scene found at current position."); + return; + } + + // Get the zone + const auto* zone = Game::zoneManager->GetZone(); + if (!zone) { + ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded."); + return; + } + + // Get the pre-generated terrain mesh + const auto& terrainMesh = zone->GetTerrainMesh(); + if (terrainMesh.vertices.empty()) { + ChatPackets::SendSystemMessage(sysAddr, u"Zone does not have valid terrain mesh data."); + return; + } + + // Spawn at ALL vertices in the current scene without any filtering or spacing + uint32_t spawnedCount = 0; + + for (const auto& vertex : terrainMesh.vertices) { + // Check if this vertex belongs to the current scene + if (LWOSCENEID(vertex.sceneID) == currentSceneID) { + // Use the vertex position + NiPoint3 spawnPos = vertex.position; + + EntityInfo info; + info.lot = lot + currentSceneID.GetSceneID(); // to differentiate scenes + info.pos = spawnPos; + info.rot = QuatUtils::IDENTITY; + info.spawner = nullptr; + info.spawnerID = entity->GetObjectID(); + info.spawnerNodeID = 0; + info.settings = { new LDFData(u"SpawnedFromSlashCommand", true) }; + + Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); + if (newEntity != nullptr) { + Game::entityManager->ConstructEntity(newEntity); + spawnedCount++; + } + } + } + + if (spawnedCount == 0) { + std::ostringstream feedback; + feedback << "No spawn points found in current scene (ID: " << currentSceneID.GetSceneID() << ")."; + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str())); + return; + } + + // Send feedback + const auto* sceneRef = zone->GetScene(currentSceneID); + const std::string sceneName = sceneRef ? sceneRef->name : "Unknown"; + std::ostringstream feedback; + feedback << "Spawned LOT " << lot + currentSceneID.GetSceneID() << " at " << spawnedCount << " points in scene " + << currentSceneID.GetSceneID() << " (" << sceneName << ")."; + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str())); + } + + void SpawnAllScenePoints(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + // Hardcoded to use LOT 33 + const uint32_t lot = 33; + + // Get the zone + const auto* zone = Game::zoneManager->GetZone(); + if (!zone) { + ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded."); + return; + } + + // Get the pre-generated terrain mesh + const auto& terrainMesh = zone->GetTerrainMesh(); + if (terrainMesh.vertices.empty()) { + ChatPackets::SendSystemMessage(sysAddr, u"Zone does not have valid terrain mesh data."); + return; + } + + // Spawn at ALL vertices without any filtering or spacing restrictions for maximum accuracy + uint32_t spawnedCount = 0; + std::map sceneSpawnCounts; // Track spawns per scene + + for (const auto& vertex : terrainMesh.vertices) { + // Skip invalid scenes (scene ID 0 typically means no scene) + if (vertex.sceneID == 0) continue; + EntityInfo info; + info.lot = lot + vertex.sceneID; // to show different scenes + info.pos = vertex.position; + info.rot = QuatUtils::IDENTITY; + info.spawner = nullptr; + info.spawnerID = entity->GetObjectID(); + info.spawnerNodeID = 0; + info.settings = { new LDFData(u"SpawnedFromSlashCommand", true) }; + + Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); + if (newEntity != nullptr) { + Game::entityManager->ConstructEntity(newEntity); + spawnedCount++; + sceneSpawnCounts[vertex.sceneID]++; + } + } + + // Send detailed feedback + std::ostringstream feedback; + feedback << "Spawned LOT " << spawnedCount << " total points across " + << sceneSpawnCounts.size() << " scenes:\n"; + + for (const auto& [sceneID, count] : sceneSpawnCounts) { + const auto* sceneRef = zone->GetScene(LWOSCENEID(sceneID)); + const std::string sceneName = sceneRef ? sceneRef->name : "Unknown"; + feedback << " Scene " << static_cast(sceneID) << ", LOT: " << (lot + sceneID) << " (" << sceneName << "): " << count << " points\n"; + } + + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str())); + } }; + diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.h b/dGame/dUtilities/SlashCommands/DEVGMCommands.h index 64783e24..228db51f 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.h +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.h @@ -77,6 +77,10 @@ namespace DEVGMCommands { void Barfight(Entity* entity, const SystemAddress& sysAddr, const std::string args); void Despawn(Entity* entity, const SystemAddress& sysAddr, const std::string args); void Execute(Entity* entity, const SystemAddress& sysAddr, const std::string args); + void GetScene(Entity* entity, const SystemAddress& sysAddr, const std::string args); + void GetAdjacentScenes(Entity* entity, const SystemAddress& sysAddr, const std::string args); + void SpawnScenePoints(Entity* entity, const SystemAddress& sysAddr, const std::string args); + void SpawnAllScenePoints(Entity* entity, const SystemAddress& sysAddr, const std::string args); } #endif //!DEVGMCOMMANDS_H diff --git a/dZoneManager/CMakeLists.txt b/dZoneManager/CMakeLists.txt index 544a01d9..e1ef9d28 100644 --- a/dZoneManager/CMakeLists.txt +++ b/dZoneManager/CMakeLists.txt @@ -1,5 +1,6 @@ set(DZONEMANAGER_SOURCES "dZoneManager.cpp" "Level.cpp" + "Raw.cpp" "Spawner.cpp" "Zone.cpp") diff --git a/dZoneManager/Raw.cpp b/dZoneManager/Raw.cpp new file mode 100644 index 00000000..ad02cf76 --- /dev/null +++ b/dZoneManager/Raw.cpp @@ -0,0 +1,353 @@ +#include "Raw.h" +#include "BinaryIO.h" +#include "Logger.h" +#include + +namespace Raw { + + /** + * @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 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.offsetWorldX); + BinaryIO::BinaryRead(stream, chunk.offsetWorldZ); + + 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 + uint32_t heightMapSize = chunk.width * chunk.height; + + chunk.heightMap.resize(heightMapSize); + for (uint32_t i = 0; i < heightMapSize; ++i) { + BinaryIO::BinaryRead(stream, chunk.heightMap[i]); + } + + if (stream.fail()) { + return false; + } + + // ColorMap (size varies by version) + if (version >= 32) { + BinaryIO::BinaryRead(stream, chunk.colorMapResolution); + } else { + chunk.colorMapResolution = chunk.width; // Default to chunk width for older versions + } + + uint32_t colorMapPixelCount = chunk.colorMapResolution * chunk.colorMapResolution * 4; // RGBA + chunk.colorMap.resize(colorMapPixelCount); + stream.read(reinterpret_cast(chunk.colorMap.data()), colorMapPixelCount); + + if (stream.fail()) { + return false; + } + + // LightMap DDS + uint32_t lightMapSize; + BinaryIO::BinaryRead(stream, lightMapSize); + + chunk.lightMap.resize(lightMapSize); + stream.read(reinterpret_cast(chunk.lightMap.data()), lightMapSize); + + if (stream.fail()) { + return false; + } + + // TextureMap (size varies by version) + if (version >= 32) { + BinaryIO::BinaryRead(stream, chunk.textureMapResolution); + } else { + chunk.textureMapResolution = chunk.width; // Default to chunk width for older versions + } + + uint32_t textureMapPixelCount = chunk.textureMapResolution * chunk.textureMapResolution * 4; + chunk.textureMap.resize(textureMapPixelCount); + stream.read(reinterpret_cast(chunk.textureMap.data()), textureMapPixelCount); + + if (stream.fail()) { + return false; + } + + // Texture settings + BinaryIO::BinaryRead(stream, chunk.textureSettings); + + // Blend map DDS + uint32_t blendMapDDSSize; + BinaryIO::BinaryRead(stream, blendMapDDSSize); + + chunk.blendMap.resize(blendMapDDSSize); + stream.read(reinterpret_cast(chunk.blendMap.data()), blendMapDDSSize); + + if (stream.fail()) { + return false; + } + + // Read flairs + uint32_t numFlairs; + BinaryIO::BinaryRead(stream, numFlairs); + + if (stream.fail()) { + return false; + } + + chunk.flairs.resize(numFlairs); + for (uint32_t i = 0; i < numFlairs; ++i) { + if (!ReadFlairAttributes(stream, chunk.flairs[i])) { + return false; + } + } + + // Scene map (version 32+ only) + if (version >= 32) { + uint32_t sceneMapSize = chunk.colorMapResolution * chunk.colorMapResolution; + + chunk.sceneMap.resize(sceneMapSize); + stream.read(reinterpret_cast(chunk.sceneMap.data()), sceneMapSize); + + if (stream.fail()) { + return false; + } + } + + // Mesh vertex usage (read size first, then check if empty) + BinaryIO::BinaryRead(stream, chunk.vertSize); + + if (stream.fail()) { + return false; + } + + // Mesh vert usage + 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; + } + + // Only continue with mesh data if we have vertex usage data + if (chunk.vertSize == 0) { + return true; + } + + // Mesh vert size (16 elements) + chunk.meshVertSize.resize(16); + for (int i = 0; i < 16; ++i) { + BinaryIO::BinaryRead(stream, chunk.meshVertSize[i]); + } + + if (stream.fail()) { + return false; + } + + // Mesh triangles (16 elements) + 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); + + // 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; + } + } + } + + 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 + } + + uint32_t vertexOffset = 0; + + for (const auto& chunk : raw.chunks) { + // Skip chunks without scene maps + if (chunk.sceneMap.empty() || chunk.colorMapResolution == 0 || chunk.heightMap.empty()) { + continue; + } + + // Generate vertices for this chunk + // Similar to RawChunk::GenerateMesh() in dTerrain but with scene IDs + for (uint32_t i = 0; i < chunk.width; ++i) { + for (uint32_t j = 0; j < chunk.height; ++j) { + // Get height at this position + const uint32_t heightIndex = chunk.width * i + j; + if (heightIndex >= chunk.heightMap.size()) continue; + + const float y = chunk.heightMap[heightIndex]; + + // Calculate world position + // Based on RawFile::GenerateFinalMeshFromChunks in dTerrain: + // 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)); + // tempVert *= chunk->m_HeightMap->m_ScaleFactor; + + float worldX = (static_cast(i) + (chunk.offsetWorldX / chunk.scaleFactor)) * chunk.scaleFactor; + float worldY = (y / chunk.scaleFactor) * chunk.scaleFactor; + float worldZ = (static_cast(j) + (chunk.offsetWorldZ / chunk.scaleFactor)) * chunk.scaleFactor; + + NiPoint3 worldPos(worldX, worldY, worldZ); + + // Get scene ID at this position + // Map heightmap position to scene map position + const float sceneMapX = (static_cast(i) / static_cast(chunk.width - 1)) * static_cast(chunk.colorMapResolution - 1); + const float sceneMapZ = (static_cast(j) / static_cast(chunk.height - 1)) * static_cast(chunk.colorMapResolution - 1); + + const uint32_t sceneX = std::min(static_cast(sceneMapX), chunk.colorMapResolution - 1); + const uint32_t sceneZ = std::min(static_cast(sceneMapZ), chunk.colorMapResolution - 1); + const uint32_t sceneIndex = sceneZ * chunk.colorMapResolution + sceneX; + + uint8_t sceneID = 0; + if (sceneIndex < chunk.sceneMap.size()) { + sceneID = chunk.sceneMap[sceneIndex]; + } + + outMesh.vertices.emplace_back(worldPos, sceneID); + + // Generate triangles (same pattern as dTerrain) + 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; + } + } + +} // namespace Raw diff --git a/dZoneManager/Raw.h b/dZoneManager/Raw.h new file mode 100644 index 00000000..2409e987 --- /dev/null +++ b/dZoneManager/Raw.h @@ -0,0 +1,143 @@ +#pragma once + +#ifndef __RAW_H__ +#define __RAW_H__ + +#include +#include +#include +#include +#include "NiPoint3.h" + +namespace Raw { + +/** + * @brief Flair attributes structure + * Represents decorative elements on the terrain + */ +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 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 vertices; + std::vector 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 offsetWorldX; + float offsetWorldZ; + uint32_t shaderId; + + // Texture IDs (4 textures per chunk) + std::vector textureIds; + + // Terrain scale factor + float scaleFactor; + + // Heightmap data (width * height floats) + std::vector heightMap; + + // Version 32+ fields + uint32_t colorMapResolution = 0; + std::vector colorMap; // RGBA pixels (colorMap * colorMap * 4) + std::vector lightMap; + + uint32_t textureMapResolution = 0; + std::vector textureMap; // (textureMapResolution * textureMapResolution * 4) + uint8_t textureSettings = 0; + std::vector blendMap; + + // Flair data + std::vector flairs; + + // Scene map (version 32+) + std::vector sceneMap; + + // Mesh data + uint32_t vertSize = 0; + std::vector meshVertUsage; + std::vector meshVertSize; + std::vector meshTri; + + // Unknown data for version < 32 + std::vector unknown1; + std::vector unknown2; +}; + +/** + * @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 chunks; +}; + +/** + * @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); + +} // namespace Raw + +#endif // __RAW_H__ diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index 282f4c45..29245ff6 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -20,6 +20,7 @@ #include "eTriggerEventType.h" #include "eWaypointCommandType.h" #include "dNavMesh.h" +#include "Raw.h" Zone::Zone(const LWOZONEID zoneID) : m_ZoneID(zoneID) { @@ -84,6 +85,23 @@ void Zone::LoadZoneIntoMemory() { BinaryIO::ReadString(file, m_ZoneName, BinaryIO::ReadType::String); BinaryIO::ReadString(file, m_ZoneDesc, BinaryIO::ReadType::String); + auto zoneFolderPath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1); + if (!Game::assetManager->HasFile(zoneFolderPath + m_ZoneRawPath)) { + LOG("Failed to find %s", (zoneFolderPath + m_ZoneRawPath).c_str()); + throw std::runtime_error("Aborting Zone loading due to no Zone Raw File."); + } + + auto rawFile = Game::assetManager->GetFile(zoneFolderPath + m_ZoneRawPath); + if (!Raw::ReadRaw(rawFile, m_Raw)) { + LOG("Failed to parse %s", (zoneFolderPath + m_ZoneRawPath).c_str()); + throw std::runtime_error("Aborting Zone loading due to invalid Raw File."); + } + + // Generate terrain mesh for fast scene lookups + Raw::GenerateTerrainMesh(m_Raw, m_TerrainMesh); + LOG("Generated terrain mesh with %llu vertices and %llu triangles", + m_TerrainMesh.vertices.size(), m_TerrainMesh.triangles.size() / 3); + if (m_FileFormatVersion >= Zone::FileFormatVersion::PreAlpha) { BinaryIO::BinaryRead(file, m_NumberOfSceneTransitionsLoaded); for (uint32_t i = 0; i < m_NumberOfSceneTransitionsLoaded; ++i) { @@ -483,3 +501,9 @@ void Zone::LoadPath(std::istream& file) { } m_Paths.push_back(path); } + +const SceneRef* Zone::GetScene(LWOSCENEID sceneID) const { + auto it = m_Scenes.find(sceneID); + if (it != m_Scenes.end()) return &it->second; + return nullptr; +} diff --git a/dZoneManager/Zone.h b/dZoneManager/Zone.h index 20010c2c..fcd0c3ff 100644 --- a/dZoneManager/Zone.h +++ b/dZoneManager/Zone.h @@ -6,6 +6,7 @@ #include #include #include +#include "Raw.h" namespace LUTriggers { struct Trigger; @@ -228,6 +229,12 @@ public: void SetSpawnPos(const NiPoint3& pos) { m_Spawnpoint = pos; } void SetSpawnRot(const NiQuaternion& rot) { m_SpawnpointRotation = rot; } + const Raw::Raw& GetZoneRaw() const { return m_Raw; } + const Raw::TerrainMesh& GetTerrainMesh() const { return m_TerrainMesh; } + const SceneRef* GetScene(LWOSCENEID sceneID) const; + const std::vector& GetSceneTransitions() const { return m_SceneTransitions; } + const std::map& GetScenes() const { return m_Scenes; } + private: LWOZONEID m_ZoneID; std::string m_ZoneFilePath; @@ -244,6 +251,8 @@ private: std::string m_ZoneName; //Name given to the zone by a level designer std::string m_ZoneDesc; //Description of the zone by a level designer std::string m_ZoneRawPath; //Path to the .raw file of this zone. + Raw::Raw m_Raw; // The Raw data for this zone + Raw::TerrainMesh m_TerrainMesh; // Pre-generated terrain mesh for fast scene lookups std::map m_Scenes; std::vector m_SceneTransitions; diff --git a/dZoneManager/dZoneManager.cpp b/dZoneManager/dZoneManager.cpp index ea19dc88..a757858e 100644 --- a/dZoneManager/dZoneManager.cpp +++ b/dZoneManager/dZoneManager.cpp @@ -11,6 +11,7 @@ #include "WorldConfig.h" #include "CDZoneTableTable.h" #include +#include #include "eObjectBits.h" #include "CDZoneTableTable.h" #include "AssetManager.h" @@ -62,6 +63,9 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { m_pZone->Initalize(); + // Build the scene graph after zone is loaded + BuildSceneGraph(); + endTime = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); LoadWorldConfig(); @@ -298,3 +302,109 @@ void dZoneManager::LoadWorldConfig() { LOG_DEBUG("Loaded WorldConfig into memory"); } + +LWOSCENEID dZoneManager::GetSceneIDFromPosition(const NiPoint3& position) const { + if (!m_pZone) return LWOSCENEID_INVALID; + + const auto& terrainMesh = m_pZone->GetTerrainMesh(); + + // If mesh is empty, no scene data available + if (terrainMesh.vertices.empty() || terrainMesh.triangles.empty()) { + return LWOSCENEID_INVALID; + } + + // Find the triangle containing this position (ignoring Y coordinate for scene lookup) + // We iterate through all triangles and find the one that contains the point in 2D (XZ plane) + for (size_t i = 0; i < terrainMesh.triangles.size(); i += 3) { + const auto& v0 = terrainMesh.vertices[terrainMesh.triangles[i]]; + const auto& v1 = terrainMesh.vertices[terrainMesh.triangles[i + 1]]; + const auto& v2 = terrainMesh.vertices[terrainMesh.triangles[i + 2]]; + + // Check if position is inside this triangle using 2D (XZ) coordinates + // Using barycentric coordinates / cross product method + const float x = position.x; + const float z = position.z; + + const float x0 = v0.position.x; + const float z0 = v0.position.z; + const float x1 = v1.position.x; + const float z1 = v1.position.z; + const float x2 = v2.position.x; + const float z2 = v2.position.z; + + // Calculate barycentric coordinates + const float denom = (z1 - z2) * (x0 - x2) + (x2 - x1) * (z0 - z2); + if (std::abs(denom) < 0.0001f) continue; // Degenerate triangle + + const float a = ((z1 - z2) * (x - x2) + (x2 - x1) * (z - z2)) / denom; + const float b = ((z2 - z0) * (x - x2) + (x0 - x2) * (z - z2)) / denom; + const float c = 1.0f - a - b; + + // Point is inside triangle if all barycentric coordinates are non-negative + if (a >= 0.0f && b >= 0.0f && c >= 0.0f) { + // Return the scene ID from the first vertex (all vertices in a triangle should have the same scene ID) + return LWOSCENEID(v0.sceneID); + } + } + + // Position not found in any triangle + return LWOSCENEID_INVALID; +} + +void dZoneManager::BuildSceneGraph() { + if (!m_pZone) return; + + // Clear any existing adjacency list + m_SceneAdjacencyList.clear(); + + // Initialize adjacency list with all scenes + const auto& scenes = m_pZone->GetScenes(); + for (const auto& [sceneID, sceneRef] : scenes) { + // Ensure every scene has an entry, even if it has no transitions + if (m_SceneAdjacencyList.find(sceneID) == m_SceneAdjacencyList.end()) { + m_SceneAdjacencyList[sceneID] = std::vector(); + } + } + + // Build adjacency list from scene transitions + const auto& transitions = m_pZone->GetSceneTransitions(); + for (const auto& transition : transitions) { + // Each transition has multiple points, each pointing to a scene + // We need to determine which scenes this transition connects + + // Group transition points by their scene IDs to find unique connections + std::set connectedScenes; + for (const auto& point : transition.points) { + if (point.sceneID != LWOSCENEID_INVALID) { + connectedScenes.insert(point.sceneID); + } + } + + // Create bidirectional edges between all scenes in this transition + // (transitions typically connect two scenes, but can be more complex) + std::vector sceneList(connectedScenes.begin(), connectedScenes.end()); + + for (size_t i = 0; i < sceneList.size(); ++i) { + for (size_t j = 0; j < sceneList.size(); ++j) { + if (i != j) { + LWOSCENEID fromScene = sceneList[i]; + LWOSCENEID toScene = sceneList[j]; + + // Add edge if it doesn't already exist + auto& adjacentScenes = m_SceneAdjacencyList[fromScene]; + if (std::find(adjacentScenes.begin(), adjacentScenes.end(), toScene) == adjacentScenes.end()) { + adjacentScenes.push_back(toScene); + } + } + } + } + } +} + +std::vector dZoneManager::GetAdjacentScenes(LWOSCENEID sceneID) const { + auto it = m_SceneAdjacencyList.find(sceneID); + if (it != m_SceneAdjacencyList.end()) { + return it->second; + } + return std::vector(); +} diff --git a/dZoneManager/dZoneManager.h b/dZoneManager/dZoneManager.h index fc07a4b8..b6164e24 100644 --- a/dZoneManager/dZoneManager.h +++ b/dZoneManager/dZoneManager.h @@ -53,6 +53,30 @@ public: uint32_t GetUniqueMissionIdStartingValue(); bool CheckIfAccessibleZone(LWOMAPID zoneID); + /** + * @brief Get the scene ID at a given position. Scenes do not care about height (Y coordinate). + * + * @param position The position to query + * @return The scene ID at that position, or LWOSCENEID_INVALID if not found + */ + LWOSCENEID GetSceneIDFromPosition(const NiPoint3& position) const; + + /** + * @brief Get the adjacency list for the scene graph. + * The adjacency list maps each scene ID to a list of scene IDs it can transition to. + * + * @return A reference to the scene adjacency list + */ + const std::map>& GetSceneAdjacencyList() const { return m_SceneAdjacencyList; } + + /** + * @brief Get all scenes adjacent to (connected to) a given scene. + * + * @param sceneID The scene ID to query + * @return A vector of scene IDs that are directly connected to this scene, or empty vector if scene not found + */ + std::vector GetAdjacentScenes(LWOSCENEID sceneID) const; + // The world config should not be modified by a caller. const WorldConfig& GetWorldConfig() { if (!m_WorldConfig) LoadWorldConfig(); @@ -60,6 +84,10 @@ public: }; private: + /** + * Builds the scene graph adjacency list from scene transitions + */ + void BuildSceneGraph(); /** * The starting unique mission ID. */ @@ -75,4 +103,9 @@ private: std::optional m_WorldConfig = std::nullopt; Entity* m_ZoneControlObject = nullptr; + + /** + * Scene graph adjacency list: maps each scene ID to a list of scenes it can transition to + */ + std::map> m_SceneAdjacencyList; };