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.
This commit is contained in:
wincent 2023-10-29 17:37:26 +01:00
parent e4320d3e63
commit cdc9dda3c4
14 changed files with 418 additions and 25 deletions

View File

@ -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) {

View File

@ -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);

View File

@ -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);
}

View File

@ -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<void()
void Cinema::Play::SignalBarrier(const std::string& barrier) {
if (m_Barriers.find(barrier) == m_Barriers.end()) {
LOG("Barrier %s does not exist", barrier.c_str());
return;
}

View File

@ -77,6 +77,8 @@ private:
bool m_CheckForAudience = false;
bool m_PlayerHasBeenInsideBounds = false;
std::unordered_map<std::string, std::vector<std::function<void()>>> m_Barriers;
};

View File

@ -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 <a href="./media/acting.mp4">media/acting.mp4</a> 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
<ConcludeRecord t="0" />
```
```
#### 4.15. VisiblityRecord
This record makes the actor visible or invisible.
```xml
<VisiblityRecord visible="false" t="0" />
```
#### 4.16. PlayEffectRecord
This record plays an effect.
```xml
<PlayEffectRecord effect="5307" t="0" />
```
### 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 <br> are measured. |
| `showingDistance` | The distance at which the scene will <br> be loaded for a player.<br><br> If the player exits this area the scene is unloaded. <br> If the scene has been registered as having been<br>viewed by the player, is is concluded. |
| `performingDistance` | The scene is registred as having been <br>viewed by the player. This doesn't mean <br>it can't be viewed again.<br><br>A signal named `"audiance"` will be <br> sent when the player enters this area. <br>This can be used to trigger the main <br>part of the scene. |
| `acceptMission` | The mission with the given id will be <br> accepted when the scene is concluded. |
| `completeMission` | The mission with the given id will be <br> completed when the scene is concluded. |
Here is an example of a scene with metadata:
```xml
<Scene x="-368.272" y="1161.89" z="-5.25745" performingDistance="50" showingDistance="200">
...
</Scene>
```
#### 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
<Scene x="-368.272" y="1161.89" z="-5.25745" performingDistance="50" showingDistance="200">
<Precondition expression="42,99"/> <!-- The player must fulfill preconditions 42 and 99. -->
<Precondition expression="666" not="1"/> <!-- The player cannot fulfill precondition 666. -->
...
</Scene>
```
### 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
<Preconditions>
<Entity lot="12261">
<Precondition not="1">1006</Precondition>
</Entity>
</Preconditions>
```
This will hide the objects with lot 12261 for players who fulfill precondition 1006.

View File

@ -13,6 +13,8 @@ using namespace Cinema::Recording;
std::unordered_map<LWOOBJID, Recorder*> m_Recorders = {};
std::unordered_map<int32_t, std::string> m_EffectAnimations = {};
Recorder::Recorder() {
this->m_StartTime = std::chrono::duration_cast<std::chrono::milliseconds>(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<VisibilityRecord*>(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<PlayerProximityRecord*>(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");
}
}
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");
}

View File

@ -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);

View File

@ -1,11 +1,14 @@
#include "Scene.h"
#include <filesystem>
#include <tinyxml2.h>
#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

View File

@ -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<std::pair<PreconditionExpression, bool>> m_Preconditions;
@ -93,6 +108,7 @@ private:
int32_t m_CompleteMission = 0;
std::unordered_set<LWOOBJID> m_Audience;
std::unordered_set<LWOOBJID> m_HasBeenOutside;
static std::unordered_map<std::string, Scene> m_Scenes;
};

View File

@ -15,11 +15,21 @@
std::unordered_map<int32_t, float> RenderComponent::m_DurationCache{};
std::unordered_map<int32_t, std::vector<int32_t>> RenderComponent::m_AnimationGroupCache{};
RenderComponent::RenderComponent(Entity* parent, int32_t componentId): Component(parent) {
m_Effects = std::vector<Effect*>();
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() {

View File

@ -147,6 +147,11 @@ private:
* Cache of queries that look for the length of each effect, indexed by effect ID
*/
static std::unordered_map<int32_t, float> m_DurationCache;
/**
* Cache for animation groups, indexed by the component ID
*/
static std::unordered_map<int32_t, std::vector<int32_t>> m_AnimationGroupCache;
};
#endif // RENDERCOMPONENT_H

View File

@ -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) {

View File

@ -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