Files
DarkflameServer/dZoneManager/Level.cpp
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

441 lines
14 KiB
C++

#include "Game.h"
#include "Level.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include "BinaryIO.h"
#include "Logger.h"
#include "Spawner.h"
#include "dZoneManager.h"
#include "GeneralUtils.h"
#include "Entity.h"
#include "EntityManager.h"
#include "CDFeatureGatingTable.h"
#include "CDClientManager.h"
#include "AssetManager.h"
#include "ClientVersion.h"
#include "dConfig.h"
#include <ranges>
Level::Level(Zone* parentZone, const std::string& filepath) {
m_ParentZone = parentZone;
auto stream = Game::assetManager->GetFile(filepath.c_str());
if (!stream) {
LOG("Failed to load %s", filepath.c_str());
return;
}
ReadChunks(stream);
}
void Level::MakeSpawner(const SceneObject& obj) {
SpawnerInfo spawnInfo = SpawnerInfo();
SpawnerNode* node = new SpawnerNode();
spawnInfo.templateID = obj.lot;
spawnInfo.spawnerID = obj.id;
spawnInfo.templateScale = obj.scale;
node->position = obj.position;
node->rotation = obj.rotation;
node->config = obj.settings;
spawnInfo.nodes.push_back(node);
for (const auto& data : obj.settings.values | std::views::values) {
if (!data) continue;
if (data->GetKey() == u"spawntemplate") {
spawnInfo.templateID = GeneralUtils::TryParse(data->GetValueAsString(), 0);
}
if (data->GetKey() == u"spawner_node_id") {
node->nodeID = GeneralUtils::TryParse(data->GetValueAsString(), 0u);
}
if (data->GetKey() == u"spawner_name") {
spawnInfo.name = data->GetValueAsString();
}
if (data->GetKey() == u"max_to_spawn") {
spawnInfo.maxToSpawn = GeneralUtils::TryParse(data->GetValueAsString(), 0);
}
if (data->GetKey() == u"spawner_active_on_load") {
spawnInfo.activeOnLoad = GeneralUtils::TryParse(data->GetValueAsString(), false);
}
if (data->GetKey() == u"active_on_load") {
spawnInfo.activeOnLoad = GeneralUtils::TryParse(data->GetValueAsString(), false);
}
if (data->GetKey() == u"respawn") {
if (data->GetValueType() == eLDFType::LDF_TYPE_FLOAT) // Floats are in seconds
{
spawnInfo.respawnTime = GeneralUtils::TryParse(data->GetValueAsString(), 0.0f);
} else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms
{
spawnInfo.respawnTime = GeneralUtils::TryParse(data->GetValueAsString(), 0) / 1000;
}
}
if (data->GetKey() == u"spawnsGroupOnSmash") {
spawnInfo.spawnsOnSmash = GeneralUtils::TryParse(data->GetValueAsString(), false);
}
if (data->GetKey() == u"spawnNetNameForSpawnGroupOnSmash") {
spawnInfo.spawnOnSmashGroupName = data->GetValueAsString();
}
if (data->GetKey() == u"groupID") { // Load object groups
spawnInfo.groups = GeneralUtils::SplitString(data->GetValueAsString(), ';');
if (spawnInfo.groups.back().empty()) spawnInfo.groups.erase(spawnInfo.groups.end() - 1);
}
if (data->GetKey() == u"no_auto_spawn") {
spawnInfo.noAutoSpawn = GeneralUtils::TryParse(data->GetValueAsString(), false);
}
if (data->GetKey() == u"no_timed_spawn") {
spawnInfo.noTimedSpawn = GeneralUtils::TryParse(data->GetValueAsString(), false);
}
if (data->GetKey() == u"spawnActivator") {
spawnInfo.spawnActivator = GeneralUtils::TryParse(data->GetValueAsString(), false);
}
}
Game::zoneManager->MakeSpawner(spawnInfo);
}
void Level::ReadChunks(std::istream& file) {
const uint32_t CHNK_HEADER = ('C' + ('H' << 8) + ('N' << 16) + ('K' << 24));
while (!file.eof()) {
uint32_t initPos = uint32_t(file.tellg());
uint32_t header = 0;
BinaryIO::BinaryRead(file, header);
if (header == CHNK_HEADER) {
Header header;
BinaryIO::BinaryRead(file, header.id);
BinaryIO::BinaryRead(file, header.chunkVersion);
BinaryIO::BinaryRead(file, header.chunkType);
BinaryIO::BinaryRead(file, header.size);
BinaryIO::BinaryRead(file, header.startPosition);
uint32_t target = initPos + header.size;
file.seekg(header.startPosition);
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)) {
// 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 hasEditorData = 0;
BinaryIO::BinaryRead(file, hasEditorData);
if (header.chunkVersion > 36) {
BinaryIO::BinaryRead(file, header.fileInfo.revision);
}
// 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;
ReadSceneObjectDataChunk(file, header);
m_ChunkHeaders.insert(std::make_pair(header.id, header));
} break;
}
}
}
void Level::ReadFileInfoChunk(std::istream& file, Header& header) {
BinaryIO::BinaryRead(file, header.fileInfo.version);
BinaryIO::BinaryRead(file, header.fileInfo.revision);
BinaryIO::BinaryRead(file, header.fileInfo.enviromentChunkStart);
BinaryIO::BinaryRead(file, header.fileInfo.objectChunkStart);
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);
CDFeatureGatingTable* featureGatingTable = CDClientManager::GetTable<CDFeatureGatingTable>();
CDFeatureGating gating;
gating.major =
GeneralUtils::TryParse<int32_t>(Game::config->GetValue("version_major")).value_or(ClientVersion::major);
gating.current =
GeneralUtils::TryParse<int32_t>(Game::config->GetValue("version_current")).value_or(ClientVersion::current);
gating.minor =
GeneralUtils::TryParse<int32_t>(Game::config->GetValue("version_minor")).value_or(ClientVersion::minor);
const auto zoneControlObject = Game::zoneManager->GetZoneControlObject();
DluAssert(zoneControlObject != nullptr);
for (uint32_t i = 0; i < objectsCount; ++i) {
std::u16string ldfString;
SceneObject obj;
BinaryIO::BinaryRead(file, obj.id);
BinaryIO::BinaryRead(file, obj.lot);
if (header.fileInfo.version >= 38) {
uint32_t nodeType;
BinaryIO::BinaryRead(file, nodeType);
obj.nodeType = (nodeType <= 15) ? nodeType : 1;
}
if (header.fileInfo.version >= 32) {
BinaryIO::BinaryRead(file, obj.glomId);
}
BinaryIO::BinaryRead(file, obj.position);
BinaryIO::BinaryRead(file, obj.rotation);
BinaryIO::BinaryRead(file, obj.scale);
BinaryIO::ReadString<uint32_t>(file, ldfString);
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.
if (obj.lot == LOT_MARKER_PLAYER_START) {
Game::zoneManager->GetZoneMut()->SetSpawnPos(obj.position);
Game::zoneManager->GetZoneMut()->SetSpawnRot(obj.rotation);
}
for (const auto& token : GeneralUtils::SplitString(GeneralUtils::UTF16ToWTF8(ldfString), '\n')) {
obj.settings.ParseInsert(token);
}
// We should never have more than 1 zone control object
bool skipLoadingObject = obj.lot == zoneControlObject->GetLOT();
for (const auto& data : obj.settings | std::views::values) {
if (!data) continue;
if (data->GetKey() == u"gatingOnFeature") {
gating.featureName = data->GetValueAsString();
if (gating.featureName == Game::config->GetValue("event_1")) continue;
else if (gating.featureName == Game::config->GetValue("event_2")) continue;
else if (gating.featureName == Game::config->GetValue("event_3")) continue;
else if (gating.featureName == Game::config->GetValue("event_4")) continue;
else if (gating.featureName == Game::config->GetValue("event_5")) continue;
else if (gating.featureName == Game::config->GetValue("event_6")) continue;
else if (gating.featureName == Game::config->GetValue("event_7")) continue;
else if (gating.featureName == Game::config->GetValue("event_8")) continue;
else if (!featureGatingTable->FeatureUnlocked(gating)) {
// The feature is not unlocked, so we can skip loading this object
skipLoadingObject = true;
break;
}
}
// If this is a client only object, we can skip loading it
if (data->GetKey() == u"loadOnClientOnly") {
skipLoadingObject |= GeneralUtils::TryParse(data->GetValueAsString(), false);
break;
}
}
if (skipLoadingObject) {
continue;
}
if (obj.lot == 176) { //Spawner
MakeSpawner(obj);
} else { //Regular object
EntityInfo info;
info.spawnerID = 0;
info.id = obj.id;
info.lot = obj.lot;
info.pos = obj.position;
info.rot = obj.rotation;
info.settings = obj.settings;
info.scale = obj.scale;
Game::entityManager->CreateEntity(info);
}
}
}