feat(Core/Chat): Provide a fully-formed protocol for addons to intera… (#19305)
* 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>
This commit is contained in:
@@ -33,6 +33,7 @@
|
|||||||
#include "World.h"
|
#include "World.h"
|
||||||
#include "WorldPacket.h"
|
#include "WorldPacket.h"
|
||||||
#include "WorldSession.h"
|
#include "WorldSession.h"
|
||||||
|
#include <boost/algorithm/string/replace.hpp>
|
||||||
|
|
||||||
Player* ChatHandler::GetPlayer() const
|
Player* ChatHandler::GetPlayer() const
|
||||||
{
|
{
|
||||||
@@ -888,3 +889,105 @@ int CliHandler::GetSessionDbLocaleIndex() const
|
|||||||
{
|
{
|
||||||
return sObjectMgr->GetDBCLocaleIndex();
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -168,4 +168,24 @@ private:
|
|||||||
Print* m_print;
|
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
|
#endif
|
||||||
|
|||||||
@@ -283,14 +283,22 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recvData)
|
|||||||
if (msg.empty())
|
if (msg.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (ChatHandler(this).ParseCommands(msg.c_str()))
|
if (lang == LANG_ADDON)
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_player->CanSpeak())
|
|
||||||
{
|
{
|
||||||
std::string timeStr = secsToTimeString(m_muteTime - GameTime::GetGameTime().count());
|
if (AddonChannelCommandHandler(this).ParseCommands(msg.c_str()))
|
||||||
SendNotification(GetAcoreString(LANG_WAIT_BEFORE_SPEAKING), timeStr.c_str());
|
return;
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user