diff --git a/dCommon/NiColor.h b/dCommon/NiColor.h new file mode 100644 index 00000000..585f0e5e --- /dev/null +++ b/dCommon/NiColor.h @@ -0,0 +1,34 @@ +#ifndef NICOLOR_H +#define NICOLOR_H + +struct NiColor { + float m_Red; + float m_Green; + float m_Blue; + + constexpr NiColor(float red, float green, float blue) : m_Red(red), m_Green(green), m_Blue(blue) {} + constexpr NiColor() : NiColor(0.0f, 0.0f, 0.0f) {} + + /* reduce RGB files to grayscale, with or without alpha + * using the equation given in Poynton's ColorFAQ at + * // dead link + * Copyright (c) 1998-01-04 Charles Poynton poynton at inforamp.net + * + * Y = 0.212671 * R + 0.715160 * G + 0.072169 * B + * + * We approximate this with + * + * Y = 0.21268 * R + 0.7151 * G + 0.07217 * B + * + * which can be expressed with integers as + * + * Y = (6969 * R + 23434 * G + 2365 * B)/32768 + * + * The calculation is to be done in a linear colorspace. + * + * Other integer coefficents can be used via png_set_rgb_to_gray(). + */ + float ToXYZ() const { return (m_Red * 0.212671f) + (m_Green * 0.71516f) + (m_Blue * 0.072169f); }; +}; + +#endif // NICOLOR_H diff --git a/dCommon/dClient/SceneColor.h b/dCommon/dClient/SceneColor.h new file mode 100644 index 00000000..2ae4e33b --- /dev/null +++ b/dCommon/dClient/SceneColor.h @@ -0,0 +1,166 @@ +#ifndef SCENE_COLOR_H +#define SCENE_COLOR_H + +#include "NiColor.h" +#include +#include + +namespace SceneColor { + // these are not random values, they are the actual template colors used by the game + static constexpr std::array TEMPLATE_COLORS = {{ + { 0.5019608f, 0.5019608f, 0.5019608f }, + { 1.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f }, + { 1.0f, 1.0f, 0.0f }, + { 1.0f, 0.0f, 1.0f }, + { 0.0f, 1.0f, 1.0f }, + { 0.5019608f, 0.0f, 1.0f }, + { 1.0f, 0.5019608f, 0.0f }, + { 1.0f, 0.5019608f, 0.5019608f }, + { 0.5019608f, 0.2509804f, 0.0f }, + { 0.5019608f, 0.0f, 0.2509804f }, + { 0.0f, 0.5019608f, 0.2509804f }, + { 0.2509804f, 0.0f, 0.5019608f }, + { 0.8745098f, 0.0f, 0.2509804f }, + { 0.2509804f, 0.8745098f, 0.5019608f }, + { 1.0f, 0.7490196f, 0.0f }, + { 1.0f, 0.2509804f, 0.0627451f }, + { 0.2509804f, 0.0f, 0.8745098f }, + { 0.7490196f, 0.0627451f, 0.0627451f }, + { 0.0627451f, 0.7490196f, 0.0627451f }, + { 1.0f, 0.5019608f, 1.0f }, + { 0.9372549f, 0.8705882f, 0.8039216f }, + { 0.8039216f, 0.5843138f, 0.4588235f }, + { 0.9921569f, 0.8509804f, 0.7098039f }, + { 0.4705882f, 0.8588235f, 0.8862745f }, + { 0.5294118f, 0.6627451f, 0.4196078f }, + { 1.0f, 0.6431373f, 0.454902f }, + { 0.9803922f, 0.9058824f, 0.7098039f }, + { 0.6235294f, 0.5058824f, 0.4392157f }, + { 0.9921569f, 0.4862745f, 0.4313726f }, + { 0.0f, 0.0f, 0.0f }, + { 0.6745098f, 0.8980392f, 0.9333333f }, + { 0.1215686f, 0.4588235f, 0.9960784f }, + { 0.6352941f, 0.6352941f, 0.8156863f }, + { 0.4f, 0.6f, 0.8f }, + { 0.05098039f, 0.5960785f, 0.7294118f }, + { 0.4509804f, 0.4f, 0.7411765f }, + { 0.8705882f, 0.3647059f, 0.5137255f }, + { 0.7960784f, 0.254902f, 0.3294118f }, + { 0.7058824f, 0.4039216f, 0.3019608f }, + { 1.0f, 0.4980392f, 0.2862745f }, + { 0.9176471f, 0.4941176f, 0.3647059f }, + { 0.6901961f, 0.7176471f, 0.7764706f }, + { 1.0f, 1.0f, 0.6f }, + { 0.1098039f, 0.827451f, 0.6352941f }, + { 1.0f, 0.6666667f, 0.8f }, + { 0.8666667f, 0.2666667f, 0.572549f }, + { 0.1137255f, 0.6745098f, 0.8392157f }, + { 0.7372549f, 0.3647059f, 0.345098f }, + { 0.8666667f, 0.5803922f, 0.4588235f }, + { 0.6039216f, 0.8078431f, 0.9215686f }, + { 1.0f, 0.7372549f, 0.8509804f }, + { 0.9921569f, 0.8588235f, 0.427451f }, + { 0.1686275f, 0.4235294f, 0.7686275f }, + { 0.9372549f, 0.8039216f, 0.7215686f }, + { 0.4313726f, 0.3176471f, 0.3764706f }, + { 0.8078431f, 1.0f, 0.1137255f }, + { 0.427451f, 0.682353f, 0.5058824f }, + { 0.7647059f, 0.3921569f, 0.772549f }, + { 0.8f, 0.4f, 0.4f }, + { 0.9058824f, 0.7764706f, 0.5921569f }, + { 0.9882353f, 0.8509804f, 0.4588235f }, + { 0.6588235f, 0.8941177f, 0.627451f }, + { 0.5843138f, 0.5686275f, 0.5490196f }, + { 0.1098039f, 0.6745098f, 0.4705882f }, + { 0.06666667f, 0.3921569f, 0.7058824f }, + { 0.9411765f, 0.9098039f, 0.5686275f }, + { 1.0f, 0.1137255f, 0.8078431f }, + { 0.6980392f, 0.9254902f, 0.3647059f }, + { 0.3647059f, 0.4627451f, 0.7960784f }, + { 0.7921569f, 0.2156863f, 0.4039216f }, + { 0.2313726f, 0.6901961f, 0.5607843f }, + { 0.9882353f, 0.7058824f, 0.8352941f }, + { 1.0f, 0.9568627f, 0.3098039f }, + { 1.0f, 0.7411765f, 0.5333334f }, + { 0.9647059f, 0.3921569f, 0.6862745f }, + { 0.6666667f, 0.9411765f, 0.8196079f }, + { 0.8039216f, 0.2901961f, 0.2980392f }, + { 0.9294118f, 0.8196079f, 0.6117647f }, + { 0.5921569f, 0.6039216f, 0.6666667f }, + { 0.7843137f, 0.2196078f, 0.3529412f }, + { 0.9372549f, 0.5960785f, 0.6666667f }, + { 0.9921569f, 0.7372549f, 0.7058824f }, + { 0.1019608f, 0.282353f, 0.4627451f }, + { 0.1882353f, 0.7294118f, 0.5607843f }, + { 0.772549f, 0.2941177f, 0.5490196f }, + { 0.09803922f, 0.454902f, 0.8235294f }, + { 0.7294118f, 0.7215686f, 0.4235294f }, + { 1.0f, 0.4588235f, 0.2196078f }, + { 1.0f, 0.1686275f, 0.1686275f }, + { 0.972549f, 0.8352941f, 0.4078431f }, + { 0.9019608f, 0.6588235f, 0.8431373f }, + { 0.254902f, 0.2901961f, 0.2980392f }, + { 1.0f, 0.4313726f, 0.2901961f }, + { 0.1098039f, 0.6627451f, 0.7882353f }, + { 1.0f, 0.8117647f, 0.6705883f }, + { 0.772549f, 0.8156863f, 0.9019608f }, + { 0.9921569f, 0.8666667f, 0.9019608f }, + { 0.08235294f, 0.5019608f, 0.4705882f }, + { 0.9882353f, 0.454902f, 0.9921569f }, + { 0.9686275f, 0.5607843f, 0.654902f }, + { 0.5568628f, 0.2705882f, 0.5215687f }, + { 0.454902f, 0.2588235f, 0.7843137f }, + { 0.6156863f, 0.5058824f, 0.7294118f }, + { 1.0f, 0.2862745f, 0.4235294f }, + { 0.8392157f, 0.5411765f, 0.3490196f }, + { 0.4431373f, 0.2941177f, 0.1372549f }, + { 1.0f, 0.282353f, 0.8156863f }, + { 0.9333333f, 0.1254902f, 0.3019608f }, + { 1.0f, 0.3254902f, 0.2862745f }, + { 0.7529412f, 0.2666667f, 0.5607843f }, + { 0.1215686f, 0.8078431f, 0.7960784f }, + { 0.4705882f, 0.3176471f, 0.6627451f }, + { 1.0f, 0.6078432f, 0.6666667f }, + { 0.9882353f, 0.1568628f, 0.2784314f }, + { 0.4627451f, 1.0f, 0.4784314f }, + { 0.6235294f, 0.8862745f, 0.7490196f }, + { 0.6470588f, 0.4117647f, 0.3098039f }, + { 0.5411765f, 0.4745098f, 0.3647059f }, + { 0.2705882f, 0.8078431f, 0.6352941f }, + { 0.8039216f, 0.772549f, 0.7607843f }, + { 0.5019608f, 0.854902f, 0.9215686f }, + { 0.9254902f, 0.9176471f, 0.7450981f }, + { 1.0f, 0.8117647f, 0.282353f }, + { 0.9921569f, 0.3686275f, 0.3254902f }, + { 0.9803922f, 0.654902f, 0.4235294f }, + { 0.09411765f, 0.654902f, 0.7098039f }, + { 0.9215686f, 0.7803922f, 0.8745098f }, + { 0.9882353f, 0.5372549f, 0.6745098f }, + { 0.8588235f, 0.8431373f, 0.8235294f }, + { 0.8705882f, 0.6666667f, 0.5333334f }, + { 0.4666667f, 0.8666667f, 0.9058824f }, + { 1.0f, 1.0f, 0.4f }, + { 0.572549f, 0.4313726f, 0.682353f }, + { 0.1960784f, 0.2901961f, 0.6980392f }, + { 0.9686275f, 0.3254902f, 0.5803922f }, + { 1.0f, 0.627451f, 0.5372549f }, + { 0.5607843f, 0.3137255f, 0.6156863f }, + { 1.0f, 1.0f, 1.0f }, + { 0.6352941f, 0.6784314f, 0.8156863f }, + { 0.9882353f, 0.4235294f, 0.5215687f }, + { 0.8039216f, 0.6431373f, 0.8705882f }, + { 0.9882353f, 0.9098039f, 0.5137255f }, + { 0.772549f, 0.8901961f, 0.5176471f }, + { 1.0f, 0.682353f, 0.2588235f }, + }}; + + static constexpr NiColor FALLBACK_COLOR{ 1.0f, 1.0f, 1.0f }; + + inline const NiColor& Get(uint8_t index) { + return (index < TEMPLATE_COLORS.size()) ? TEMPLATE_COLORS[index] : FALLBACK_COLOR; + } +} // namespace SceneColor + +#endif // SCENE_COLOR_H diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index e4782db4..fdc40ee5 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 5b464ef0..e47bb25f 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -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(i) / static_cast(chunk.width - 1)) * static_cast(chunk.colorMapResolution - 1); + const float sceneMapJ = (static_cast(j) / static_cast(chunk.height - 1)) * static_cast(chunk.colorMapResolution - 1); + + const uint32_t sceneI = std::min(static_cast(sceneMapI), chunk.colorMapResolution - 1); + const uint32_t sceneJ = std::min(static_cast(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(i) + (chunk.offsetX / chunk.scaleFactor)) * chunk.scaleFactor; + const float worldY = y; + const float worldZ = (static_cast(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 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(i) / static_cast(chunk.width - 1)) * static_cast(chunk.colorMapResolution - 1); + const float sceneMapJ = (static_cast(j) / static_cast(chunk.height - 1)) * static_cast(chunk.colorMapResolution - 1); + + const uint32_t sceneI = std::min(static_cast(sceneMapI), chunk.colorMapResolution - 1); + const uint32_t sceneJ = std::min(static_cast(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(i) + (chunk.offsetX / chunk.scaleFactor)) * chunk.scaleFactor; + const float worldY = y; + const float worldZ = (static_cast(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(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/dNavigation/CMakeLists.txt b/dNavigation/CMakeLists.txt index e2a1c6ef..2c776136 100644 --- a/dNavigation/CMakeLists.txt +++ b/dNavigation/CMakeLists.txt @@ -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 diff --git a/dNavigation/dNavMesh.cpp b/dNavigation/dNavMesh.cpp index a65ed0a3..430bc0a7 100644 --- a/dNavigation/dNavMesh.cpp +++ b/dNavigation/dNavMesh.cpp @@ -1,6 +1,5 @@ #include "dNavMesh.h" -#include "RawFile.h" #include "Game.h" #include "Logger.h" diff --git a/dNavigation/dTerrain/CMakeLists.txt b/dNavigation/dTerrain/CMakeLists.txt deleted file mode 100644 index 91d0741b..00000000 --- a/dNavigation/dTerrain/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -set(DNAVIGATIONS_DTERRAIN_SOURCES "RawFile.cpp" - "RawChunk.cpp" - "RawHeightMap.cpp" PARENT_SCOPE) diff --git a/dNavigation/dTerrain/RawChunk.cpp b/dNavigation/dTerrain/RawChunk.cpp deleted file mode 100644 index df4950d4..00000000 --- a/dNavigation/dTerrain/RawChunk.cpp +++ /dev/null @@ -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(stream.tellg()) + (colorMapSize * colorMapSize * 4)); - - uint32_t lightmapSize; - BinaryIO::BinaryRead(stream, lightmapSize); - stream.seekg(static_cast(stream.tellg()) + (lightmapSize)); - - uint32_t colorMapSize2; - BinaryIO::BinaryRead(stream, colorMapSize2); - stream.seekg(static_cast(stream.tellg()) + (colorMapSize2 * colorMapSize2 * 4)); - - uint8_t unknown; - BinaryIO::BinaryRead(stream, unknown); - - uint32_t blendmapSize; - BinaryIO::BinaryRead(stream, blendmapSize); - stream.seekg(static_cast(stream.tellg()) + (blendmapSize)); - - uint32_t pointSize; - BinaryIO::BinaryRead(stream, pointSize); - stream.seekg(static_cast(stream.tellg()) + (pointSize * 9 * 4)); - - stream.seekg(static_cast(stream.tellg()) + (colorMapSize * colorMapSize)); - - uint32_t endCounter; - BinaryIO::BinaryRead(stream, endCounter); - stream.seekg(static_cast(stream.tellg()) + (endCounter * 2)); - - if (endCounter != 0) { - stream.seekg(static_cast(stream.tellg()) + (32)); - - for (int i = 0; i < 0x10; i++) { - uint16_t finalCountdown; - BinaryIO::BinaryRead(stream, finalCountdown); - stream.seekg(static_cast(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; -} diff --git a/dNavigation/dTerrain/RawChunk.h b/dNavigation/dTerrain/RawChunk.h deleted file mode 100644 index 1e26f11d..00000000 --- a/dNavigation/dTerrain/RawChunk.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include -#include - -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; -}; diff --git a/dNavigation/dTerrain/RawFile.cpp b/dNavigation/dTerrain/RawFile.cpp deleted file mode 100644 index efc2b39d..00000000 --- a/dNavigation/dTerrain/RawFile.cpp +++ /dev/null @@ -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'; - } -} diff --git a/dNavigation/dTerrain/RawFile.h b/dNavigation/dTerrain/RawFile.h deleted file mode 100644 index b1fb4024..00000000 --- a/dNavigation/dTerrain/RawFile.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include -#include - -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 m_Chunks; - RawMesh* m_FinalMesh = nullptr; -}; diff --git a/dNavigation/dTerrain/RawHeightMap.cpp b/dNavigation/dTerrain/RawHeightMap.cpp deleted file mode 100644 index e1310669..00000000 --- a/dNavigation/dTerrain/RawHeightMap.cpp +++ /dev/null @@ -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() { - -} diff --git a/dNavigation/dTerrain/RawHeightMap.h b/dNavigation/dTerrain/RawHeightMap.h deleted file mode 100644 index 9a4bda3b..00000000 --- a/dNavigation/dTerrain/RawHeightMap.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include -#include - -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 m_FloatMap = {}; -}; diff --git a/dNavigation/dTerrain/RawMesh.h b/dNavigation/dTerrain/RawMesh.h deleted file mode 100644 index ed8eb4f3..00000000 --- a/dNavigation/dTerrain/RawMesh.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -#include "NiPoint3.h" - -struct RawMesh { - std::vector m_Vertices; - std::vector m_Triangles; -}; diff --git a/dZoneManager/CMakeLists.txt b/dZoneManager/CMakeLists.txt index 544a01d9..ba6eff68 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") @@ -14,6 +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}/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 new file mode 100644 index 00000000..2b7e5090 --- /dev/null +++ b/dZoneManager/Raw.cpp @@ -0,0 +1,481 @@ +#include "Raw.h" +#include "BinaryIO.h" +#include "Logger.h" +#include "SceneColor.h" +#include +#include +#include + +namespace { +constexpr uint32_t kMaxResolution = 4096; +constexpr size_t kMaxBlobBytes = 64ULL * 1024 * 1024; // 64 MiB +constexpr uint32_t kMaxChunks = 1024; +} // namespace + +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.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(chunk.width); + const size_t height = static_cast(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::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::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; // Default to chunk width for older versions + } + + if (chunk.colorMapResolution > kMaxResolution) { + LOG("Chunk colorMapResolution %u exceeds maximum %u", chunk.colorMapResolution, kMaxResolution); + return false; + } + const size_t colorMapPixelCount = static_cast(chunk.colorMapResolution) * chunk.colorMapResolution * 4; // RGBA + if (colorMapPixelCount > kMaxBlobBytes) { + LOG("Chunk colorMap size %zu exceeds maximum %zu bytes", colorMapPixelCount, kMaxBlobBytes); + return false; + } + chunk.colorMap.resize(colorMapPixelCount); + stream.read(reinterpret_cast(chunk.colorMap.data()), static_cast(colorMapPixelCount)); + + if (stream.fail()) { + return false; + } + // LightMap/diffusemap.dds + 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(chunk.lightMap.data()), static_cast(lightMapSize)); + + if (stream.fail()) { + return false; + } + + // TextureMap + if (version >= 32) { + BinaryIO::BinaryRead(stream, chunk.textureMapResolution); + } else { + chunk.textureMapResolution = chunk.width; // Default to chunk width for older versions + } + + if (chunk.textureMapResolution > kMaxResolution) { + LOG("Chunk textureMapResolution %u exceeds maximum %u", chunk.textureMapResolution, kMaxResolution); + return false; + } + const size_t textureMapPixelCount = static_cast(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(chunk.textureMap.data()), static_cast(textureMapPixelCount)); + + if (stream.fail()) { + return false; + } + + // Texture settings + BinaryIO::BinaryRead(stream, chunk.textureSettings); + + // Blend map DDS + 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(chunk.blendMap.data()), static_cast(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(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 (version 32+ only) + if (version >= 32) { + const size_t sceneMapSize = static_cast(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(chunk.sceneMap.data()), static_cast(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 + const size_t vertBytes = static_cast(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; + } + + // 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); + + 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::max(); + outRaw.minBoundsZ = std::numeric_limits::max(); + outRaw.maxBoundsX = std::numeric_limits::lowest(); + 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); + } + LOG("Raw terrain bounds: X[%.2f, %.2f], Z[%.2f, %.2f]", + outRaw.minBoundsX, outRaw.maxBoundsX, outRaw.minBoundsZ, outRaw.maxBoundsZ); + } + } + 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 + } + + LOG("GenerateTerrainMesh: Processing %zu chunks", raw.chunks.size()); + + uint32_t vertexOffset = 0; + + for (const auto& chunk : raw.chunks) { + // Skip chunks without scene maps or with invalid dimensions/scale + if (chunk.sceneMap.empty() || chunk.colorMapResolution == 0 || chunk.heightMap.empty() + || chunk.scaleFactor <= 0.0f || chunk.width <= 1 || chunk.height <= 1) { + LOG("Skipping chunk %u (sceneMap: %zu, colorMapRes: %u, heightMap: %zu, scaleFactor: %f, width: %u, height: %u)", + chunk.id, chunk.sceneMap.size(), chunk.colorMapResolution, chunk.heightMap.size(), + chunk.scaleFactor, chunk.width, chunk.height); + continue; + } + + LOG("Processing chunk %u: width=%u, height=%u, colorMapRes=%u, sceneMapSize=%zu", + chunk.id, chunk.width, chunk.height, chunk.colorMapResolution, chunk.sceneMap.size()); + + // Generate vertices for this chunk + 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]; + + const float worldX = (static_cast(i) + (chunk.offsetX / chunk.scaleFactor)) * chunk.scaleFactor; + const float worldY = y; + const float worldZ = (static_cast(j) + (chunk.offsetZ / chunk.scaleFactor)) * chunk.scaleFactor; + + const NiPoint3 worldPos(worldX, worldY, worldZ); + + const float sceneMapI = (static_cast(i) / static_cast(chunk.width - 1)) * static_cast(chunk.colorMapResolution - 1); + const float sceneMapJ = (static_cast(j) / static_cast(chunk.height - 1)) * static_cast(chunk.colorMapResolution - 1); + + const uint32_t sceneI = std::min(static_cast(sceneMapI), chunk.colorMapResolution - 1); + const uint32_t sceneJ = std::min(static_cast(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]; + } + outMesh.vertices.emplace_back(worldPos, sceneID); + 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; + } + } + + 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 diff --git a/dZoneManager/Raw.h b/dZoneManager/Raw.h new file mode 100644 index 00000000..593d4e00 --- /dev/null +++ b/dZoneManager/Raw.h @@ -0,0 +1,158 @@ +#pragma once + +#ifndef __RAW_H__ +#define __RAW_H__ + +#include +#include +#include +#include +#include "NiPoint3.h" +#include "dCommonVars.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 offsetX; + float offsetZ; + 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; + +}; + +/** + * @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; + + // Calculated bounds of the entire terrain + float minBoundsX = 0.0f; + float minBoundsZ = 0.0f; + float maxBoundsX = 0.0f; + float maxBoundsZ = 0.0f; +}; + +/** + * @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); + +/** + * @brief Write terrain mesh to OBJ file for debugging/visualization + * Merged from dTerrain's WriteFinalMeshToOBJ functionality + * Vertices are colored based on their scene ID using a hash function + * + * @param mesh The terrain mesh to export + * @param path Output path for the OBJ file + * @return true if successfully written, false otherwise + */ +bool WriteTerrainMeshToOBJ(const TerrainMesh& mesh, const std::string& path); + +} // namespace Raw + +#endif // __RAW_H__ diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index a5a8fa97..32b9bc7d 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -8,6 +8,7 @@ #include "GeneralUtils.h" #include "BinaryIO.h" #include "LUTriggers.h" +#include "dConfig.h" #include "AssetManager.h" #include "CDClientManager.h" @@ -20,6 +21,7 @@ #include "eTriggerEventType.h" #include "eWaypointCommandType.h" #include "dNavMesh.h" +#include "Raw.h" Zone::Zone(const LWOZONEID zoneID) : m_ZoneID(zoneID) { @@ -78,12 +80,51 @@ void Zone::LoadZoneIntoMemory() { LoadScene(file); } - //Read generic zone info: - BinaryIO::ReadString(file, m_ZonePath, BinaryIO::ReadType::String); + // Zone boundary lines — skip past them for correct file positioning + uint8_t numBoundaries = 0; + BinaryIO::BinaryRead(file, numBoundaries); + for (uint8_t i = 0; i < numBoundaries; ++i) { + NiPoint3 normal, point, spawnLocation; + uint32_t packed, destSceneID; + BinaryIO::BinaryRead(file, normal); + BinaryIO::BinaryRead(file, point); + BinaryIO::BinaryRead(file, packed); + BinaryIO::BinaryRead(file, destSceneID); + BinaryIO::BinaryRead(file, spawnLocation); + } + BinaryIO::ReadString(file, m_ZoneRawPath, BinaryIO::ReadType::String); 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."); + } + LOG("Loaded Raw Terrain with %u chunks", m_Raw.numChunks); + + + // Optionally export terrain mesh to OBJ for debugging/visualization + if (Game::config->GetValue("export_terrain_to_obj") == "1") { + + // Generate terrain mesh + Raw::GenerateTerrainMesh(m_Raw, m_TerrainMesh); + LOG("Generated terrain mesh with %zu vertices and %zu triangles", m_TerrainMesh.vertices.size(), m_TerrainMesh.triangles.size() / 3); + + // Write to OBJ + std::string objFileName = "terrain_" + std::to_string(m_ZoneID.GetMapID()) + ".obj"; + if (Raw::WriteTerrainMeshToOBJ(m_TerrainMesh, objFileName)) { + LOG("Exported terrain mesh to %s", objFileName.c_str()); + } + } + if (m_FileFormatVersion >= Zone::FileFormatVersion::PreAlpha) { BinaryIO::BinaryRead(file, m_NumberOfSceneTransitionsLoaded); for (uint32_t i = 0; i < m_NumberOfSceneTransitionsLoaded; ++i) { @@ -236,20 +277,23 @@ void Zone::LoadScene(std::istream& file) { } if (m_FileFormatVersion >= Zone::FileFormatVersion::LatePreAlpha) { BinaryIO::BinaryRead(file, scene.sceneType); - lwoSceneID.SetLayerID(scene.sceneType); + lwoSceneID.SetLayerID(static_cast(scene.sceneType)); + BinaryIO::ReadString(file, scene.name, BinaryIO::ReadType::String); } if (m_FileFormatVersion == Zone::FileFormatVersion::LatePreAlpha) { - BinaryIO::BinaryRead(file, scene.unknown1); - BinaryIO::BinaryRead(file, scene.unknown2); + BinaryIO::BinaryRead(file, scene.scenePosition); + BinaryIO::BinaryRead(file, scene.sceneRadius); } if (m_FileFormatVersion >= Zone::FileFormatVersion::LatePreAlpha) { - BinaryIO::BinaryRead(file, scene.color_r); - BinaryIO::BinaryRead(file, scene.color_b); - BinaryIO::BinaryRead(file, scene.color_g); + uint8_t r, b, g; + BinaryIO::BinaryRead(file, r); + BinaryIO::BinaryRead(file, b); + BinaryIO::BinaryRead(file, g); + scene.color = NiColor(r / 255.0f, g / 255.0f, b / 255.0f); } m_Scenes[lwoSceneID] = std::move(scene); @@ -350,7 +394,10 @@ void Zone::LoadSceneTransition(std::istream& file) { SceneTransitionInfo Zone::LoadSceneTransitionInfo(std::istream& file) { SceneTransitionInfo info; - BinaryIO::BinaryRead(file, info.sceneID); + uint32_t sceneID, layerID; + BinaryIO::BinaryRead(file, sceneID); + BinaryIO::BinaryRead(file, layerID); + info.sceneID = LWOSCENEID(static_cast(sceneID), layerID); BinaryIO::BinaryRead(file, info.position); return info; } @@ -422,7 +469,6 @@ void Zone::LoadPath(std::istream& file) { BinaryIO::BinaryRead(file, waypoint.position.y); BinaryIO::BinaryRead(file, waypoint.position.z); - if (path.pathType == PathType::Spawner || path.pathType == PathType::MovingPlatform || path.pathType == PathType::Race || path.pathType == PathType::Camera || path.pathType == PathType::Rail) { BinaryIO::BinaryRead(file, waypoint.rotation.w); BinaryIO::BinaryRead(file, waypoint.rotation.x); @@ -483,7 +529,7 @@ void Zone::LoadPath(std::istream& file) { } // We verify the waypoint heights against the navmesh because in many movement paths, - // the waypoint is located near 0 height, + // the waypoint is located near 0 height, if (path.pathType == PathType::Movement) { if (dpWorld::IsLoaded()) { // 2000 should be large enough for every world. @@ -494,3 +540,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 4cb74efa..7ad05204 100644 --- a/dZoneManager/Zone.h +++ b/dZoneManager/Zone.h @@ -2,10 +2,12 @@ #include "dZMCommon.h" #include "LDFFormat.h" +#include "NiColor.h" #include "tinyxml2.h" #include #include #include +#include "Raw.h" namespace LUTriggers { struct Trigger; @@ -21,22 +23,25 @@ struct WaypointCommand { }; +enum class eSceneType : uint32_t { + General = 0, + Audio = 1, +}; + struct SceneRef { std::string filename; uint32_t id{}; - uint32_t sceneType{}; //0 = general, 1 = audio? + eSceneType sceneType{}; std::string name; - NiPoint3 unknown1; - float unknown2{}; - uint8_t color_r{}; - uint8_t color_g{}; - uint8_t color_b{}; + NiPoint3 scenePosition; // version 33 only: editor bounding sphere center + float sceneRadius{}; // version 33 only: editor bounding sphere radius + NiColor color; std::unique_ptr level; std::map triggers; }; struct SceneTransitionInfo { - uint64_t sceneID{}; //id of the scene being transitioned to. + LWOSCENEID sceneID; NiPoint3 position; }; @@ -228,6 +233,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 +255,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..9e029b42 100644 --- a/dZoneManager/dZoneManager.cpp +++ b/dZoneManager/dZoneManager.cpp @@ -10,9 +10,11 @@ #include "VanityUtilities.h" #include "WorldConfig.h" #include "CDZoneTableTable.h" +#include #include +#include +#include #include "eObjectBits.h" -#include "CDZoneTableTable.h" #include "AssetManager.h" #include @@ -62,6 +64,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 +303,131 @@ void dZoneManager::LoadWorldConfig() { LOG_DEBUG("Loaded WorldConfig into memory"); } + +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.sceneMap.empty() || chunk.scaleFactor <= 0.0f || chunk.width <= 1 || chunk.height <= 1 || chunk.colorMapResolution == 0) continue; + + // Reverse: worldX = (i + offsetX/scaleFactor) * scaleFactor + // Therefore: i = worldX/scaleFactor - offsetX/scaleFactor + const float heightI = posX / chunk.scaleFactor - (chunk.offsetX / chunk.scaleFactor); + const float heightJ = posZ / chunk.scaleFactor - (chunk.offsetZ / chunk.scaleFactor); + + // Check if position is within this chunk's heightmap bounds + if (heightI >= 0.0f && heightI < static_cast(chunk.width) && + heightJ >= 0.0f && heightJ < static_cast(chunk.height)) { + + const float sceneMapI = (heightI / static_cast(chunk.width - 1)) * static_cast(chunk.colorMapResolution - 1); + const float sceneMapJ = (heightJ / static_cast(chunk.height - 1)) * static_cast(chunk.colorMapResolution - 1); + + const uint32_t sceneI = std::min(static_cast(sceneMapI), chunk.colorMapResolution - 1); + const uint32_t sceneJ = std::min(static_cast(sceneMapJ), chunk.colorMapResolution - 1); + + // Scene map uses the same indexing pattern as heightmap: row * width + col + const uint32_t sceneIndex = sceneI * chunk.colorMapResolution + sceneJ; + + // Bounds check: if this chunk's sceneMap is inconsistent, skip this chunk + if (sceneIndex >= chunk.sceneMap.size()) { + LOG_DEBUG("GetSceneIDFromPosition: sceneIndex %u out of bounds (sceneMap size: %zu), skipping malformed chunk.", sceneIndex, chunk.sceneMap.size()); + continue; + } + + // Get scene ID from sceneMap + const uint8_t sceneID = chunk.sceneMap[sceneIndex]; + + // Return the scene ID + return LWOSCENEID(sceneID, 0); + } + } + + // Position not found in any chunk + 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) { + if (sceneRef.sceneType != eSceneType::General) continue; + m_SceneAdjacencyList.try_emplace(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 && m_SceneAdjacencyList.contains(point.sceneID)) { + 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); + } + } + } + } + } + + // Scene 0 (global scene) is always loaded and adjacent to all other scenes + LWOSCENEID globalScene = LWOSCENEID(0, 0); + for (auto& [sceneID, adjacentScenes] : m_SceneAdjacencyList) { + if (sceneID != globalScene) { + // Add global scene to this scene's adjacency list if not already present + if (std::find(adjacentScenes.begin(), adjacentScenes.end(), globalScene) == adjacentScenes.end()) { + adjacentScenes.push_back(globalScene); + } + + // Add this scene to global scene's adjacency list if not already present + auto& globalAdjacent = m_SceneAdjacencyList[globalScene]; + if (std::find(globalAdjacent.begin(), globalAdjacent.end(), sceneID) == globalAdjacent.end()) { + globalAdjacent.push_back(sceneID); + } + } + } +} + +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; }; diff --git a/resources/worldconfig.ini b/resources/worldconfig.ini index 5d23a32f..bf9b6e2a 100644 --- a/resources/worldconfig.ini +++ b/resources/worldconfig.ini @@ -103,5 +103,9 @@ hardcore_disabled_worlds= # Keeps this percentage of a players' coins on death in hardcore hardcore_coin_keep= +# Export terrain meshes to OBJ files when zones load +# OBJ files will be saved as terrain_.obj in the server directory +export_terrain_to_obj=0 + # save pre-split lxfmls to disk for debugging save_lxfmls=0