mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 15:57:13 +00:00
The world read only a scene's objects and skipped the rest with file.ignore. Level now reads a .lvl the way SceneLoader::LoadLvlFile does (1.10.64) and keeps what it reads. Chunked files: the file info chunk gives where the environment, object and particle chunks start (the chunks were walked in order before); the environment chunk gives where the lighting, skydome and editor settings start, and the client reads all three only when there is lighting. Files from before chunks: the no-editor-settings byte only from version 32, the revision from 37, a file newer than 48 is not read, a file older than 3 has no objects; then lighting, skydome, editor settings and objects in turn. Kept, by version: - lighting: blend time (45 on, default 10), ambient, specular, upper hemisphere, position, min and max draw distances (31 on; before 39 fog near and far only), cull settings (40 on), fog colour (31 on), directional colour (36 on), and before 42 the zone's spawn point (before 33 the fixed one the client uses); - the skydome and its five other files (34 on; they were skipped from 33); - the editor's colours (37 on, the first 32); - each object's node type (38 on), glom ID (32 on) and render techniques (7 on: a 64 byte name, then per technique a 64 byte name, u32, u8 and 16 floats); before, a count of techniques was read and the techniques themselves were not, so an object with any misread the rest of the file. Object config is split at line breaks as before. A file that ends early keeps the objects read before its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
199 lines
7.3 KiB
C++
199 lines
7.3 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);
|
|
}
|
|
|
|
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) {
|
|
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, m_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 = m_SceneID;
|
|
Game::entityManager->CreateEntity(info);
|
|
}
|
|
}
|
|
}
|