Substational additions to dCinema

Includes documentation of how to create acts, prefabs and scenes.
This commit is contained in:
wincent 2023-10-27 23:51:38 +02:00
parent 01b40ffa08
commit b274ea1b8f
30 changed files with 2784 additions and 664 deletions

View File

@ -203,6 +203,7 @@ set(INCLUDED_DIRECTORIES
"dGame/dInventory" "dGame/dInventory"
"dGame/dMission" "dGame/dMission"
"dGame/dEntity" "dGame/dEntity"
"dGame/dCinema"
"dGame/dPropertyBehaviors" "dGame/dPropertyBehaviors"
"dGame/dPropertyBehaviors/ControlBehaviorMessages" "dGame/dPropertyBehaviors/ControlBehaviorMessages"
"dGame/dUtilities" "dGame/dUtilities"

View File

@ -56,6 +56,12 @@ foreach(file ${DGAME_DUTILITIES_SOURCES})
set(DGAME_SOURCES ${DGAME_SOURCES} "dUtilities/${file}") set(DGAME_SOURCES ${DGAME_SOURCES} "dUtilities/${file}")
endforeach() endforeach()
add_subdirectory(dCinema)
foreach(file ${DGAME_DCINEMA_SOURCES})
set(DGAME_SOURCES ${DGAME_SOURCES} "dCinema/${file}")
endforeach()
foreach(file ${DSCRIPTS_SOURCES}) foreach(file ${DSCRIPTS_SOURCES})
set(DGAME_SOURCES ${DGAME_SOURCES} "${PROJECT_SOURCE_DIR}/dScripts/${file}") set(DGAME_SOURCES ${DGAME_SOURCES} "${PROJECT_SOURCE_DIR}/dScripts/${file}")
endforeach() endforeach()

View File

@ -0,0 +1,5 @@
set(DGAME_DCINEMA_SOURCES "Recorder.cpp"
"Prefab.cpp"
"Scene.cpp"
"Play.cpp"
PARENT_SCOPE)

80
dGame/dCinema/Play.cpp Normal file
View File

@ -0,0 +1,80 @@
#include "Play.h"
#include "Scene.h"
#include "EntityManager.h"
using namespace Cinema;
void Cinema::Play::Conclude() {
auto* player = Game::entityManager->GetEntity(this->player);
if (player == nullptr) {
return;
}
scene->Conclude(player);
}
void Cinema::Play::SetupCheckForAudience() {
if (m_CheckForAudience) {
return;
}
m_CheckForAudience = true;
CheckForAudience();
}
void Cinema::Play::CheckForAudience() {
auto* player = Game::entityManager->GetEntity(this->player);
if (player == nullptr) {
CleanUp();
return;
}
if (!scene->IsPlayerInBounds(player)) {
Conclude();
CleanUp();
return;
}
// Still don't care
Game::entityManager->GetZoneControlEntity()->AddCallbackTimer(1.0f, [this]() {
CheckForAudience();
});
}
void Cinema::Play::CleanUp() {
for (const auto& entity : entities) {
Game::entityManager->DestroyEntity(entity);
}
}
void Cinema::Play::SetupBarrier(const std::string& barrier, std::function<void()> callback) {
// Add the callback to the barrier
if (m_Barriers.find(barrier) == m_Barriers.end()) {
m_Barriers[barrier] = std::vector<std::function<void()>>();
}
m_Barriers[barrier].push_back(callback);
}
void Cinema::Play::SignalBarrier(const std::string& barrier) {
if (m_Barriers.find(barrier) == m_Barriers.end()) {
Game::logger->Log("Cinema::Play", "Barrier %s does not exist", barrier.c_str());
return;
}
for (const auto& callback : m_Barriers[barrier]) {
callback();
}
m_Barriers.erase(barrier);
}

84
dGame/dCinema/Play.h Normal file
View File

@ -0,0 +1,84 @@
#pragma once
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <vector>
#include <functional>
#include "dCommonVars.h"
namespace Cinema
{
/**
* @brief A collection of variables and other information to be shared between scenes and acts.
*/
struct Play
{
public:
/**
* @brief The variables in the collection.
*/
std::unordered_map<std::string, std::string> variables;
/**
* @brief Associated player which is watching the scene.
*/
LWOOBJID player;
/**
* @brief A set of all entities involved in the scene.
*/
std::unordered_set<LWOOBJID> entities;
/**
* @brief The scene that is currently being performed.
*/
class Scene* scene;
/**
* @brief Conclude the play. Accepting/completing missions. This will not remove the entities.
*/
void Conclude();
/**
* @brief Setup a check for if the audience is still present.
*
* If the audience is no longer present, the play will be concluded and the entities will be removed.
*/
void SetupCheckForAudience();
/**
* @brief Clean up the play.
*/
void CleanUp();
/**
* @brief Setup a barrier. A callback is given when a signal is sent with the given barrier name.
*
* @param barrier The name of the barrier.
* @param callback The callback to call when the barrier is signaled.
*/
void SetupBarrier(const std::string& barrier, std::function<void()> callback);
/**
* @brief Signal a barrier.
*
* @param barrier The name of the barrier to signal.
*/
void SignalBarrier(const std::string& barrier);
private:
/**
* @brief Check if the audience is still present.
*/
void CheckForAudience();
bool m_CheckForAudience = false;
std::unordered_map<std::string, std::vector<std::function<void()>>> m_Barriers;
};
}

172
dGame/dCinema/Prefab.cpp Normal file
View File

@ -0,0 +1,172 @@
#include "Prefab.h"
#include "tinyxml2.h"
#include "../../dWorldServer/ObjectIDManager.h"
#include "EntityManager.h"
#include "EntityInfo.h"
#include "RenderComponent.h"
using namespace Cinema;
std::unordered_map<std::string, Prefab> Prefab::m_Prefabs;
std::unordered_map<size_t, Prefab::Instance> Prefab::m_Instances;
size_t Prefab::AddObject(LOT lot, NiPoint3 position, NiQuaternion rotation, float scale) {
const auto id = ObjectIDManager::GenerateRandomObjectID();
m_Pieces.emplace(id, Prefab::Piece { lot, position, rotation, scale, {} });
return id;
}
void Prefab::AddEffect(size_t id, int32_t effect) {
m_Pieces[id].m_Effects.push_back(effect);
}
void Prefab::RemoveObject(size_t id) {
m_Pieces.erase(id);
}
const Prefab& Prefab::LoadFromFile(std::string file) {
if (m_Prefabs.find(file) != m_Prefabs.end()) {
return m_Prefabs[file];
}
Prefab prefab;
tinyxml2::XMLDocument doc;
doc.LoadFile(file.c_str());
tinyxml2::XMLElement* root = doc.FirstChildElement("Prefab");
if (!root) {
Game::logger->Log("Prefab", "Failed to load prefab from file: %s", file.c_str());
m_Prefabs.emplace(file, prefab);
return m_Prefabs[file];
}
for (tinyxml2::XMLElement* element = root->FirstChildElement("Object"); element; element = element->NextSiblingElement("Object")) {
const auto lot = element->UnsignedAttribute("lot");
const auto position = NiPoint3(element->FloatAttribute("x"), element->FloatAttribute("y"), element->FloatAttribute("z"));
NiQuaternion rotation;
// Check if the qx attribute exists, if so the rotation is a quaternion, otherwise it's a vector
if (!element->Attribute("qx")) {
rotation = NiQuaternion::FromEulerAngles( {
element->FloatAttribute("rx") * 0.0174532925f,
element->FloatAttribute("ry") * 0.0174532925f,
element->FloatAttribute("rz") * 0.0174532925f
} );
}
else {
rotation = NiQuaternion(element->FloatAttribute("qx"), element->FloatAttribute("qy"), element->FloatAttribute("qz"), element->FloatAttribute("qw"));
}
float scale = 1.0f;
if (element->Attribute("scale")) {
scale = element->FloatAttribute("scale");
}
const auto id = prefab.AddObject(lot, position, rotation, scale);
for (tinyxml2::XMLElement* effect = element->FirstChildElement("Effect"); effect; effect = effect->NextSiblingElement("Effect")) {
prefab.AddEffect(id, effect->IntAttribute("id"));
}
}
m_Prefabs.emplace(file, prefab);
return m_Prefabs[file];
}
void Prefab::SaveToFile(std::string file) {
tinyxml2::XMLDocument doc;
tinyxml2::XMLElement* root = doc.NewElement("Prefab");
doc.InsertFirstChild(root);
for (const auto& [id, piece] : m_Pieces) {
tinyxml2::XMLElement* object = doc.NewElement("Object");
object->SetAttribute("lot", piece.m_Lot);
object->SetAttribute("x", piece.m_Position.x);
object->SetAttribute("y", piece.m_Position.y);
object->SetAttribute("z", piece.m_Position.z);
object->SetAttribute("qx", piece.m_Rotation.x);
object->SetAttribute("qy", piece.m_Rotation.y);
object->SetAttribute("qz", piece.m_Rotation.z);
object->SetAttribute("qw", piece.m_Rotation.w);
object->SetAttribute("scale", piece.m_Scale);
for (const auto& effect : piece.m_Effects) {
tinyxml2::XMLElement* effectElement = doc.NewElement("Effect");
effectElement->SetAttribute("id", effect);
object->InsertEndChild(effectElement);
}
root->InsertEndChild(object);
}
doc.SaveFile(file.c_str());
}
size_t Prefab::Instantiate(NiPoint3 position, float scale) const {
if (m_Pieces.empty()) {
return 0;
}
const auto id = ObjectIDManager::GenerateRandomObjectID();
std::vector<LWOOBJID> entities;
for (const auto& [_, piece] : m_Pieces) {
EntityInfo info;
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
info.lot = piece.m_Lot;
info.pos = (piece.m_Position * scale) + position;
info.rot = piece.m_Rotation;
info.scale = piece.m_Scale * scale;
const auto entity = Game::entityManager->CreateEntity(info);
for (const auto& effect : piece.m_Effects) {
auto* renderComponent = entity->GetComponent<RenderComponent>();
if (!renderComponent) {
continue;
}
// Generate random name
std::string effectName = "Effect_";
for (int i = 0; i < 10; ++i) {
effectName += std::to_string(rand() % 10);
}
renderComponent->PlayEffect(effect, u"create", effectName);
}
entities.push_back(entity->GetObjectID());
Game::entityManager->ConstructEntity(entity);
}
m_Instances.emplace(id, Prefab::Instance { entities });
return id;
}
const std::vector<LWOOBJID>& Cinema::Prefab::GetEntities(size_t instanceID) {
return m_Instances[instanceID].m_Entities;
}
void Prefab::DestroyInstance(size_t id) {
const auto& instance = m_Instances[id];
for (const auto& entity : instance.m_Entities) {
Game::entityManager->DestroyEntity(entity);
}
m_Instances.erase(id);
}

114
dGame/dCinema/Prefab.h Normal file
View File

@ -0,0 +1,114 @@
#pragma once
#include "dCommonVars.h"
#include "Entity.h"
#include <unordered_map>
#include <cstdint>
namespace Cinema
{
/**
* @brief A prefab is a collection of objects that can be placed in the world together.
*
* Can be saved and loaded from XML.
*/
class Prefab
{
public:
Prefab() = default;
/**
* @brief Adds an object to the prefab.
*
* @param lot The LOT of the object to add.
* @param position The position of the object to add.
* @param rotation The rotation of the object to add.
* @param scale The scale of the object to add.
*
* @return The ID of the object that was added.
*/
size_t AddObject(LOT lot, NiPoint3 position, NiQuaternion rotation, float scale = 1.0f);
/**
* @brief Adds an effect to the prefab.
*
* @param objectID The ID of the object to add the effect to.
* @param effectID The ID of the effect to add.
*/
void AddEffect(size_t objectID, int32_t effectID);
/**
* @brief Removes an object from the prefab.
*
* @param objectID The ID of the object to remove.
*/
void RemoveObject(size_t objectID);
/**
* @brief Loads a prefab from the given file.
*
* @param file The file to load the prefab from.
* @return The prefab that was loaded.
*/
static const Prefab& LoadFromFile(std::string file);
/**
* @brief Saves the prefab to the given file.
*
* @param file The file to save the prefab to.
*/
void SaveToFile(std::string file);
/**
* @brief Instantiates the prefab in the world.
*
* @param position The position to instantiate the prefab at.
* @param scale The scale to instantiate the prefab with.
*
* @return The ID of the instance that was created.
*/
size_t Instantiate(NiPoint3 position, float scale = 1.0f) const;
/**
* @brief Get the list of entities in the instance with the given ID.
*
* @param instanceID The ID of the instance to get the entities for.
*
* @return The list of entities in the instance.
*/
static const std::vector<LWOOBJID>& GetEntities(size_t instanceID);
/**
* @brief Destroys the instance with the given ID.
*
* @param instanceID The ID of the instance to destroy.
*/
static void DestroyInstance(size_t instanceID);
~Prefab() = default;
private:
struct Piece
{
LOT m_Lot;
NiPoint3 m_Position;
NiQuaternion m_Rotation;
float m_Scale;
std::vector<int32_t> m_Effects;
};
struct Instance
{
std::vector<LWOOBJID> m_Entities;
};
std::unordered_map<size_t, Piece> m_Pieces;
static std::unordered_map<std::string, Prefab> m_Prefabs;
static std::unordered_map<size_t, Instance> m_Instances;
};
}

261
dGame/dCinema/README.md Normal file
View File

@ -0,0 +1,261 @@
# Darkflame Cinema
A more emersive way to experience story in Darkflame Universe.
## What is Darkflame Cinema?
Darkflame Cinema is a way to create interactive — acted — experiences in Darkflame Universe. It aims to provide a complement to the ordinary mission structure with static NPCs and mission texts to convey story. Insperation is drawn from games likes of World of Warcraft, where NPCs frequently act out scenes, alone and in combination with both the player and other NPCs.
## Top-level design
Cinema works with a few concepts:
* **Actors** are the non player characters that act and perform actions.
* **Props** are objects in the world which complement the actors.
* **Scenes** are a collection of actors and props that perform an interactive play.
* **Plays** are scenes which are performed to a player.
## Play isolation
When a play is performed to a player, it is always isolated from the rest of the players in the world. This is to ensure that the player can experience the play without being disturbed by others, and others can't be disturbed by the play. This is achieved by ghosting all NPCs and props in the play to all players except the one experiencing the play.
## How to create a scene
A play is created is a couple of steps:
1. Acts out the scene in the world as how the NPCs should reenact it.
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.
### 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.
To start acting type: `/record-start (clear/copy)` in the chat. This will start recording your actions in the world. The actions recorded include:
* Movement
* Talking
* Equipping and unequipping items
* Explicit animations
By appending `clear` to the command, all gear on the NPC will be removed before reenactment. By appending `copy` to the command, all gear on the NPC will be copied from the player before reenactment. Which, if either, of these options to use depends on which NPC object will be acting. Usually you want to use `copy`. This can be edited manually later.
This acting is the core of the scene. Later we will learn how to add some interactivity to the scene.
When you are done acting, type `/record-stop` in the chat. This will stop recording your actions in the world.
Save your amazing performance by typing `/record-save <file.xml>` in the chat. **Do not forget to do this before starting your next recording!** This will save your performance to a file relative to where the server is running.
If your scene has multiple NPCs acting, now is the time to switch to the next NPC and repeat the steps above.
#### 1.1. Previewing your performances
See <a href="./media/preview.mp4">media/preview.mp4</a> for an example of how to preview a performance.
To show a preview of the performance you can type `/record-act (lot) (name)`, this will play the performance. If a lot is specified, the actor will be of that object. If a name is specified, the actor will have that name. If neither is specified, the actor will be a predetermined NPC.
You may load a performance by typing `/record-load <file.xml>` in the chat. This will load a performance from a file relative to where the server is running. You may than type `/record-act` to show a preview of the performance.
### 2. Create prefabs of props in the scene
Prefabs are a collection of objects that can be instantiated together. This is currently done exclusively by writing xml files.
Here is an example of a prefab:
```xml
<Prefab>
<!--
Lot is the id of the object to spawn.
Position is defined in either coordinates (x,y,z).
These are relative to the position of the prefab.
Rotation is defined in either euler angles (rx,ry,rz);
or in a quaternion (qx,qy,qz,qw).
Use /pos and /rot in the chat to get your current position and rotation.
-->
<Object lot="359" x="0" y="0" z="0" rx="0" ry="0" rz="0"/>
<Object lot="53" x="0" y="5" z="0" rx="0" rz="0">
<Effect id="60073"/> <!-- Custom effect -->
</Object>
<Object lot="359" x="0" y="5" z="0" rx="0" ry="0" rz="0"/>
<Object lot="359" x="0" y="10" z="0" rx="0" ry="0" rz="0"/>
<Object lot="53" x="0" y="0" z="0" rx="0" rz="0"/>
<Object lot="53" x="0" y="10" z="0" rx="0" rz="0"/>
<Object lot="53" x="0" y="15" z="0" rx="0" rz="0"/>
</Prefab>
```
This creates the following prefab:
<img src="media/image.png" width="800">
You can spawn a prefab by typing `/prefab-spawn <file.xml>`. This will spawn the prefab at your current position.
### 3. Put the NPCs and props in a scene file
A scene file is a collection of actors and props that perform an interactive play. This is currently done exclusively by writing xml files.
Here is an example of a scene:
```xml
<!--
The scene is defined by a collection of actors and props.
These are defined by their name and prefab file.
-->
<Scene>
<!--
Props are defined by their file name and a position.
See media/SFlagSmall.xml for this file.
-->
<Prefab file="vanity/prefabs/SFlagSmall.xml" x="-655.564" y="1123.76" z="75.993"/>
<!--
Actors are defined by their name and the file which holds the performance.
See media/my-act.xml and media/my-act-2.xml for these files.
-->
<NPC name="John" act="vanity/acts/my-act.xml"/>
<NPC name="Bill" act="vanity/acts/my-act-2.xml"/>
</Scene>
```
See <a href="./media/scene.mp4">media/scene.mp4</a> for how the above scene looks in the world.
You preview a scene by typing `/scene-act <file.xml>` in the chat. This will act out the scene for you.
### 4. Edit the performed acts
When you preview a newly created scene the NPCs will act out exactly what you did when you recorded the performance. This is not always what you want. You may want to add some interactivity to the scene. This is done by editing the performed acts.
An act is made out of a collection of records which will be evaluated in order. A record either performs some visual action or provides for flow control.
Here is an example of a slightly modified act which adds some interactivity:
```xml
<!--
See media/my-act-mod.xml for this file.
-->
<Recorder>
<!-- The following three records were added automaticly because I appended 'copy' to the command when I acted, but these may be modified freely here. -->
<ClearEquippedRecord t="0"/>
<EquipRecord item="9856" t="0"/>
<EquipRecord item="6500" t="0"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.39199999"/>
...
<!--
The following records will assure that before the act continues, the player is within 10 units of the NPC.
If the player is not within 10 units of them within 5 seconds, the NPC will ask the player to come closer. The NPC will continue to ask the player to come closer every 5 seconds until the player is within 10 units of them.
-->
<!--
This is flow-control, the act will jump to the record with the name "check-player". This makes sure that if the player is already close enough, the NPC does not tell them to come closer.
-->
<JumpRecord label="check-player" t="0" />
<!--
This is a manually added speak record, which will have the NPC speak, telling the player to come closer.
-->
<SpeakRecord name="join-me" text="Join me!" t="3.0" />
<!--
This is a player proximity record, it can branch two different ways. If the player comes within 10 units of the NPC within 5 seconds, the act will continue to the record. If however, the player is not within 10 units within 5 seconds, the act will jump to the record named "join-me", the record above this one.
-->
<PlayerProximityRecord name="check-player" distance="10" timeout="5" timeoutLabel="join-me" t="0" />
<SpeakRecord text="Welcome!" t="4.026"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="2.046"/>
...
<SpeakRecord text="Great!&quot;" t="2.3759999"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="3.6960001"/>
</Recorder>
```
See <a href="./media/interactive.mp4">media/interactive.mp4</a> for how the above scene looks in the world.
The list of available records is extensive.
#### 4.1. Records
Each record specifies a time `t`. This is how long after this record is evaluated that the next record will be evaluated.
A record may also specify a `name`. This is used for flow control. If a record specifies a name, it may be jumped to by for example `JumpRecord` or `ForkRecord`.
#### 4.1. MovementRecord
This record sets the properties of the actors controllable physics component. This is the only record which isn't meant to be edited manually.
#### 4.2. SpeakRecord
This record makes the actor speak.
```xml
<SpeakRecord text="Hello!" t="3.0" />
```
#### 4.3. EquipRecord
This record makes the actor equip an item.
```xml
<EquipRecord item="9856" t="0"/>
```
#### 4.4. UnequipRecord
This record makes the actor unequip an item.
```xml
<UnequipRecord item="9856" t="0"/>
```
#### 4.5. ClearEquippedRecord
This record makes the actor unequip all items.
```xml
<ClearEquippedRecord t="0"/>
```
#### 4.6. AnimationRecord
This record makes the actor play an animation.
```xml
<AnimationRecord animation="salute" t="2.0"/>
```
#### 4.7. WaitRecord
This record makes the actor wait for a specified amount of time.
```xml
<WaitRecord t="5.5"/>
```
#### 4.8. JumpRecord
This record makes the act jump to a record with a specified name.
```xml
<JumpRecord label="check-player" t="0" />
```
#### 4.9. ForkRecord
This record makes a decision based on a condition, and jumps accordingly. The condition may either be a scene variable or a precondtion.
```xml
<!-- Jump to 'jump-true' if my-variable=1, else jump to 'jump-false'. -->
<ForkRecord variable="my-variable" value="1" success="jump-true" failure="jump-false" t="0" />
<!-- Jump to 'jump-true' if the given precondition holds for the player, else jump to 'jump-false'. -->
<ForkRecord precondition="42,99" success="jump-true" failure="jump-false" t="0" />
```
#### 4.10. PlayerProximityRecord
This record checks if the player is within a specified distance of the actor. If the player is within the distance, the act will continue to the next record. If the player is not within the distance, the act will jump to a record with a specified name.
```xml
<!-- Specifying the timeout is optional. If it is not specified, the act will wait forever for the player to come within the distance. -->
<PlayerProximityRecord name="check-player" distance="10" timeout="5" timeoutLabel="join-me" t="0" />
```
#### 4.11. SetVariableRecord
This record sets a scene variable to a specified value. These are shared between all actors in the scene.
```xml
<SetVariableRecord variable="my-variable" value="1" t="0" />
```
#### 4.12. BarrierRecord
This record makes the act wait until it gets a signal to continue from another act, or until an optional timeout is reached. This is useful for synchronizing multiple actors, for example in a conversation.
```xml
<!-- Specifying the timeout is optional. If it is not specified, the act will wait forever for the signal. -->
<BarrierRecord signal="my-signal" timeout="5" timeoutLabel="do-something-else" t="0" />
```
#### 4.13. SignalRecord
This record sends a signal to all acts waiting for it.
```xml
<SignalRecord signal="my-signal" t="0" />
```
#### 4.14. ConcludeRecord
This record concludes the play.
```xml
<ConcludeRecord t="0" />
```

942
dGame/dCinema/Recorder.cpp Normal file
View File

@ -0,0 +1,942 @@
#include "Recorder.h"
#include "ControllablePhysicsComponent.h"
#include "GameMessages.h"
#include "InventoryComponent.h"
#include "../dWorldServer/ObjectIDManager.h"
#include "ChatPackets.h"
#include "EntityManager.h"
#include "EntityInfo.h"
#include "ServerPreconditions.hpp"
using namespace Cinema::Recording;
std::unordered_map<LWOOBJID, Recorder*> m_Recorders = {};
Recorder::Recorder() {
this->m_StartTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());
this->m_IsRecording = false;
this->m_LastRecordTime = this->m_StartTime;
}
Recorder::~Recorder() {
}
void Recorder::AddRecord(Record* record)
{
if (!this->m_IsRecording) {
return;
}
Game::logger->Log("Recorder", "Adding record");
const auto currentTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());
// Time since the last record time in seconds
record->m_Delay = (currentTime - this->m_LastRecordTime).count() / 1000.0f;
m_LastRecordTime = currentTime;
this->m_Records.push_back(record);
}
void Recorder::Act(Entity* actor, Play* variables) {
Game::logger->Log("Recorder", "Acting %d steps", m_Records.size());
// Loop through all records
ActingDispatch(actor, 0, variables);
}
void Recorder::ActingDispatch(Entity* actor, size_t index, Play* variables) {
if (index >= m_Records.size()) {
return;
}
auto* record = m_Records[index];
// Check if the record is a fork
auto* forkRecord = dynamic_cast<ForkRecord*>(record);
if (forkRecord) {
if (variables == nullptr) {
// Skip the fork
ActingDispatch(actor, index + 1, variables);
return;
}
bool success = false;
if (!forkRecord->variable.empty()) {
const auto& variable = variables->variables.find(forkRecord->variable);
if (variable != variables->variables.end()) {
success = variable->second == forkRecord->value;
}
} else if (!forkRecord->precondition.empty()) {
auto precondtion = Preconditions::CreateExpression(forkRecord->precondition);
success = precondtion.Check(actor);
} else {
success = true;
}
if (success) {
// Find the success record
for (auto i = 0; i < m_Records.size(); ++i) {
auto* record = m_Records[i];
if (record->m_Name == forkRecord->success) {
ActingDispatch(actor, i, variables);
return;
}
}
Game::logger->Log("Recorder", "Failed to find fork label success: %s", forkRecord->success.c_str());
return;
}
else {
// Find the failure record
for (auto i = 0; i < m_Records.size(); ++i) {
auto* record = m_Records[i];
if (record->m_Name == forkRecord->failure) {
ActingDispatch(actor, i, variables);
return;
}
}
Game::logger->Log("Recorder", "Failed to find fork label failure: %s", forkRecord->failure.c_str());
return;
}
}
// Check if the record is a jump
auto* jumpRecord = dynamic_cast<JumpRecord*>(record);
if (jumpRecord) {
// Find the jump record
for (auto i = 0; i < m_Records.size(); ++i) {
auto* record = m_Records[i];
if (record->m_Name == jumpRecord->label) {
ActingDispatch(actor, i, variables);
return;
}
}
Game::logger->Log("Recorder", "Failed to find jump label: %s", jumpRecord->label.c_str());
return;
}
// Check if the record is a set variable
auto* setVariableRecord = dynamic_cast<SetVariableRecord*>(record);
if (setVariableRecord) {
if (variables == nullptr) {
// Skip the set variable
ActingDispatch(actor, index + 1, variables);
return;
}
variables->variables[setVariableRecord->variable] = setVariableRecord->value;
ActingDispatch(actor, index + 1, variables);
return;
}
// Check if the record is a barrier
auto* barrierRecord = dynamic_cast<BarrierRecord*>(record);
if (barrierRecord) {
if (variables == nullptr) {
// Skip the barrier
ActingDispatch(actor, index + 1, variables);
return;
}
auto actionTaken = std::make_shared<bool>(false);
variables->SetupBarrier(barrierRecord->signal, [this, actor, index, variables, actionTaken]() {
if (*actionTaken) {
return;
}
*actionTaken = true;
ActingDispatch(actor, index + 1, variables);
});
if (barrierRecord->timeout <= 0.0f) {
return;
}
Game::entityManager->GetZoneControlEntity()->AddCallbackTimer(barrierRecord->timeout, [this, barrierRecord, actor, index, variables, actionTaken]() {
if (*actionTaken) {
return;
}
*actionTaken = true;
for (auto i = 0; i < m_Records.size(); ++i) {
auto* record = m_Records[i];
if (record->m_Name == barrierRecord->timeoutLabel) {
ActingDispatch(actor, i, variables);
return;
}
}
});
return;
}
// Check if the record is a signal
auto* signalRecord = dynamic_cast<SignalRecord*>(record);
if (signalRecord) {
if (variables != nullptr) {
variables->SignalBarrier(signalRecord->signal);
}
}
// Check if the record is a conclude
auto* concludeRecord = dynamic_cast<ConcludeRecord*>(record);
if (concludeRecord) {
if (variables != nullptr) {
variables->Conclude();
}
}
// Check if the record is a player proximity record
auto* playerProximityRecord = dynamic_cast<PlayerProximityRecord*>(record);
if (playerProximityRecord) {
if (variables == nullptr) {
// Skip the player proximity record
ActingDispatch(actor, index + 1, variables);
return;
}
auto actionTaken = std::make_shared<bool>(false);
PlayerProximityDispatch(actor, index, variables, actionTaken);
if (playerProximityRecord->timeout <= 0.0f) {
return;
}
Game::entityManager->GetZoneControlEntity()->AddCallbackTimer(playerProximityRecord->timeout, [this, playerProximityRecord, actor, index, variables, actionTaken]() {
if (*actionTaken) {
return;
}
*actionTaken = true;
for (auto i = 0; i < m_Records.size(); ++i) {
auto* record = m_Records[i];
if (record->m_Name == playerProximityRecord->timeoutLabel) {
ActingDispatch(actor, i, variables);
return;
}
}
});
return;
}
actor->AddCallbackTimer(record->m_Delay, [this, actor, index, variables]() {
ActingDispatch(actor, index + 1, variables);
});
record->Act(actor);
}
void Cinema::Recording::Recorder::PlayerProximityDispatch(Entity* actor, size_t index, Play* variables, std::shared_ptr<bool> actionTaken) {
auto* record = dynamic_cast<PlayerProximityRecord*>(m_Records[index]);
auto* player = Game::entityManager->GetEntity(variables->player);
if (player == nullptr || record == nullptr) {
return;
}
const auto& playerPosition = player->GetPosition();
const auto& actorPosition = actor->GetPosition();
const auto distance = NiPoint3::Distance(playerPosition, actorPosition);
if (distance <= record->distance) {
if (*actionTaken) {
return;
}
*actionTaken = true;
ActingDispatch(actor, index + 1, variables);
return;
}
Game::entityManager->GetZoneControlEntity()->AddCallbackTimer(1.0f, [this, actor, index, variables, actionTaken]() {
PlayerProximityDispatch(actor, index, variables, actionTaken);
});
}
Entity* Recorder::ActFor(Entity* actorTemplate, Entity* player, Play* variables) {
EntityInfo info;
info.lot = actorTemplate->GetLOT();
info.pos = actorTemplate->GetPosition();
info.rot = actorTemplate->GetRotation();
info.scale = 1;
info.spawner = nullptr;
info.spawnerID = player->GetObjectID();
info.spawnerNodeID = 0;
info.settings = {
new LDFData<std::vector<std::u16string>>(u"syncLDF", { u"custom_script_client" }),
new LDFData<std::u16string>(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua")
};
// Spawn it
auto* actor = Game::entityManager->CreateEntity(info);
// Hide the template from the player
ServerPreconditions::AddExcludeFor(player->GetObjectID(), actorTemplate->GetObjectID());
// Solo act for the player
ServerPreconditions::AddSoloActor(actor->GetObjectID(), player->GetObjectID());
Game::entityManager->ConstructEntity(actor);
Act(actor, variables);
return actor;
}
void Recorder::StopActingFor(Entity* actor, Entity* actorTemplate, LWOOBJID playerID) {
// Remove the exclude for the player
ServerPreconditions::RemoveExcludeFor(playerID, actorTemplate->GetObjectID());
Game::entityManager->DestroyEntity(actor);
}
bool Recorder::IsRecording() const {
return this->m_IsRecording;
}
void Recorder::StartRecording(LWOOBJID actorID) {
const auto& it = m_Recorders.find(actorID);
// Delete the old recorder if it exists
if (it != m_Recorders.end()) {
delete it->second;
m_Recorders.erase(it);
}
Recorder* recorder = new Recorder();
m_Recorders.insert_or_assign(actorID, recorder);
recorder->m_IsRecording = true;
}
void Recorder::StopRecording(LWOOBJID actorID) {
auto iter = m_Recorders.find(actorID);
if (iter == m_Recorders.end()) {
return;
}
iter->second->m_IsRecording = false;
}
Recorder* Recorder::GetRecorder(LWOOBJID actorID) {
auto iter = m_Recorders.find(actorID);
if (iter == m_Recorders.end()) {
return nullptr;
}
return iter->second;
}
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;
this->velocity = velocity;
this->angularVelocity = angularVelocity;
this->onGround = onGround;
this->dirtyVelocity = dirtyVelocity;
this->dirtyAngularVelocity = dirtyAngularVelocity;
}
void MovementRecord::Act(Entity* actor) {
auto* controllableComponent = actor->GetComponent<ControllablePhysicsComponent>();
if (controllableComponent) {
controllableComponent->SetPosition(position);
controllableComponent->SetRotation(rotation);
controllableComponent->SetVelocity(velocity);
controllableComponent->SetAngularVelocity(angularVelocity);
controllableComponent->SetIsOnGround(onGround);
controllableComponent->SetDirtyVelocity(dirtyVelocity);
controllableComponent->SetDirtyAngularVelocity(dirtyAngularVelocity);
}
Game::entityManager->SerializeEntity(actor);
}
void MovementRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("MovementRecord");
element->SetAttribute("x", position.x);
element->SetAttribute("y", position.y);
element->SetAttribute("z", position.z);
element->SetAttribute("qx", rotation.x);
element->SetAttribute("qy", rotation.y);
element->SetAttribute("qz", rotation.z);
element->SetAttribute("qw", rotation.w);
element->SetAttribute("vx", velocity.x);
element->SetAttribute("vy", velocity.y);
element->SetAttribute("vz", velocity.z);
element->SetAttribute("avx", angularVelocity.x);
element->SetAttribute("avy", angularVelocity.y);
element->SetAttribute("avz", angularVelocity.z);
element->SetAttribute("g", onGround);
element->SetAttribute("dv", dirtyVelocity);
element->SetAttribute("dav", dirtyAngularVelocity);
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void MovementRecord::Deserialize(tinyxml2::XMLElement* element) {
position.x = element->FloatAttribute("x");
position.y = element->FloatAttribute("y");
position.z = element->FloatAttribute("z");
rotation.x = element->FloatAttribute("qx");
rotation.y = element->FloatAttribute("qy");
rotation.z = element->FloatAttribute("qz");
rotation.w = element->FloatAttribute("qw");
velocity.x = element->FloatAttribute("vx");
velocity.y = element->FloatAttribute("vy");
velocity.z = element->FloatAttribute("vz");
angularVelocity.x = element->FloatAttribute("avx");
angularVelocity.y = element->FloatAttribute("avy");
angularVelocity.z = element->FloatAttribute("avz");
onGround = element->BoolAttribute("g");
dirtyVelocity = element->BoolAttribute("dv");
dirtyAngularVelocity = element->BoolAttribute("dav");
m_Delay = element->DoubleAttribute("t");
}
SpeakRecord::SpeakRecord(const std::string& text) {
this->text = text;
}
void SpeakRecord::Act(Entity* actor) {
GameMessages::SendNotifyClientZoneObject(
actor->GetObjectID(), u"sendToclient_bubble", 0, 0, actor->GetObjectID(), text, UNASSIGNED_SYSTEM_ADDRESS);
Game::entityManager->SerializeEntity(actor);
}
void SpeakRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("SpeakRecord");
element->SetAttribute("text", text.c_str());
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void SpeakRecord::Deserialize(tinyxml2::XMLElement* element) {
text = element->Attribute("text");
m_Delay = element->DoubleAttribute("t");
}
AnimationRecord::AnimationRecord(const std::string& animation) {
this->animation = animation;
}
void AnimationRecord::Act(Entity* actor) {
GameMessages::SendPlayAnimation(actor, GeneralUtils::ASCIIToUTF16(animation));
Game::entityManager->SerializeEntity(actor);
}
void AnimationRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("AnimationRecord");
element->SetAttribute("animation", animation.c_str());
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void AnimationRecord::Deserialize(tinyxml2::XMLElement* element) {
animation = element->Attribute("animation");
m_Delay = element->DoubleAttribute("t");
}
EquipRecord::EquipRecord(LOT item) {
this->item = item;
}
void EquipRecord::Act(Entity* actor) {
auto* inventoryComponent = actor->GetComponent<InventoryComponent>();
const LWOOBJID id = ObjectIDManager::Instance()->GenerateObjectID();
const auto& info = Inventory::FindItemComponent(item);
if (inventoryComponent) {
inventoryComponent->UpdateSlot(info.equipLocation, { id, item, 1, 0 });
}
Game::entityManager->SerializeEntity(actor);
}
void EquipRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("EquipRecord");
element->SetAttribute("item", item);
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void EquipRecord::Deserialize(tinyxml2::XMLElement* element) {
item = element->IntAttribute("item");
m_Delay = element->DoubleAttribute("t");
}
UnequipRecord::UnequipRecord(LOT item) {
this->item = item;
}
void UnequipRecord::Act(Entity* actor) {
auto* inventoryComponent = actor->GetComponent<InventoryComponent>();
const auto& info = Inventory::FindItemComponent(item);
if (inventoryComponent) {
inventoryComponent->RemoveSlot(info.equipLocation);
}
Game::entityManager->SerializeEntity(actor);
}
void UnequipRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("UnequipRecord");
element->SetAttribute("item", item);
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void UnequipRecord::Deserialize(tinyxml2::XMLElement* element) {
item = element->IntAttribute("item");
m_Delay = element->DoubleAttribute("t");
}
void ClearEquippedRecord::Act(Entity* actor) {
auto* inventoryComponent = actor->GetComponent<InventoryComponent>();
if (inventoryComponent) {
auto equipped = inventoryComponent->GetEquippedItems();
for (auto entry : equipped) {
inventoryComponent->RemoveSlot(entry.first);
}
}
Game::entityManager->SerializeEntity(actor);
}
void ClearEquippedRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("ClearEquippedRecord");
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void ClearEquippedRecord::Deserialize(tinyxml2::XMLElement* element) {
m_Delay = element->DoubleAttribute("t");
}
void Recorder::SaveToFile(const std::string& filename) {
tinyxml2::XMLDocument document;
auto* root = document.NewElement("Recorder");
for (auto* record : m_Records) {
record->Serialize(document, root);
}
document.InsertFirstChild(root);
document.SaveFile(filename.c_str());
}
float Recorder::GetDuration() const {
// Return the sum of all the record delays
float duration = 0.0f;
for (auto* record : m_Records) {
duration += record->m_Delay;
}
return duration;
}
Recorder* Recorder::LoadFromFile(const std::string& filename) {
tinyxml2::XMLDocument document;
if (document.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) {
return nullptr;
}
auto* root = document.FirstChildElement("Recorder");
if (!root) {
return nullptr;
}
Recorder* recorder = new Recorder();
for (auto* element = root->FirstChildElement(); element; element = element->NextSiblingElement()) {
const std::string name = element->Name();
if (name == "MovementRecord") {
MovementRecord* record = new MovementRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "SpeakRecord") {
SpeakRecord* record = new SpeakRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "AnimationRecord") {
AnimationRecord* record = new AnimationRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "EquipRecord") {
EquipRecord* record = new EquipRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "UnequipRecord") {
UnequipRecord* record = new UnequipRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "ClearEquippedRecord") {
ClearEquippedRecord* record = new ClearEquippedRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "ForkRecord") {
ForkRecord* record = new ForkRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "WaitRecord") {
WaitRecord* record = new WaitRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "JumpRecord") {
JumpRecord* record = new JumpRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "SetVariableRecord") {
SetVariableRecord* record = new SetVariableRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "BarrierRecord") {
BarrierRecord* record = new BarrierRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "SignalRecord") {
SignalRecord* record = new SignalRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "ConcludeRecord") {
ConcludeRecord* record = new ConcludeRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
} else if (name == "PlayerProximityRecord") {
PlayerProximityRecord* record = new PlayerProximityRecord();
record->Deserialize(element);
recorder->m_Records.push_back(record);
}
if (element->Attribute("name")) {
recorder->m_Records.back()->m_Name = element->Attribute("name");
}
}
return recorder;
}
void Recorder::AddRecording(LWOOBJID actorID, Recorder* recorder) {
const auto& it = m_Recorders.find(actorID);
// Delete the old recorder if it exists
if (it != m_Recorders.end()) {
delete it->second;
m_Recorders.erase(it);
}
m_Recorders.insert_or_assign(actorID, recorder);
}
Cinema::Recording::ForkRecord::ForkRecord(const std::string& variable, const std::string& value, const std::string& success, const std::string& failure) {
this->variable = variable;
this->value = value;
this->success = success;
this->failure = failure;
}
void Cinema::Recording::ForkRecord::Act(Entity* actor) {
}
void Cinema::Recording::ForkRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("ForkRecord");
if (!variable.empty()) {
element->SetAttribute("variable", variable.c_str());
element->SetAttribute("value", value.c_str());
}
element->SetAttribute("success", success.c_str());
element->SetAttribute("failure", failure.c_str());
if (!precondition.empty()) {
element->SetAttribute("precondtion", precondition.c_str());
}
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void Cinema::Recording::ForkRecord::Deserialize(tinyxml2::XMLElement* element) {
if (element->Attribute("variable")) {
variable = element->Attribute("variable");
value = element->Attribute("value");
}
success = element->Attribute("success");
failure = element->Attribute("failure");
if (element->Attribute("precondition")) {
precondition = element->Attribute("precondition");
}
m_Delay = element->DoubleAttribute("t");
}
Cinema::Recording::WaitRecord::WaitRecord(float delay) {
this->m_Delay = delay;
}
void Cinema::Recording::WaitRecord::Act(Entity* actor) {
}
void Cinema::Recording::WaitRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("WaitRecord");
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void Cinema::Recording::WaitRecord::Deserialize(tinyxml2::XMLElement* element) {
m_Delay = element->DoubleAttribute("t");
}
Cinema::Recording::JumpRecord::JumpRecord(const std::string& label) {
this->label = label;
}
void Cinema::Recording::JumpRecord::Act(Entity* actor) {
}
void Cinema::Recording::JumpRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("JumpRecord");
element->SetAttribute("label", label.c_str());
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void Cinema::Recording::JumpRecord::Deserialize(tinyxml2::XMLElement* element) {
label = element->Attribute("label");
m_Delay = element->DoubleAttribute("t");
}
Cinema::Recording::SetVariableRecord::SetVariableRecord(const std::string& variable, const std::string& value) {
this->variable = variable;
this->value = value;
}
void Cinema::Recording::SetVariableRecord::Act(Entity* actor) {
}
void Cinema::Recording::SetVariableRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("SetVariableRecord");
element->SetAttribute("variable", variable.c_str());
element->SetAttribute("value", value.c_str());
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void Cinema::Recording::SetVariableRecord::Deserialize(tinyxml2::XMLElement* element) {
variable = element->Attribute("variable");
value = element->Attribute("value");
m_Delay = element->DoubleAttribute("t");
}
Cinema::Recording::BarrierRecord::BarrierRecord(const std::string& signal, float timeout, const std::string& timeoutLabel) {
this->signal = signal;
this->timeout = timeout;
this->timeoutLabel = timeoutLabel;
}
void Cinema::Recording::BarrierRecord::Act(Entity* actor) {
}
void Cinema::Recording::BarrierRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("BarrierRecord");
element->SetAttribute("signal", signal.c_str());
if (timeout > 0.0f) {
element->SetAttribute("timeout", timeout);
}
if (!timeoutLabel.empty()) {
element->SetAttribute("timeoutLabel", timeoutLabel.c_str());
}
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void Cinema::Recording::BarrierRecord::Deserialize(tinyxml2::XMLElement* element) {
signal = element->Attribute("signal");
if (element->Attribute("timeout")) {
timeout = element->FloatAttribute("timeout");
}
if (element->Attribute("timeoutLabel")) {
timeoutLabel = element->Attribute("timeoutLabel");
}
m_Delay = element->DoubleAttribute("t");
}
Cinema::Recording::SignalRecord::SignalRecord(const std::string& signal) {
this->signal = signal;
}
void Cinema::Recording::SignalRecord::Act(Entity* actor) {
}
void Cinema::Recording::SignalRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("SignalRecord");
element->SetAttribute("signal", signal.c_str());
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void Cinema::Recording::SignalRecord::Deserialize(tinyxml2::XMLElement* element) {
signal = element->Attribute("signal");
m_Delay = element->DoubleAttribute("t");
}
Cinema::Recording::PlayerProximityRecord::PlayerProximityRecord(float distance, float timeout, const std::string& timeoutLabel) {
this->distance = distance;
this->timeout = timeout;
this->timeoutLabel = timeoutLabel;
}
void Cinema::Recording::PlayerProximityRecord::Act(Entity* actor) {
}
void Cinema::Recording::PlayerProximityRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("PlayerProximityRecord");
element->SetAttribute("distance", distance);
if (timeout > 0.0f) {
element->SetAttribute("timeout", timeout);
}
if (!timeoutLabel.empty()) {
element->SetAttribute("timeoutLabel", timeoutLabel.c_str());
}
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void Cinema::Recording::PlayerProximityRecord::Deserialize(tinyxml2::XMLElement* element) {
distance = element->FloatAttribute("distance");
if (element->Attribute("timeout")) {
timeout = element->FloatAttribute("timeout");
}
if (element->Attribute("timeoutLabel")) {
timeoutLabel = element->Attribute("timeoutLabel");
}
m_Delay = element->DoubleAttribute("t");
}
void Cinema::Recording::ConcludeRecord::Act(Entity* actor) {
}
void Cinema::Recording::ConcludeRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("ConcludeRecord");
element->SetAttribute("t", m_Delay);
parent->InsertEndChild(element);
}
void Cinema::Recording::ConcludeRecord::Deserialize(tinyxml2::XMLElement* element) {
m_Delay = element->DoubleAttribute("t");
}

316
dGame/dCinema/Recorder.h Normal file
View File

@ -0,0 +1,316 @@
#pragma once
#include "Player.h"
#include "Game.h"
#include "EntityManager.h"
#include "tinyxml2.h"
#include "Play.h"
#include <chrono>
namespace Cinema::Recording
{
class Record
{
public:
virtual void Act(Entity* actor) = 0;
virtual void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) = 0;
virtual void Deserialize(tinyxml2::XMLElement* element) = 0;
float m_Delay;
std::string m_Name;
};
class MovementRecord : public Record
{
public:
NiPoint3 position;
NiQuaternion rotation;
NiPoint3 velocity;
NiPoint3 angularVelocity;
bool onGround;
bool dirtyVelocity;
bool dirtyAngularVelocity;
MovementRecord() = default;
MovementRecord(
const NiPoint3& position,
const NiQuaternion& rotation,
const NiPoint3& velocity,
const NiPoint3& angularVelocity,
bool onGround, bool dirtyVelocity, bool dirtyAngularVelocity
);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class SpeakRecord : public Record
{
public:
std::string text;
SpeakRecord() = default;
SpeakRecord(const std::string& text);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class AnimationRecord : public Record
{
public:
std::string animation;
AnimationRecord() = default;
AnimationRecord(const std::string& animation);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class EquipRecord : public Record
{
public:
LOT item;
EquipRecord() = default;
EquipRecord(LOT item);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class UnequipRecord : public Record
{
public:
LOT item;
UnequipRecord() = default;
UnequipRecord(LOT item);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class ClearEquippedRecord : public Record
{
public:
ClearEquippedRecord() = default;
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class WaitRecord : public Record
{
public:
WaitRecord() = default;
WaitRecord(float delay);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class ForkRecord : public Record
{
public:
std::string variable;
std::string value;
std::string precondition;
std::string success;
std::string failure;
ForkRecord() = default;
ForkRecord(const std::string& variable, const std::string& value, const std::string& success, const std::string& failure);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class JumpRecord : public Record
{
public:
std::string label;
JumpRecord() = default;
JumpRecord(const std::string& label);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class SetVariableRecord : public Record
{
public:
std::string variable;
std::string value;
SetVariableRecord() = default;
SetVariableRecord(const std::string& variable, const std::string& value);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class BarrierRecord : public Record
{
public:
std::string signal;
float timeout;
std::string timeoutLabel;
BarrierRecord() = default;
BarrierRecord(const std::string& signal, float timeout, const std::string& timeoutLabel);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class SignalRecord : public Record
{
public:
std::string signal;
SignalRecord() = default;
SignalRecord(const std::string& signal);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class ConcludeRecord : public Record
{
public:
ConcludeRecord() = default;
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class PlayerProximityRecord : public Record
{
public:
float distance;
float timeout;
std::string timeoutLabel;
PlayerProximityRecord() = default;
PlayerProximityRecord(float distance, float timeout, const std::string& timeoutLabel);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class Recorder
{
public:
Recorder();
~Recorder();
void AddRecord(Record* record);
void Act(Entity* actor, Play* variables = nullptr);
Entity* ActFor(Entity* actorTemplate, Entity* player, Play* variables = nullptr);
void StopActingFor(Entity* actor, Entity* actorTemplate, LWOOBJID playerID);
bool IsRecording() const;
void SaveToFile(const std::string& filename);
float GetDuration() const;
static Recorder* LoadFromFile(const std::string& filename);
static void AddRecording(LWOOBJID actorID, Recorder* recorder);
static void StartRecording(LWOOBJID actorID);
static void StopRecording(LWOOBJID actorID);
static Recorder* GetRecorder(LWOOBJID actorID);
private:
void ActingDispatch(Entity* actor, size_t index, Play* variables);
void PlayerProximityDispatch(Entity* actor, size_t index, Play* variables, std::shared_ptr<bool> actionTaken);
std::vector<Record*> m_Records;
bool m_IsRecording;
std::chrono::milliseconds m_StartTime;
std::chrono::milliseconds m_LastRecordTime;
};
}

279
dGame/dCinema/Scene.cpp Normal file
View File

@ -0,0 +1,279 @@
#include "Scene.h"
#include <tinyxml2.h>
#include "ServerPreconditions.hpp"
#include "EntityManager.h"
#include "EntityInfo.h"
#include "MissionComponent.h"
using namespace Cinema;
std::unordered_map<std::string, Scene> Scene::m_Scenes;
void Cinema::Scene::AddObject(LOT lot, NiPoint3 position, NiQuaternion rotation) {
m_Objects.push_back(std::make_pair(lot, std::make_pair(position, rotation)));
}
void Scene::AddPrefab(const Prefab& prefab, NiPoint3 position) {
m_Prefabs.push_back(std::make_pair(prefab, position));
}
void Cinema::Scene::AddNPC(LOT npc, const std::string& name, Recording::Recorder* act) {
m_NPCs.push_back(std::make_pair(npc, std::make_pair(act, name)));
}
void Cinema::Scene::Rehearse() {
CheckForShowings();
}
void Cinema::Scene::Conclude(Entity* player) const {
if (player == nullptr) {
return;
}
if (m_Audience.find(player->GetObjectID()) == m_Audience.end()) {
return;
}
auto* missionComponent = player->GetComponent<MissionComponent>();
if (missionComponent == nullptr) {
return;
}
if (m_CompleteMission != 0) {
missionComponent->CompleteMission(m_CompleteMission);
}
if (m_AcceptMission != 0) {
missionComponent->AcceptMission(m_AcceptMission);
}
}
bool Cinema::Scene::IsPlayerInBounds(Entity* player) const {
if (player == nullptr) {
return false;
}
if (m_Bounds == 0.0f) {
return true;
}
const auto& position = player->GetPosition();
auto distance = NiPoint3::Distance(position, m_Center);
// The player may be within 20% of the bounds
return distance <= (m_Bounds * 1.2f);
}
void Cinema::Scene::CheckForShowings() {
auto audience = m_Audience;
for (const auto& member : audience) {
if (Game::entityManager->GetEntity(member) == nullptr) {
m_Audience.erase(member);
}
}
// I don't care
Game::entityManager->GetZoneControlEntity()->AddCallbackTimer(1.0f, [this]() {
for (auto* player : Player::GetAllPlayers()) {
if (m_Audience.find(player->GetObjectID()) != m_Audience.end()) {
continue;
}
CheckTicket(player);
}
});
}
void Cinema::Scene::CheckTicket(Entity* player) {
if (m_Audience.find(player->GetObjectID()) != m_Audience.end()) {
return;
}
for (const auto& [expression, invert] : m_Preconditions) {
if (expression.Check(player) == invert) {
return;
}
}
m_Audience.emplace(player->GetObjectID());
Act(player);
}
Play* Cinema::Scene::Act(Entity* player) {
auto* play = new Play();
if (player != nullptr) {
play->player = player->GetObjectID();
}
play->scene = this;
for (const auto& [lot, transform] : m_Objects) {
const auto& [position, rotation] = transform;
EntityInfo info;
info.pos = position;
info.rot = rotation;
info.lot = lot;
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
auto* entity = Game::entityManager->CreateEntity(info);
Game::entityManager->ConstructEntity(entity);
if (player != nullptr) {
ServerPreconditions::AddSoloActor(entity->GetObjectID(), player->GetObjectID());
}
Game::logger->Log("Scene", "Spawing object %d", entity->GetObjectID());
}
for (const auto& [prefab, position] : m_Prefabs) {
const auto instanceId = prefab.Instantiate(position);
const auto& entities = prefab.GetEntities(instanceId);
if (player != nullptr) {
for (const auto& entity : entities) {
ServerPreconditions::AddSoloActor(entity, player->GetObjectID());
play->entities.emplace(entity);
}
}
Game::logger->Log("Scene", "Spawing prefab %d", instanceId);
}
for (const auto& [npc, meta] : m_NPCs) {
const auto& [act, name] = meta;
EntityInfo info;
info.pos = NiPoint3();
info.rot = NiQuaternion();
info.lot = npc;
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
info.settings = {
new LDFData<std::vector<std::u16string>>(u"syncLDF", { u"custom_script_client" }),
new LDFData<std::u16string>(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua")
};
auto* entity = Game::entityManager->CreateEntity(info);
play->entities.emplace(entity->GetObjectID());
Game::entityManager->ConstructEntity(entity);
Entity* actor;
if (player != nullptr) {
actor = act->ActFor(entity, player, play);
}
else
{
act->Act(entity, play);
actor = entity;
}
if (actor != nullptr) {
actor->SetVar(u"npcName", name);
}
play->entities.emplace(entity->GetObjectID());
Game::logger->Log("Scene", "Spawing npc %d", entity->GetObjectID());
}
if (player != nullptr) {
play->SetupCheckForAudience();
}
return play;
}
Scene& Cinema::Scene::LoadFromFile(std::string file) {
if (m_Scenes.find(file) != m_Scenes.end()) {
return m_Scenes[file];
}
Scene scene;
tinyxml2::XMLDocument doc;
doc.LoadFile(file.c_str());
tinyxml2::XMLElement* root = doc.FirstChildElement("Scene");
if (!root) {
Game::logger->Log("Scene", "Failed to load scene from file: %s", file.c_str());
m_Scenes.emplace(file, scene);
return m_Scenes[file];
}
// Load center and bounds
if (root->Attribute("x")) {
scene.m_Center = NiPoint3(root->FloatAttribute("x"), root->FloatAttribute("y"), root->FloatAttribute("z"));
}
if (root->Attribute("bounds")) {
scene.m_Bounds = root->FloatAttribute("bounds");
}
// Load accept and complete mission
if (root->Attribute("acceptMission")) {
scene.m_AcceptMission = root->IntAttribute("acceptMission");
}
if (root->Attribute("completeMission")) {
scene.m_CompleteMission = root->IntAttribute("completeMission");
}
// Load preconditions
for (tinyxml2::XMLElement* element = root->FirstChildElement("Precondition"); element; element = element->NextSiblingElement("Precondition")) {
scene.m_Preconditions.push_back(std::make_pair(Preconditions::CreateExpression(element->Attribute("expression")), element->BoolAttribute("not")));
}
for (tinyxml2::XMLElement* element = root->FirstChildElement("Object"); element; element = element->NextSiblingElement("Object")) {
const auto lot = element->UnsignedAttribute("lot");
const auto position = NiPoint3(element->FloatAttribute("x"), element->FloatAttribute("y"), element->FloatAttribute("z"));
const auto rotation = NiQuaternion(element->FloatAttribute("qx"), element->FloatAttribute("qy"), element->FloatAttribute("qz"), element->FloatAttribute("qw"));
scene.AddObject(lot, position, rotation);
}
for (tinyxml2::XMLElement* element = root->FirstChildElement("Prefab"); element; element = element->NextSiblingElement("Prefab")) {
const auto prefab = element->Attribute("file");
const auto position = NiPoint3(element->FloatAttribute("x"), element->FloatAttribute("y"), element->FloatAttribute("z"));
scene.AddPrefab(Prefab::LoadFromFile(prefab), position);
}
for (tinyxml2::XMLElement* element = root->FirstChildElement("NPC"); element; element = element->NextSiblingElement("NPC")) {
LOT npc = 2097253;
if (element->Attribute("lot")) {
npc = element->UnsignedAttribute("lot");
}
std::string name = "";
if (element->Attribute("name")) {
name = element->Attribute("name");
}
const auto act = element->Attribute("act");
scene.AddNPC(npc, name, Recording::Recorder::LoadFromFile(act));
}
Game::logger->Log("Scene", "Loaded scene from file: %s", file.c_str());
m_Scenes.emplace(file, scene);
return m_Scenes[file];
}

100
dGame/dCinema/Scene.h Normal file
View File

@ -0,0 +1,100 @@
#pragma once
#include "Prefab.h"
#include "Recorder.h"
#include "Preconditions.h"
namespace Cinema
{
/**
* @brief A scene is a collection of prefabs, npcs and behaviors that can be loaded into the world.
*/
class Scene
{
public:
Scene() = default;
/**
* @brief Adds an object to the scene.
*
* @param lot The LOT of the object to add.
* @param position The position of the object to add.
* @param rotation The rotation of the object to add.
*/
void AddObject(LOT lot, NiPoint3 position, NiQuaternion rotation);
/**
* @brief Adds a prefab to the scene.
*
* @param prefab The prefab to add.
* @param position The position to add the prefab at.
*/
void AddPrefab(const Prefab& prefab, NiPoint3 position);
/**
* @brief Adds an NPC to the scene.
*
* @param npc The NPC to add.
* @param act The act to set for the NPC.
*/
void AddNPC(LOT npc, const std::string& name, Recording::Recorder* act);
/**
* @brief Set up the scene to be acted when a player enters the theater and meets the preconditions.
*/
void Rehearse();
/**
* @brief Conclude the scene for a given player.
*
* @param player The player to conclude the scene for (not nullptr).
*/
void Conclude(Entity* player) const;
/**
* @brief Checks if a given player is within the bounds of the scene.
*
* @param player The player to check.
*/
bool IsPlayerInBounds(Entity* player) const;
/**
* @brief Act the scene.
*
* @param player The player to act the scene for (or nullptr to act for all players).
* @return The variables that were set by the scene.
*/
Play* Act(Entity* player);
/**
* @brief Loads a scene from the given file.
*
* @param file The file to load the scene from.
* @return The scene that was loaded.
*/
static Scene& LoadFromFile(std::string file);
private:
void CheckForShowings();
void CheckTicket(Entity* player);
std::vector<std::pair<LOT, std::pair<NiPoint3, NiQuaternion>>> m_Objects;
std::vector<std::pair<Prefab, NiPoint3>> m_Prefabs;
std::vector<std::pair<LOT, std::pair<Recording::Recorder*, std::string>>> m_NPCs;
NiPoint3 m_Center;
float m_Bounds = 0.0f;
std::vector<std::pair<PreconditionExpression, bool>> m_Preconditions;
int32_t m_AcceptMission = 0;
int32_t m_CompleteMission = 0;
std::unordered_set<LWOOBJID> m_Audience;
static std::unordered_map<std::string, Scene> m_Scenes;
};
} // namespace Cinema

View File

@ -0,0 +1,11 @@
<Prefab>
<Object lot="359" x="0" y="0" z="0" rx="0" ry="0" rz="0"/>
<Object lot="53" x="0" y="5" z="0" rx="0" rz="0">
<Effect id="60073"/>
</Object>
<Object lot="359" x="0" y="5" z="0" rx="0" ry="0" rz="0"/>
<Object lot="359" x="0" y="10" z="0" rx="0" ry="0" rz="0"/>
<Object lot="53" x="0" y="0" z="0" rx="0" rz="0"/>
<Object lot="53" x="0" y="10" z="0" rx="0" rz="0"/>
<Object lot="53" x="0" y="15" z="0" rx="0" rz="0"/>
</Prefab>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

View File

@ -0,0 +1,90 @@
<Recorder>
<MovementRecord x="-666.4151" y="1123.7156" z="-5.6110449" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.028999999"/>
<MovementRecord x="-666.3476" y="1123.7156" z="-5.2612491" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.3501134" vy="0.0033733833" vz="6.9959145" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.89099997"/>
<MovementRecord x="-666.01447" y="1123.717" z="-3.5337467" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-665.68048" y="1123.7185" z="-1.8018351" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-665.3465" y="1123.72" z="-0.069923788" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-664.98468" y="1123.7216" z="1.8063134" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-664.6507" y="1123.723" z="3.5382249" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-664.28888" y="1123.7246" z="5.4144602" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-663.92706" y="1123.7262" z="7.2906947" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-663.62091" y="1123.7275" z="8.8782806" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-663.25909" y="1123.7291" z="10.754521" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-662.92511" y="1123.7306" z="12.486436" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-662.56329" y="1123.7322" z="14.362677" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-662.20148" y="1123.7338" z="16.238916" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-661.86749" y="1123.7352" z="17.970819" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-661.53351" y="1123.7367" z="19.702723" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-661.19952" y="1123.7382" z="21.434626" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-660.86554" y="1123.7396" z="23.166529" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-660.50372" y="1123.7412" z="25.042757" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-660.16974" y="1123.7427" z="26.77466" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-659.80792" y="1123.7443" z="28.650888" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.004175582" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-659.50177" y="1123.7456" z="30.238466" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-659.16779" y="1123.7471" z="31.970369" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-658.80597" y="1123.7487" z="33.8466" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-658.44415" y="1123.7502" z="35.722828" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-658.11017" y="1123.7517" z="37.454731" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-657.75244" y="1123.7533" z="39.331692" qx="0" qy="0.081039831" qz="0" qw="0.99671084" vx="1.4247334" vy="0.0041755605" vz="8.703496" avx="0" avy="-1.7030833" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-657.45453" y="1123.7382" z="41.2188" qx="0" qy="0.078293413" qz="0" qw="0.99693036" vx="1.3767529" vy="0.0041751657" vz="8.711215" avx="0" avy="0.020716017" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-657.17914" y="1123.7396" z="42.961048" qx="0" qy="0.078293413" qz="0" qw="0.99693036" vx="1.3767529" vy="0.0041751661" vz="8.711215" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-656.9267" y="1123.741" z="44.558109" qx="0" qy="0.078293413" qz="0" qw="0.99693036" vx="1.3767529" vy="0.0041751661" vz="8.711215" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-656.63043" y="1123.7426" z="46.445869" qx="0" qy="0.075789861" qz="0" qw="0.99712384" vx="1.3329875" vy="0.0041746967" vz="8.7180195" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-656.36383" y="1123.744" z="48.189491" qx="0" qy="0.075789861" qz="0" qw="0.99712384" vx="1.3329875" vy="0.0041746967" vz="8.7180195" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-656.10962" y="1123.7456" z="50.083252" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="1.0742536" vy="0.0041697873" vz="8.7536678" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.89502" y="1123.7471" z="51.833969" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="1.0742536" vy="0.0041697873" vz="8.7536678" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.66541" y="1123.7485" z="53.707008" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0.90067899" vy="0.0034960457" vz="7.3392773" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-655.63428" y="1123.7485" z="53.960564" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0.20638135" vy="0.0008010827" vz="1.6817197" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.63373" y="1123.7485" z="53.965019" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<SpeakRecord text="Hi!" t="1.584"/>
<MovementRecord x="-655.63373" y="1123.7485" z="53.965019" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="4.5869999"/>
<AnimationRecord animation="salute" t="4.0939999"/>
<MovementRecord x="-655.63373" y="1123.7485" z="53.965019" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="1.978"/>
<MovementRecord x="-655.63373" y="1123.7485" z="53.965019" qx="0" qy="0.0056772209" qz="0" qw="0.99998391" vx="0" vy="0" vz="0" avx="0" avy="-1.02" avz="0" g="true" dv="false" dav="true" t="1.914"/>
<MovementRecord x="-655.91522" y="1123.7493" z="55.306641" qx="0" qy="-0.16600978" qz="0" qw="0.9861241" vx="-2.8875616" vy="0.0036441826" vz="8.3332291" avx="0" avy="-2.2004657" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-656.8963" y="1123.7493" z="56.298153" qx="0" qy="-0.60278374" qz="0" qw="0.79790461" vx="-8.4835682" vy="0.00041772827" vz="2.4103563" avx="0" avy="-5.2572789" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-658.90973" y="1123.7493" z="56.085861" qx="0" qy="-0.81736016" qz="0" qw="0.57612699" vx="-8.3061113" vy="-0.0020730058" vz="-2.964668" avx="0" avy="-0.59541434" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-660.45416" y="1123.7493" z="55.155827" qx="0" qy="0.90611506" qz="0" qw="-0.42303139" vx="-6.7611709" vy="-0.0032021664" vz="-5.6627994" avx="0" avy="-1.0992103" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-661.45642" y="1123.7483" z="53.758194" qx="0" qy="0.97830099" qz="0" qw="-0.20718889" vx="-3.5752378" vy="-0.0040555298" vz="-8.0621586" avx="0" avy="-2.5476563" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-661.73975" y="1123.7469" z="51.782887" qx="0" qy="0.99977756" qz="0" qw="-0.021090925" vx="-0.37193322" vy="-0.0041383081" vz="-8.811492" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-661.96057" y="1123.7456" z="50.093998" qx="0" qy="0.99636191" qz="0" qw="-0.085222892" vx="-1.49775" vy="-0.0041759168" vz="-8.6912289" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-662.29297" y="1123.744" z="48.224056" qx="0" qy="0.99613321" qz="0" qw="-0.087855853" vx="-1.5436686" vy="-0.0041759908" vz="-8.6831913" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-662.57861" y="1123.7426" z="46.483566" qx="0" qy="0.99748456" qz="0" qw="-0.070884049" vx="-1.2471558" vy="-0.004173473" vz="-8.7307119" avx="0" avy="-0.30132243" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-662.83026" y="1123.741" z="44.589401" qx="0" qy="0.99784493" qz="0" qw="-0.065616116" vx="-1.1548871" vy="-0.0041717072" vz="-8.7433949" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-663.065" y="1123.7395" z="42.841267" qx="0" qy="0.99736089" qz="0" qw="-0.07260374" vx="-1.2772541" vy="-0.0041739475" vz="-8.7263594" avx="0" avy="0.30980942" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-663.34338" y="1123.7379" z="40.95084" qx="0" qy="0.99732316" qz="0" qw="-0.073120199" vx="-1.286291" vy="-0.0041740802" vz="-8.7250319" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-663.62146" y="1123.7363" z="39.060375" qx="0" qy="0.99733573" qz="0" qw="-0.072948046" vx="-1.2832787" vy="-0.0041740369" vz="-8.7254753" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-663.89001" y="1123.7351" z="37.317089" qx="0" qy="0.99676812" qz="0" qw="-0.080332734" vx="-1.4123834" vy="-0.0036389432" vz="-8.7055092" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-664.20569" y="1123.7341" z="35.432461" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036370354" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-664.52148" y="1123.7332" z="33.547852" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.003687297" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-664.81299" y="1123.7324" z="31.808212" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0037204367" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-665.10449" y="1123.7316" z="30.068573" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036486434" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-665.396" y="1123.7307" z="28.328934" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036817831" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-665.6875" y="1123.7299" z="26.589294" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036099749" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-666.0033" y="1123.729" z="24.704685" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036602514" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-666.31311" y="1123.728" z="22.855684" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036934214" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-666.604" y="1123.7273" z="21.119551" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036277049" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-666.9198" y="1123.7263" z="19.234941" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036779814" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-667.2356" y="1123.7255" z="17.350332" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0037282431" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-667.52557" y="1123.7247" z="15.619577" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036350512" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-667.84137" y="1123.7238" z="13.734968" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036328458" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-668.15717" y="1123.7228" z="11.850359" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036831074" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-668.47296" y="1123.7219" z="9.9657497" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.003733384" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-668.76447" y="1123.7212" z="8.2261105" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036444538" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-669.05597" y="1123.7202" z="6.4864712" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036947303" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-669.37177" y="1123.7194" z="4.601862" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.003744992" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-669.66327" y="1123.7186" z="2.8622227" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036560618" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-669.93048" y="1123.7178" z="1.2675533" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.003689202" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-670.24628" y="1123.7169" z="-0.61705661" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036174082" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-670.56207" y="1123.7159" z="-2.5016661" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036152028" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-670.85358" y="1123.7151" z="-4.2413054" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0037179464" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-671.16937" y="1123.7142" z="-6.1259146" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036986046" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-671.46088" y="1123.7134" z="-7.8655539" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036268109" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-671.77667" y="1123.7124" z="-9.7501631" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036770874" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.04388" y="1123.7118" z="-11.344832" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036406391" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.35968" y="1123.7108" z="-13.229442" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036384189" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.65118" y="1123.71" z="-14.969081" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0037411624" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.89941" y="1123.7094" z="-16.451059" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-0.27985972" vy="-0.00032467872" vz="-1.6710598" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.90015" y="1123.7094" z="-16.455486" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-3.193491e-08" vy="0.00038383523" vz="-1.7891979e-07" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-672.90015" y="1123.7094" z="-16.455486" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-3.193491e-08" vy="0.00038383523" vz="-1.7891979e-07" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="4.8179998"/>
</Recorder>

View File

@ -0,0 +1,58 @@
<Recorder>
<ClearEquippedRecord t="0"/>
<EquipRecord item="9856" t="0"/>
<EquipRecord item="6500" t="0"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.39199999"/>
<JumpRecord label="check-player" t="0" />
<SpeakRecord name="join-me" text="Join me!" t="3.0" />
<PlayerProximityRecord name="check-player" distance="10" timeout="5" timeoutLabel="join-me" t="0" />
<SpeakRecord text="Welcome!" t="4.026"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="2.046"/>
<MovementRecord x="-557.55542" y="1128.7369" z="50.891663" qx="0" qy="0.77823329" qz="0" qw="0.62797529" vx="-1.4221454" vy="3.3353501e-07" vz="0.090151384" avx="0" avy="188.49556" avz="0" g="true" dv="true" dav="true" t="0.46200001"/>
<MovementRecord x="-559.65845" y="1128.7369" z="51.024967" qx="0" qy="0.97620082" qz="0" qw="-0.21686859" vx="-16.871138" vy="3.9568704e-06" vz="1.0694356" avx="0" avy="80.788872" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-563.03271" y="1128.7369" z="51.238667" qx="0" qy="-0.68442959" qz="0" qw="0.72907895" vx="-16.871298" vy="3.9620759e-06" vz="1.0669085" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-566.12579" y="1128.7369" z="51.43425" qx="0" qy="-0.68442959" qz="0" qw="0.72907895" vx="-16.871298" vy="3.9620759e-06" vz="1.0669085" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-569.76996" y="1128.7369" z="51.779564" qx="0" qy="-0.65352851" qz="0" qw="0.75690192" vx="-16.724352" vy="1.0602414e-06" vz="2.4647665" avx="0" avy="0.29643202" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-573.31696" y="1128.7369" z="52.304493" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-576.93988" y="1128.7369" z="52.840672" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-580.00543" y="1128.7369" z="53.294361" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-583.6225" y="1128.7369" z="53.869492" qx="0" qy="-0.64578879" qz="0" qw="0.76351613" vx="-16.6707" vy="3.4790952e-07" vz="2.8047707" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-587.23431" y="1128.7369" z="54.47718" qx="0" qy="-0.64578879" qz="0" qw="0.76351613" vx="-16.6707" vy="3.4790952e-07" vz="2.8047707" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-590.78857" y="1128.7369" z="55.154781" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-594.10425" y="1128.7369" z="55.814648" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-597.41992" y="1128.7369" z="56.474514" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-600.37299" y="1129.1835" z="57.067936" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.550844" vy="-0.02764282" vz="3.293468" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-603.67401" y="1129.1776" z="57.794476" qx="0" qy="-0.60967135" qz="0" qw="0.79265428" vx="-16.338949" vy="-0.027341407" vz="4.3378429" avx="0" avy="0.68940604" avz="0" g="true" dv="true" dav="true" t="0.264"/>
<MovementRecord x="-607.30255" y="1128.5726" z="58.873291" qx="0" qy="-0.58935732" qz="0" qw="0.80787253" vx="-16.122616" vy="-15" vz="5.0832543" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-610.39264" y="1126.5226" z="59.848522" qx="0" qy="-0.58935732" qz="0" qw="0.80787253" vx="-13.727316" vy="-8.8299694" vz="4.4012947" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-612.91327" y="1124.842" z="60.493713" qx="0" qy="-0.64014912" qz="0" qw="0.7682507" vx="-13.939328" vy="-9.2160997" vz="2.5568676" avx="0" avy="-0.90714848" avz="0" g="true" dv="true" dav="true" t="0.495"/>
<MovementRecord x="-615.60901" y="1123.9226" z="60.914558" qx="0" qy="-0.65074474" qz="0" qw="0.7592966" vx="-16.705799" vy="0" vz="2.5875225" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-619.20642" y="1123.9226" z="61.475426" qx="0" qy="-0.64724869" qz="0" qw="0.76227891" vx="-16.681313" vy="0" vz="2.7409482" avx="0" avy="0.31377235" avz="0" g="true" dv="true" dav="true" t="0.264"/>
<MovementRecord x="-622.5293" y="1123.9226" z="62.100258" qx="0" qy="-0.63752168" qz="0" qw="0.77043241" vx="-16.606367" vy="0" vz="3.163466" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-626.12823" y="1123.7505" z="62.781879" qx="0" qy="-0.64108664" qz="0" qw="0.76746851" vx="-16.634985" vy="4.6775982e-05" vz="3.0093575" avx="0" avy="-0.30132243" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-629.71057" y="1123.7505" z="63.393055" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-16.665606" vy="-4.7832516e-05" vz="2.8348761" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-633.59003" y="1124.8367" z="64.052917" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-16.896" vy="20.297354" vz="2.8736222" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-637.22083" y="1127.7493" z="64.670441" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-19.241386" vy="8.9419651" vz="3.2726972" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-641.23969" y="1128.4006" z="65.354004" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-20.97077" vy="-2.3861933" vz="3.5669582" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-645.92755" y="1128.7273" z="66.15139" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-22.068434" vy="11.44259" vz="3.7537298" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-650.06995" y="1129.8671" z="66.85601" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-22.994522" vy="0.9925909" vz="3.9113069" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.15118" y="1128.7443" z="67.720322" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-23.803282" vy="-11.357409" vz="4.0489192" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-659.56635" y="1124.9456" z="68.471329" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-7.8723621" vy="-23.707413" vz="1.3390911" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-660.31464" y="1123.7504" z="68.5979" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-1.6779066" vy="1.007928e-05" vz="0.28236043" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-660.31915" y="1123.7504" z="68.598663" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<MovementRecord x="-660.29572" y="1123.7504" z="68.594681" qx="0" qy="-0.5861969" qz="0" qw="0.81016856" vx="1.4048207" vy="2.2637594e-05" vz="-0.23896487" avx="0" avy="188.49556" avz="0" g="true" dv="true" dav="true" t="0.29699999"/>
<MovementRecord x="-658.21826" y="1123.7504" z="68.241364" qx="0" qy="0.26788518" qz="0" qw="0.96345085" vx="16.665703" vy="8.9682879e-05" vz="-2.8343182" avx="0" avy="80.787056" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-655.93872" y="1123.7504" z="67.853683" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="4.0222421" vy="4.9844002e-05" vz="-0.6840589" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.87488" y="1123.7504" z="67.842827" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="1.2125849" vy="3.9280545e-05" vz="-0.20622317" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.87488" y="1123.7504" z="67.842827" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.23100001"/>
<MovementRecord x="-655.04889" y="1123.7504" z="67.242249" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="4.6101446" vy="-0.0011672372" vz="-3.3521008" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="1.1525371" vy="-0.00029180956" vz="-0.83802569" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<SpeakRecord text="Great!&quot;" t="2.3759999"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="3.6960001"/>
</Recorder>

View File

@ -0,0 +1,50 @@
<Recorder>
<ClearEquippedRecord t="0"/>
<EquipRecord item="9856" t="0"/>
<EquipRecord item="6500" t="0"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.39199999"/>
<SpeakRecord text="Welcome!" t="4.026"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="2.046"/>
<MovementRecord x="-557.55542" y="1128.7369" z="50.891663" qx="0" qy="0.77823329" qz="0" qw="0.62797529" vx="-1.4221454" vy="3.3353501e-07" vz="0.090151384" avx="0" avy="188.49556" avz="0" g="true" dv="true" dav="true" t="0.46200001"/>
<MovementRecord x="-559.65845" y="1128.7369" z="51.024967" qx="0" qy="0.97620082" qz="0" qw="-0.21686859" vx="-16.871138" vy="3.9568704e-06" vz="1.0694356" avx="0" avy="80.788872" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-563.03271" y="1128.7369" z="51.238667" qx="0" qy="-0.68442959" qz="0" qw="0.72907895" vx="-16.871298" vy="3.9620759e-06" vz="1.0669085" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-566.12579" y="1128.7369" z="51.43425" qx="0" qy="-0.68442959" qz="0" qw="0.72907895" vx="-16.871298" vy="3.9620759e-06" vz="1.0669085" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-569.76996" y="1128.7369" z="51.779564" qx="0" qy="-0.65352851" qz="0" qw="0.75690192" vx="-16.724352" vy="1.0602414e-06" vz="2.4647665" avx="0" avy="0.29643202" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-573.31696" y="1128.7369" z="52.304493" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-576.93988" y="1128.7369" z="52.840672" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-580.00543" y="1128.7369" z="53.294361" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-583.6225" y="1128.7369" z="53.869492" qx="0" qy="-0.64578879" qz="0" qw="0.76351613" vx="-16.6707" vy="3.4790952e-07" vz="2.8047707" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-587.23431" y="1128.7369" z="54.47718" qx="0" qy="-0.64578879" qz="0" qw="0.76351613" vx="-16.6707" vy="3.4790952e-07" vz="2.8047707" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-590.78857" y="1128.7369" z="55.154781" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-594.10425" y="1128.7369" z="55.814648" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-597.41992" y="1128.7369" z="56.474514" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-600.37299" y="1129.1835" z="57.067936" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.550844" vy="-0.02764282" vz="3.293468" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-603.67401" y="1129.1776" z="57.794476" qx="0" qy="-0.60967135" qz="0" qw="0.79265428" vx="-16.338949" vy="-0.027341407" vz="4.3378429" avx="0" avy="0.68940604" avz="0" g="true" dv="true" dav="true" t="0.264"/>
<MovementRecord x="-607.30255" y="1128.5726" z="58.873291" qx="0" qy="-0.58935732" qz="0" qw="0.80787253" vx="-16.122616" vy="-15" vz="5.0832543" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-610.39264" y="1126.5226" z="59.848522" qx="0" qy="-0.58935732" qz="0" qw="0.80787253" vx="-13.727316" vy="-8.8299694" vz="4.4012947" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-612.91327" y="1124.842" z="60.493713" qx="0" qy="-0.64014912" qz="0" qw="0.7682507" vx="-13.939328" vy="-9.2160997" vz="2.5568676" avx="0" avy="-0.90714848" avz="0" g="true" dv="true" dav="true" t="0.495"/>
<MovementRecord x="-615.60901" y="1123.9226" z="60.914558" qx="0" qy="-0.65074474" qz="0" qw="0.7592966" vx="-16.705799" vy="0" vz="2.5875225" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-619.20642" y="1123.9226" z="61.475426" qx="0" qy="-0.64724869" qz="0" qw="0.76227891" vx="-16.681313" vy="0" vz="2.7409482" avx="0" avy="0.31377235" avz="0" g="true" dv="true" dav="true" t="0.264"/>
<MovementRecord x="-622.5293" y="1123.9226" z="62.100258" qx="0" qy="-0.63752168" qz="0" qw="0.77043241" vx="-16.606367" vy="0" vz="3.163466" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-626.12823" y="1123.7505" z="62.781879" qx="0" qy="-0.64108664" qz="0" qw="0.76746851" vx="-16.634985" vy="4.6775982e-05" vz="3.0093575" avx="0" avy="-0.30132243" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-629.71057" y="1123.7505" z="63.393055" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-16.665606" vy="-4.7832516e-05" vz="2.8348761" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-633.59003" y="1124.8367" z="64.052917" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-16.896" vy="20.297354" vz="2.8736222" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-637.22083" y="1127.7493" z="64.670441" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-19.241386" vy="8.9419651" vz="3.2726972" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-641.23969" y="1128.4006" z="65.354004" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-20.97077" vy="-2.3861933" vz="3.5669582" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-645.92755" y="1128.7273" z="66.15139" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-22.068434" vy="11.44259" vz="3.7537298" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-650.06995" y="1129.8671" z="66.85601" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-22.994522" vy="0.9925909" vz="3.9113069" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.15118" y="1128.7443" z="67.720322" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-23.803282" vy="-11.357409" vz="4.0489192" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-659.56635" y="1124.9456" z="68.471329" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-7.8723621" vy="-23.707413" vz="1.3390911" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-660.31464" y="1123.7504" z="68.5979" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-1.6779066" vy="1.007928e-05" vz="0.28236043" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-660.31915" y="1123.7504" z="68.598663" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<MovementRecord x="-660.29572" y="1123.7504" z="68.594681" qx="0" qy="-0.5861969" qz="0" qw="0.81016856" vx="1.4048207" vy="2.2637594e-05" vz="-0.23896487" avx="0" avy="188.49556" avz="0" g="true" dv="true" dav="true" t="0.29699999"/>
<MovementRecord x="-658.21826" y="1123.7504" z="68.241364" qx="0" qy="0.26788518" qz="0" qw="0.96345085" vx="16.665703" vy="8.9682879e-05" vz="-2.8343182" avx="0" avy="80.787056" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-655.93872" y="1123.7504" z="67.853683" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="4.0222421" vy="4.9844002e-05" vz="-0.6840589" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.87488" y="1123.7504" z="67.842827" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="1.2125849" vy="3.9280545e-05" vz="-0.20622317" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.87488" y="1123.7504" z="67.842827" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.23100001"/>
<MovementRecord x="-655.04889" y="1123.7504" z="67.242249" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="4.6101446" vy="-0.0011672372" vz="-3.3521008" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="1.1525371" vy="-0.00029180956" vz="-0.83802569" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<SpeakRecord text="Great!&quot;" t="2.3759999"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="3.6960001"/>
</Recorder>

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,5 @@
<Scene>
<Prefab file="vanity/prefabs/SFlagSmall.xml" x="-655.564" y="1123.76" z="75.993"/>
<NPC name="John" act="vanity/acts/my-act.xml"/>
<NPC name="Bill" act="vanity/acts/my-act-2.xml"/>
</Scene>

View File

@ -192,10 +192,10 @@ void GameMessages::SendPlayAnimation(Entity* entity, const std::u16string& anima
SEND_PACKET_BROADCAST; SEND_PACKET_BROADCAST;
auto* recorder = Recording::Recorder::GetRecorder(entity->GetObjectID()); auto* recorder = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (recorder != nullptr) { if (recorder != nullptr) {
recorder->AddRecord(new Recording::AnimationRecord(GeneralUtils::UTF16ToWTF8(animationName))); recorder->AddRecord(new Cinema::Recording::AnimationRecord(GeneralUtils::UTF16ToWTF8(animationName)));
} }
} }
@ -5368,10 +5368,10 @@ void GameMessages::HandleEquipItem(RakNet::BitStream* inStream, Entity* entity)
Game::entityManager->SerializeEntity(entity); Game::entityManager->SerializeEntity(entity);
auto* recorder = Recording::Recorder::GetRecorder(entity->GetObjectID()); auto* recorder = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (recorder != nullptr) { if (recorder != nullptr) {
recorder->AddRecord(new Recording::EquipRecord(item->GetLot())); recorder->AddRecord(new Cinema::Recording::EquipRecord(item->GetLot()));
} }
} }
@ -5394,10 +5394,10 @@ void GameMessages::HandleUnequipItem(RakNet::BitStream* inStream, Entity* entity
Game::entityManager->SerializeEntity(entity); Game::entityManager->SerializeEntity(entity);
auto* recorder = Recording::Recorder::GetRecorder(entity->GetObjectID()); auto* recorder = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (recorder != nullptr) { if (recorder != nullptr) {
recorder->AddRecord(new Recording::UnequipRecord(item->GetLot())); recorder->AddRecord(new Cinema::Recording::UnequipRecord(item->GetLot()));
} }
} }

View File

@ -5,6 +5,5 @@ set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp"
"Mail.cpp" "Mail.cpp"
"Preconditions.cpp" "Preconditions.cpp"
"SlashCommandHandler.cpp" "SlashCommandHandler.cpp"
"Recorder.cpp"
"ServerPreconditions.cpp" "ServerPreconditions.cpp"
"VanityUtilities.cpp" PARENT_SCOPE) "VanityUtilities.cpp" PARENT_SCOPE)

View File

@ -1,474 +0,0 @@
#include "Recorder.h"
#include "ControllablePhysicsComponent.h"
#include "GameMessages.h"
#include "InventoryComponent.h"
#include "../dWorldServer/ObjectIDManager.h"
#include "ChatPackets.h"
#include "EntityManager.h"
#include "EntityInfo.h"
#include "ServerPreconditions.hpp"
using namespace Recording;
std::unordered_map<LWOOBJID, Recorder*> m_Recorders = {};
Recorder::Recorder() {
this->m_StartTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());
this->m_IsRecording = false;
}
Recorder::~Recorder() {
}
void Recorder::AddRecord(Record* record)
{
if (!this->m_IsRecording) {
return;
}
Game::logger->Log("Recorder", "Adding record");
// Time since start of recording
record->m_Timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()) - this->m_StartTime;
this->m_MovementRecords.push_back(record);
}
void Recorder::Act(Entity* actor) {
Game::logger->Log("Recorder", "Acting %d steps", m_MovementRecords.size());
// Loop through all records
for (auto* record : m_MovementRecords) {
record->Act(actor);
}
}
Entity* Recording::Recorder::ActFor(Entity* actorTemplate, Entity* player) {
EntityInfo info;
info.lot = actorTemplate->GetLOT();
info.pos = actorTemplate->GetPosition();
info.rot = actorTemplate->GetRotation();
info.scale = 1;
info.spawner = nullptr;
info.spawnerID = player->GetObjectID();
info.spawnerNodeID = 0;
info.settings = {
new LDFData<std::vector<std::u16string>>(u"syncLDF", { u"custom_script_client" }),
new LDFData<std::u16string>(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua")
};
// Spawn it
auto* actor = Game::entityManager->CreateEntity(info);
// Hide the template from the player
ServerPreconditions::AddExcludeFor(player->GetObjectID(), actorTemplate->GetObjectID());
// Solo act for the player
ServerPreconditions::AddSoloActor(actor->GetObjectID(), player->GetObjectID());
Game::entityManager->ConstructEntity(actor);
Act(actor);
return actor;
}
void Recording::Recorder::StopActingFor(Entity* actor, Entity* actorTemplate, LWOOBJID playerID) {
// Remove the exclude for the player
ServerPreconditions::RemoveExcludeFor(playerID, actorTemplate->GetObjectID());
Game::entityManager->DestroyEntity(actor);
}
bool Recorder::IsRecording() const {
return this->m_IsRecording;
}
void Recorder::StartRecording(LWOOBJID actorID) {
const auto& it = m_Recorders.find(actorID);
// Delete the old recorder if it exists
if (it != m_Recorders.end()) {
delete it->second;
m_Recorders.erase(it);
}
Recorder* recorder = new Recorder();
m_Recorders.insert_or_assign(actorID, recorder);
recorder->m_IsRecording = true;
}
void Recorder::StopRecording(LWOOBJID actorID) {
auto iter = m_Recorders.find(actorID);
if (iter == m_Recorders.end()) {
return;
}
iter->second->m_IsRecording = false;
}
Recorder* Recorder::GetRecorder(LWOOBJID actorID) {
auto iter = m_Recorders.find(actorID);
if (iter == m_Recorders.end()) {
return nullptr;
}
return iter->second;
}
Recording::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;
this->velocity = velocity;
this->angularVelocity = angularVelocity;
this->onGround = onGround;
this->dirtyVelocity = dirtyVelocity;
this->dirtyAngularVelocity = dirtyAngularVelocity;
}
void Recording::MovementRecord::Act(Entity* actor) {
// Calculate the amount of seconds (as a float) since the start of the recording
float time = m_Timestamp.count() / 1000.0f;
auto r = *this;
actor->AddCallbackTimer(time, [actor, r] () {
auto* controllableComponent = actor->GetComponent<ControllablePhysicsComponent>();
if (controllableComponent) {
controllableComponent->SetPosition(r.position);
controllableComponent->SetRotation(r.rotation);
controllableComponent->SetVelocity(r.velocity);
controllableComponent->SetAngularVelocity(r.angularVelocity);
controllableComponent->SetIsOnGround(r.onGround);
controllableComponent->SetDirtyVelocity(r.dirtyVelocity);
controllableComponent->SetDirtyAngularVelocity(r.dirtyAngularVelocity);
}
Game::entityManager->SerializeEntity(actor);
});
}
void Recording::MovementRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("MovementRecord");
element->SetAttribute("x", position.x);
element->SetAttribute("y", position.y);
element->SetAttribute("z", position.z);
element->SetAttribute("qx", rotation.x);
element->SetAttribute("qy", rotation.y);
element->SetAttribute("qz", rotation.z);
element->SetAttribute("qw", rotation.w);
element->SetAttribute("vx", velocity.x);
element->SetAttribute("vy", velocity.y);
element->SetAttribute("vz", velocity.z);
element->SetAttribute("avx", angularVelocity.x);
element->SetAttribute("avy", angularVelocity.y);
element->SetAttribute("avz", angularVelocity.z);
element->SetAttribute("g", onGround);
element->SetAttribute("dv", dirtyVelocity);
element->SetAttribute("dav", dirtyAngularVelocity);
element->SetAttribute("t", m_Timestamp.count());
parent->InsertEndChild(element);
}
void Recording::MovementRecord::Deserialize(tinyxml2::XMLElement* element) {
position.x = element->FloatAttribute("x");
position.y = element->FloatAttribute("y");
position.z = element->FloatAttribute("z");
rotation.x = element->FloatAttribute("qx");
rotation.y = element->FloatAttribute("qy");
rotation.z = element->FloatAttribute("qz");
rotation.w = element->FloatAttribute("qw");
velocity.x = element->FloatAttribute("vx");
velocity.y = element->FloatAttribute("vy");
velocity.z = element->FloatAttribute("vz");
angularVelocity.x = element->FloatAttribute("avx");
angularVelocity.y = element->FloatAttribute("avy");
angularVelocity.z = element->FloatAttribute("avz");
onGround = element->BoolAttribute("g");
dirtyVelocity = element->BoolAttribute("dv");
dirtyAngularVelocity = element->BoolAttribute("dav");
m_Timestamp = std::chrono::milliseconds(element->Int64Attribute("t"));
}
Recording::SpeakRecord::SpeakRecord(const std::string& text) {
this->text = text;
}
void Recording::SpeakRecord::Act(Entity* actor) {
// Calculate the amount of seconds (as a float) since the start of the recording
float time = m_Timestamp.count() / 1000.0f;
auto r = *this;
actor->AddCallbackTimer(time, [actor, r] () {
GameMessages::SendNotifyClientZoneObject(
actor->GetObjectID(), u"sendToclient_bubble", 0, 0, actor->GetObjectID(), r.text, UNASSIGNED_SYSTEM_ADDRESS);
Game::entityManager->SerializeEntity(actor);
});
}
void Recording::SpeakRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("SpeakRecord");
element->SetAttribute("text", text.c_str());
element->SetAttribute("t", m_Timestamp.count());
parent->InsertEndChild(element);
}
void Recording::SpeakRecord::Deserialize(tinyxml2::XMLElement* element) {
text = element->Attribute("text");
m_Timestamp = std::chrono::milliseconds(element->Int64Attribute("t"));
}
Recording::AnimationRecord::AnimationRecord(const std::string& animation) {
this->animation = animation;
}
void Recording::AnimationRecord::Act(Entity* actor) {
// Calculate the amount of seconds (as a float) since the start of the recording
float time = m_Timestamp.count() / 1000.0f;
auto r = *this;
actor->AddCallbackTimer(time, [actor, r] () {
GameMessages::SendPlayAnimation(actor, GeneralUtils::ASCIIToUTF16(r.animation));
Game::entityManager->SerializeEntity(actor);
});
}
void Recording::AnimationRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("AnimationRecord");
element->SetAttribute("animation", animation.c_str());
element->SetAttribute("t", m_Timestamp.count());
parent->InsertEndChild(element);
}
void Recording::AnimationRecord::Deserialize(tinyxml2::XMLElement* element) {
animation = element->Attribute("animation");
m_Timestamp = std::chrono::milliseconds(element->Int64Attribute("t"));
}
Recording::EquipRecord::EquipRecord(LOT item) {
this->item = item;
}
void Recording::EquipRecord::Act(Entity* actor) {
// Calculate the amount of seconds (as a float) since the start of the recording
float time = m_Timestamp.count() / 1000.0f;
auto r = *this;
actor->AddCallbackTimer(time, [actor, r] () {
auto* inventoryComponent = actor->GetComponent<InventoryComponent>();
const LWOOBJID id = ObjectIDManager::Instance()->GenerateObjectID();
const auto& info = Inventory::FindItemComponent(r.item);
if (inventoryComponent) {
inventoryComponent->UpdateSlot(info.equipLocation, { id, r.item, 1, 0 });
}
Game::entityManager->SerializeEntity(actor);
});
}
void Recording::EquipRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("EquipRecord");
element->SetAttribute("item", item);
element->SetAttribute("t", m_Timestamp.count());
parent->InsertEndChild(element);
}
void Recording::EquipRecord::Deserialize(tinyxml2::XMLElement* element) {
item = element->IntAttribute("item");
m_Timestamp = std::chrono::milliseconds(element->Int64Attribute("t"));
}
Recording::UnequipRecord::UnequipRecord(LOT item) {
this->item = item;
}
void Recording::UnequipRecord::Act(Entity* actor) {
// Calculate the amount of seconds (as a float) since the start of the recording
float time = m_Timestamp.count() / 1000.0f;
auto r = *this;
actor->AddCallbackTimer(time, [actor, r] () {
auto* inventoryComponent = actor->GetComponent<InventoryComponent>();
const auto& info = Inventory::FindItemComponent(r.item);
if (inventoryComponent) {
inventoryComponent->RemoveSlot(info.equipLocation);
}
Game::entityManager->SerializeEntity(actor);
});
}
void Recording::UnequipRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("UnequipRecord");
element->SetAttribute("item", item);
element->SetAttribute("t", m_Timestamp.count());
parent->InsertEndChild(element);
}
void Recording::UnequipRecord::Deserialize(tinyxml2::XMLElement* element) {
item = element->IntAttribute("item");
m_Timestamp = std::chrono::milliseconds(element->Int64Attribute("t"));
}
void Recording::ClearEquippedRecord::Act(Entity* actor) {
// Calculate the amount of seconds (as a float) since the start of the recording
float time = m_Timestamp.count() / 1000.0f;
auto r = *this;
actor->AddCallbackTimer(time, [actor, r] () {
auto* inventoryComponent = actor->GetComponent<InventoryComponent>();
if (inventoryComponent) {
auto equipped = inventoryComponent->GetEquippedItems();
for (auto entry : equipped) {
inventoryComponent->RemoveSlot(entry.first);
}
}
Game::entityManager->SerializeEntity(actor);
});
}
void Recording::ClearEquippedRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) {
auto* element = document.NewElement("ClearEquippedRecord");
element->SetAttribute("t", m_Timestamp.count());
parent->InsertEndChild(element);
}
void Recording::ClearEquippedRecord::Deserialize(tinyxml2::XMLElement* element) {
m_Timestamp = std::chrono::milliseconds(element->Int64Attribute("t"));
}
void Recording::Recorder::SaveToFile(const std::string& filename) {
tinyxml2::XMLDocument document;
auto* root = document.NewElement("Recorder");
for (auto* record : m_MovementRecords) {
record->Serialize(document, root);
}
document.InsertFirstChild(root);
document.SaveFile(filename.c_str());
}
float Recording::Recorder::GetDuration() const {
// Return the highest timestamp
float duration = 0.0f;
for (auto* record : m_MovementRecords) {
duration = std::max(duration, record->m_Timestamp.count() / 1000.0f);
}
return duration;
}
Recorder* Recording::Recorder::LoadFromFile(const std::string& filename) {
tinyxml2::XMLDocument document;
if (document.LoadFile(filename.c_str()) != tinyxml2::XML_SUCCESS) {
return nullptr;
}
auto* root = document.FirstChildElement("Recorder");
if (!root) {
return nullptr;
}
Recorder* recorder = new Recorder();
for (auto* element = root->FirstChildElement(); element; element = element->NextSiblingElement()) {
const std::string name = element->Name();
if (name == "MovementRecord") {
MovementRecord* record = new MovementRecord();
record->Deserialize(element);
recorder->m_MovementRecords.push_back(record);
} else if (name == "SpeakRecord") {
SpeakRecord* record = new SpeakRecord();
record->Deserialize(element);
recorder->m_MovementRecords.push_back(record);
} else if (name == "AnimationRecord") {
AnimationRecord* record = new AnimationRecord();
record->Deserialize(element);
recorder->m_MovementRecords.push_back(record);
} else if (name == "EquipRecord") {
EquipRecord* record = new EquipRecord();
record->Deserialize(element);
recorder->m_MovementRecords.push_back(record);
} else if (name == "UnequipRecord") {
UnequipRecord* record = new UnequipRecord();
record->Deserialize(element);
recorder->m_MovementRecords.push_back(record);
} else if (name == "ClearEquippedRecord") {
ClearEquippedRecord* record = new ClearEquippedRecord();
record->Deserialize(element);
recorder->m_MovementRecords.push_back(record);
}
}
return recorder;
}
void Recording::Recorder::AddRecording(LWOOBJID actorID, Recorder* recorder) {
const auto& it = m_Recorders.find(actorID);
// Delete the old recorder if it exists
if (it != m_Recorders.end()) {
delete it->second;
m_Recorders.erase(it);
}
m_Recorders.insert_or_assign(actorID, recorder);
}

View File

@ -1,170 +0,0 @@
#pragma once
#include "Player.h"
#include "Game.h"
#include "EntityManager.h"
#include "tinyxml2.h"
#include <chrono>
namespace Recording
{
class Record
{
public:
virtual void Act(Entity* actor) = 0;
virtual void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) = 0;
virtual void Deserialize(tinyxml2::XMLElement* element) = 0;
std::chrono::milliseconds m_Timestamp;
};
class MovementRecord : public Record
{
public:
NiPoint3 position;
NiQuaternion rotation;
NiPoint3 velocity;
NiPoint3 angularVelocity;
bool onGround;
bool dirtyVelocity;
bool dirtyAngularVelocity;
MovementRecord() = default;
MovementRecord(
const NiPoint3& position,
const NiQuaternion& rotation,
const NiPoint3& velocity,
const NiPoint3& angularVelocity,
bool onGround, bool dirtyVelocity, bool dirtyAngularVelocity
);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class SpeakRecord : public Record
{
public:
std::string text;
SpeakRecord() = default;
SpeakRecord(const std::string& text);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class AnimationRecord : public Record
{
public:
std::string animation;
AnimationRecord() = default;
AnimationRecord(const std::string& animation);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class EquipRecord : public Record
{
public:
LOT item;
EquipRecord() = default;
EquipRecord(LOT item);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class UnequipRecord : public Record
{
public:
LOT item;
UnequipRecord() = default;
UnequipRecord(LOT item);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class ClearEquippedRecord : public Record
{
public:
ClearEquippedRecord() = default;
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class Recorder
{
public:
Recorder();
~Recorder();
void AddRecord(Record* record);
void Act(Entity* actor);
Entity* ActFor(Entity* actorTemplate, Entity* player);
void StopActingFor(Entity* actor, Entity* actorTemplate, LWOOBJID playerID);
bool IsRecording() const;
void SaveToFile(const std::string& filename);
float GetDuration() const;
static Recorder* LoadFromFile(const std::string& filename);
static void AddRecording(LWOOBJID actorID, Recorder* recorder);
static void StartRecording(LWOOBJID actorID);
static void StopRecording(LWOOBJID actorID);
static Recorder* GetRecorder(LWOOBJID actorID);
private:
std::vector<Record*> m_MovementRecords;
bool m_IsRecording;
std::chrono::milliseconds m_StartTime;
};
}

View File

@ -8,6 +8,10 @@
class Entity; class Entity;
/**
* @brief Contains a series of additions to the server-side checks for whether or not an entity
* is shown to the client or not.
*/
namespace ServerPreconditions namespace ServerPreconditions
{ {
@ -17,22 +21,89 @@ extern std::unordered_map<LWOOBJID, LWOOBJID> m_SoloActors;
extern std::unordered_map<LWOOBJID, std::unordered_set<LWOOBJID>> m_ExcludeForPlayer; extern std::unordered_map<LWOOBJID, std::unordered_set<LWOOBJID>> m_ExcludeForPlayer;
/**
* @brief Loads the preconditions from the given file.
*
* @param file The file to load the preconditions from.
*
* @section Example
* <Preconditions>
* <Entity lot="2097254">
* <Precondition>1006</Precondition>
* </Entity>
* <Entity lot="12261">
* <Precondition not="1">1006</Precondition>
* </Entity>
* </Preconditions>
*/
void LoadPreconditions(std::string file); void LoadPreconditions(std::string file);
/**
* @brief Checks the additional server-side preconditions for the given entity.
*
* @param target The entity to check the preconditions for.
* @param entity The entity to check the preconditions against (usually the player).
*
* @return Whether or not the entity passes the preconditions.
*/
bool CheckPreconditions(Entity* target, Entity* entity); bool CheckPreconditions(Entity* target, Entity* entity);
/**
* @brief Checks if a given entity is a solo actor.
*
* Solo actors are entities that are only shown to the client if they are acting for the player.
*/
bool IsSoloActor(LWOOBJID actor); bool IsSoloActor(LWOOBJID actor);
/**
* @brief Checks if a given entity is acting for another entity.
*
* @param actor The entity to check if it is acting for another entity.
* @param target The entity to check if the actor is acting for (usually the player).
*
* @return Whether or not the actor is acting for the target.
*/
bool IsActingFor(LWOOBJID actor, LWOOBJID target); bool IsActingFor(LWOOBJID actor, LWOOBJID target);
/**
* @brief Adds an entity to the list of solo actors.
*
* @param actor The entity to add to the list of solo actors.
* @param target The entity to add the actor to the list of solo actors for (usually the player).
*/
void AddSoloActor(LWOOBJID actor, LWOOBJID target); void AddSoloActor(LWOOBJID actor, LWOOBJID target);
/**
* @brief Removes an entity from the list of solo actors.
*
* @param actor The entity to remove from the list of solo actors.
*/
void RemoveSoloActor(LWOOBJID actor); void RemoveSoloActor(LWOOBJID actor);
/**
* @brief Adds an entity to the list of entities to exclude for another entity.
*
* @param player The entity to exclude the target for (usually the player).
* @param target The entity to exclude for the player.
*/
void AddExcludeFor(LWOOBJID player, LWOOBJID target); void AddExcludeFor(LWOOBJID player, LWOOBJID target);
/**
* @brief Removes an entity from the list of entities to exclude for another entity.
*
* @param player The entity to remove the target from the list of entities to exclude for (usually the player).
* @param target The entity to remove from the list of entities to exclude for the player.
*/
void RemoveExcludeFor(LWOOBJID player, LWOOBJID target); void RemoveExcludeFor(LWOOBJID player, LWOOBJID target);
/**
* @brief Checks if an entity is excluded for another entity.
*
* @param player The entity to check if the target is excluded for (usually the player).
* @param target The entity to check if it is excluded for the player.
*
* @return Whether or not the target is excluded for the player.
*/
bool IsExcludedFor(LWOOBJID player, LWOOBJID target); bool IsExcludedFor(LWOOBJID player, LWOOBJID target);
} }

View File

@ -88,6 +88,8 @@
#include "Recorder.h" #include "Recorder.h"
#include "ServerPreconditions.hpp" #include "ServerPreconditions.hpp"
#include "Prefab.h"
#include "Scene.h"
void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entity* entity, const SystemAddress& sysAddr) { void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entity* entity, const SystemAddress& sysAddr) {
auto commandCopy = command; auto commandCopy = command;
@ -946,7 +948,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
// Construct it // Construct it
Game::entityManager->ConstructEntity(actor); Game::entityManager->ConstructEntity(actor);
auto* record = Recording::Recorder::GetRecorder(entity->GetObjectID()); auto* record = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (record) { if (record) {
record->Act(actor); record->Act(actor);
@ -956,13 +958,27 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
} }
if (chatCommand == "record-start" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { if (chatCommand == "record-start" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) {
Recording::Recorder::StartRecording(entity->GetObjectID()); Cinema::Recording::Recorder::StartRecording(entity->GetObjectID());
auto* record = Recording::Recorder::GetRecorder(entity->GetObjectID()); auto* record = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (record) { if (record) {
if (args.size() > 0 && args[0] == "clear") { if (args.size() > 0 && args[0] == "clear") {
record->AddRecord(new Recording::ClearEquippedRecord()); record->AddRecord(new Cinema::Recording::ClearEquippedRecord());
}
else if (args.size() > 0 && args[0] == "copy") {
record->AddRecord(new Cinema::Recording::ClearEquippedRecord());
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent) {
for (const auto& [k, v] : inventoryComponent->GetEquippedItems()) {
auto* equipmentRecord = new Cinema::Recording::EquipRecord();
equipmentRecord->item = v.lot;
record->AddRecord(equipmentRecord);
}
}
} }
} else { } else {
Game::logger->Log("SlashCommandHandler", "Failed to get recorder for objectID: %llu", entity->GetObjectID()); Game::logger->Log("SlashCommandHandler", "Failed to get recorder for objectID: %llu", entity->GetObjectID());
@ -970,11 +986,11 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
} }
if (chatCommand == "record-stop" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { if (chatCommand == "record-stop" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) {
Recording::Recorder::StopRecording(entity->GetObjectID()); Cinema::Recording::Recorder::StopRecording(entity->GetObjectID());
} }
if (chatCommand == "record-save" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() == 1) { if (chatCommand == "record-save" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() == 1) {
auto* record = Recording::Recorder::GetRecorder(entity->GetObjectID()); auto* record = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (record) { if (record) {
record->SaveToFile(args[0]); record->SaveToFile(args[0]);
@ -984,15 +1000,57 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
} }
if (chatCommand == "record-load" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() == 1) { if (chatCommand == "record-load" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() == 1) {
auto* record = Recording::Recorder::LoadFromFile(args[0]); auto* record = Cinema::Recording::Recorder::LoadFromFile(args[0]);
if (record) { if (record) {
Recording::Recorder::AddRecording(entity->GetObjectID(), record); Cinema::Recording::Recorder::AddRecording(entity->GetObjectID(), record);
} else { } else {
Game::logger->Log("SlashCommandHandler", "Failed to load recording from file: %s", args[0].c_str()); Game::logger->Log("SlashCommandHandler", "Failed to load recording from file: %s", args[0].c_str());
} }
} }
if (chatCommand == "prefab-spawn" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) {
const auto& prefab = Cinema::Prefab::LoadFromFile(args[0]);
float scale = 1.0f;
if (args.size() >= 2) {
if (!GeneralUtils::TryParse(args[1], scale)) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid scale.");
return;
}
}
size_t id = prefab.Instantiate(entity->GetPosition(), scale);
ChatPackets::SendSystemMessage(sysAddr, u"Spawned prefab with ID: " + GeneralUtils::to_u16string(id));
return;
}
if (chatCommand == "prefab-destroy" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() == 1) {
size_t id;
if (!GeneralUtils::TryParse(args[0], id)) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid prefab ID.");
return;
}
Cinema::Prefab::DestroyInstance(id);
ChatPackets::SendSystemMessage(sysAddr, u"Destroyed prefab with ID: " + GeneralUtils::to_u16string(id));
return;
}
if (chatCommand == "scene-act" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) {
auto& scene = Cinema::Scene::LoadFromFile(args[0]);
scene.Act(entity);
return;
}
if ((chatCommand == "teleport" || chatCommand == "tele") && entity->GetGMLevel() >= eGameMasterLevel::JUNIOR_MODERATOR) { if ((chatCommand == "teleport" || chatCommand == "tele") && entity->GetGMLevel() >= eGameMasterLevel::JUNIOR_MODERATOR) {
NiPoint3 pos{}; NiPoint3 pos{};
if (args.size() == 3) { if (args.size() == 3) {
@ -2196,6 +2254,68 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
ChatPackets::SendSystemMessage(sysAddr, u"Trigger: " + (GeneralUtils::to_u16string(trigger->id))); ChatPackets::SendSystemMessage(sysAddr, u"Trigger: " + (GeneralUtils::to_u16string(trigger->id)));
} }
} }
} else if (args[1] == "-rotate" && args.size() >= 4) {
const auto& rx = args[2];
const auto& ry = args[3];
const auto& rz = args[4];
float x, y, z;
if (!GeneralUtils::TryParse(rx, x) || !GeneralUtils::TryParse(ry, y) || !GeneralUtils::TryParse(rz, z)) {
return;
}
// Degrees to radia
x *= 0.0174533f;
y *= 0.0174533f;
z *= 0.0174533f;
const auto rotation = NiQuaternion::FromEulerAngles(NiPoint3(x, y, z));
closest->SetRotation(rotation);
Game::entityManager->SerializeEntity(closest);
} else if (args[1] == "-translate" && args.size() >= 4) {
const auto& tx = args[2];
const auto& ty = args[3];
const auto& tz = args[4];
float x, y, z;
if (!GeneralUtils::TryParse(tx, x) || !GeneralUtils::TryParse(ty, y) || !GeneralUtils::TryParse(tz, z)) {
return;
}
const auto translation = NiPoint3(x, y, z);
closest->SetPosition(closest->GetPosition() + translation);
Game::entityManager->SerializeEntity(closest);
} else if (args[1] == "-warp" && args.size() >= 4) {
const auto& tx = args[2];
const auto& ty = args[3];
const auto& tz = args[4];
float x, y, z;
if (!GeneralUtils::TryParse(tx, x) || !GeneralUtils::TryParse(ty, y) || !GeneralUtils::TryParse(tz, z)) {
return;
}
const auto translation = NiPoint3(x, y, z);
closest->SetPosition(translation);
Game::entityManager->SerializeEntity(closest);
} else if (args[1] == "-fx" && args.size() >= 4) {
int32_t effectID = 0;
if (!GeneralUtils::TryParse(args[2], effectID)) {
return;
}
// FIXME: use fallible ASCIIToUTF16 conversion, because non-ascii isn't valid anyway
GameMessages::SendPlayFXEffect(closest->GetObjectID(), effectID, GeneralUtils::ASCIIToUTF16(args[3]), args[4]);
} }
} }
} }

View File

@ -84,10 +84,10 @@ void ClientPackets::HandleChatMessage(const SystemAddress& sysAddr, Packet* pack
LOG("%s: %s", playerName.c_str(), sMessage.c_str()); LOG("%s: %s", playerName.c_str(), sMessage.c_str());
ChatPackets::SendChatMessage(sysAddr, chatChannel, playerName, user->GetLoggedInChar(), isMythran, message); ChatPackets::SendChatMessage(sysAddr, chatChannel, playerName, user->GetLoggedInChar(), isMythran, message);
auto* recorder = Recording::Recorder::GetRecorder(user->GetLoggedInChar()); auto* recorder = Cinema::Recording::Recorder::GetRecorder(user->GetLoggedInChar());
if (recorder != nullptr) { if (recorder != nullptr) {
recorder->AddRecord(new Recording::SpeakRecord(sMessage)); recorder->AddRecord(new Cinema::Recording::SpeakRecord(sMessage));
} }
} }
@ -243,10 +243,10 @@ void ClientPackets::HandleClientPositionUpdate(const SystemAddress& sysAddr, Pac
comp->SetAngularVelocity(angVelocity); comp->SetAngularVelocity(angVelocity);
comp->SetDirtyAngularVelocity(angVelocityFlag); comp->SetDirtyAngularVelocity(angVelocityFlag);
auto* recorder = Recording::Recorder::GetRecorder(entity->GetObjectID()); auto* recorder = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (recorder != nullptr) { if (recorder != nullptr) {
recorder->AddRecord(new Recording::MovementRecord( recorder->AddRecord(new Cinema::Recording::MovementRecord(
position, position,
rotation, rotation,
velocity, velocity,

View File

@ -22,7 +22,7 @@ void DukeDialogueGlowingBrick::OnMissionDialogueOK(Entity* self, Entity* target,
return; return;
} }
auto* recorder = Recording::Recorder::LoadFromFile("DukeGlowing.xml"); auto* recorder = Cinema::Recording::Recorder::LoadFromFile("DukeGlowing.xml");
if (recorder == nullptr) { if (recorder == nullptr) {
return; return;