From ebf5f6710a0de5efa3067d3b58491ef8d25b8ae8 Mon Sep 17 00:00:00 2001 From: Kitzunu <24550914+Kitzunu@users.noreply.github.com> Date: Sat, 6 Jul 2024 19:13:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(Core/Chat):=20Provide=20a=20fully-formed?= =?UTF-8?q?=20protocol=20for=20addons=20to=20intera=E2=80=A6=20(#19305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(Core/Chat): Provide a fully-formed protocol for addons to interact with GM commands * Send success/fail state, allow interleaving, and indicate end of output. Add framework for supporting non-human-readable output in commands. * cherry-pick commit (https://github.com/TrinityCore/TrinityCore/commit/508c9d2fc1b20dc2cb40df533e823e1dfe2becc3) This PR implements a well-formed protocol for addons to communicate with the server, outlined below: - All communication happens over the addon channel (`LANG_ADDON` in terms of the core, `CHAT_MSG_ADDON`/`SendAddonMessage` for the client). The prefix used for all messages is `AzerothCore` (in client terms - in core terms, every message starts with `AzerothCore\t`). - In each message, the first character is the opcode. The following four characters are a unique identifier for the invocation in question, and will be echoed back by the server in every message related to that invocation. Following is the message body, if any. - The following opcodes are supported: - Client to server: - `p` - Ping request. The core will always respond by ACKing with the passed identifier. No body. - `i` or `h` - Command invocation. The message body is the command text without prefix. `i` requests machine-readable output, `h` requests human-readable. - Server to client: - `a` - ACK. The first message sent in response to any invocation (before any output). No body. - `m` - Message. Sent once per line of output the server generates. Body = output line. - `o` - OK. Indicates that the command finished processing with no errors. No body. - `f` - Failed. Indicates that command processing is done, but there was an error. No body. Expected overhead is minimal, and this integrates seamlessly with existing command scripts (no changes necessary). PS: There's also a client-side addon library that exposes this protocol in a developer-friendly way over at https://github.com/azerothcore/LibAzerothCore-1.0 --------- Co-authored-by: Treeston <14020072+Treeston@users.noreply.github.com> --- src/server/game/Chat/Chat.cpp | 103 +++++++++++++++++++++++ src/server/game/Chat/Chat.h | 20 +++++ src/server/game/Handlers/ChatHandler.cpp | 22 +++-- 3 files changed, 138 insertions(+), 7 deletions(-) diff --git a/src/server/game/Chat/Chat.cpp b/src/server/game/Chat/Chat.cpp index 170782631..57836678c 100644 --- a/src/server/game/Chat/Chat.cpp +++ b/src/server/game/Chat/Chat.cpp @@ -33,6 +33,7 @@ #include "World.h" #include "WorldPacket.h" #include "WorldSession.h" +#include Player* ChatHandler::GetPlayer() const { @@ -888,3 +889,105 @@ int CliHandler::GetSessionDbLocaleIndex() const { return sObjectMgr->GetDBCLocaleIndex(); } + +bool AddonChannelCommandHandler::ParseCommands(std::string_view str) +{ + if (memcmp(str.data(), "AzerothCore\t", 12)) + return false; + char opcode = str[12]; + if (!opcode) // str[12] is opcode + return false; + if (!str[13] || !str[14] || !str[15] || !str[16]) // str[13] through str[16] is 4-character command counter + return false; + echo = str.substr(13); + + switch (opcode) + { + case 'p': // p Ping + SendAck(); + return true; + case 'h': // h Issue human-readable command + case 'i': // i Issue command + if (!str[17]) + return false; + humanReadable = (opcode == 'h'); + if (_ParseCommands(str.substr(17))) // actual command starts at str[17] + { + if (!hadAck) + SendAck(); + if (HasSentErrorMessage()) + SendFailed(); + else + SendOK(); + } + else + { + SendSysMessage(LANG_CMD_INVALID); + SendFailed(); + } + return true; + default: + return false; + } +} + +void AddonChannelCommandHandler::Send(std::string const& msg) +{ + WorldPacket data; + ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER, LANG_ADDON, GetSession()->GetPlayer(), GetSession()->GetPlayer(), msg); + GetSession()->SendPacket(&data); +} + +void AddonChannelCommandHandler::SendAck() // a Command acknowledged, no body +{ + ASSERT(echo.size()); + std::string ack = "AzerothCore\ta"; + ack.resize(18); + memcpy(&ack[13], echo.data(), 4); + ack[17] = '\0'; + Send(ack); + hadAck = true; +} + +void AddonChannelCommandHandler::SendOK() // o Command OK, no body +{ + ASSERT(echo.size()); + std::string ok = "AzerothCore\to"; + ok.resize(18); + memcpy(&ok[13], echo.data(), 4); + ok[17] = '\0'; + Send(ok); +} + +void AddonChannelCommandHandler::SendFailed() // f Command failed, no body +{ + ASSERT(echo.size()); + std::string fail = "AzerothCore\tf"; + fail.resize(18); + memcpy(&fail[13], echo.data(), 4); + fail[17] = '\0'; + Send(fail); +} + +// m Command message, message in body +void AddonChannelCommandHandler::SendSysMessage(std::string_view str, bool escapeCharacters) +{ + ASSERT(echo.size()); + if (!hadAck) + SendAck(); + + std::string msg = "AzerothCore\tm"; + msg.append(echo.data(), 4); + std::string body(str); + if (escapeCharacters) + boost::replace_all(body, "|", "||"); + size_t pos, lastpos; + for (lastpos = 0, pos = body.find('\n', lastpos); pos != std::string::npos; lastpos = pos + 1, pos = body.find('\n', lastpos)) + { + std::string line(msg); + line.append(body, lastpos, pos - lastpos); + Send(line); + } + msg.append(body, lastpos, pos - lastpos); + Send(msg); +} diff --git a/src/server/game/Chat/Chat.h b/src/server/game/Chat/Chat.h index d6f91bb86..c4873f2cc 100644 --- a/src/server/game/Chat/Chat.h +++ b/src/server/game/Chat/Chat.h @@ -168,4 +168,24 @@ private: Print* m_print; }; +class AC_GAME_API AddonChannelCommandHandler : public ChatHandler +{ + public: + using ChatHandler::ChatHandler; + bool ParseCommands(std::string_view str) override; + void SendSysMessage(std::string_view str, bool escapeCharacters) override; + using ChatHandler::SendSysMessage; + bool IsHumanReadable() const override { return humanReadable; } + + private: + void Send(std::string const& msg); + void SendAck(); + void SendOK(); + void SendFailed(); + + std::string echo; + bool hadAck = false; + bool humanReadable = false; +}; + #endif diff --git a/src/server/game/Handlers/ChatHandler.cpp b/src/server/game/Handlers/ChatHandler.cpp index 874d34aa6..6ce76ef06 100644 --- a/src/server/game/Handlers/ChatHandler.cpp +++ b/src/server/game/Handlers/ChatHandler.cpp @@ -283,14 +283,22 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recvData) if (msg.empty()) return; - if (ChatHandler(this).ParseCommands(msg.c_str())) - return; - - if (!_player->CanSpeak()) + if (lang == LANG_ADDON) { - std::string timeStr = secsToTimeString(m_muteTime - GameTime::GetGameTime().count()); - SendNotification(GetAcoreString(LANG_WAIT_BEFORE_SPEAKING), timeStr.c_str()); - return; + if (AddonChannelCommandHandler(this).ParseCommands(msg.c_str())) + return; + } + else + { + if (ChatHandler(this).ParseCommands(msg.c_str())) + return; + + if (!_player->CanSpeak()) + { + std::string timeStr = secsToTimeString(m_muteTime - GameTime::GetGameTime().count()); + SendNotification(GetAcoreString(LANG_WAIT_BEFORE_SPEAKING), timeStr.c_str()); + return; + } } }