mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
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>
377 lines
13 KiB
C++
377 lines
13 KiB
C++
#include "Zone.h"
|
|
#include "Level.h"
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <ranges>
|
|
#include "Game.h"
|
|
#include "Logger.h"
|
|
#include "GeneralUtils.h"
|
|
#include "BinaryIO.h"
|
|
#include "LUTriggers.h"
|
|
#include "dConfig.h"
|
|
|
|
#include "AssetManager.h"
|
|
#include "CDClientManager.h"
|
|
#include "CDSceneTableTable.h"
|
|
#include "CDZoneTableTable.h"
|
|
#include "CDClientDatabase.h"
|
|
#include "Spawner.h"
|
|
#include "dZoneManager.h"
|
|
#include "dpWorld.h"
|
|
|
|
#include "eTriggerCommandType.h"
|
|
#include "eTriggerEventType.h"
|
|
#include "eWaypointCommandType.h"
|
|
#include "dNavMesh.h"
|
|
#include "ZoneFileLog.h"
|
|
#include "Raw.h"
|
|
|
|
Zone::Zone(const LWOZONEID zoneID) :
|
|
m_ZoneID(zoneID) {
|
|
m_NumberOfObjectsLoaded = 0;
|
|
m_NumberOfSceneTransitionsLoaded = 0;
|
|
m_CheckSum = 0;
|
|
m_WorldID = 0;
|
|
m_SceneCount = 0;
|
|
}
|
|
|
|
Zone::~Zone() {
|
|
LOG("Destroying zone %i", m_ZoneID.GetMapID());
|
|
}
|
|
|
|
void Zone::Initalize() {
|
|
LoadZoneIntoMemory();
|
|
LoadLevelsIntoMemory();
|
|
m_CheckSum = CalculateChecksum();
|
|
}
|
|
|
|
void Zone::LoadZoneIntoMemory() {
|
|
m_ZoneFilePath = GetFilePathForZoneID();
|
|
m_ZonePath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1);
|
|
if (m_ZoneFilePath == "ERR") return;
|
|
|
|
auto file = Game::assetManager->GetFile(m_ZoneFilePath.c_str());
|
|
|
|
if (!file) {
|
|
LOG("Failed to load %s", m_ZoneFilePath.c_str());
|
|
throw std::runtime_error("Aborting Zone loading due to no Zone File.");
|
|
}
|
|
|
|
if (file) {
|
|
// Master watches the files a world loaded (docs/WorldHotReload.md)
|
|
ZoneFileLog::RecordAsset(Game::assetManager, ZoneFileLog::eKind::ZONE, m_ZoneFilePath, file);
|
|
// The file itself is read by ZoneFile (dCommon), which the dashboard uses too
|
|
ZoneFile zoneFile;
|
|
zoneFile.Read(file);
|
|
// The oldest files name their scenes by SceneTable ID
|
|
zoneFile.ResolveSceneTable([](uint32_t sceneTableID) -> std::optional<std::string> {
|
|
const auto* sceneTableEntry = CDSceneTableTable::Query(sceneTableID);
|
|
if (!sceneTableEntry) return std::nullopt;
|
|
return sceneTableEntry->sceneName;
|
|
});
|
|
m_FileFormatVersion = zoneFile.fileFormatVersion;
|
|
m_WorldID = zoneFile.worldID;
|
|
if (static_cast<LWOMAPID>(m_WorldID) != m_ZoneID.GetMapID()) LOG("WorldID: %i doesn't match MapID %i! Is this intended?", m_WorldID, m_ZoneID.GetMapID());
|
|
|
|
AddRevision(LWOSCENEID_INVALID, zoneFile.mapRevision);
|
|
|
|
m_Spawnpoint = zoneFile.spawnpoint;
|
|
m_SpawnpointRotation = zoneFile.spawnpointRotation;
|
|
|
|
m_SceneCount = zoneFile.scenes.size();
|
|
m_SceneGraph = ZoneScenes::SceneGraph(zoneFile.scenes, zoneFile.sceneTransitions);
|
|
for (auto& scene : zoneFile.scenes) {
|
|
LoadScene(std::move(scene));
|
|
}
|
|
|
|
m_ZoneRawPath = zoneFile.zoneRawPath;
|
|
m_ZoneName = zoneFile.zoneName;
|
|
m_ZoneDesc = zoneFile.zoneDesc;
|
|
|
|
m_SceneTransitions = std::move(zoneFile.sceneTransitions);
|
|
m_NumberOfSceneTransitionsLoaded = m_SceneTransitions.size();
|
|
|
|
m_Boundaries = std::move(zoneFile.zoneBoundaries);
|
|
|
|
m_PathDataLength = zoneFile.pathDataLength;
|
|
m_PathChunkVersion = zoneFile.pathChunkVersion;
|
|
m_Paths = std::move(zoneFile.paths);
|
|
|
|
// The terrain gives the scene at a position. Like the client, a missing or unreadable .raw is logged
|
|
// and the zone loads without it.
|
|
const auto rawPath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1) + m_ZoneRawPath;
|
|
if (m_FileFormatVersion < Zone::FileFormatVersion::PrePreAlpha) {
|
|
LOG("Zone %s uses the legacy raw terrain format (version %u), which is not supported; no scene lookups", m_ZoneFilePath.c_str(), static_cast<uint32_t>(m_FileFormatVersion));
|
|
} else if (!Game::assetManager->HasFile(rawPath)) {
|
|
LOG("Failed to find %s; no scene lookups", rawPath.c_str());
|
|
} else {
|
|
auto rawFile = Game::assetManager->GetFile(rawPath);
|
|
if (Raw::ReadRaw(rawFile, m_Raw)) {
|
|
LOG_DEBUG("Successfully parsed %s", rawPath.c_str());
|
|
} else {
|
|
LOG("Failed to parse %s; no scene lookups", rawPath.c_str());
|
|
m_Raw = Raw::Raw{};
|
|
}
|
|
}
|
|
|
|
// Optionally export terrain mesh to OBJ for debugging/visualization
|
|
if (Game::config->GetValue("export_terrain_to_obj") == "1") {
|
|
Raw::TerrainMesh terrainMesh;
|
|
Raw::GenerateTerrainMesh(m_Raw, terrainMesh);
|
|
LOG("Generated terrain mesh with %zu vertices and %zu triangles", terrainMesh.vertices.size(), terrainMesh.triangles.size() / 3);
|
|
|
|
const std::string objFileName = "terrain_" + std::to_string(m_ZoneID.GetMapID()) + ".obj";
|
|
if (Raw::WriteTerrainMeshToOBJ(terrainMesh, objFileName)) {
|
|
LOG("Exported terrain mesh to %s", objFileName.c_str());
|
|
}
|
|
}
|
|
|
|
// We verify the waypoint heights against the navmesh because in many movement paths,
|
|
// the waypoint is located near 0 height,
|
|
if (dpWorld::IsLoaded()) {
|
|
for (auto& path : m_Paths) {
|
|
if (path.pathType != PathType::Movement) continue;
|
|
for (auto& waypoint : path.pathWaypoints) {
|
|
// 2000 should be large enough for every world.
|
|
waypoint.position.y = dpWorld::GetNavMesh()->GetHeightAtPoint(waypoint.position, 2000.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Edit mode: no spawner networks (the paths themselves are kept, they are only data)
|
|
if (m_FileFormatVersion >= Zone::FileFormatVersion::EarlyAlpha && !Game::zoneManager->GetEditMode()) {
|
|
for (const Path& path : m_Paths) {
|
|
if (path.pathType != PathType::Spawner) continue;
|
|
SpawnerInfo info{};
|
|
for (size_t i = 0; i < path.pathWaypoints.size(); i++) {
|
|
const auto& waypoint = path.pathWaypoints[i];
|
|
SpawnerNode* node = new SpawnerNode();
|
|
node->position = waypoint.position;
|
|
node->rotation = waypoint.rotation;
|
|
node->nodeID = 0;
|
|
node->config = path.pathWaypoints[0].config;
|
|
// All spawner waypoints get the config data of the first waypoint, but then we
|
|
// overwrite settings on this waypoint if we have another one defined of the same name
|
|
if (i != 0) {
|
|
for (const auto& [key, value] : waypoint.config) {
|
|
node->config.ParseInsert(value->GetString());
|
|
}
|
|
}
|
|
|
|
for (const auto& data : waypoint.config | std::views::values) {
|
|
if (!data) continue;
|
|
|
|
if (data->GetKey() == u"spawner_node_id") {
|
|
node->nodeID = GeneralUtils::TryParse(data->GetValueAsString(), 0);
|
|
} else if (data->GetKey() == u"spawner_max_per_node") {
|
|
node->nodeMax = GeneralUtils::TryParse(data->GetValueAsString(), 0);
|
|
} else if (data->GetKey() == u"groupID") { // Load object group
|
|
info.groups = GeneralUtils::SplitString(data->GetValueAsString(), ';');
|
|
if (info.groups.back().empty()) info.groups.erase(info.groups.end() - 1);
|
|
} else if (data->GetKey() == u"grpNameQBShowBricks") {
|
|
info.grpNameQBShowBricks = data->GetValueAsString();
|
|
} else if (data->GetKey() == u"spawner_name") {
|
|
info.name = data->GetValueAsString();
|
|
} else if (data->GetKey() == u"weight") {
|
|
node->weight = GeneralUtils::TryParse(data->GetValueAsString(), 1);
|
|
if (node->weight <= 0) {
|
|
LOG("Found a spawner with a weight of <= 0, is this intentional? %s:%i", info.name.c_str(), node->nodeID);
|
|
node->weight = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
info.nodes.push_back(node);
|
|
}
|
|
info.templateID = path.spawner.spawnedLOT;
|
|
info.spawnerID = path.spawner.spawnerObjID;
|
|
info.respawnTime = path.spawner.respawnTime;
|
|
info.amountMaintained = path.spawner.amountMaintained;
|
|
info.maxToSpawn = path.spawner.maxToSpawn;
|
|
info.activeOnLoad = path.spawner.spawnerNetActive;
|
|
info.isNetwork = true;
|
|
Spawner* spawner = new Spawner(info);
|
|
Game::zoneManager->AddSpawner(info.spawnerID, spawner);
|
|
}
|
|
}
|
|
} else {
|
|
LOG("Failed to open: %s", m_ZoneFilePath.c_str());
|
|
}
|
|
|
|
m_ZonePath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1);
|
|
}
|
|
|
|
std::string Zone::GetFilePathForZoneID() const {
|
|
//We're gonna go ahead and presume we've got the db loaded already:
|
|
const CDZoneTable* zone = CDZoneTableTable::Query(this->GetZoneID().GetMapID());
|
|
std::string toReturn("ERR");
|
|
if (zone != nullptr) {
|
|
toReturn = "maps/" + zone->zoneName;
|
|
std::transform(toReturn.begin(), toReturn.end(), toReturn.begin(), ::tolower);
|
|
|
|
/* Normalize to one slash type */
|
|
std::ranges::replace(toReturn, '\\', '/');
|
|
}
|
|
|
|
return toReturn;
|
|
}
|
|
|
|
//Based off code from: https://www.liquisearch.com/fletchers_checksum/implementation/optimizations
|
|
uint32_t Zone::CalculateChecksum() const {
|
|
uint32_t sum1 = 0xffff;
|
|
uint32_t sum2 = 0xffff;
|
|
|
|
for (const auto& [scene, sceneRevision] : m_MapRevisions) {
|
|
uint32_t sceneID = scene.GetSceneID();
|
|
sum2 += sum1 += (sceneID >> 16);
|
|
sum2 += sum1 += (sceneID & 0xffff);
|
|
|
|
uint32_t layerID = GeneralUtils::ToUnderlying(scene.GetLayerID());
|
|
sum2 += sum1 += (layerID >> 16);
|
|
sum2 += sum1 += (layerID & 0xffff);
|
|
|
|
uint32_t revision = sceneRevision;
|
|
sum2 += sum1 += (revision >> 16);
|
|
sum2 += sum1 += (revision & 0xffff);
|
|
}
|
|
|
|
sum1 = (sum1 & 0xffff) + (sum1 >> 16);
|
|
sum2 = (sum2 & 0xffff) + (sum2 >> 16);
|
|
|
|
return sum2 << 16 | sum1;
|
|
}
|
|
|
|
void Zone::LoadLevelsIntoMemory() {
|
|
for (auto& [sceneID, scene] : m_Scenes) {
|
|
if (scene.level) continue;
|
|
scene.level = std::make_unique<Level>(this, m_ZonePath + scene.filename, sceneID.GetSceneID());
|
|
|
|
if (scene.level->m_ChunkHeaders.empty()) continue;
|
|
|
|
scene.level->m_ChunkHeaders.begin()->second.lwoSceneID = sceneID;
|
|
AddRevision(scene.level->m_ChunkHeaders.begin()->second.lwoSceneID, scene.level->m_ChunkHeaders.begin()->second.fileInfo.revision);
|
|
}
|
|
}
|
|
|
|
bool Zone::LoadSceneMap() {
|
|
if (m_ZoneRawPath.empty()) return false;
|
|
// Zones older than version 30 have terrain of an older format, whose scene map the game client never reads
|
|
// (OpenTerrain: only the editor does)
|
|
if (m_FileFormatVersion < FileFormatVersion::PrePreAlpha) return false;
|
|
auto file = Game::assetManager->GetFile((m_ZonePath + m_ZoneRawPath).c_str());
|
|
if (!file) {
|
|
LOG("Could not open the terrain file %s for its scene map", (m_ZonePath + m_ZoneRawPath).c_str());
|
|
return false;
|
|
}
|
|
ZoneFileLog::RecordAsset(Game::assetManager, ZoneFileLog::eKind::TERRAIN, m_ZonePath + m_ZoneRawPath, file);
|
|
Raw::Raw raw;
|
|
if (!Raw::ReadRaw(file, raw)) {
|
|
LOG("Could not read the terrain file %s for its scene map", (m_ZonePath + m_ZoneRawPath).c_str());
|
|
return false;
|
|
}
|
|
m_SceneMap = ZoneScenes::SceneMap(raw);
|
|
return !m_SceneMap.Empty();
|
|
}
|
|
|
|
void Zone::AddRevision(LWOSCENEID sceneID, uint32_t revision) {
|
|
if (m_MapRevisions.find(sceneID) == m_MapRevisions.end()) {
|
|
m_MapRevisions.insert(std::make_pair(sceneID, revision));
|
|
}
|
|
}
|
|
|
|
void Zone::LoadScene(ZoneScene&& zoneScene) {
|
|
SceneRef scene;
|
|
static_cast<ZoneScene&>(scene) = std::move(zoneScene);
|
|
scene.level = nullptr;
|
|
// Older files have no scene ID or layer: ZoneFile numbers their scenes, and the layer stays General
|
|
LWOSCENEID lwoSceneID(scene.id, scene.sceneType);
|
|
|
|
std::string luTriggersPath = scene.filename.substr(0, scene.filename.size() - 4) + ".lutriggers";
|
|
// Edit mode: no triggers
|
|
if (!Game::zoneManager->GetEditMode() && Game::assetManager->HasFile((m_ZonePath + luTriggersPath).c_str())) LoadLUTriggers(luTriggersPath, scene);
|
|
|
|
m_Scenes[lwoSceneID] = std::move(scene);
|
|
}
|
|
|
|
void Zone::LoadLUTriggers(std::string triggerFile, SceneRef& scene) {
|
|
auto file = Game::assetManager->GetFile((m_ZonePath + triggerFile).c_str());
|
|
if (file) ZoneFileLog::RecordAsset(Game::assetManager, ZoneFileLog::eKind::TRIGGERS, m_ZonePath + triggerFile, file);
|
|
|
|
std::stringstream data;
|
|
data << file.rdbuf();
|
|
|
|
data.seekg(0, std::ios::end);
|
|
int32_t size = data.tellg();
|
|
data.seekg(0, std::ios::beg);
|
|
|
|
if (size == 0) return;
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
|
|
if (doc.Parse(data.str().c_str(), size) != tinyxml2::XML_SUCCESS) {
|
|
LOG("Failed to load LUTriggers from file %s", triggerFile.c_str());
|
|
return;
|
|
}
|
|
|
|
auto* triggers = doc.FirstChildElement("triggers");
|
|
if (!triggers) return;
|
|
|
|
auto* currentTrigger = triggers->FirstChildElement("trigger");
|
|
while (currentTrigger) {
|
|
LUTriggers::Trigger* newTrigger = new LUTriggers::Trigger();
|
|
currentTrigger->QueryAttribute("enabled", &newTrigger->enabled);
|
|
currentTrigger->QueryAttribute("id", &newTrigger->id);
|
|
|
|
auto* currentEvent = currentTrigger->FirstChildElement("event");
|
|
while (currentEvent) {
|
|
LUTriggers::Event* newEvent = new LUTriggers::Event();
|
|
newEvent->id = TriggerEventType::StringToTriggerEventType(currentEvent->Attribute("id"));
|
|
auto* currentCommand = currentEvent->FirstChildElement("command");
|
|
while (currentCommand) {
|
|
LUTriggers::Command* newCommand = new LUTriggers::Command();
|
|
newCommand->id = TriggerCommandType::StringToTriggerCommandType(currentCommand->Attribute("id"));
|
|
newCommand->target = currentCommand->Attribute("target");
|
|
if (currentCommand->Attribute("targetName")) {
|
|
newCommand->targetName = currentCommand->Attribute("targetName");
|
|
}
|
|
if (currentCommand->Attribute("args")) {
|
|
newCommand->args = currentCommand->Attribute("args");
|
|
}
|
|
|
|
newEvent->commands.push_back(newCommand);
|
|
currentCommand = currentCommand->NextSiblingElement("command");
|
|
}
|
|
newTrigger->events.push_back(newEvent);
|
|
currentEvent = currentEvent->NextSiblingElement("event");
|
|
}
|
|
currentTrigger = currentTrigger->NextSiblingElement("trigger");
|
|
scene.triggers.insert(std::make_pair(newTrigger->id, newTrigger));
|
|
}
|
|
}
|
|
|
|
LUTriggers::Trigger* Zone::GetTrigger(uint32_t sceneID, uint32_t triggerID) const {
|
|
auto scene = m_Scenes.find(sceneID);
|
|
if (scene == m_Scenes.end()) return nullptr;
|
|
|
|
auto trigger = scene->second.triggers.find(triggerID);
|
|
if (trigger == scene->second.triggers.end()) return nullptr;
|
|
|
|
return trigger->second;
|
|
}
|
|
|
|
const Path* Zone::GetPath(std::string name) const {
|
|
for (const auto& path : m_Paths) {
|
|
if (name == path.pathName) {
|
|
return &path;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const SceneRef* Zone::GetScene(LWOSCENEID sceneID) const {
|
|
auto it = m_Scenes.find(sceneID);
|
|
if (it != m_Scenes.end()) return &it->second;
|
|
return nullptr;
|
|
}
|