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>
This commit is contained in:
Aaron Kimbrell
2026-06-24 18:31:28 -05:00
parent 35337291fa
commit feeaf339d4
23 changed files with 1404 additions and 316 deletions

View File

@@ -1890,4 +1890,273 @@ 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 Raw terrain data
const auto& raw = zone->GetZoneRaw();
if (raw.chunks.empty()) {
ChatPackets::SendSystemMessage(sysAddr, u"Zone does not have valid terrain data.");
return;
}
// Spawn at all sceneMap points in the current scene
uint32_t spawnedCount = 0;
for (const auto& chunk : raw.chunks) {
if (chunk.sceneMap.empty() || chunk.colorMapResolution == 0 || chunk.heightMap.empty()
|| chunk.width <= 1 || chunk.height <= 1 || chunk.scaleFactor <= 0.0f) 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;
const float y = chunk.heightMap[heightIndex];
const float sceneMapI = (static_cast<float>(i) / static_cast<float>(chunk.width - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const float sceneMapJ = (static_cast<float>(j) / static_cast<float>(chunk.height - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const uint32_t sceneI = std::min(static_cast<uint32_t>(sceneMapI), chunk.colorMapResolution - 1);
const uint32_t sceneJ = std::min(static_cast<uint32_t>(sceneMapJ), chunk.colorMapResolution - 1);
const uint32_t sceneIndex = sceneI * chunk.colorMapResolution + sceneJ;
uint8_t sceneID = 0;
if (sceneIndex < chunk.sceneMap.size()) {
sceneID = chunk.sceneMap[sceneIndex];
}
if (sceneID == currentSceneID.GetSceneID()) {
const float worldX = (static_cast<float>(i) + (chunk.offsetX / chunk.scaleFactor)) * chunk.scaleFactor;
const float worldY = y;
const float worldZ = (static_cast<float>(j) + (chunk.offsetZ / chunk.scaleFactor)) * chunk.scaleFactor;
NiPoint3 spawnPos(worldX, worldY, worldZ);
EntityInfo info;
info.lot = lot + currentSceneID.GetSceneID();
info.pos = spawnPos;
info.rot = QuatUtils::IDENTITY;
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings.Insert(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 " << spawnedCount << " points (LOT " << lot + currentSceneID.GetSceneID() << ") 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 Raw terrain data
const auto& raw = zone->GetZoneRaw();
if (raw.chunks.empty()) {
ChatPackets::SendSystemMessage(sysAddr, u"Zone does not have valid terrain data.");
return;
}
// Spawn at all sceneMap points across all scenes
uint32_t spawnedCount = 0;
std::map<uint8_t, uint32_t> sceneSpawnCounts; // Track spawns per scene
for (const auto& chunk : raw.chunks) {
if (chunk.sceneMap.empty() || chunk.colorMapResolution == 0 || chunk.heightMap.empty()
|| chunk.width <= 1 || chunk.height <= 1 || chunk.scaleFactor <= 0.0f) 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;
const float y = chunk.heightMap[heightIndex];
const float sceneMapI = (static_cast<float>(i) / static_cast<float>(chunk.width - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const float sceneMapJ = (static_cast<float>(j) / static_cast<float>(chunk.height - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const uint32_t sceneI = std::min(static_cast<uint32_t>(sceneMapI), chunk.colorMapResolution - 1);
const uint32_t sceneJ = std::min(static_cast<uint32_t>(sceneMapJ), chunk.colorMapResolution - 1);
const uint32_t sceneIndex = sceneI * chunk.colorMapResolution + sceneJ;
uint8_t sceneID = 0;
if (sceneIndex < chunk.sceneMap.size()) {
sceneID = chunk.sceneMap[sceneIndex];
}
if (sceneID == 0) continue;
const float worldX = (static_cast<float>(i) + (chunk.offsetX / chunk.scaleFactor)) * chunk.scaleFactor;
const float worldY = y;
const float worldZ = (static_cast<float>(j) + (chunk.offsetZ / chunk.scaleFactor)) * chunk.scaleFactor;
NiPoint3 spawnPos(worldX, worldY, worldZ);
EntityInfo info;
info.lot = lot + sceneID;
info.pos = spawnPos;
info.rot = QuatUtils::IDENTITY;
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings.Insert(u"SpawnedFromSlashCommand", true);
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
if (newEntity != nullptr) {
Game::entityManager->ConstructEntity(newEntity);
spawnedCount++;
sceneSpawnCounts[sceneID]++;
}
}
}
}
// Send detailed feedback
std::ostringstream feedback;
feedback << "Spawned " << spawnedCount << " total points (base LOT " << lot << ") 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<int>(sceneID) << ", LOT: " << (lot + sceneID) << " (" << sceneName << "): " << count << " points\n";
}
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
}
};