Files
DarkflameServer/dZoneManager/Level.cpp
Aaron Kimbrell f6914940c1 feat(world): an edit mode world constructs none of the zone's objects
With -editmode 1 the world loads the zone file, terrain scene map, navmesh and
level revisions, but makes no level objects, carver blockers, spawners, spawner
networks, triggers, vanity objects or live event objects. The zone control
object stays with the zone's own template, as the client makes the same one
itself (LevelManager::LoadZoneControlObject, 0x01082660); its server script
does nothing and a race track's does not take the player into a race. Such a
world lets in only accounts at editor_gm_level.

Plumbing only: no command starts an edit mode instance yet (docs/EditMode.md).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 17:34:13 -06:00

210 lines
7.8 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 "LevelFile.h"
#include "ZoneFileLog.h"
#include "ClientVersion.h"
#include "PhysicsComponent.h"
#include "dConfig.h"
#include <ranges>
#include <stdexcept>
Level::Level(Zone* parentZone, const std::string& filepath, int32_t sceneID) {
m_ParentZone = parentZone;
m_SceneID = sceneID;
auto stream = Game::assetManager->GetFile(filepath.c_str());
if (!stream) {
LOG("Failed to load %s", filepath.c_str());
return;
}
ZoneFileLog::RecordAsset(Game::assetManager, ZoneFileLog::eKind::SCENE, filepath, stream);
// The file itself is read by LevelFile (dCommon), which the dashboard uses too
LevelFile levelFile;
try {
levelFile.Read(stream);
} catch (const std::runtime_error&) {
// The client reads past the end of such a file; what was read before it is kept
LOG("%s ends early; loading the %zu objects read before its end", filepath.c_str(), levelFile.objects.size());
}
for (const auto& [id, chunkHeader] : levelFile.chunkHeaders) {
Header header;
static_cast<LevelFile::ChunkHeader&>(header) = chunkHeader;
m_ChunkHeaders.insert(std::make_pair(id, header));
}
LoadSceneObjects(levelFile.objects, m_SceneID);
}
void Level::MakeSpawner(const SceneObject& obj, int32_t sceneID) {
SpawnerInfo spawnInfo = SpawnerInfo();
spawnInfo.scene = sceneID;
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::LoadSceneObjects(const std::vector<SceneObject>& objects, const int32_t sceneID) {
// Edit mode: the GM's client has the level's objects from its own files, so none is made here. The oldest files
// keep the spawn point in an object, which is still read.
if (Game::zoneManager->GetEditMode()) {
for (const auto& obj : objects) {
if (obj.lot != LOT_MARKER_PLAYER_START) continue;
Game::zoneManager->GetZoneMut()->SetSpawnPos(obj.position);
Game::zoneManager->GetZoneMut()->SetSpawnRot(obj.rotation);
}
return;
}
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 (const auto& obj : objects) {
//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);
}
// We should never have more than 1 zone control object, and the client never loads a scene object of LOT 1 (the
// player; ReadLvlObjectData)
bool skipLoadingObject = obj.lot == zoneControlObject->GetLOT() || obj.lot == 1;
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;
}
// The client never loads a carver_only object (and live never sent one), but it still carves the AI navmesh:
// keep it as a wall the server's movers can't walk through, without an object
if (obj.lot != 176 && PhysicsComponent::IsCarverOnly(obj.settings)) {
PhysicsComponent::AddLevelMovementBlocker(obj.id, obj.lot, obj.settings, obj.position, obj.rotation, obj.scale);
continue;
}
if (obj.lot == 176) { //Spawner
MakeSpawner(obj, sceneID);
} 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;
info.scene = sceneID;
Game::entityManager->CreateEntity(info);
}
}
}