mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-22 14:44:22 +00:00
- 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>
441 lines
14 KiB
C++
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);
|
|
}
|
|
}
|
|
}
|