diff --git a/CHANGELOG.md b/CHANGELOG.md index af26046145e..1b15f08ca38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). https://github.com/nwnxee/unified/compare/build8193.16...HEAD ### Added +- Creature: targeted messages can now be sent on party or dm channel with updated SendMessage() - Events: added skippable Acquire events to ItemEvents - Events: added skippable Disarm event to CombatEvents - Events: added `ACTION_RESULT` to Feat/Skill/Lock events for use in the _AFTER - Events: added Spell Interruption events to SpellEvents +- Events: added skippable Chat events - Tweaks: `NWNX_TWEAKS_HIDE_PLAYERS_ON_CHAR_LIST` - Tweaks: `NWNX_TWEAKS_FIX_ARMOR_DEX_BONUS_UNDER_ONE` - Tweaks: `NWNX_TWEAKS_FIX_ITEM_NULLPTR_IN_CITEMREPOSITORY` @@ -28,9 +30,11 @@ The following plugins were added: - Creature: {Get|Set}WalkAnimation() - Creature: SetAttackRollOverride() - Creature: SetParryAllAttacks() +- Creature: SendMessage() - Feat: SetFeatModifier() - Object: GetCurrentAnimation() - Player: AddCustomJournalEntry() and GetJournalEntry() +- Player: {Get|Set}ChatHearingDistance() - Race: SetFavoredEnemyFeat() - Util: GetScriptParamIsSet() @@ -46,6 +50,7 @@ The following plugins were added: - We now only allow builds with the `CMAKE_BUILD_TYPE=RelWithDebInfo` configuration. `Debug` builds produce unexpected behaviour and `Release` builds are generally unnecessary and mess with Assert functionality. ### Deprecated +- Chat: NWNX_Chat has been deprecated and handled now through Chat Events (NWNX_ON_CHAT_SEND_*), NWNX_Creature_SendMessage() and NWNX_Player_{Get|Set}ChatHearingDistance(). - Tweaks: `NWNX_TWEAKS_HIDE_DMS_ON_CHAR_LIST` has been deprecated, use `NWNX_TWEAKS_HIDE_PLAYERS_ON_CHAR_LIST` now ### Removed diff --git a/Core/NWScript/nwnx_consts.nss b/Core/NWScript/nwnx_consts.nss index d89a88a547d..d449b31e4c4 100644 --- a/Core/NWScript/nwnx_consts.nss +++ b/Core/NWScript/nwnx_consts.nss @@ -1,9 +1,30 @@ /// @ingroup nwnx /// @addtogroup consts NWNX Constants /// @brief Provides various NWScript <-> NWNX Constants Translation Table functions +/// as well as defining shared constants for multiple plugins /// @{ /// @file nwnx_consts.nss +/// @name Chat Channels +/// @anchor chat_channels +/// +/// Constants defining the various chat channels. +/// @{ +const int NWNX_CHAT_CHANNEL_PLAYER_TALK = 1; +const int NWNX_CHAT_CHANNEL_PLAYER_SHOUT = 2; +const int NWNX_CHAT_CHANNEL_PLAYER_WHISPER = 3; +const int NWNX_CHAT_CHANNEL_PLAYER_TELL = 4; +const int NWNX_CHAT_CHANNEL_SERVER_MSG = 5; +const int NWNX_CHAT_CHANNEL_PLAYER_PARTY = 6; +const int NWNX_CHAT_CHANNEL_PLAYER_DM = 14; +const int NWNX_CHAT_CHANNEL_DM_TALK = 17; +const int NWNX_CHAT_CHANNEL_DM_SHOUT = 18; +const int NWNX_CHAT_CHANNEL_DM_WHISPER = 19; +const int NWNX_CHAT_CHANNEL_DM_TELL = 20; +const int NWNX_CHAT_CHANNEL_DM_PARTY = 22; +const int NWNX_CHAT_CHANNEL_DM_DM = 30; +/// @} + /// @brief Translates ANIMATION_LOOPING_* and ANIMATION_FIREFORGET_* constants to their NWNX equivalent. /// @param nAnimation The nwn animation constant /// @return The NWNX equivalent of the constant diff --git a/Plugins/Chat/Chat.cpp b/Plugins/Chat/Chat.cpp index db1dc59b4b1..5e75e343947 100644 --- a/Plugins/Chat/Chat.cpp +++ b/Plugins/Chat/Chat.cpp @@ -60,8 +60,9 @@ Chat::Chat(Services::ProxyServiceList* services) m_hearingDistances[Constants::ChatChannel::DmWhisper] = 3.0f; m_hearingDistances[Constants::ChatChannel::PlayerWhisper] = 3.0f; m_customHearingDistances = false; - - m_hook = GetServices()->m_hooks->RequestExclusiveHook(&Chat::SendServerToPlayerChatMessage); + LOG_INFO("NWNX_Chat has been deprecated. Please use NWNX_Creature_SendMessage(), " + "NWNX_Player_{Get|Set}ChatHearingDistance() and the NWNX_ON_CHAT_SEND_* event for chat functionality."); + //m_hook = GetServices()->m_hooks->RequestExclusiveHook(&Chat::SendServerToPlayerChatMessage); } Chat::~Chat() @@ -101,6 +102,7 @@ void Chat::SendServerToPlayerChatMessage(CNWSMessage* thisPtr, Constants::ChatCh if (plugin.m_depth > 0 || !plugin.m_skipMessage) { + // TODO: Only keep this functionality after deprecation period, it may make sense to just put it in Player if (g_plugin->m_customHearingDistances) { auto server = Globals::AppManager()->m_pServerExoApp; @@ -204,7 +206,7 @@ Events::ArgumentStack Chat::SendMessage(Events::ArgumentStack&& args) if (playerId != Constants::PLAYERID_INVALIDID) { bool sentMessage = false; - CNWSMessage* messageDispatch = static_cast(Globals::AppManager()->m_pServerExoApp->GetNWSMessage()); + auto* messageDispatch = static_cast(Globals::AppManager()->m_pServerExoApp->GetNWSMessage()); if (hasManualPlayerId) { @@ -236,6 +238,16 @@ Events::ArgumentStack Chat::SendMessage(Events::ArgumentStack&& args) messageDispatch->SendServerToPlayerChat_DM_Whisper(playerId, speaker, message.c_str()); sentMessage = true; } + else if (channel == Constants::ChatChannel::PlayerParty) + { + messageDispatch->SendServerToPlayerChat_Party(playerId, speaker, message.c_str()); + sentMessage = true; + } + else if (channel == Constants::ChatChannel::DmParty) + { + messageDispatch->SendServerToPlayerChat_Party(playerId, speaker, message.c_str()); + sentMessage = true; + } } if (!sentMessage) @@ -249,6 +261,7 @@ Events::ArgumentStack Chat::SendMessage(Events::ArgumentStack&& args) return Events::Arguments(retVal); } +// TODO: Remove next six functions after deprecation period Events::ArgumentStack Chat::RegisterChatScript(Events::ArgumentStack&& args) { m_chatScript = Events::ExtractArgument(args); @@ -281,6 +294,7 @@ Events::ArgumentStack Chat::GetTarget(Events::ArgumentStack&&) return Events::Arguments(m_activeTargetObjectId); } +// TODO Should these be moved to Player after deprecation period? Events::ArgumentStack Chat::SetChatHearingDistance(Events::ArgumentStack&& args) { const auto distance = Services::Events::ExtractArgument(args); diff --git a/Plugins/Chat/NWScript/nwnx_chat.nss b/Plugins/Chat/NWScript/nwnx_chat.nss index 90ca95e4910..a067188dc65 100644 --- a/Plugins/Chat/NWScript/nwnx_chat.nss +++ b/Plugins/Chat/NWScript/nwnx_chat.nss @@ -3,29 +3,10 @@ /// @{ /// @file nwnx_chat.nss #include "nwnx" +#include "nwnx_consts" const string NWNX_Chat = "NWNX_Chat"; ///< @private -/// @name Chat Channels -/// @anchor chat_channels -/// -/// Constants defining the various chat channels. -/// @{ -const int NWNX_CHAT_CHANNEL_PLAYER_TALK = 1; -const int NWNX_CHAT_CHANNEL_PLAYER_SHOUT = 2; -const int NWNX_CHAT_CHANNEL_PLAYER_WHISPER = 3; -const int NWNX_CHAT_CHANNEL_PLAYER_TELL = 4; -const int NWNX_CHAT_CHANNEL_SERVER_MSG = 5; -const int NWNX_CHAT_CHANNEL_PLAYER_PARTY = 6; -const int NWNX_CHAT_CHANNEL_PLAYER_DM = 14; -const int NWNX_CHAT_CHANNEL_DM_TALK = 17; -const int NWNX_CHAT_CHANNEL_DM_SHOUT = 18; -const int NWNX_CHAT_CHANNEL_DM_WHISPER = 19; -const int NWNX_CHAT_CHANNEL_DM_TELL = 20; -const int NWNX_CHAT_CHANNEL_DM_PARTY = 22; -const int NWNX_CHAT_CHANNEL_DM_DM = 30; -/// @} - /// @brief Sends a chat message. /// @remark If no target is provided, then it broadcasts to all eligible targets. /// @param channel The @ref chat_channels "channel" to send the message. @@ -33,35 +14,42 @@ const int NWNX_CHAT_CHANNEL_DM_DM = 30; /// @param sender The sender of the message. /// @param target The receiver of the message. /// @return TRUE if successful, FALSE otherwise. +/// @deprecated Please use NWNX_Creature_SendMessage() int NWNX_Chat_SendMessage(int channel, string message, object sender = OBJECT_SELF, object target = OBJECT_INVALID); /// @brief Registers the script which receives all chat messages. /// @note If a script was previously registered, this one will take over. /// @param script The script name to handle the chat events. +/// @deprecated Please use the events system (NWNX_ON_CHAT_SEND_*) void NWNX_Chat_RegisterChatScript(string script); /// @brief Skips a chat message /// @note Must be called from a chat or system script handler. +/// @deprecated Please use the events system (NWNX_ON_CHAT_SEND_*) and NWNX_Events_SkipEvent() void NWNX_Chat_SkipMessage(); /// @brief Gets the chat @ref chat_channels "channel". /// @note Must be called from a chat or system script handler. /// @return The @ref chat_channels "channel" the message is sent. +/// @deprecated Please use the events system (NWNX_ON_CHAT_SEND_*) and NWNX_Events_GetEventData("CHANNEL") int NWNX_Chat_GetChannel(); /// @brief Gets the message. /// @note Must be called from a chat or system script handler. /// @return The message sent. +/// @deprecated Please use the events system (NWNX_ON_CHAT_SEND_*) and NWNX_Events_GetEventData("MESSAGE") string NWNX_Chat_GetMessage(); /// @brief Gets the sender of the message. /// @note Must be called from a chat or system script handler. /// @return The object sending the message. +/// @deprecated Please use the events system (NWNX_ON_CHAT_SEND_*) OBJECT_SELF object NWNX_Chat_GetSender(); /// @brief Gets the target of the message. /// @note Must be called from an chat or system script handler. /// @return The target of the message or OBJECT_INVALID if no target. +/// @deprecated Please use the events system (NWNX_ON_CHAT_SEND_*) and NWNX_Events_GetEventData("TARGET") object NWNX_Chat_GetTarget(); /// @brief Sets the distance with which the player hears talks or whispers. @@ -80,79 +68,89 @@ float NWNX_Chat_GetChatHearingDistance(object listener = OBJECT_INVALID, int cha int NWNX_Chat_SendMessage(int channel, string message, object sender = OBJECT_SELF, object target = OBJECT_INVALID) { + WriteTimestampedLogEntry("NWNX_Chat: NWNX_Chat_SendMessage() is deprecated. Please use NWNX_Creature_SendMessage() now. Note the argument order has changed"); string sFunc = "SendMessage"; + NWNX_PushArgumentObject("NWNX_Creature", sFunc, target); + NWNX_PushArgumentInt("NWNX_Creature", sFunc, channel); + NWNX_PushArgumentString("NWNX_Creature", sFunc, message); + NWNX_PushArgumentObject("NWNX_Creature", sFunc, sender); + NWNX_CallFunction("NWNX_Creature", sFunc); - NWNX_PushArgumentObject(NWNX_Chat, sFunc, target); - NWNX_PushArgumentObject(NWNX_Chat, sFunc, sender); - NWNX_PushArgumentString(NWNX_Chat, sFunc, message); - NWNX_PushArgumentInt(NWNX_Chat, sFunc, channel); - NWNX_CallFunction(NWNX_Chat, sFunc); - return NWNX_GetReturnValueInt(NWNX_Chat, sFunc); + return NWNX_GetReturnValueInt("NWNX_Creature", sFunc); } void NWNX_Chat_RegisterChatScript(string script) { - string sFunc = "RegisterChatScript"; + WriteTimestampedLogEntry("NWNX_Chat: RegisterChatScript() is deprecated. Please use the event system (NWNX_ON_CHAT_SEND_*)"); - NWNX_PushArgumentString(NWNX_Chat, sFunc, script); - NWNX_CallFunction(NWNX_Chat, sFunc); + NWNX_PushArgumentString("NWNX_Events", "SubscribeEvent", script); + NWNX_PushArgumentString("NWNX_Events", "SubscribeEvent", "NWNX_ON_CHAT_SEND_BEFORE"); + NWNX_CallFunction("NWNX_Events", "SubscribeEvent"); } void NWNX_Chat_SkipMessage() { - string sFunc = "SkipMessage"; + WriteTimestampedLogEntry("NWNX_Chat: SkipMessage() is deprecated. Please use the event system (NWNX_ON_CHAT_SEND_*)"); - NWNX_CallFunction(NWNX_Chat, sFunc); + NWNX_CallFunction("NWNX_Events", "SkipEvent"); } int NWNX_Chat_GetChannel() { - string sFunc = "GetChannel"; + WriteTimestampedLogEntry("NWNX_Chat: GetChannel() is deprecated. Please use the event system (NWNX_ON_CHAT_SEND_*)"); - NWNX_CallFunction(NWNX_Chat, sFunc); - return NWNX_GetReturnValueInt(NWNX_Chat, sFunc); + NWNX_PushArgumentString("NWNX_Events", "GetEventData", "CHANNEL"); + NWNX_CallFunction("NWNX_Events", "GetEventData"); + return StringToInt(NWNX_GetReturnValueString("NWNX_Events", "GetEventData")); } string NWNX_Chat_GetMessage() { - string sFunc = "GetMessage"; + WriteTimestampedLogEntry("NWNX_Chat: GetMessage() is deprecated. Please use the event system (NWNX_ON_CHAT_SEND_*)"); - NWNX_CallFunction(NWNX_Chat, sFunc); - return NWNX_GetReturnValueString(NWNX_Chat, sFunc); + NWNX_PushArgumentString("NWNX_Events", "GetEventData", "MESSAGE"); + NWNX_CallFunction("NWNX_Events", "GetEventData"); + return NWNX_GetReturnValueString("NWNX_Events", "GetEventData"); } object NWNX_Chat_GetSender() { - string sFunc = "GetSender"; + WriteTimestampedLogEntry("NWNX_Chat: GetSender() is deprecated. Please use the event system (NWNX_ON_CHAT_SEND_*)"); - NWNX_CallFunction(NWNX_Chat, sFunc); - return NWNX_GetReturnValueObject(NWNX_Chat, sFunc); + NWNX_PushArgumentString("NWNX_Events", "GetEventData", "SENDER"); + NWNX_CallFunction("NWNX_Events", "GetEventData"); + return StringToObject(NWNX_GetReturnValueString("NWNX_Events", "GetEventData")); } object NWNX_Chat_GetTarget() { - string sFunc = "GetTarget"; + WriteTimestampedLogEntry("NWNX_Chat: GetTarget() is deprecated. Please use the event system (NWNX_ON_CHAT_SEND_*)"); - NWNX_CallFunction(NWNX_Chat, sFunc); - return NWNX_GetReturnValueObject(NWNX_Chat, sFunc); + NWNX_PushArgumentString("NWNX_Events", "GetEventData", "TARGET"); + NWNX_CallFunction("NWNX_Events", "GetEventData"); + return StringToObject(NWNX_GetReturnValueString("NWNX_Events", "GetEventData")); } void NWNX_Chat_SetChatHearingDistance(float distance, object listener = OBJECT_INVALID, int channel = NWNX_CHAT_CHANNEL_PLAYER_TALK) { + WriteTimestampedLogEntry("NWNX_Chat: SetChatHearingDistance() is deprecated. This function has been moved to NWNX_Player. Note the argument order has changed"); + string sFunc = "SetChatHearingDistance"; - NWNX_PushArgumentInt(NWNX_Chat, sFunc, channel); - NWNX_PushArgumentObject(NWNX_Chat, sFunc, listener); - NWNX_PushArgumentFloat(NWNX_Chat, sFunc, distance); - NWNX_CallFunction(NWNX_Chat, sFunc); + NWNX_PushArgumentInt("NWNX_Player", sFunc, channel); + NWNX_PushArgumentFloat("NWNX_Player", sFunc, distance); + NWNX_PushArgumentObject("NWNX_Player", sFunc, listener); + NWNX_CallFunction("NWNX_Player", sFunc); } float NWNX_Chat_GetChatHearingDistance(object listener = OBJECT_INVALID, int channel = NWNX_CHAT_CHANNEL_PLAYER_TALK) { + WriteTimestampedLogEntry("NWNX_Chat: GetChatHearingDistance() is deprecated. This function has been moved to NWNX_Player. Note the argument order has changed."); + string sFunc = "GetChatHearingDistance"; - NWNX_PushArgumentInt(NWNX_Chat, sFunc, channel); - NWNX_PushArgumentObject(NWNX_Chat, sFunc, listener); - NWNX_CallFunction(NWNX_Chat, sFunc); - return NWNX_GetReturnValueFloat(NWNX_Chat, sFunc); + NWNX_PushArgumentInt("NWNX_Player", sFunc, channel); + NWNX_PushArgumentObject("NWNX_Player", sFunc, listener); + NWNX_CallFunction("NWNX_Player", sFunc); + return NWNX_GetReturnValueFloat("NWNX_Player", sFunc); } diff --git a/Plugins/Chat/README.md b/Plugins/Chat/README.md index 37e0e26ef40..30e70ba8061 100644 --- a/Plugins/Chat/README.md +++ b/Plugins/Chat/README.md @@ -3,8 +3,10 @@ Allows chat events to be captured, skipped, and manual chat messages to be dispatched. +@note The functionality of this plugin has been deprecated and reduced to the sending of manual chat messages and altering chat distances for which players can hear. Please use the Events plugin for capturing and skipping chat messages now. + ## Environment Variables | Variable Name | Value | Notes | | ------------- | :---: | ----- | -| `NWNX_CHAT_CHAT_SCRIPT` | string | Set the nwscript that receives all the chat messages +| `NWNX_CHAT_CHAT_SCRIPT` | string | Set the nwscript that receives all the chat messages @deprecated diff --git a/Plugins/Creature/Creature.cpp b/Plugins/Creature/Creature.cpp index fc62df403e4..60136b9ef8a 100644 --- a/Plugins/Creature/Creature.cpp +++ b/Plugins/Creature/Creature.cpp @@ -176,6 +176,7 @@ Creature::Creature(Services::ProxyServiceList* services) REGISTER(SetWalkAnimation); REGISTER(SetAttackRollOverride); REGISTER(SetParryAllAttacks); + REGISTER(SendMessage); #undef REGISTER } @@ -2957,5 +2958,75 @@ ArgumentStack Creature::SetParryAllAttacks(ArgumentStack&& args) return Services::Events::Arguments(); } +ArgumentStack Creature::SendMessage(ArgumentStack&& args) +{ + int32_t retVal = false; + const auto speaker = Services::Events::ExtractArgument(args); + const auto message = Services::Events::ExtractArgument(args); + const auto channel = static_cast(Services::Events::ExtractArgument(args)); + const auto target = Services::Events::ExtractArgument(args); + + const bool hasManualPlayerId = target != Constants::OBJECT_INVALID; + + const PlayerID playerId = hasManualPlayerId ? + Globals::AppManager()->m_pServerExoApp->GetPlayerIDByGameObjectID(target) : + Constants::PLAYERID_ALL_CLIENTS; + + if (playerId != Constants::PLAYERID_INVALIDID) + { + bool sentMessage = false; + auto* messageDispatch = static_cast(Globals::AppManager()->m_pServerExoApp->GetNWSMessage()); + + if (hasManualPlayerId) + { + // This means we're sending this to one player only. + // The normal function broadcasts in an area for talk, shout, and whisper, therefore + // we need to call these functions directly if we are in those categories. + if (channel == Constants::ChatChannel::PlayerTalk) + { + messageDispatch->SendServerToPlayerChat_Talk(playerId, speaker, message.c_str()); + sentMessage = true; + } + else if (channel == Constants::ChatChannel::DmTalk) + { + messageDispatch->SendServerToPlayerChat_DM_Talk(playerId, speaker, message.c_str()); + sentMessage = true; + } + else if (channel == Constants::ChatChannel::DmDm || channel == Constants::ChatChannel::PlayerDm) + { + messageDispatch->SendServerToPlayerChat_DM_Silent_Shout(playerId, speaker, message.c_str()); + sentMessage = true; + } + else if (channel == Constants::ChatChannel::PlayerShout || channel == Constants::ChatChannel::DmShout) + { + messageDispatch->SendServerToPlayerChat_Shout(playerId, speaker, message.c_str()); + sentMessage = true; + } + else if (channel == Constants::ChatChannel::PlayerWhisper) + { + messageDispatch->SendServerToPlayerChat_Whisper(playerId, speaker, message.c_str()); + sentMessage = true; + } + else if (channel == Constants::ChatChannel::DmWhisper) + { + messageDispatch->SendServerToPlayerChat_DM_Whisper(playerId, speaker, message.c_str()); + sentMessage = true; + } + else if (channel == Constants::ChatChannel::PlayerParty || channel == Constants::ChatChannel::DmParty) + { + messageDispatch->SendServerToPlayerChat_Party(playerId, speaker, message.c_str()); + sentMessage = true; + } + } + if (!sentMessage) + { + messageDispatch->SendServerToPlayerChatMessage(static_cast(channel), speaker, message.c_str(), playerId, nullptr); + } + + retVal = true; + } + + return Services::Events::Arguments(retVal); +} } diff --git a/Plugins/Creature/Creature.hpp b/Plugins/Creature/Creature.hpp index bb701dc24f2..04f19200311 100644 --- a/Plugins/Creature/Creature.hpp +++ b/Plugins/Creature/Creature.hpp @@ -130,6 +130,7 @@ class Creature : public NWNXLib::Plugin ArgumentStack SetWalkAnimation (ArgumentStack&& args); ArgumentStack SetAttackRollOverride (ArgumentStack&& args); ArgumentStack SetParryAllAttacks (ArgumentStack&& args); + ArgumentStack SendMessage (ArgumentStack&& args); CNWSCreature *creature(ArgumentStack& args); std::unordered_map> m_RollModifier; diff --git a/Plugins/Creature/NWScript/nwnx_creature.nss b/Plugins/Creature/NWScript/nwnx_creature.nss index ec294971062..083277123d4 100644 --- a/Plugins/Creature/NWScript/nwnx_creature.nss +++ b/Plugins/Creature/NWScript/nwnx_creature.nss @@ -3,6 +3,7 @@ /// @{ /// @file nwnx_creature.nss #include "nwnx" +#include "nwnx_consts" const string NWNX_Creature = "NWNX_Creature"; ///< @private @@ -814,6 +815,15 @@ void NWNX_Creature_SetAttackRollOverride(object oCreature, int nRoll, int nModif /// @note Use this command on_module_load instead of the NWNX_TWEAKS_PARRY_ALL_ATTACKS tweak if using NWNX_Creature_SetAttackRollOverride() void NWNX_Creature_SetParryAllAttacks(object oCreature, int bParry); +/// @brief Sends a chat message from a creature. +/// @remark If no target is provided, then it broadcasts to all eligible targets. +/// @param oCreature The sender of the message. +/// @param nChannel The @ref chat_channels "channel" to send the message. +/// @param sMessage The message to send. +/// @param oTarget The receiver of the message +/// @return TRUE if successful, FALSE otherwise. +int NWNX_Creature_SendMessage(object oCreature, string sMessage, int nChannel = NWNX_CHAT_CHANNEL_PLAYER_TALK, object oTarget = OBJECT_INVALID); + /// @} void NWNX_Creature_AddFeat(object creature, int feat) @@ -2061,3 +2071,15 @@ void NWNX_Creature_SetParryAllAttacks(object oCreature, int bParry) NWNX_PushArgumentObject(NWNX_Creature, sFunc, oCreature); NWNX_CallFunction(NWNX_Creature, sFunc); } + +int NWNX_Creature_SendMessage(object oCreature, string sMessage, int nChannel = NWNX_CHAT_CHANNEL_PLAYER_TALK, object oTarget = OBJECT_INVALID) +{ + string sFunc = "SendMessage"; + NWNX_PushArgumentObject(NWNX_Creature, sFunc, oTarget); + NWNX_PushArgumentInt(NWNX_Creature, sFunc, nChannel); + NWNX_PushArgumentString(NWNX_Creature, sFunc, sMessage); + NWNX_PushArgumentObject(NWNX_Creature, sFunc, oCreature); + NWNX_CallFunction(NWNX_Creature, sFunc); + + return NWNX_GetReturnValueInt(NWNX_Creature, sFunc); +} \ No newline at end of file diff --git a/Plugins/Events/CMakeLists.txt b/Plugins/Events/CMakeLists.txt index edfca9cad36..08eaa2e128f 100644 --- a/Plugins/Events/CMakeLists.txt +++ b/Plugins/Events/CMakeLists.txt @@ -30,4 +30,5 @@ add_plugin(Events "Events/ResourceEvents.cpp" "Events/QuickbarEvents.cpp" "Events/DebugEvents.cpp" - "Events/StoreEvents.cpp") + "Events/StoreEvents.cpp" + "Events/ChatEvents.cpp") diff --git a/Plugins/Events/Events.cpp b/Plugins/Events/Events.cpp index 008f25d7118..6fcc1c355da 100644 --- a/Plugins/Events/Events.cpp +++ b/Plugins/Events/Events.cpp @@ -34,6 +34,7 @@ #include "Events/QuickbarEvents.hpp" #include "Events/DebugEvents.hpp" #include "Events/StoreEvents.hpp" +#include "Events/ChatEvents.hpp" #include "Services/Config/Config.hpp" #include "Services/Messaging/Messaging.hpp" @@ -125,15 +126,16 @@ Events::Events(Services::ProxyServiceList* services) m_quickbarEvents = std::make_unique(hooker); m_debugEvents = std::make_unique(hooker); m_storeEvents = std::make_unique(hooker); + m_chatEvents = std::make_unique(hooker); } Events::~Events() { } -void Events::PushEventData(const std::string& tag, const std::string& data) +void Events::PushEventData(const std::string& tag, const std::string& data, bool bHideDataInLog) { - LOG_DEBUG("Pushing event data: '%s' -> '%s'.", tag, data); + LOG_DEBUG("Pushing event data: '%s' -> '%s'.", tag, !bHideDataInLog ? data : ""); g_plugin->CreateNewEventDataIfNeeded(); g_plugin->m_eventData.top().m_EventDataMap[tag] = data; } diff --git a/Plugins/Events/Events.hpp b/Plugins/Events/Events.hpp index b117bffb3b3..b5c58be876d 100644 --- a/Plugins/Events/Events.hpp +++ b/Plugins/Events/Events.hpp @@ -44,6 +44,7 @@ class ResourceEvents; class QuickbarEvents; class DebugEvents; class StoreEvents; +class ChatEvents; class Events : public NWNXLib::Plugin { @@ -68,7 +69,7 @@ class Events : public NWNXLib::Plugin virtual ~Events(); // Pushes event data to the stack - won't do anything until SignalEvent is called. - static void PushEventData(const std::string& tag, const std::string& data); + static void PushEventData(const std::string& tag, const std::string& data, bool bHideDataInLog = false); // Get event data static std::string GetEventData(const std::string& tag); @@ -138,6 +139,7 @@ class Events : public NWNXLib::Plugin std::unique_ptr m_quickbarEvents; std::unique_ptr m_debugEvents; std::unique_ptr m_storeEvents; + std::unique_ptr m_chatEvents; }; } diff --git a/Plugins/Events/Events/ChatEvents.cpp b/Plugins/Events/Events/ChatEvents.cpp new file mode 100644 index 00000000000..17a19a08657 --- /dev/null +++ b/Plugins/Events/Events/ChatEvents.cpp @@ -0,0 +1,78 @@ +#include "Events/ChatEvents.hpp" +#include "API/CAppManager.hpp" +#include "API/CServerExoApp.hpp" +#include "API/CNWSPlayer.hpp" +#include "API/Functions.hpp" +#include "Events.hpp" +#include "Utils.hpp" + +namespace Events { + +using namespace NWNXLib; +using namespace NWNXLib::API; +using namespace NWNXLib::API::Constants; + +static NWNXLib::Hooking::FunctionHook* s_HandlePlayerToServerChatMessageHook; + +ChatEvents::ChatEvents(Services::HooksProxy* hooker) +{ + Events::InitOnFirstSubscribe("NWNX_ON_CHAT_SEND_.*", [hooker]() { + s_HandlePlayerToServerChatMessageHook = hooker->RequestExclusiveHook< + API::Functions::_ZN11CNWSMessage31HandlePlayerToServerChatMessageEP10CNWSPlayerh>(&ChatEvents::HandlePlayerToServerChatMessageHook); + }); +} + +int32_t ChatEvents::HandlePlayerToServerChatMessageHook(CNWSMessage* pMessage, CNWSPlayer* pPlayer, uint8_t nMinor) +{ + int32_t retVal = 0; + + auto channel = nMinor; + CExoString sMessage; + + ObjectID oidTarget = Constants::OBJECT_INVALID; + + if (nMinor == ChatChannel::PlayerTell) + { + auto nID = Utils::PeekMessage(pMessage, 0); + sMessage = Utils::PeekMessage(pMessage, 4); + + if (auto *pClient = Globals::AppManager()->m_pServerExoApp->GetClientObjectByPlayerId(nID, 0)) + { + oidTarget = static_cast(pClient)->m_oidPCObject; + } + } + else + { + sMessage = Utils::PeekMessage(pMessage, 0); + } + + Globals::AppManager()->m_pServerExoApp->StripColorTokens(sMessage); + + ObjectID senderOid = API::Constants::OBJECT_INVALID; + if (pPlayer != nullptr) + { + if (pPlayer->GetIsDM()) + channel |= 16; + senderOid = pPlayer->m_oidNWSObject; + } + + Events::PushEventData("TARGET", Utils::ObjectIDToString(oidTarget)); + Events::PushEventData("CHANNEL", std::to_string(channel)); + + // Hide the message from appearing in the log if the target is valid to keep tell content out of logs + Events::PushEventData("MESSAGE", sMessage.CStr(), oidTarget != OBJECT_INVALID); + + if (Events::SignalEvent("NWNX_ON_CHAT_SEND_BEFORE", senderOid)) + { + retVal = s_HandlePlayerToServerChatMessageHook->CallOriginal(pMessage, pPlayer, nMinor); + } + else + { + retVal = 0; + } + + Events::SignalEvent("NWNX_ON_CHAT_SEND_AFTER", senderOid); + return retVal; +} + +} diff --git a/Plugins/Events/Events/ChatEvents.hpp b/Plugins/Events/Events/ChatEvents.hpp new file mode 100644 index 00000000000..d8aec3584d9 --- /dev/null +++ b/Plugins/Events/Events/ChatEvents.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "API/Vector.hpp" +#include "Common.hpp" +#include "Services/Hooks/Hooks.hpp" + +namespace Events { + +class ChatEvents +{ +public: + ChatEvents(NWNXLib::Services::HooksProxy* hooker); + +private: + static int32_t HandlePlayerToServerChatMessageHook(CNWSMessage*, CNWSPlayer*, uint8_t); + +}; + +} diff --git a/Plugins/Events/NWScript/nwnx_events.nss b/Plugins/Events/NWScript/nwnx_events.nss index 0558c996d32..2bc2453f126 100644 --- a/Plugins/Events/NWScript/nwnx_events.nss +++ b/Plugins/Events/NWScript/nwnx_events.nss @@ -1231,6 +1231,19 @@ _______________________________________ AREA | object | The area the server is sending. Convert to object with StringToObject() | PLAYER_NEW_TO_MODULE | int | TRUE if it's the player's first time logging into the server since a restart | +_______________________________________ + ## Chat Events + - NWNX_ON_CHAT_SEND_BEFORE + - NWNX_ON_CHAT_SEND_AFTER + + `OBJECT_SELF` = The player who sent the chat message + + Event Data Tag | Type | Notes + ----------------------|--------|------- + CHANNEL | int | The channel the chat message was sent (see @ref chat_channels nwnx_consts.nss) + MESSAGE | string | The message sent + TARGET | object | The recipient of the message (for tells only) + _______________________________________ */ /* @@ -1318,6 +1331,7 @@ string NWNX_Events_GetEventData(string tag); /// - Debug events /// - Store events /// - Disarm event +/// - Chat events void NWNX_Events_SkipEvent(); /// Set the return value of the event. diff --git a/Plugins/Player/NWScript/nwnx_player.nss b/Plugins/Player/NWScript/nwnx_player.nss index 517963f2017..69f1a3f7453 100644 --- a/Plugins/Player/NWScript/nwnx_player.nss +++ b/Plugins/Player/NWScript/nwnx_player.nss @@ -3,6 +3,7 @@ /// @{ /// @file nwnx_player.nss #include "nwnx" +#include "nwnx_consts" const string NWNX_Player = "NWNX_Player"; ///< @private @@ -378,6 +379,17 @@ int NWNX_Player_AddCustomJournalEntry(object oPlayer, struct NWNX_Player_Journal /// that is the active one that the player currently sees. struct NWNX_Player_JournalEntry NWNX_Player_GetJournalEntry(object oPlayer, string questTag); +/// @brief Sets the distance with which the player hears talks or whispers. +/// @remark Per player settings override server wide. +/// @param oPlayer The listener, if OBJECT_INVALID then it will be set server wide. +/// @param fDistance The distance in meters. +/// @param nChannel The @ref chat_channels "channel" to modify the distance heard. Only applicable for talk and whisper. +void NWNX_Player_SetChatHearingDistance(object oPlayer, float fDistance, int nChannel); + +/// @brief Gets the distance with which the player hears talks or whisper +/// @param oPlayer The listener, if OBJECT_INVALID then will return server wide setting. +/// @param nChannel The @ref chat_channels "channel". Only applicable for talk and whisper. +float NWNX_Player_GetChatHearingDistance(object oPlayer = OBJECT_INVALID, int nChannel = NWNX_CHAT_CHANNEL_PLAYER_TALK); /// @} void NWNX_Player_ForcePlaceableExamineWindow(object player, object placeable) @@ -949,3 +961,23 @@ struct NWNX_Player_JournalEntry NWNX_Player_GetJournalEntry(object oPlayer, stri entry.sTag = questTag; return entry; } + +void NWNX_Player_SetChatHearingDistance(object oPlayer, float fDistance, int nChannel) +{ + string sFunc = "SetChatHearingDistance"; + + NWNX_PushArgumentInt(NWNX_Player, sFunc, nChannel); + NWNX_PushArgumentFloat(NWNX_Player, sFunc, fDistance); + NWNX_PushArgumentObject(NWNX_Player, sFunc, oPlayer); + NWNX_CallFunction(NWNX_Player, sFunc); +} + +float NWNX_Player_GetChatHearingDistance(object oPlayer = OBJECT_INVALID, int nChannel = NWNX_CHAT_CHANNEL_PLAYER_TALK) +{ + string sFunc = "GetChatHearingDistance"; + + NWNX_PushArgumentInt(NWNX_Player, sFunc, nChannel); + NWNX_PushArgumentObject(NWNX_Player, sFunc, oPlayer); + NWNX_CallFunction(NWNX_Player, sFunc); + return NWNX_GetReturnValueFloat(NWNX_Player, sFunc); +} \ No newline at end of file diff --git a/Plugins/Player/NWScript/nwnx_player_t.nss b/Plugins/Player/NWScript/nwnx_player_t.nss index 574431cb990..c7ae33ebfd5 100644 --- a/Plugins/Player/NWScript/nwnx_player_t.nss +++ b/Plugins/Player/NWScript/nwnx_player_t.nss @@ -11,6 +11,14 @@ void main() WriteTimestampedLogEntry("NWNX_Player test: No PC found"); return; } + float fDefaultTalk = NWNX_Player_GetChatHearingDistance(); + NWNX_Player_SetChatHearingDistance(OBJECT_INVALID, fDefaultTalk + 10.0f, NWNX_CHAT_CHANNEL_PLAYER_TALK); + NWNX_Tests_Report("NWNX_Player", "SetChatHearingDistance Default", fDefaultTalk == NWNX_Player_GetChatHearingDistance() - 10.0f); + + float fPCWhisper = NWNX_Player_GetChatHearingDistance(oPC, NWNX_CHAT_CHANNEL_PLAYER_WHISPER); + NWNX_Player_SetChatHearingDistance(oPC, fPCWhisper + 2.0f, NWNX_CHAT_CHANNEL_PLAYER_WHISPER); + + NWNX_Tests_Report("NWNX_Player", "SetChatHearingDistance Per PC", fPCWhisper == NWNX_Player_GetChatHearingDistance(oPC, NWNX_CHAT_CHANNEL_PLAYER_WHISPER) - 2.0f); WriteTimestampedLogEntry("NWNX_Player unit test end."); } diff --git a/Plugins/Player/Player.cpp b/Plugins/Player/Player.cpp index e5d1686db8e..62ea41916b1 100644 --- a/Plugins/Player/Player.cpp +++ b/Plugins/Player/Player.cpp @@ -107,9 +107,17 @@ Player::Player(Services::ProxyServiceList* services) REGISTER(SendDMAllCreatorLists); REGISTER(AddCustomJournalEntry); REGISTER(GetJournalEntry); + REGISTER(SetChatHearingDistance); + REGISTER(GetChatHearingDistance); #undef REGISTER + m_hearingDistances[Constants::ChatChannel::DmTalk] = 20.0f; + m_hearingDistances[Constants::ChatChannel::PlayerTalk] = 20.0f; + m_hearingDistances[Constants::ChatChannel::DmWhisper] = 3.0f; + m_hearingDistances[Constants::ChatChannel::PlayerWhisper] = 3.0f; + m_customHearingDistances = false; + } Player::~Player() @@ -1848,4 +1856,131 @@ ArgumentStack Player::GetJournalEntry(ArgumentStack&& args) return Services::Events::Arguments(-1); } +Services::Events::ArgumentStack Player::SetChatHearingDistance(Services::Events::ArgumentStack&& args) +{ + const auto playerOid = Services::Events::ExtractArgument(args); + const auto distance = Services::Events::ExtractArgument(args); + const auto channel = (Constants::ChatChannel::TYPE)Services::Events::ExtractArgument(args); + + static NWNXLib::Hooking::FunctionHook* pSendServerToPlayerChatMessage_hook; + + if (!pSendServerToPlayerChatMessage_hook) + { + pSendServerToPlayerChatMessage_hook = GetServices()->m_hooks->RequestExclusiveHook + ( + +[](CNWSMessage* thisPtr, Constants::ChatChannel::TYPE channel, ObjectID sender, + CExoString message, PlayerID target, CExoString* tellName) -> int32_t + { + auto server = Globals::AppManager()->m_pServerExoApp; + auto *playerList = server->m_pcExoAppInternal->m_pNWSPlayerList->m_pcExoLinkedListInternal; + if (playerList) + { + if (channel == Constants::ChatChannel::PlayerShout && + server->GetServerInfo()->m_PlayOptions.bDisallowShouting) + { + channel = Constants::ChatChannel::PlayerTalk; + } + if (channel == Constants::ChatChannel::PlayerTalk || + channel == Constants::ChatChannel::PlayerWhisper || + channel == Constants::ChatChannel::DmTalk || + channel == Constants::ChatChannel::DmWhisper) + { + auto *pPOS = g_plugin->GetServices()->m_perObjectStorage.get(); + auto pSpeaker = Utils::AsNWSObject(server->GetGameObject(sender)); + auto distance = g_plugin->m_hearingDistances[channel]; + auto speakerPos = Vector{0.0f, 0.0f, 0.0f}; + CNWSArea *speakerArea = nullptr; + if (pSpeaker != nullptr) + { + speakerArea = pSpeaker->GetArea(); + speakerPos = pSpeaker->m_vPosition; + pSpeaker->BroadcastDialog(*tellName, distance); + } + + for (auto *head = playerList->pHead; head; head = head->pNext) + { + auto *pClient = static_cast(head->pObject); + auto *listenerClient = server->GetClientObjectByPlayerId(pClient->m_nPlayerID, 0); + auto *listener = static_cast(listenerClient); + auto *listenerObj = Utils::AsNWSObject(listener->GetGameObject()); + + auto pDistance = *pPOS->Get(listenerObj->m_idSelf, "HEARING_DISTANCE:" + std::to_string(channel)); + if (pDistance >= 0) + { + distance = pDistance; + + auto v = listenerObj->m_vPosition; + v.x -= speakerPos.x; + v.y -= speakerPos.y; + v.z -= speakerPos.z; + float vSquared = v.x*v.x + v.y*v.y + v.z*v.z; + if (speakerArea == listenerObj->GetArea() && vSquared <= distance*distance) + { + switch (channel) + { + case Constants::ChatChannel::PlayerTalk: + thisPtr->SendServerToPlayerChat_Talk(listener->m_nPlayerID, sender, message); + break; + case Constants::ChatChannel::DmTalk: + thisPtr->SendServerToPlayerChat_DM_Talk(listener->m_nPlayerID, sender, message); + break; + case Constants::ChatChannel::PlayerWhisper: + thisPtr->SendServerToPlayerChat_Whisper(listener->m_nPlayerID, sender, message); + break; + case Constants::ChatChannel::DmWhisper: + thisPtr->SendServerToPlayerChat_DM_Whisper(listener->m_nPlayerID, sender, message); + break; + default: + break; + } + } + } + } + } + else + { + return pSendServerToPlayerChatMessage_hook->CallOriginal(thisPtr, channel, sender, message, target, tellName); + } + } + return pSendServerToPlayerChatMessage_hook->CallOriginal(thisPtr, channel, sender, message, target, tellName); + }); + } + + if (playerOid == Constants::OBJECT_INVALID) + { + m_customHearingDistances = true; + m_hearingDistances[channel] = distance; + } + else + { + auto *pPlayer = Globals::AppManager()->m_pServerExoApp->GetClientObjectByObjectId(playerOid); + if (!pPlayer) + { + LOG_NOTICE("NWNX_Player function called on non-player object %x", playerOid); + } + else + { + auto *pPOS = g_plugin->GetServices()->m_perObjectStorage.get(); + m_customHearingDistances = true; + pPOS->Set(playerOid, "HEARING_DISTANCE:" + std::to_string(channel), distance, true); + } + } + + return Services::Events::Arguments(); +} + +Services::Events::ArgumentStack Player::GetChatHearingDistance(Services::Events::ArgumentStack&& args) +{ + const auto playerOid = Services::Events::ExtractArgument(args); + const auto channel = (Constants::ChatChannel::TYPE)Services::Events::ExtractArgument(args); + float retVal = m_hearingDistances[channel]; + + if (playerOid != Constants::OBJECT_INVALID) + { + auto *pPOS = g_plugin->GetServices()->m_perObjectStorage.get(); + auto playerHearingDistance = *pPOS->Get(playerOid, "HEARING_DISTANCE:" + std::to_string(channel)); + retVal = playerHearingDistance > 0 ? playerHearingDistance : m_hearingDistances[channel]; + } + return Services::Events::Arguments(retVal); +} } diff --git a/Plugins/Player/Player.hpp b/Plugins/Player/Player.hpp index a7d76202121..7fa8147d1aa 100644 --- a/Plugins/Player/Player.hpp +++ b/Plugins/Player/Player.hpp @@ -63,10 +63,14 @@ class Player : public NWNXLib::Plugin ArgumentStack SendDMAllCreatorLists (ArgumentStack&& args); ArgumentStack AddCustomJournalEntry (ArgumentStack&& args); ArgumentStack GetJournalEntry (ArgumentStack&& args); + ArgumentStack SetChatHearingDistance (ArgumentStack&& args); + ArgumentStack GetChatHearingDistance (ArgumentStack&& args); CNWSPlayer *player(ArgumentStack& args); std::unordered_map> m_PersistentLocationWP; + bool m_customHearingDistances; + std::unordered_map m_hearingDistances; }; }