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

@@ -5,6 +5,5 @@ set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp"
"Mail.cpp"
"Preconditions.cpp"
"SlashCommandHandler.cpp"
"Recorder.cpp"
"ServerPreconditions.cpp"
"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;
/**
* @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
{
@@ -17,22 +21,89 @@ extern std::unordered_map<LWOOBJID, LWOOBJID> m_SoloActors;
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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
}

View File

@@ -88,6 +88,8 @@
#include "Recorder.h"
#include "ServerPreconditions.hpp"
#include "Prefab.h"
#include "Scene.h"
void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entity* entity, const SystemAddress& sysAddr) {
auto commandCopy = command;
@@ -946,7 +948,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
// Construct it
Game::entityManager->ConstructEntity(actor);
auto* record = Recording::Recorder::GetRecorder(entity->GetObjectID());
auto* record = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (record) {
record->Act(actor);
@@ -956,13 +958,27 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
}
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 (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 {
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) {
Recording::Recorder::StopRecording(entity->GetObjectID());
Cinema::Recording::Recorder::StopRecording(entity->GetObjectID());
}
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) {
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) {
auto* record = Recording::Recorder::LoadFromFile(args[0]);
auto* record = Cinema::Recording::Recorder::LoadFromFile(args[0]);
if (record) {
Recording::Recorder::AddRecording(entity->GetObjectID(), record);
Cinema::Recording::Recorder::AddRecording(entity->GetObjectID(), record);
} else {
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) {
NiPoint3 pos{};
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)));
}
}
} 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]);
}
}
}