From 04487efa2541ec99996fb0e4f5572c352b036218 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:34:52 -0700 Subject: [PATCH] feat: add chat behaviors (#1818) * Move in all directions is functional * feat: add movement behaviors the following behaviors will function MoveRight MoveLeft FlyUp FlyDown MoveForward MoveBackward The behavior of the behaviors is once a move in an axis is active, that behavior must finish its movement before another one on that axis can do another movement on it. * feat: add chat behaviors Tested that models can correctly send chat messages, silently and publically. Tested as well that the filter is used by the client for behaviors and added a security check to not broadcast messages that fail the check if words are removed. --- dGame/dComponents/ModelComponent.cpp | 4 +++ dGame/dComponents/ModelComponent.h | 2 ++ .../PropertyManagementComponent.cpp | 11 ++++++++ .../dComponents/PropertyManagementComponent.h | 4 ++- dGame/dPropertyBehaviors/CMakeLists.txt | 1 + dGame/dPropertyBehaviors/PropertyBehavior.cpp | 4 +++ dGame/dPropertyBehaviors/PropertyBehavior.h | 1 + dGame/dPropertyBehaviors/State.cpp | 4 +++ dGame/dPropertyBehaviors/State.h | 2 ++ dGame/dPropertyBehaviors/Strip.cpp | 26 +++++++++++++++++++ dGame/dPropertyBehaviors/Strip.h | 2 ++ dWorldServer/WorldServer.cpp | 1 + 12 files changed, 61 insertions(+), 1 deletion(-) diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index d1e19cc8..250365b2 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -237,3 +237,7 @@ bool ModelComponent::TrySetVelocity(const NiPoint3& velocity) const { void ModelComponent::SetVelocity(const NiPoint3& velocity) const { m_Parent->SetVelocity(velocity); } + +void ModelComponent::OnChatMessageReceived(const std::string& sMessage) { + for (auto& behavior : m_Behaviors) behavior.OnChatMessageReceived(sMessage); +} diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h index 8d430d2e..b67a53d2 100644 --- a/dGame/dComponents/ModelComponent.h +++ b/dGame/dComponents/ModelComponent.h @@ -138,6 +138,8 @@ public: // Force sets the velocity to a value. void SetVelocity(const NiPoint3& velocity) const; + + void OnChatMessageReceived(const std::string& sMessage); private: // Number of Actions that are awaiting an UnSmash to finish. uint32_t m_NumActiveUnSmash{}; diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 5dba7c19..581e28f3 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -817,3 +817,14 @@ void PropertyManagementComponent::SetOwnerId(const LWOOBJID value) { const std::map& PropertyManagementComponent::GetModels() const { return models; } + +void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMessage) const { + for (const auto& modelID : models | std::views::keys) { + auto* const model = Game::entityManager->GetEntity(modelID); + if (!model) continue; + auto* const modelComponent = model->GetComponent(); + if (!modelComponent) continue; + + modelComponent->OnChatMessageReceived(sMessage); + } +} diff --git a/dGame/dComponents/PropertyManagementComponent.h b/dGame/dComponents/PropertyManagementComponent.h index 4dd482ef..708f0f14 100644 --- a/dGame/dComponents/PropertyManagementComponent.h +++ b/dGame/dComponents/PropertyManagementComponent.h @@ -164,6 +164,8 @@ public: LWOOBJID GetId() const noexcept { return propertyId; } + + void OnChatMessageReceived(const std::string& sMessage) const; private: /** * This @@ -193,7 +195,7 @@ private: /** * The models that are placed on this property */ - std::map models = {}; + std::map models = {}; /** * The name of this property diff --git a/dGame/dPropertyBehaviors/CMakeLists.txt b/dGame/dPropertyBehaviors/CMakeLists.txt index 3e03ba1d..c33c5860 100644 --- a/dGame/dPropertyBehaviors/CMakeLists.txt +++ b/dGame/dPropertyBehaviors/CMakeLists.txt @@ -20,6 +20,7 @@ target_include_directories(dPropertyBehaviors PUBLIC "." "ControlBehaviorMessage "${PROJECT_SOURCE_DIR}/dGame/dUtilities" # ObjectIdManager.h "${PROJECT_SOURCE_DIR}/dGame/dGameMessages" # GameMessages.h "${PROJECT_SOURCE_DIR}/dGame/dComponents" # ModelComponent.h + "${PROJECT_SOURCE_DIR}/dChatFilter" # dChatFilter.h ) target_precompile_headers(dPropertyBehaviors REUSE_FROM dGameBase) diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.cpp b/dGame/dPropertyBehaviors/PropertyBehavior.cpp index c4c9d359..1a9bb867 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.cpp +++ b/dGame/dPropertyBehaviors/PropertyBehavior.cpp @@ -172,3 +172,7 @@ void PropertyBehavior::Deserialize(const tinyxml2::XMLElement& behavior) { void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) { for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent); } + +void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage) { + for (auto& state : m_States | std::views::values) state.OnChatMessageReceived(sMessage); +} diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.h b/dGame/dPropertyBehaviors/PropertyBehavior.h index 5232d7f0..67df78df 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.h +++ b/dGame/dPropertyBehaviors/PropertyBehavior.h @@ -34,6 +34,7 @@ public: void Deserialize(const tinyxml2::XMLElement& behavior); void Update(float deltaTime, ModelComponent& modelComponent); + void OnChatMessageReceived(const std::string& sMessage); private: // The current active behavior state. Behaviors can only be in ONE state at a time. diff --git a/dGame/dPropertyBehaviors/State.cpp b/dGame/dPropertyBehaviors/State.cpp index 0655c78c..7f95c2a0 100644 --- a/dGame/dPropertyBehaviors/State.cpp +++ b/dGame/dPropertyBehaviors/State.cpp @@ -166,3 +166,7 @@ void State::Deserialize(const tinyxml2::XMLElement& state) { void State::Update(float deltaTime, ModelComponent& modelComponent) { for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent); } + +void State::OnChatMessageReceived(const std::string& sMessage) { + for (auto& strip : m_Strips) strip.OnChatMessageReceived(sMessage); +} diff --git a/dGame/dPropertyBehaviors/State.h b/dGame/dPropertyBehaviors/State.h index ab6d76a4..580b647d 100644 --- a/dGame/dPropertyBehaviors/State.h +++ b/dGame/dPropertyBehaviors/State.h @@ -22,6 +22,8 @@ public: void Deserialize(const tinyxml2::XMLElement& state); void Update(float deltaTime, ModelComponent& modelComponent); + + void OnChatMessageReceived(const std::string& sMessage); private: // The strips contained within this state. diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index 9ceae743..d42c1f91 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -5,8 +5,12 @@ #include "tinyxml2.h" #include "dEntity/EntityInfo.h" #include "ModelComponent.h" +#include "ChatPackets.h" +#include "PropertyManagementComponent.h" #include "PlayerManager.h" +#include "dChatFilter.h" + #include "DluAssert.h" template <> @@ -103,6 +107,16 @@ void Strip::HandleMsg(GameMessages::ResetModelToDefaults& msg) { m_PreviousFramePosition = NiPoint3Constant::ZERO; } +void Strip::OnChatMessageReceived(const std::string& sMessage) { + if (m_PausedTime > 0.0f || !HasMinimumActions()) return; + + const auto& nextAction = GetNextAction(); + if (nextAction.GetValueParameterString() == sMessage) { + IncrementAction(); + m_WaitingForAction = false; + } +} + void Strip::IncrementAction() { if (m_Actions.empty()) return; m_NextActionIndex++; @@ -131,6 +145,7 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { auto& entity = *modelComponent.GetParent(); auto& nextAction = GetNextAction(); auto number = nextAction.GetValueParameterDouble(); + auto valueStr = nextAction.GetValueParameterString(); auto numberAsInt = static_cast(number); auto nextActionType = GetNextAction().GetType(); @@ -183,6 +198,14 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { m_PausedTime = number; } else if (nextActionType == "Wait") { m_PausedTime = number; + } else if (nextActionType == "Chat") { + bool isOk = Game::chatFilter->IsSentenceOkay(valueStr.data(), eGameMasterLevel::CIVILIAN).empty(); + // In case a word is removed from the whitelist after it was approved + const auto modelName = "%[Objects_" + std::to_string(entity.GetLOT()) + "_name]"; + if (isOk) ChatPackets::SendChatMessage(UNASSIGNED_SYSTEM_ADDRESS, 12, modelName, entity.GetObjectID(), false, GeneralUtils::ASCIIToUTF16(valueStr)); + PropertyManagementComponent::Instance()->OnChatMessageReceived(valueStr.data()); + } else if (nextActionType == "PrivateMessage") { + PropertyManagementComponent::Instance()->OnChatMessageReceived(valueStr.data()); } else if (nextActionType == "PlaySound") { GameMessages::PlayBehaviorSound sound; sound.target = modelComponent.GetParent()->GetObjectID(); @@ -304,6 +327,9 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) { Game::entityManager->SerializeEntity(entity); m_WaitingForAction = true; + } else if (nextAction.GetType() == "OnChat") { + Game::entityManager->SerializeEntity(entity); + m_WaitingForAction = true; } } else { // should be a normal block ProcNormalAction(deltaTime, modelComponent); diff --git a/dGame/dPropertyBehaviors/Strip.h b/dGame/dPropertyBehaviors/Strip.h index a0085f9e..984a145c 100644 --- a/dGame/dPropertyBehaviors/Strip.h +++ b/dGame/dPropertyBehaviors/Strip.h @@ -40,6 +40,8 @@ public: // 2 actions are required for strips to work bool HasMinimumActions() const { return m_Actions.size() >= 2; } + + void OnChatMessageReceived(const std::string& sMessage); private: // Indicates this Strip is waiting for an action to be taken upon it to progress to its actions bool m_WaitingForAction{ false }; diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 9e9581b7..a1fc8046 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1367,6 +1367,7 @@ void HandlePacket(Packet* packet) { std::string sMessage = GeneralUtils::UTF16ToWTF8(chatMessage.message); LOG("%s: %s", playerName.c_str(), sMessage.c_str()); ChatPackets::SendChatMessage(packet->systemAddress, chatMessage.chatChannel, playerName, user->GetLoggedInChar(), isMythran, chatMessage.message); + PropertyManagementComponent::Instance()->OnChatMessageReceived(sMessage); } break;