Compare commits

...

35 Commits

Author SHA1 Message Date
Aaron Kimbrell
7dd918d894 feat: add eSceneType enum, filter scene graph to general scenes, zone parsing improvements
- Add eSceneType enum (General, Audio) replacing raw uint32_t in SceneRef
- Filter BuildSceneGraph to only include General scenes
- Skip transitions referencing non-general scenes in adjacency graph
- Rename SceneRef unknown fields to scenePosition/sceneRadius
- Zone parsing and Level improvements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 11:38:47 -05:00
Aaron Kimbrell
1aeede3cd1 fix: address PR review feedback for raw terrain parsing
- Fix integer division bug in scene map lookups (was truncating to 0)
- Fix indentation throughout Raw.cpp, DEVGMCommands.cpp
- Add missing <algorithm> and <set> includes in dZoneManager.cpp
- Add missing width/height/scaleFactor guards in SpawnAllScenePoints
- Fix %llu -> %zu for size_t format specifiers
- Simplify no-op worldY calculation (y / scale * scale -> y)
- Remove redundant ternary guards in GetSceneIDFromPosition
- Fix misleading "Spawned LOT" feedback message
- Update info.settings to use LwoNameValue::Insert API (post-merge fix)
- Refactor SceneColor to static constexpr std::array (no heap alloc)
- Make NiColor constructors constexpr
- Remove duplicate CDZoneTableTable.h include

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 01:50:08 -05:00
Aaron Kimbrell
83e2ea4278 Merge remote-tracking branch 'origin/main' into raw-parsing-for-scene-data
# Conflicts:
#	dNavigation/dTerrain/RawChunk.cpp
2026-06-21 01:13:34 -05:00
copilot-swe-agent[bot]
d725da7d7c fix: add scaleFactor/width/height guards in GenerateTerrainMesh, fix %zu format specifier
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/43d6190e-34b2-4d7f-8aaf-dd16bd77cc25

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-03-31 23:38:49 +00:00
copilot-swe-agent[bot]
f6230532df fix: change SceneColor::Get parameter from unsigned char to uint8_t
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/804e2235-77e8-458f-9a0b-519c496ba3b3

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-03-31 16:18:20 +00:00
copilot-swe-agent[bot]
d5bacabbdc fix: add width/height/scaleFactor guards in SpawnScenePoints to prevent division by zero
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/386caefa-6a0f-4445-b3a5-3534ca2a5a93

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-03-31 16:12:51 +00:00
copilot-swe-agent[bot]
f66716f027 fix: validate numChunks, numFlairs, vertSize before resize to prevent OOM from malformed raws
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/39d7ce79-bc9a-4960-8259-f11bcb5947f8

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-03-31 09:48:22 +00:00
Aaron Kimbrell
d51ad3e769 Update dZoneManager/Raw.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 04:45:15 -05:00
Aaron Kimbrell
fbac3241ec Update dZoneManager/dZoneManager.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 04:45:00 -05:00
Aaron Kimbrell
c6a38e37c2 Update dZoneManager/Raw.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 04:44:38 -05:00
copilot-swe-agent[bot]
5b9f7e0d8b fix: add explicit guards before sceneMapI/J computation in GetSceneIDFromPosition
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/e331a9a3-fb6b-4574-9456-2a94e4ecdd33

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-03-31 09:21:19 +00:00
copilot-swe-agent[bot]
6ea6ca4ac2 fix: prevent overflow/OOM in Raw chunk parsing and fix global scene ID in BuildSceneGraph
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/5cf247c9-7028-4f94-9ab9-8dfd8e6101fa

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-03-31 08:47:34 +00:00
copilot-swe-agent[bot]
64c3319487 fix: guard against division by zero in GetSceneIDFromPosition
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/8e30d854-e439-4b25-bb15-891ba389f0fd

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-03-31 08:37:02 +00:00
Aaron Kimbrell
a8269497b1 Update dGame/dUtilities/SlashCommands/DEVGMCommands.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 03:35:24 -05:00
Aaron Kimbrell
aea9009281 Update dZoneManager/Zone.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 03:34:22 -05:00
Aaron Kimbrell
b50e9d9339 Update dCommon/NiColor.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:19:03 -05:00
Aaron Kimbrell
705a9ab236 Update dZoneManager/dZoneManager.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:08:59 -05:00
Aaron Kimbrell
ec8d5147de Update dGame/dUtilities/SlashCommands/DEVGMCommands.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:07:28 -05:00
Aaron Kimbrell
0c8411dffa Update dGame/dUtilities/SlashCommands/DEVGMCommands.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:06:05 -05:00
Aaron Kimbrell
87f221b791 Update dZoneManager/Raw.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:03:59 -05:00
Aaron Kimbrell
02b102d4fc Update dZoneManager/Raw.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:03:46 -05:00
Aaron Kimbrell
a1ab5958e8 Update dZoneManager/Raw.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:03:27 -05:00
Aaron Kimbrell
c9bcad349d Update dZoneManager/Raw.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:03:10 -05:00
Aaron Kimbrell
b7a1ef3d19 whoops 2025-10-17 09:27:55 -05:00
Aaron Kimbrell
0b4f888db2 Merge branch 'main' into raw-parsing-for-scene-data 2025-10-17 09:20:13 -05:00
Aaron Kimbrell
20c05cb2f2 clamp search to bounds 2025-10-17 09:17:10 -05:00
Aaron Kimbrell
c87c9c20be don't try to load the raw if the map version is too old
fixup some comments
2025-10-16 15:20:31 -05:00
Aaron Kimbrell
19637a9795 missed staging a line 2025-10-16 13:51:06 -05:00
Aaron Kimbrell
ae20974c36 better algo for finding what scene we are in 2025-10-16 01:19:05 -05:00
Aaron Kimbrell
09ca19383c Merge branch 'raw-parsing-for-scene-data' of https://github.com/DarkflameUniverse/DarkflameServer into raw-parsing-for-scene-data 2025-10-15 20:10:43 -05:00
Aaron Kimbrell
6ea46ef6b8 NL@EOF 2025-10-15 20:10:38 -05:00
Aaron Kimbrell
2d744af20a Merge branch 'main' into raw-parsing-for-scene-data 2025-10-15 20:08:21 -05:00
Aaron Kimbrell
893d127e72 idk 2025-10-15 19:59:37 -05:00
Aaron Kimbrell
accdb4f9a1 Correct scene making, merged the old raw into the new.
added option to automatically write the raw obj file
Added scene colors to the obj
use proper scene colors from hf
2025-10-15 19:56:46 -05:00
Aaron Kimbrell
12fd9d0a21 Impl raw reading, and some slash commands to test with scenes 2025-10-14 22:44:56 -05:00
26 changed files with 1755 additions and 405 deletions

34
dCommon/NiColor.h Normal file
View File

@@ -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
* <http://www.inforamp.net/~poynton/> // 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

View File

@@ -0,0 +1,166 @@
#ifndef SCENE_COLOR_H
#define SCENE_COLOR_H
#include "NiColor.h"
#include <array>
#include <cstdint>
namespace SceneColor {
// these are not random values, they are the actual template colors used by the game
static constexpr std::array<NiColor, 146> 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

View File

@@ -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{

View File

@@ -1890,4 +1890,273 @@ namespace DEVGMCommands {
}
}
}
void GetScene(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto position = entity->GetPosition();
// Get the scene ID from the zone manager
const auto sceneID = Game::zoneManager->GetSceneIDFromPosition(position);
if (sceneID == LWOSCENEID_INVALID) {
ChatPackets::SendSystemMessage(sysAddr, u"No scene found at current position.");
return;
}
// Get the scene reference from the zone to get the name
const auto* zone = Game::zoneManager->GetZone();
if (!zone) {
ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded.");
return;
}
// Build the feedback message
std::ostringstream feedback;
feedback << "Scene ID: " << sceneID.GetSceneID();
feedback << " (Layer: " << sceneID.GetLayerID() << ")";
// Get the scene name
const auto* sceneRef = zone->GetScene(sceneID);
if (sceneRef && !sceneRef->name.empty()) {
feedback << " - Name: " << sceneRef->name;
}
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
}
void GetAdjacentScenes(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto position = entity->GetPosition();
// Get the scene ID from the zone manager
const auto sceneID = Game::zoneManager->GetSceneIDFromPosition(position);
if (sceneID == LWOSCENEID_INVALID) {
ChatPackets::SendSystemMessage(sysAddr, u"No scene found at current position.");
return;
}
// Get the zone reference
const auto* zone = Game::zoneManager->GetZone();
if (!zone) {
ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded.");
return;
}
// Get current scene info
const auto* currentScene = zone->GetScene(sceneID);
std::string currentSceneName = currentScene && !currentScene->name.empty() ? currentScene->name : "Unknown";
// Get adjacent scenes
const auto adjacentSceneIDs = Game::zoneManager->GetAdjacentScenes(sceneID);
if (adjacentSceneIDs.empty()) {
std::ostringstream feedback;
feedback << "Current Scene: " << sceneID.GetSceneID() << " (" << currentSceneName << ")";
feedback << " - No adjacent scenes found.";
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
return;
}
// Build the feedback message with current scene
std::ostringstream feedback;
feedback << "Current Scene: " << sceneID.GetSceneID() << " (" << currentSceneName << ")";
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
// List all adjacent scenes
feedback.str("");
feedback << "Adjacent Scenes (" << adjacentSceneIDs.size() << "):";
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
for (const auto& adjSceneID : adjacentSceneIDs) {
feedback.str("");
feedback << " - Scene ID: " << adjSceneID.GetSceneID();
feedback << " (Layer: " << adjSceneID.GetLayerID() << ")";
// Get the scene name if available
const auto* sceneRef = zone->GetScene(adjSceneID);
if (sceneRef && !sceneRef->name.empty()) {
feedback << " - " << sceneRef->name;
}
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
}
}
void SpawnScenePoints(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
// Hardcoded to use LOT 33
const uint32_t lot = 33;
// Get player's current position and scene
const auto position = entity->GetPosition();
const auto currentSceneID = Game::zoneManager->GetSceneIDFromPosition(position);
if (currentSceneID == LWOSCENEID_INVALID) {
ChatPackets::SendSystemMessage(sysAddr, u"No scene found at current position.");
return;
}
// Get the zone
const auto* zone = Game::zoneManager->GetZone();
if (!zone) {
ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded.");
return;
}
// Get the Raw terrain data
const auto& raw = zone->GetZoneRaw();
if (raw.chunks.empty()) {
ChatPackets::SendSystemMessage(sysAddr, u"Zone does not have valid terrain data.");
return;
}
// Spawn at all sceneMap points in the current scene
uint32_t spawnedCount = 0;
for (const auto& chunk : raw.chunks) {
if (chunk.sceneMap.empty() || chunk.colorMapResolution == 0 || chunk.heightMap.empty()
|| chunk.width <= 1 || chunk.height <= 1 || chunk.scaleFactor <= 0.0f) continue;
for (uint32_t i = 0; i < chunk.width; ++i) {
for (uint32_t j = 0; j < chunk.height; ++j) {
const uint32_t heightIndex = chunk.width * i + j;
if (heightIndex >= chunk.heightMap.size()) continue;
const float y = chunk.heightMap[heightIndex];
const float sceneMapI = (static_cast<float>(i) / static_cast<float>(chunk.width - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const float sceneMapJ = (static_cast<float>(j) / static_cast<float>(chunk.height - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const uint32_t sceneI = std::min(static_cast<uint32_t>(sceneMapI), chunk.colorMapResolution - 1);
const uint32_t sceneJ = std::min(static_cast<uint32_t>(sceneMapJ), chunk.colorMapResolution - 1);
const uint32_t sceneIndex = sceneI * chunk.colorMapResolution + sceneJ;
uint8_t sceneID = 0;
if (sceneIndex < chunk.sceneMap.size()) {
sceneID = chunk.sceneMap[sceneIndex];
}
if (sceneID == currentSceneID.GetSceneID()) {
const float worldX = (static_cast<float>(i) + (chunk.offsetX / chunk.scaleFactor)) * chunk.scaleFactor;
const float worldY = y;
const float worldZ = (static_cast<float>(j) + (chunk.offsetZ / chunk.scaleFactor)) * chunk.scaleFactor;
NiPoint3 spawnPos(worldX, worldY, worldZ);
EntityInfo info;
info.lot = lot + currentSceneID.GetSceneID();
info.pos = spawnPos;
info.rot = QuatUtils::IDENTITY;
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings.Insert(u"SpawnedFromSlashCommand", true);
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
if (newEntity != nullptr) {
Game::entityManager->ConstructEntity(newEntity);
spawnedCount++;
}
}
}
}
}
if (spawnedCount == 0) {
std::ostringstream feedback;
feedback << "No spawn points found in current scene (ID: " << currentSceneID.GetSceneID() << ").";
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
return;
}
// Send feedback
const auto* sceneRef = zone->GetScene(currentSceneID);
const std::string sceneName = sceneRef ? sceneRef->name : "Unknown";
std::ostringstream feedback;
feedback << "Spawned " << spawnedCount << " points (LOT " << lot + currentSceneID.GetSceneID() << ") in scene "
<< currentSceneID.GetSceneID() << " (" << sceneName << ").";
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
}
void SpawnAllScenePoints(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
// Hardcoded to use LOT 33
const uint32_t lot = 33;
// Get the zone
const auto* zone = Game::zoneManager->GetZone();
if (!zone) {
ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded.");
return;
}
// Get the Raw terrain data
const auto& raw = zone->GetZoneRaw();
if (raw.chunks.empty()) {
ChatPackets::SendSystemMessage(sysAddr, u"Zone does not have valid terrain data.");
return;
}
// Spawn at all sceneMap points across all scenes
uint32_t spawnedCount = 0;
std::map<uint8_t, uint32_t> sceneSpawnCounts; // Track spawns per scene
for (const auto& chunk : raw.chunks) {
if (chunk.sceneMap.empty() || chunk.colorMapResolution == 0 || chunk.heightMap.empty()
|| chunk.width <= 1 || chunk.height <= 1 || chunk.scaleFactor <= 0.0f) continue;
for (uint32_t i = 0; i < chunk.width; ++i) {
for (uint32_t j = 0; j < chunk.height; ++j) {
const uint32_t heightIndex = chunk.width * i + j;
if (heightIndex >= chunk.heightMap.size()) continue;
const float y = chunk.heightMap[heightIndex];
const float sceneMapI = (static_cast<float>(i) / static_cast<float>(chunk.width - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const float sceneMapJ = (static_cast<float>(j) / static_cast<float>(chunk.height - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const uint32_t sceneI = std::min(static_cast<uint32_t>(sceneMapI), chunk.colorMapResolution - 1);
const uint32_t sceneJ = std::min(static_cast<uint32_t>(sceneMapJ), chunk.colorMapResolution - 1);
const uint32_t sceneIndex = sceneI * chunk.colorMapResolution + sceneJ;
uint8_t sceneID = 0;
if (sceneIndex < chunk.sceneMap.size()) {
sceneID = chunk.sceneMap[sceneIndex];
}
if (sceneID == 0) continue;
const float worldX = (static_cast<float>(i) + (chunk.offsetX / chunk.scaleFactor)) * chunk.scaleFactor;
const float worldY = y;
const float worldZ = (static_cast<float>(j) + (chunk.offsetZ / chunk.scaleFactor)) * chunk.scaleFactor;
NiPoint3 spawnPos(worldX, worldY, worldZ);
EntityInfo info;
info.lot = lot + sceneID;
info.pos = spawnPos;
info.rot = QuatUtils::IDENTITY;
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings.Insert(u"SpawnedFromSlashCommand", true);
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
if (newEntity != nullptr) {
Game::entityManager->ConstructEntity(newEntity);
spawnedCount++;
sceneSpawnCounts[sceneID]++;
}
}
}
}
// Send detailed feedback
std::ostringstream feedback;
feedback << "Spawned " << spawnedCount << " total points (base LOT " << lot << ") across "
<< sceneSpawnCounts.size() << " scenes:\n";
for (const auto& [sceneID, count] : sceneSpawnCounts) {
const auto* sceneRef = zone->GetScene(LWOSCENEID(sceneID));
const std::string sceneName = sceneRef ? sceneRef->name : "Unknown";
feedback << " Scene " << static_cast<int>(sceneID) << ", LOT: " << (lot + sceneID) << " (" << sceneName << "): " << count << " points\n";
}
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
}
};

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,5 @@
#include "dNavMesh.h"
#include "RawFile.h"
#include "Game.h"
#include "Logger.h"

View File

@@ -1,3 +0,0 @@
set(DNAVIGATIONS_DTERRAIN_SOURCES "RawFile.cpp"
"RawChunk.cpp"
"RawHeightMap.cpp" PARENT_SCOPE)

View File

@@ -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<uint32_t>(stream.tellg()) + (colorMapSize * colorMapSize * 4));
uint32_t lightmapSize;
BinaryIO::BinaryRead(stream, lightmapSize);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (lightmapSize));
uint32_t colorMapSize2;
BinaryIO::BinaryRead(stream, colorMapSize2);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (colorMapSize2 * colorMapSize2 * 4));
uint8_t unknown;
BinaryIO::BinaryRead(stream, unknown);
uint32_t blendmapSize;
BinaryIO::BinaryRead(stream, blendmapSize);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (blendmapSize));
uint32_t pointSize;
BinaryIO::BinaryRead(stream, pointSize);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (pointSize * 9 * 4));
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (colorMapSize * colorMapSize));
uint32_t endCounter;
BinaryIO::BinaryRead(stream, endCounter);
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (endCounter * 2));
if (endCounter != 0) {
stream.seekg(static_cast<uint32_t>(stream.tellg()) + (32));
for (int i = 0; i < 0x10; i++) {
uint16_t finalCountdown;
BinaryIO::BinaryRead(stream, finalCountdown);
stream.seekg(static_cast<uint32_t>(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;
}

View File

@@ -1,24 +0,0 @@
#pragma once
#include <cstdint>
#include <fstream>
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;
};

View File

@@ -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';
}
}

View File

@@ -1,28 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
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<RawChunk*> m_Chunks;
RawMesh* m_FinalMesh = nullptr;
};

View File

@@ -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() {
}

View File

@@ -1,21 +0,0 @@
#pragma once
#include <cstdint>
#include <vector>
#include <fstream>
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<float> m_FloatMap = {};
};

View File

@@ -1,10 +0,0 @@
#pragma once
#include <vector>
#include "NiPoint3.h"
struct RawMesh {
std::vector<NiPoint3> m_Vertices;
std::vector<uint32_t> m_Triangles;
};

View File

@@ -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

View File

@@ -107,7 +107,7 @@ void Level::ReadChunks(std::istream& file) {
uint32_t initPos = uint32_t(file.tellg());
uint32_t header = 0;
BinaryIO::BinaryRead(file, header);
if (header == CHNK_HEADER) { //Make sure we're reading a valid CHNK
if (header == CHNK_HEADER) {
Header header;
BinaryIO::BinaryRead(file, header.id);
BinaryIO::BinaryRead(file, header.chunkVersion);
@@ -118,83 +118,39 @@ void Level::ReadChunks(std::istream& file) {
uint32_t target = initPos + header.size;
file.seekg(header.startPosition);
//We're currently not loading env or particle data
if (header.id == ChunkTypeID::FileInfo) {
ReadFileInfoChunk(file, header);
} else if (header.id == ChunkTypeID::SceneEnviroment) {
ReadEnvironmentChunk(file, header);
} else if (header.id == ChunkTypeID::SceneObjectData) {
ReadSceneObjectDataChunk(file, header);
} else if (header.id == ChunkTypeID::SceneParticleData) {
ReadParticleChunk(file, header);
}
m_ChunkHeaders.insert(std::make_pair(header.id, header));
file.seekg(target);
} else {
if (initPos == std::streamoff(0)) { //Really old chunk version
if (initPos == std::streamoff(0)) {
// Old LVL format without CHNK headers — environment + objects inline
file.seekg(0);
Header header;
header.id = ChunkTypeID::FileInfo;
BinaryIO::BinaryRead(file, header.chunkVersion);
BinaryIO::BinaryRead(file, header.chunkType);
uint8_t important = 0;
BinaryIO::BinaryRead(file, important);
// file.ignore(1); //probably used
uint8_t hasEditorData = 0;
BinaryIO::BinaryRead(file, hasEditorData);
if (header.chunkVersion > 36) {
BinaryIO::BinaryRead(file, header.fileInfo.revision);
}
// HARDCODED 3
if (header.chunkVersion >= 45) file.ignore(4);
file.ignore(4 * (4 * 3));
if (header.chunkVersion >= 31) {
if (header.chunkVersion >= 39) {
file.ignore(12 * 4);
if (header.chunkVersion >= 40) {
uint32_t s = 0;
BinaryIO::BinaryRead(file, s);
for (uint32_t i = 0; i < s; ++i) {
file.ignore(4 * 3); //a uint and two floats
}
}
} else {
file.ignore(8);
}
file.ignore(3 * 4);
}
if (header.chunkVersion >= 36) {
file.ignore(3 * 4);
}
if (header.chunkVersion < 42) {
file.ignore(3 * 4);
if (header.chunkVersion >= 33) {
file.ignore(4 * 4);
}
}
// skydome info
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
file.ignore(count);
if (header.chunkVersion >= 33) {
for (uint32_t i = 0; i < 5; ++i) {
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
file.ignore(count);
}
}
// editor settings
if (!important && header.chunkVersion >= 37){
file.ignore(4);
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
file.ignore(count * 12);
// Read environment data inline (no absolute offsets in old format)
ReadLighting(file, header.chunkVersion);
ReadSkydome(file, header.chunkVersion);
if (!hasEditorData && header.chunkVersion >= 37) {
ReadEditor(file);
}
m_HasEnvironment = true;
header.id = ChunkTypeID::SceneObjectData;
header.fileInfo.version = header.chunkVersion;
@@ -213,6 +169,157 @@ void Level::ReadFileInfoChunk(std::istream& file, Header& header) {
BinaryIO::BinaryRead(file, header.fileInfo.particleChunkStart);
}
void Level::ReadLighting(std::istream& file, uint32_t version) {
auto& li = m_Environment.lighting;
if (version >= 45) BinaryIO::BinaryRead(file, li.blendTime);
for (float& f : li.ambient) BinaryIO::BinaryRead(file, f);
for (float& f : li.specular) BinaryIO::BinaryRead(file, f);
for (float& f : li.upperHemi) BinaryIO::BinaryRead(file, f);
BinaryIO::BinaryRead(file, li.position);
if (version >= 39) {
li.hasDrawDistances = true;
auto readDD = [&](LvlDrawDistances& dd) {
BinaryIO::BinaryRead(file, dd.fogNear);
BinaryIO::BinaryRead(file, dd.fogFar);
BinaryIO::BinaryRead(file, dd.postFogSolid);
BinaryIO::BinaryRead(file, dd.postFogFade);
BinaryIO::BinaryRead(file, dd.staticObjDistance);
BinaryIO::BinaryRead(file, dd.dynamicObjDistance);
};
readDD(li.minDraw);
readDD(li.maxDraw);
}
if (version >= 40) {
uint32_t numCull = 0;
BinaryIO::BinaryRead(file, numCull);
li.cullVals.reserve(numCull);
for (uint32_t i = 0; i < numCull; ++i) {
LvlCullVal cv;
BinaryIO::BinaryRead(file, cv.groupID);
BinaryIO::BinaryRead(file, cv.min);
BinaryIO::BinaryRead(file, cv.max);
li.cullVals.push_back(cv);
}
}
if (version >= 31 && version < 39) {
BinaryIO::BinaryRead(file, li.fogNear);
BinaryIO::BinaryRead(file, li.fogFar);
}
if (version >= 31) {
for (float& f : li.fogColor) BinaryIO::BinaryRead(file, f);
}
if (version >= 36) {
for (float& f : li.dirLight) BinaryIO::BinaryRead(file, f);
}
if (version < 42) {
li.hasSpawn = true;
BinaryIO::BinaryRead(file, li.startPosition);
if (version >= 33) {
BinaryIO::BinaryRead(file, li.startRotation.w);
BinaryIO::BinaryRead(file, li.startRotation.x);
BinaryIO::BinaryRead(file, li.startRotation.y);
BinaryIO::BinaryRead(file, li.startRotation.z);
}
}
}
void Level::ReadSkydome(std::istream& file, uint32_t version) {
auto& si = m_Environment.skydome;
BinaryIO::ReadString<uint32_t>(file, si.filename, BinaryIO::ReadType::String);
if (version >= 34) {
BinaryIO::ReadString<uint32_t>(file, si.skyLayerFilename, BinaryIO::ReadType::String);
for (auto& rl : si.ringLayer) {
BinaryIO::ReadString<uint32_t>(file, rl, BinaryIO::ReadType::String);
}
}
}
void Level::ReadEditor(std::istream& file) {
auto& ed = m_Environment.editor;
uint32_t blockSize = 0;
BinaryIO::BinaryRead(file, blockSize);
uint32_t numColors = 0;
BinaryIO::BinaryRead(file, numColors);
ed.savedColors.reserve(numColors);
for (uint32_t i = 0; i < numColors; ++i) {
LvlEditorColor c;
BinaryIO::BinaryRead(file, c.r);
BinaryIO::BinaryRead(file, c.g);
BinaryIO::BinaryRead(file, c.b);
ed.savedColors.push_back(c);
}
m_Environment.hasEditor = true;
}
void Level::ReadEnvironmentChunk(std::istream& file, Header& header) {
uint32_t version = 0;
// Find the version from the fib chunk if we've already read it
auto fibIt = m_ChunkHeaders.find(ChunkTypeID::FileInfo);
if (fibIt != m_ChunkHeaders.end()) {
version = fibIt->second.fileInfo.version;
}
// Environment chunk payload: 3 absolute u32 offsets
uint32_t ofsLighting, ofsSkydome, ofsEditor;
BinaryIO::BinaryRead(file, ofsLighting);
BinaryIO::BinaryRead(file, ofsSkydome);
BinaryIO::BinaryRead(file, ofsEditor);
if (ofsLighting > 0) {
file.seekg(ofsLighting);
ReadLighting(file, version);
}
if (ofsSkydome > 0) {
file.seekg(ofsSkydome);
ReadSkydome(file, version);
}
if (version >= 37 && ofsEditor > 0) {
file.seekg(ofsEditor);
ReadEditor(file);
}
m_HasEnvironment = true;
}
void Level::ReadParticleChunk(std::istream& file, Header& header) {
uint32_t version = 0;
auto fibIt = m_ChunkHeaders.find(ChunkTypeID::FileInfo);
if (fibIt != m_ChunkHeaders.end()) {
version = fibIt->second.fileInfo.version;
}
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
m_Particles.reserve(count);
for (uint32_t i = 0; i < count; ++i) {
LvlParticle p;
if (version >= 43) BinaryIO::BinaryRead(file, p.priority);
BinaryIO::BinaryRead(file, p.position);
BinaryIO::BinaryRead(file, p.rotation.w);
BinaryIO::BinaryRead(file, p.rotation.x);
BinaryIO::BinaryRead(file, p.rotation.y);
BinaryIO::BinaryRead(file, p.rotation.z);
// effect_names: u4_wstr
BinaryIO::ReadString<uint32_t>(file, p.effectNames, BinaryIO::ReadType::WideString);
// null terminator (version < 46)
if (version < 46) {
uint16_t null_term;
BinaryIO::BinaryRead(file, null_term);
}
// config: u4_wstr parsed as LDF
std::string configStr;
BinaryIO::ReadString<uint32_t>(file, configStr, BinaryIO::ReadType::WideString);
for (const auto& token : GeneralUtils::SplitString(configStr, '\n')) {
p.config.ParseInsert(token);
}
m_Particles.push_back(std::move(p));
}
}
void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
uint32_t objectsCount = 0;
BinaryIO::BinaryRead(file, objectsCount);
@@ -236,9 +343,9 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
BinaryIO::BinaryRead(file, obj.lot);
if (header.fileInfo.version >= 38) {
int32_t tmp = 1;
BinaryIO::BinaryRead(file, tmp);
if (tmp > -1 && tmp < 11) obj.nodeType = tmp;
uint32_t nodeType;
BinaryIO::BinaryRead(file, nodeType);
obj.nodeType = (nodeType <= 15) ? nodeType : 1;
}
if (header.fileInfo.version >= 32) {
@@ -249,7 +356,29 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
BinaryIO::BinaryRead(file, obj.rotation);
BinaryIO::BinaryRead(file, obj.scale);
BinaryIO::ReadString<uint32_t>(file, ldfString);
BinaryIO::BinaryRead(file, obj.value3);
if (header.fileInfo.version >= 7) {
uint32_t numRenderAttrs = 0;
BinaryIO::BinaryRead(file, numRenderAttrs);
if (numRenderAttrs > 0) {
char nameBuf[64]{};
file.read(nameBuf, 64);
obj.renderTechnique.name.assign(nameBuf, strnlen(nameBuf, 64));
obj.renderTechnique.attrs.resize(numRenderAttrs);
for (uint32_t a = 0; a < numRenderAttrs; ++a) {
auto& attr = obj.renderTechnique.attrs[a];
char attrName[64]{};
file.read(attrName, 64);
attr.name.assign(attrName, strnlen(attrName, 64));
BinaryIO::BinaryRead(file, attr.numFloats);
uint8_t isColor = 0;
BinaryIO::BinaryRead(file, isColor);
attr.isColor = isColor != 0;
for (float& f : attr.values) BinaryIO::BinaryRead(file, f);
}
}
}
//This is a little bit of a bodge, but because the alpha client (HF) doesn't store the
//spawn position / rotation like the later versions do, we need to check the LOT for the spawn pos & set it.

View File

@@ -4,6 +4,67 @@
#include <iostream>
#include "Zone.h"
struct LvlDrawDistances {
float fogNear{};
float fogFar{};
float postFogSolid{};
float postFogFade{};
float staticObjDistance{};
float dynamicObjDistance{};
};
struct LvlCullVal {
uint32_t groupID{};
float min{};
float max{};
};
struct LvlLightingInfo {
float blendTime{};
float ambient[3]{};
float specular[3]{};
float upperHemi[3]{};
NiPoint3 position;
bool hasDrawDistances{};
LvlDrawDistances minDraw;
LvlDrawDistances maxDraw;
std::vector<LvlCullVal> cullVals;
float fogNear{};
float fogFar{};
float fogColor[3]{};
float dirLight[3]{};
NiPoint3 startPosition;
NiQuaternion startRotation = QuatUtils::IDENTITY;
bool hasSpawn{};
};
struct LvlSkydomeInfo {
std::string filename;
std::string skyLayerFilename;
std::string ringLayer[4];
};
struct LvlEditorColor { float r{}, g{}, b{}; };
struct LvlEditorSettings {
std::vector<LvlEditorColor> savedColors;
};
struct LvlEnvironmentData {
LvlLightingInfo lighting;
LvlSkydomeInfo skydome;
LvlEditorSettings editor;
bool hasEditor{};
};
struct LvlParticle {
uint16_t priority{};
NiPoint3 position;
NiQuaternion rotation = QuatUtils::IDENTITY;
std::string effectNames;
LwoNameValue config;
};
class Level {
public:
enum ChunkTypeID : uint16_t {
@@ -43,11 +104,19 @@ public:
static void MakeSpawner(const SceneObject& obj);
std::map<uint32_t, Header> m_ChunkHeaders;
LvlEnvironmentData m_Environment;
bool m_HasEnvironment{};
std::vector<LvlParticle> m_Particles;
private:
Zone* m_ParentZone;
//private functions:
void ReadChunks(std::istream& file);
void ReadFileInfoChunk(std::istream& file, Header& header);
void ReadEnvironmentChunk(std::istream& file, Header& header);
void ReadLighting(std::istream& file, uint32_t version);
void ReadSkydome(std::istream& file, uint32_t version);
void ReadEditor(std::istream& file);
void ReadSceneObjectDataChunk(std::istream& file, Header& header);
void ReadParticleChunk(std::istream& file, Header& header);
};

481
dZoneManager/Raw.cpp Normal file
View File

@@ -0,0 +1,481 @@
#include "Raw.h"
#include "BinaryIO.h"
#include "Logger.h"
#include "SceneColor.h"
#include <fstream>
#include <algorithm>
#include <limits>
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<size_t>(chunk.width);
const size_t height = static_cast<size_t>(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<size_t>::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<size_t>::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<size_t>(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<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(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<char*>(chunk.lightMap.data()), static_cast<std::streamsize>(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<size_t>(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<char*>(chunk.textureMap.data()), static_cast<std::streamsize>(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<char*>(chunk.blendMap.data()), static_cast<std::streamsize>(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<size_t>(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<size_t>(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<char*>(chunk.sceneMap.data()), static_cast<std::streamsize>(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<size_t>(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<float>::max();
outRaw.minBoundsZ = std::numeric_limits<float>::max();
outRaw.maxBoundsX = std::numeric_limits<float>::lowest();
outRaw.maxBoundsZ = std::numeric_limits<float>::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<float>(i) + (chunk.offsetX / chunk.scaleFactor)) * chunk.scaleFactor;
const float worldY = y;
const float worldZ = (static_cast<float>(j) + (chunk.offsetZ / chunk.scaleFactor)) * chunk.scaleFactor;
const NiPoint3 worldPos(worldX, worldY, worldZ);
const float sceneMapI = (static_cast<float>(i) / static_cast<float>(chunk.width - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const float sceneMapJ = (static_cast<float>(j) / static_cast<float>(chunk.height - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const uint32_t sceneI = std::min(static_cast<uint32_t>(sceneMapI), chunk.colorMapResolution - 1);
const uint32_t sceneJ = std::min(static_cast<uint32_t>(sceneMapJ), chunk.colorMapResolution - 1);
const uint32_t sceneIndex = sceneI * chunk.colorMapResolution + sceneJ;
uint8_t sceneID = 0;
if (sceneIndex < chunk.sceneMap.size()) {
sceneID = chunk.sceneMap[sceneIndex];
}
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

158
dZoneManager/Raw.h Normal file
View File

@@ -0,0 +1,158 @@
#pragma once
#ifndef __RAW_H__
#define __RAW_H__
#include <cstdint>
#include <vector>
#include <string>
#include <istream>
#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<uint16_t> 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<SceneVertex> vertices;
std::vector<uint32_t> 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<uint32_t> textureIds;
// Terrain scale factor
float scaleFactor;
// Heightmap data (width * height floats)
std::vector<float> heightMap;
// Version 32+ fields
uint32_t colorMapResolution = 0;
std::vector<uint8_t> colorMap; // RGBA pixels (colorMap * colorMap * 4)
std::vector<uint8_t> lightMap;
uint32_t textureMapResolution = 0;
std::vector<uint8_t> textureMap; // (textureMapResolution * textureMapResolution * 4)
uint8_t textureSettings = 0;
std::vector<uint8_t> blendMap;
// Flair data
std::vector<FlairAttributes> flairs;
// Scene map (version 32+)
std::vector<uint8_t> sceneMap;
// Mesh data
uint32_t vertSize = 0;
std::vector<uint16_t> meshVertUsage;
std::vector<uint16_t> meshVertSize;
std::vector<MeshTri> 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<Chunk> 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__

View File

@@ -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) {
@@ -55,6 +57,11 @@ void Zone::LoadZoneIntoMemory() {
if (file) {
BinaryIO::BinaryRead(file, m_FileFormatVersion);
if (m_FileFormatVersion < Zone::FileFormatVersion::PrePreAlpha) {
LOG("Zone %s is too old to be supported, please update the map", m_ZoneFilePath.c_str());
throw std::runtime_error("Aborting Zone loading due to old Zone File.");
}
uint32_t mapRevision = 0;
if (m_FileFormatVersion >= Zone::FileFormatVersion::Alpha) BinaryIO::BinaryRead(file, mapRevision);
@@ -78,11 +85,57 @@ void Zone::LoadZoneIntoMemory() {
LoadScene(file);
}
//Read generic zone info:
BinaryIO::ReadString<uint8_t>(file, m_ZonePath, BinaryIO::ReadType::String);
// Zone boundary lines
uint8_t numBoundaries = 0;
BinaryIO::BinaryRead(file, numBoundaries);
m_Boundaries.reserve(numBoundaries);
for (uint8_t i = 0; i < numBoundaries; ++i) {
ZoneBoundary boundary;
BinaryIO::BinaryRead(file, boundary.normal);
BinaryIO::BinaryRead(file, boundary.point);
uint32_t packed;
BinaryIO::BinaryRead(file, packed);
boundary.destMapID = static_cast<uint16_t>(packed & 0xFFFF);
boundary.destInstanceID = static_cast<uint16_t>((packed >> 16) & 0xFFFF);
BinaryIO::BinaryRead(file, boundary.destSceneID);
BinaryIO::BinaryRead(file, boundary.spawnLocation);
m_Boundaries.push_back(boundary);
}
// Zone info strings
BinaryIO::ReadString<uint8_t>(file, m_ZoneRawPath, BinaryIO::ReadType::String);
BinaryIO::ReadString<uint8_t>(file, m_ZoneName, BinaryIO::ReadType::String);
BinaryIO::ReadString<uint8_t>(file, m_ZoneDesc, BinaryIO::ReadType::String);
if (m_FileFormatVersion > Zone::FileFormatVersion::PrePreAlpha) {
BinaryIO::ReadString<uint8_t>(file, m_ZoneName, BinaryIO::ReadType::String);
BinaryIO::ReadString<uint8_t>(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);
@@ -236,14 +289,15 @@ 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<uint32_t>(scene.sceneType));
BinaryIO::ReadString<uint8_t>(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) {
@@ -350,11 +404,39 @@ 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<int32_t>(sceneID), layerID);
BinaryIO::BinaryRead(file, info.position);
return info;
}
static void ReadLdfConfig(std::istream& file, PathWaypoint& waypoint, PathType pathType) {
uint32_t count;
BinaryIO::BinaryRead(file, count);
for (uint32_t i = 0; i < count; ++i) {
std::string parameter;
BinaryIO::ReadString<uint8_t>(file, parameter, BinaryIO::ReadType::WideString);
std::string value;
BinaryIO::ReadString<uint8_t>(file, value, BinaryIO::ReadType::WideString);
if (pathType == PathType::Movement || pathType == PathType::Rail) {
parameter.erase(std::remove_if(parameter.begin(), parameter.end(), ::isspace), parameter.end());
auto waypointCommand = WaypointCommandType::StringToWaypointCommandType(parameter);
if (waypointCommand == eWaypointCommandType::DELAY) value.erase(std::remove_if(value.begin(), value.end(), ::isspace), value.end());
if (waypointCommand != eWaypointCommandType::INVALID) {
auto& command = waypoint.commands.emplace_back();
command.command = waypointCommand;
command.data = value;
} else LOG("Tried to load invalid waypoint command '%s'", parameter.c_str());
} else {
waypoint.config.ParseInsert(parameter + "=" + value);
}
}
}
void Zone::LoadPath(std::istream& file) {
Path path = Path();
@@ -362,6 +444,34 @@ void Zone::LoadPath(std::istream& file) {
BinaryIO::ReadString<uint8_t>(file, path.pathName, BinaryIO::ReadType::WideString);
if (path.pathVersion <= 2) {
// Legacy format: type_name string present, no behavior field,
// unified waypoint format (pos + rot + lock + speed + wait + config).
std::string typeName;
BinaryIO::ReadString<uint8_t>(file, typeName, BinaryIO::ReadType::WideString);
BinaryIO::BinaryRead(file, path.pathType);
BinaryIO::BinaryRead(file, path.flags);
BinaryIO::BinaryRead(file, path.waypointCount);
path.pathWaypoints.reserve(path.waypointCount);
for (uint32_t i = 0; i < path.waypointCount; ++i) {
PathWaypoint waypoint = PathWaypoint();
BinaryIO::BinaryRead(file, waypoint.position);
BinaryIO::BinaryRead(file, waypoint.rotation.w);
BinaryIO::BinaryRead(file, waypoint.rotation.x);
BinaryIO::BinaryRead(file, waypoint.rotation.y);
BinaryIO::BinaryRead(file, waypoint.rotation.z);
BinaryIO::BinaryRead(file, waypoint.movingPlatform.lockPlayer);
BinaryIO::BinaryRead(file, waypoint.speed);
BinaryIO::BinaryRead(file, waypoint.movingPlatform.wait);
ReadLdfConfig(file, waypoint, path.pathType);
path.pathWaypoints.push_back(waypoint);
}
m_Paths.push_back(path);
return;
}
BinaryIO::BinaryRead(file, path.pathType);
BinaryIO::BinaryRead(file, path.flags);
BinaryIO::BinaryRead(file, path.pathBehavior);
@@ -400,7 +510,6 @@ void Zone::LoadPath(std::istream& file) {
BinaryIO::ReadString<uint8_t>(file, path.camera.nextPath, BinaryIO::ReadType::WideString);
if (path.pathVersion >= 14) {
BinaryIO::BinaryRead(file, path.camera.rotatePlayer);
}
} else if (path.pathType == PathType::Spawner) {
BinaryIO::BinaryRead(file, path.spawner.spawnedLOT);
@@ -408,7 +517,9 @@ void Zone::LoadPath(std::istream& file) {
BinaryIO::BinaryRead(file, path.spawner.maxToSpawn);
BinaryIO::BinaryRead(file, path.spawner.amountMaintained);
BinaryIO::BinaryRead(file, path.spawner.spawnerObjID);
BinaryIO::BinaryRead(file, path.spawner.spawnerNetActive);
if (path.pathVersion >= 9) {
BinaryIO::BinaryRead(file, path.spawner.spawnerNetActive);
}
}
// Read waypoints
@@ -422,7 +533,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);
@@ -456,37 +566,11 @@ void Zone::LoadPath(std::istream& file) {
// object LDF configs
if (path.pathType == PathType::Movement || path.pathType == PathType::Spawner || path.pathType == PathType::Rail) {
uint32_t count;
BinaryIO::BinaryRead(file, count);
for (uint32_t i = 0; i < count; ++i) {
std::string parameter;
BinaryIO::ReadString<uint8_t>(file, parameter, BinaryIO::ReadType::WideString);
std::string value;
BinaryIO::ReadString<uint8_t>(file, value, BinaryIO::ReadType::WideString);
if (path.pathType == PathType::Movement || path.pathType == PathType::Rail) {
// cause NetDevil puts spaces in things that don't need spaces
parameter.erase(std::remove_if(parameter.begin(), parameter.end(), ::isspace), parameter.end());
auto waypointCommand = WaypointCommandType::StringToWaypointCommandType(parameter);
if (waypointCommand == eWaypointCommandType::DELAY) value.erase(std::remove_if(value.begin(), value.end(), ::isspace), value.end());
if (waypointCommand != eWaypointCommandType::INVALID) {
auto& command = waypoint.commands.emplace_back();
command.command = waypointCommand;
command.data = value;
} else LOG("Tried to load invalid waypoint command '%s'", parameter.c_str());
} else {
waypoint.config.ParseInsert(parameter + "=" + value);
}
}
ReadLdfConfig(file, waypoint, path.pathType);
}
// We verify the waypoint heights against the navmesh because in many movement paths,
// the waypoint is located near 0 height,
if (path.pathType == PathType::Movement) {
if (dpWorld::IsLoaded()) {
// 2000 should be large enough for every world.
waypoint.position.y = dpWorld::GetNavMesh()->GetHeightAtPoint(waypoint.position, 2000.0f);
}
}
@@ -494,3 +578,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;
}

View File

@@ -6,6 +6,7 @@
#include <string>
#include <vector>
#include <map>
#include "Raw.h"
namespace LUTriggers {
struct Trigger;
@@ -21,13 +22,18 @@ 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{};
NiPoint3 scenePosition; // version 33 only: editor bounding sphere center
float sceneRadius{}; // version 33 only: editor bounding sphere radius
uint8_t color_r{};
uint8_t color_g{};
uint8_t color_b{};
@@ -35,8 +41,17 @@ struct SceneRef {
std::map<uint32_t, LUTriggers::Trigger*> triggers;
};
struct ZoneBoundary {
NiPoint3 normal;
NiPoint3 point;
uint16_t destMapID{};
uint16_t destInstanceID{};
uint32_t destSceneID{};
NiPoint3 spawnLocation;
};
struct SceneTransitionInfo {
uint64_t sceneID{}; //id of the scene being transitioned to.
LWOSCENEID sceneID;
NiPoint3 position;
};
@@ -228,6 +243,13 @@ 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<ZoneBoundary>& GetBoundaries() const { return m_Boundaries; }
const std::vector<SceneTransition>& GetSceneTransitions() const { return m_SceneTransitions; }
const std::map<LWOSCENEID, SceneRef>& GetScenes() const { return m_Scenes; }
private:
LWOZONEID m_ZoneID;
std::string m_ZoneFilePath;
@@ -244,8 +266,11 @@ 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<LWOSCENEID, SceneRef> m_Scenes;
std::vector<ZoneBoundary> m_Boundaries;
std::vector<SceneTransition> m_SceneTransitions;
uint32_t m_PathDataLength;

View File

@@ -4,6 +4,20 @@
#include "NiQuaternion.h"
#include "LDFFormat.h"
#include <vector>
#include <string>
#include <array>
struct RenderAttr {
std::string name;
uint32_t numFloats{};
bool isColor{};
std::array<float, 4> values{};
};
struct RenderTechnique {
std::string name;
std::vector<RenderAttr> attrs;
};
struct SceneObject {
LWOOBJID id;
@@ -13,7 +27,7 @@ struct SceneObject {
NiPoint3 position;
NiQuaternion rotation = QuatUtils::IDENTITY;
float scale = 1.0f;
uint32_t value3;
RenderTechnique renderTechnique;
LwoNameValue settings;
};

View File

@@ -10,9 +10,11 @@
#include "VanityUtilities.h"
#include "WorldConfig.h"
#include "CDZoneTableTable.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <set>
#include "eObjectBits.h"
#include "CDZoneTableTable.h"
#include "AssetManager.h"
#include <ranges>
@@ -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::milliseconds>(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<float>(chunk.width) &&
heightJ >= 0.0f && heightJ < static_cast<float>(chunk.height)) {
const float sceneMapI = (heightI / static_cast<float>(chunk.width - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const float sceneMapJ = (heightJ / static_cast<float>(chunk.height - 1)) * static_cast<float>(chunk.colorMapResolution - 1);
const uint32_t sceneI = std::min(static_cast<uint32_t>(sceneMapI), chunk.colorMapResolution - 1);
const uint32_t sceneJ = std::min(static_cast<uint32_t>(sceneMapJ), chunk.colorMapResolution - 1);
// 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<LWOSCENEID>());
}
// 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<LWOSCENEID> 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<LWOSCENEID> 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<LWOSCENEID> dZoneManager::GetAdjacentScenes(LWOSCENEID sceneID) const {
auto it = m_SceneAdjacencyList.find(sceneID);
if (it != m_SceneAdjacencyList.end()) {
return it->second;
}
return std::vector<LWOSCENEID>();
}

View File

@@ -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<LWOSCENEID, std::vector<LWOSCENEID>>& 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<LWOSCENEID> 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<WorldConfig> 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<LWOSCENEID, std::vector<LWOSCENEID>> m_SceneAdjacencyList;
};

View File

@@ -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_<zoneID>.obj in the server directory
export_terrain_to_obj=0
# save pre-split lxfmls to disk for debugging
save_lxfmls=0