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.
This commit is contained in:
David Markowitz 2025-06-17 15:34:52 -07:00 committed by GitHub
parent 2f315d9288
commit 04487efa25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 61 additions and 1 deletions

View File

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

View File

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

View File

@ -817,3 +817,14 @@ void PropertyManagementComponent::SetOwnerId(const LWOOBJID value) {
const std::map<LWOOBJID, LWOOBJID>& 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<ModelComponent>();
if (!modelComponent) continue;
modelComponent->OnChatMessageReceived(sMessage);
}
}

View File

@ -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<LWOOBJID, LWOOBJID> models = {};
std::map<LWOOBJID /* ObjectID */, LWOOBJID /* SpawnerID */> models = {};
/**
* The name of this property

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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