#include "Game.h" #include "Level.h" #include #include #include #include #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 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(file, si.filename, BinaryIO::ReadType::String); if (version >= 34) { BinaryIO::ReadString(file, si.skyLayerFilename, BinaryIO::ReadType::String); for (auto& rl : si.ringLayer) { BinaryIO::ReadString(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(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(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(); CDFeatureGating gating; gating.major = GeneralUtils::TryParse(Game::config->GetValue("version_major")).value_or(ClientVersion::major); gating.current = GeneralUtils::TryParse(Game::config->GetValue("version_current")).value_or(ClientVersion::current); gating.minor = GeneralUtils::TryParse(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(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); } } }