From cdc9dda3c49d3aef859ce2d044b8aca81b0e546c Mon Sep 17 00:00:00 2001 From: wincent Date: Sun, 29 Oct 2023 17:37:26 +0100 Subject: [PATCH] dCinema improvements * Visiblity and effect records * Recorder will catch effects from behaviors * Documentation for setting up a scene to play automatically. * Documentation for server-side preconditions. --- dCommon/dConfig.cpp | 8 +- dGame/dBehaviors/AttackDelayBehavior.cpp | 4 + dGame/dBehaviors/PlayEffectBehavior.cpp | 8 +- dGame/dCinema/Play.cpp | 13 ++- dGame/dCinema/Play.h | 2 + dGame/dCinema/README.md | 91 ++++++++++++++++- dGame/dCinema/Recorder.cpp | 124 ++++++++++++++++++++++- dGame/dCinema/Recorder.h | 43 +++++++- dGame/dCinema/Scene.cpp | 94 +++++++++++++++-- dGame/dCinema/Scene.h | 18 +++- dGame/dComponents/RenderComponent.cpp | 12 +++ dGame/dComponents/RenderComponent.h | 5 + dGame/dUtilities/SlashCommandHandler.cpp | 8 ++ dWorldServer/WorldServer.cpp | 13 ++- 14 files changed, 418 insertions(+), 25 deletions(-) diff --git a/dCommon/dConfig.cpp b/dCommon/dConfig.cpp index f09a44c1..195d5a5f 100644 --- a/dCommon/dConfig.cpp +++ b/dCommon/dConfig.cpp @@ -34,7 +34,13 @@ void dConfig::ReloadConfig() { } const std::string& dConfig::GetValue(std::string key) { - return this->m_ConfigValues[key]; + static std::string emptyString{}; + + const auto& it = this->m_ConfigValues.find(key); + + if (it == this->m_ConfigValues.end()) return emptyString; + + return it->second; } void dConfig::ProcessLine(const std::string& line) { diff --git a/dGame/dBehaviors/AttackDelayBehavior.cpp b/dGame/dBehaviors/AttackDelayBehavior.cpp index 1bf1048a..6c3d877e 100644 --- a/dGame/dBehaviors/AttackDelayBehavior.cpp +++ b/dGame/dBehaviors/AttackDelayBehavior.cpp @@ -4,6 +4,8 @@ #include "Game.h" #include "Logger.h" +#include "Recorder.h" + void AttackDelayBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStream, const BehaviorBranchContext branch) { uint32_t handle{}; @@ -11,6 +13,8 @@ void AttackDelayBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bi LOG("Unable to read handle from bitStream, aborting Handle! %i", bitStream->GetNumberOfUnreadBits()); return; }; + + Cinema::Recording::Recorder::RegisterEffectForActor(context->originator, this->m_effectId); for (auto i = 0u; i < this->m_numIntervals; ++i) { context->RegisterSyncBehavior(handle, this, branch, this->m_delay * i, m_ignoreInterrupts); diff --git a/dGame/dBehaviors/PlayEffectBehavior.cpp b/dGame/dBehaviors/PlayEffectBehavior.cpp index acd606a9..1f05b0ff 100644 --- a/dGame/dBehaviors/PlayEffectBehavior.cpp +++ b/dGame/dBehaviors/PlayEffectBehavior.cpp @@ -3,13 +3,17 @@ #include "BehaviorContext.h" #include "BehaviorBranchContext.h" +#include "Recorder.h" + void PlayEffectBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) { + const auto& target = branch.target == LWOOBJID_EMPTY ? context->originator : branch.target; + + Cinema::Recording::Recorder::RegisterEffectForActor(target, this->m_effectId); + // On managed behaviors this is handled by the client if (!context->unmanaged) return; - const auto& target = branch.target == LWOOBJID_EMPTY ? context->originator : branch.target; - PlayFx(u"", target); } diff --git a/dGame/dCinema/Play.cpp b/dGame/dCinema/Play.cpp index a71a4cd5..ec539bb0 100644 --- a/dGame/dCinema/Play.cpp +++ b/dGame/dCinema/Play.cpp @@ -36,9 +36,17 @@ void Cinema::Play::CheckForAudience() { return; } + + if (scene->IsPlayerInBounds(player)) { + SignalBarrier("audience"); + + m_PlayerHasBeenInsideBounds = true; + } - if (!scene->IsPlayerInBounds(player)) { - Conclude(); + if (!scene->IsPlayerInShowingDistance(player)) { + if (m_PlayerHasBeenInsideBounds) { + Conclude(); + } CleanUp(); @@ -70,7 +78,6 @@ void Cinema::Play::SetupBarrier(const std::string& barrier, std::function>> m_Barriers; }; diff --git a/dGame/dCinema/README.md b/dGame/dCinema/README.md index 7a3b8f5e..ff97d08e 100644 --- a/dGame/dCinema/README.md +++ b/dGame/dCinema/README.md @@ -22,6 +22,8 @@ A play is created is a couple of steps: 2. Create prefabs of props in the scene. 3. Put the NPCs and props in a scene file. 4. Edit the performed acts to add synchronization, conditions and additional actions. +5. Setting up the scene to be performed automatically. +6. Hiding zone objects while performing. ### 1. Acts out the scene See media/acting.mp4 for an example of how to act out a scene. @@ -260,4 +262,91 @@ This record sends a signal to all acts waiting for it. This record concludes the play. ```xml -``` \ No newline at end of file +``` + +#### 4.15. VisiblityRecord +This record makes the actor visible or invisible. +```xml + +``` + +#### 4.16. PlayEffectRecord +This record plays an effect. +```xml + +``` + +### 5. Setting up the scene to be performed automatically +Scenes can be appended with metadata to describe when they should be performed and what consequences they have. This is done by editing the scene file. + +#### 5.1. Scene metadata +There attributes can be added to the `Scene` tag: + +| Attribute | Description | +| --- | --- | +| `x y z` | The center of where the following two attributes
are measured. | +| `showingDistance` | The distance at which the scene will
be loaded for a player.

If the player exits this area the scene is unloaded.
If the scene has been registered as having been
viewed by the player, is is concluded. | +| `performingDistance` | The scene is registred as having been
viewed by the player. This doesn't mean
it can't be viewed again.

A signal named `"audiance"` will be
sent when the player enters this area.
This can be used to trigger the main
part of the scene. | +| `acceptMission` | The mission with the given id will be
accepted when the scene is concluded. | +| `completeMission` | The mission with the given id will be
completed when the scene is concluded. | + +Here is an example of a scene with metadata: +```xml + + ... + +``` + +#### 5.2. Automatic scene setup +In either the worldconfig.ini or sharedconfig.ini file, add the following: +``` +# Path to where scenes are located. +scenes_directory=vanity/scenes/ +``` + +Now move the scene into a subdirectory of the scenes directory. The name of the subdirectory should be **the zone id** of the zone the scene is located in. + +For example: +``` +build/ +├── vanity/ +│ ├── scenes/ +│ │ ├── 1900/ +│ │ │ ├── my-scene.xml +``` + +Now the scene will be setup automatically and loaded when the player enters the `showingDistance` of the scene. + +#### 5.3. Adding conditions +Conditions can be added to the scene to make it only performable when the player fulfills specified preconditions. This is done by editing the scene file. + +Here is an example of a scene with conditions: +```xml + + + + ... + +``` + +### 6. Hiding zone objects while performing +When a scene should be performed, you might want to hide some objects in the zone. This is done by adding server preconditions. This is a seperate file. + +In either the worldconfig.ini or sharedconfig.ini file, add the following: +``` +# Path to where server preconditions are located. +server_preconditions_directory=vanity/server-preconditions.xml +``` + +Now create the server preconditions file in the directory specified. + +Here is an example of a server preconditions file: +```xml + + + 1006 + + +``` + +This will hide the objects with lot 12261 for players who fulfill precondition 1006. diff --git a/dGame/dCinema/Recorder.cpp b/dGame/dCinema/Recorder.cpp index 9a8f6036..a467a7bb 100644 --- a/dGame/dCinema/Recorder.cpp +++ b/dGame/dCinema/Recorder.cpp @@ -13,6 +13,8 @@ using namespace Cinema::Recording; std::unordered_map m_Recorders = {}; +std::unordered_map m_EffectAnimations = {}; + Recorder::Recorder() { this->m_StartTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()); this->m_IsRecording = false; @@ -169,7 +171,7 @@ void Recorder::ActingDispatch(Entity* actor, size_t index, Play* variables) { ActingDispatch(actor, index + 1, variables); }); - if (barrierRecord->timeout <= 0.0f) { + if (barrierRecord->timeout <= 0.0001f) { return; } @@ -211,6 +213,17 @@ void Recorder::ActingDispatch(Entity* actor, size_t index, Play* variables) { } } + // Check if the record is a visibility record + auto* visibilityRecord = dynamic_cast(record); + + if (visibilityRecord) { + if (visibilityRecord->visible) { + ServerPreconditions::AddExcludeFor(actor->GetObjectID(), variables->player); + } else { + ServerPreconditions::RemoveExcludeFor(actor->GetObjectID(), variables->player); + } + } + // Check if the record is a player proximity record auto* playerProximityRecord = dynamic_cast(record); @@ -359,6 +372,45 @@ Recorder* Recorder::GetRecorder(LWOOBJID actorID) { return iter->second; } +void Cinema::Recording::Recorder::RegisterEffectForActor(LWOOBJID actorID, const int32_t& effectId) { + auto iter = m_Recorders.find(actorID); + if (iter == m_Recorders.end()) { + return; + } + + auto& recorder = iter->second; + + const auto& effectIter = m_EffectAnimations.find(effectId); + + if (effectIter == m_EffectAnimations.end()) { + auto statement = CDClientDatabase::CreatePreppedStmt("SELECT animationName FROM BehaviorEffect WHERE effectID = ? LIMIT 1;"); + + statement.bind(1, effectId); + + auto result = statement.execQuery(); + + if (result.eof()) { + result.finalize(); + + m_EffectAnimations.emplace(effectId, ""); + } + else { + const auto animationName = result.getStringField(0); + + m_EffectAnimations.emplace(effectId, animationName); + + result.finalize(); + + recorder->AddRecord(new AnimationRecord(animationName)); + } + } + else { + recorder->AddRecord(new AnimationRecord(effectIter->second)); + } + + recorder->AddRecord(new PlayEffectRecord(std::to_string(effectId))); +} + MovementRecord::MovementRecord(const NiPoint3& position, const NiQuaternion& rotation, const NiPoint3& velocity, const NiPoint3& angularVelocity, bool onGround, bool dirtyVelocity, bool dirtyAngularVelocity) { this->position = position; this->rotation = rotation; @@ -691,6 +743,14 @@ Recorder* Recorder::LoadFromFile(const std::string& filename) { PlayerProximityRecord* record = new PlayerProximityRecord(); record->Deserialize(element); recorder->m_Records.push_back(record); + } else if (name == "VisibilityRecord") { + VisibilityRecord* record = new VisibilityRecord(); + record->Deserialize(element); + recorder->m_Records.push_back(record); + } else if (name == "PlayEffectRecord") { + PlayEffectRecord* record = new PlayEffectRecord(); + record->Deserialize(element); + recorder->m_Records.push_back(record); } if (element->Attribute("name")) { @@ -943,4 +1003,64 @@ void Cinema::Recording::ConcludeRecord::Serialize(tinyxml2::XMLDocument& documen void Cinema::Recording::ConcludeRecord::Deserialize(tinyxml2::XMLElement* element) { m_Delay = element->DoubleAttribute("t"); -} \ No newline at end of file +} + +Cinema::Recording::VisibilityRecord::VisibilityRecord(bool visible) { + this->visible = visible; +} + +void Cinema::Recording::VisibilityRecord::Act(Entity* actor) { +} + +void Cinema::Recording::VisibilityRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) { + auto* element = document.NewElement("VisibilityRecord"); + + element->SetAttribute("visible", visible); + + element->SetAttribute("t", m_Delay); + + parent->InsertEndChild(element); +} + +void Cinema::Recording::VisibilityRecord::Deserialize(tinyxml2::XMLElement* element) { + visible = element->BoolAttribute("visible"); + + m_Delay = element->DoubleAttribute("t"); +} + +Cinema::Recording::PlayEffectRecord::PlayEffectRecord(const std::string& effect) { + this->effect = effect; +} + +void Cinema::Recording::PlayEffectRecord::Act(Entity* actor) { + int32_t effectID = 0; + + if (!GeneralUtils::TryParse(effect, effectID)) { + return; + } + + GameMessages::SendPlayFXEffect( + actor->GetObjectID(), + effectID, + u"cast", + std::to_string(ObjectIDManager::GenerateRandomObjectID()) + ); + + Game::entityManager->SerializeEntity(actor); +} + +void Cinema::Recording::PlayEffectRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) { + auto* element = document.NewElement("PlayEffectRecord"); + + element->SetAttribute("effect", effect.c_str()); + + element->SetAttribute("t", m_Delay); + + parent->InsertEndChild(element); +} + +void Cinema::Recording::PlayEffectRecord::Deserialize(tinyxml2::XMLElement* element) { + effect = element->Attribute("effect"); + + m_Delay = element->DoubleAttribute("t"); +} diff --git a/dGame/dCinema/Recorder.h b/dGame/dCinema/Recorder.h index ca628165..e113a430 100644 --- a/dGame/dCinema/Recorder.h +++ b/dGame/dCinema/Recorder.h @@ -89,7 +89,7 @@ public: class EquipRecord : public Record { public: - LOT item; + LOT item = LOT_NULL; EquipRecord() = default; @@ -105,7 +105,7 @@ public: class UnequipRecord : public Record { public: - LOT item; + LOT item = LOT_NULL; UnequipRecord() = default; @@ -204,7 +204,7 @@ class BarrierRecord : public Record public: std::string signal; - float timeout; + float timeout = 0.0f; std::string timeoutLabel; @@ -250,9 +250,9 @@ public: class PlayerProximityRecord : public Record { public: - float distance; + float distance = 0.0f; - float timeout; + float timeout = 0.0f; std::string timeoutLabel; @@ -267,6 +267,37 @@ public: void Deserialize(tinyxml2::XMLElement* element) override; }; +class VisibilityRecord : public Record +{ +public: + bool visible = false; + + VisibilityRecord() = default; + + VisibilityRecord(bool visible); + + void Act(Entity* actor) override; + + void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override; + + void Deserialize(tinyxml2::XMLElement* element) override; +}; + +class PlayEffectRecord : public Record +{ +public: + std::string effect; + + PlayEffectRecord() = default; + + PlayEffectRecord(const std::string& effect); + + void Act(Entity* actor) override; + + void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override; + + void Deserialize(tinyxml2::XMLElement* element) override; +}; class Recorder { @@ -299,6 +330,8 @@ public: static Recorder* GetRecorder(LWOOBJID actorID); + static void RegisterEffectForActor(LWOOBJID actorID, const int32_t& effectId); + private: void ActingDispatch(Entity* actor, size_t index, Play* variables); diff --git a/dGame/dCinema/Scene.cpp b/dGame/dCinema/Scene.cpp index a146530a..37d1c5fd 100644 --- a/dGame/dCinema/Scene.cpp +++ b/dGame/dCinema/Scene.cpp @@ -1,11 +1,14 @@ #include "Scene.h" +#include + #include #include "ServerPreconditions.hpp" #include "EntityManager.h" #include "EntityInfo.h" #include "MissionComponent.h" +#include "dConfig.h" using namespace Cinema; @@ -27,7 +30,7 @@ void Cinema::Scene::Rehearse() { CheckForShowings(); } -void Cinema::Scene::Conclude(Entity* player) const { +void Cinema::Scene::Conclude(Entity* player) { if (player == nullptr) { return; } @@ -49,6 +52,10 @@ void Cinema::Scene::Conclude(Entity* player) const { if (m_AcceptMission != 0) { missionComponent->AcceptMission(m_AcceptMission); } + + // Remove the player from the audience + m_Audience.erase(player->GetObjectID()); + m_HasBeenOutside.erase(player->GetObjectID()); } bool Cinema::Scene::IsPlayerInBounds(Entity* player) const { @@ -64,14 +71,60 @@ bool Cinema::Scene::IsPlayerInBounds(Entity* player) const { auto distance = NiPoint3::Distance(position, m_Center); - LOG("Player distance from scene: %f, with bounds %f", distance, m_Bounds); + return distance <= m_Bounds; +} - // The player may be within 20% of the bounds - return distance <= (m_Bounds * 1.2f); +bool Cinema::Scene::IsPlayerInShowingDistance(Entity* player) const { + if (player == nullptr) { + return false; + } + + if (m_ShowingDistance == 0.0f) { + return true; + } + + const auto& position = player->GetPosition(); + + auto distance = NiPoint3::Distance(position, m_Center); + + return distance <= m_ShowingDistance; +} + +void Cinema::Scene::AutoLoadScenesForZone(LWOMAPID zone) { + const auto& scenesRoot = Game::config->GetValue("scenes_directory"); + + if (scenesRoot.empty()) { + return; + } + + const auto path = std::filesystem::path(scenesRoot) / std::to_string(zone); + + if (!std::filesystem::exists(path)) { + return; + } + + // Recursively iterate through the directory + for (const auto& entry : std::filesystem::recursive_directory_iterator(path)) { + if (!entry.is_regular_file()) { + continue; + } + + // Check that extension is .xml + if (entry.path().extension() != ".xml") { + continue; + } + + const auto& file = entry.path().string(); + + auto& scene = LoadFromFile(file); + + scene.Rehearse(); + } } void Cinema::Scene::CheckForShowings() { auto audience = m_Audience; + auto hasBeenOutside = m_HasBeenOutside; for (const auto& member : audience) { if (Game::entityManager->GetEntity(member) == nullptr) { @@ -79,6 +132,15 @@ void Cinema::Scene::CheckForShowings() { } } + for (const auto& member : hasBeenOutside) { + if (Game::entityManager->GetEntity(member) == nullptr) { + m_HasBeenOutside.erase(member); + } + } + + m_Audience = audience; + m_HasBeenOutside = hasBeenOutside; + // I don't care Game::entityManager->GetZoneControlEntity()->AddCallbackTimer(1.0f, [this]() { for (auto* player : Player::GetAllPlayers()) { @@ -87,9 +149,9 @@ void Cinema::Scene::CheckForShowings() { } CheckTicket(player); - - } + + CheckForShowings(); }); } @@ -104,6 +166,16 @@ void Cinema::Scene::CheckTicket(Entity* player) { } } + if (!IsPlayerInShowingDistance(player)) { + m_HasBeenOutside.emplace(player->GetObjectID()); + + return; + } + + if (m_HasBeenOutside.find(player->GetObjectID()) == m_HasBeenOutside.end()) { + return; + } + m_Audience.emplace(player->GetObjectID()); Act(player); @@ -225,8 +297,14 @@ Scene& Cinema::Scene::LoadFromFile(std::string file) { scene.m_Center = NiPoint3(root->FloatAttribute("x"), root->FloatAttribute("y"), root->FloatAttribute("z")); } - if (root->Attribute("bounds")) { - scene.m_Bounds = root->FloatAttribute("bounds"); + if (root->Attribute("performingDistance")) { + scene.m_Bounds = root->FloatAttribute("performingDistance"); + } + + if (root->Attribute("showingDistance")) { + scene.m_ShowingDistance = root->FloatAttribute("showingDistance"); + } else { + scene.m_ShowingDistance = scene.m_Bounds * 2.0f; } // Load accept and complete mission diff --git a/dGame/dCinema/Scene.h b/dGame/dCinema/Scene.h index a2903e68..b32b3124 100644 --- a/dGame/dCinema/Scene.h +++ b/dGame/dCinema/Scene.h @@ -50,7 +50,7 @@ public: * * @param player The player to conclude the scene for (not nullptr). */ - void Conclude(Entity* player) const; + void Conclude(Entity* player); /** * @brief Checks if a given player is within the bounds of the scene. @@ -59,6 +59,13 @@ public: */ bool IsPlayerInBounds(Entity* player) const; + /** + * @brief Checks if a given player is within the showing distance of the scene. + * + * @param player The player to check. + */ + bool IsPlayerInShowingDistance(Entity* player) const; + /** * @brief Act the scene. * @@ -75,6 +82,13 @@ public: */ static Scene& LoadFromFile(std::string file); + /** + * @brief Automatically loads the scenes for a given zone. + * + * @param zone The zone to load the scenes for. + */ + static void AutoLoadScenesForZone(LWOMAPID zone); + private: void CheckForShowings(); @@ -86,6 +100,7 @@ private: NiPoint3 m_Center; float m_Bounds = 0.0f; + float m_ShowingDistance = 0.0f; std::vector> m_Preconditions; @@ -93,6 +108,7 @@ private: int32_t m_CompleteMission = 0; std::unordered_set m_Audience; + std::unordered_set m_HasBeenOutside; static std::unordered_map m_Scenes; }; diff --git a/dGame/dComponents/RenderComponent.cpp b/dGame/dComponents/RenderComponent.cpp index f1b8ea0c..a81a3458 100644 --- a/dGame/dComponents/RenderComponent.cpp +++ b/dGame/dComponents/RenderComponent.cpp @@ -15,11 +15,21 @@ std::unordered_map RenderComponent::m_DurationCache{}; +std::unordered_map> RenderComponent::m_AnimationGroupCache{}; + RenderComponent::RenderComponent(Entity* parent, int32_t componentId): Component(parent) { m_Effects = std::vector(); m_LastAnimationName = ""; if (componentId == -1) return; + const auto& it = m_AnimationGroupCache.find(componentId); + + if (it != m_AnimationGroupCache.end()) { + m_animationGroupIds = it->second; + + return; + } + auto query = CDClientDatabase::CreatePreppedStmt("SELECT * FROM RenderComponent WHERE id = ?;"); query.bind(1, componentId); auto result = query.execQuery(); @@ -41,6 +51,8 @@ RenderComponent::RenderComponent(Entity* parent, int32_t componentId): Component } } result.finalize(); + + m_AnimationGroupCache[componentId] = m_animationGroupIds; } RenderComponent::~RenderComponent() { diff --git a/dGame/dComponents/RenderComponent.h b/dGame/dComponents/RenderComponent.h index d4d1e5c3..a159f1cc 100644 --- a/dGame/dComponents/RenderComponent.h +++ b/dGame/dComponents/RenderComponent.h @@ -147,6 +147,11 @@ private: * Cache of queries that look for the length of each effect, indexed by effect ID */ static std::unordered_map m_DurationCache; + + /** + * Cache for animation groups, indexed by the component ID + */ + static std::unordered_map> m_AnimationGroupCache; }; #endif // RENDERCOMPONENT_H diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index f289a74c..14a56d62 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -1051,6 +1051,14 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit return; } + if (chatCommand == "scene-setup" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { + auto& scene = Cinema::Scene::LoadFromFile(args[0]); + + scene.Rehearse(); + + return; + } + if ((chatCommand == "teleport" || chatCommand == "tele") && entity->GetGMLevel() >= eGameMasterLevel::JUNIOR_MODERATOR) { NiPoint3 pos{}; if (args.size() == 3) { diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 3630326e..6d383366 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -76,6 +76,7 @@ #include "CheatDetection.h" #include "ServerPreconditions.hpp" +#include "Scene.h" namespace Game { Logger* logger = nullptr; @@ -257,8 +258,6 @@ int main(int argc, char** argv) { PerformanceManager::SelectProfile(zoneID); - ServerPreconditions::LoadPreconditions("vanity/preconditions.xml"); - Game::entityManager = new EntityManager(); Game::zoneManager = new dZoneManager(); //Load our level: @@ -312,6 +311,16 @@ int main(int argc, char** argv) { LOG("FDB Checksum calculated as: %s", databaseChecksum.c_str()); } + // Load server-side preconditions if they exist + const auto& preconditionsPath = Game::config->GetValue("server_preconditions_path"); + + if (!preconditionsPath.empty()) { + ServerPreconditions::LoadPreconditions(preconditionsPath); + } + + // Load scenes for the zone + Cinema::Scene::AutoLoadScenesForZone(zoneID); + uint32_t currentFrameDelta = highFrameDelta; // These values are adjust them selves to the current framerate should it update. uint32_t logFlushTime = 15 * currentFramerate; // 15 seconds in frames