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