feat(Core/Chat): new argument parsing and unify chat hyperlink parsing (#6243)

This commit is contained in:
Kargatum
2021-10-23 15:15:42 +07:00
committed by GitHub
parent 1101f9dd2a
commit bc9473482e
90 changed files with 4280 additions and 2508 deletions

View File

@@ -15,10 +15,9 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Chat.h"
#include "AccountMgr.h"
#include "CellImpl.h"
#include "Chat.h"
#include "ChatLink.h"
#include "Common.h"
#include "DatabaseEnv.h"
#include "GridNotifiersImpl.h"
@@ -30,6 +29,7 @@
#include "Realm.h"
#include "ScriptMgr.h"
#include "SpellMgr.h"
#include "Tokenize.h"
#include "UpdateMask.h"
#include "World.h"
#include "WorldPacket.h"
@@ -39,45 +39,9 @@
#include "LuaEngine.h"
#endif
bool ChatHandler::load_command_table = true;
std::vector<ChatCommand> const& ChatHandler::getCommandTable()
Player* ChatHandler::GetPlayer() const
{
static std::vector<ChatCommand> commandTableCache;
if (LoadCommandTable())
{
SetLoadCommandTable(false);
std::vector<ChatCommand> cmds = sScriptMgr->GetChatCommands();
commandTableCache.swap(cmds);
WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_COMMANDS);
PreparedQueryResult result = WorldDatabase.Query(stmt);
if (result)
{
do
{
Field* fields = result->Fetch();
std::string name = fields[0].GetString();
SetDataForCommandInTable(commandTableCache, name.c_str(), fields[1].GetUInt8(), fields[2].GetString(), name);
} while (result->NextRow());
}
}
return commandTableCache;
}
std::string ChatHandler::PGetParseString(uint32 entry, ...) const
{
const char* format = GetAcoreString(entry);
char str[1024];
va_list ap;
va_start(ap, entry);
vsnprintf(str, 1024, format, ap);
va_end(ap);
return std::string(str);
return m_session ? m_session->GetPlayer() : nullptr;
}
char const* ChatHandler::GetAcoreString(uint32 entry) const
@@ -85,10 +49,10 @@ char const* ChatHandler::GetAcoreString(uint32 entry) const
return m_session->GetAcoreString(entry);
}
bool ChatHandler::isAvailable(ChatCommand const& cmd) const
bool ChatHandler::IsAvailable(uint32 securityLevel) const
{
// check security level only for simple command (without child commands)
return m_session->GetSecurity() >= AccountTypes(cmd.SecurityLevel);
return m_session->GetSecurity() >= AccountTypes(securityLevel);
}
bool ChatHandler::HasLowerSecurity(Player* target, ObjectGuid guid, bool strong)
@@ -141,83 +105,50 @@ bool ChatHandler::HasLowerSecurityAccount(WorldSession* target, uint32 target_ac
return false;
}
bool ChatHandler::hasStringAbbr(const char* name, const char* part)
void ChatHandler::SendSysMessage(std::string_view str, bool escapeCharacters)
{
// non "" command
if (*name)
std::string msg{ str };
// Replace every "|" with "||" in msg
if (escapeCharacters && msg.find('|') != std::string::npos)
{
// "" part from non-"" command
if (!*part)
return false;
std::vector<std::string_view> tokens = Acore::Tokenize(msg, '|', true);
std::ostringstream stream;
while (true)
{
if (!*part)
return true;
else if (!*name)
return false;
else if (tolower(*name) != tolower(*part))
return false;
++name;
++part;
}
for (size_t i = 0; i < tokens.size() - 1; ++i)
stream << tokens[i] << "||";
stream << tokens[tokens.size() - 1];
msg = stream.str();
}
// allow with any for ""
return true;
}
void ChatHandler::SendSysMessage(const char* str)
{
WorldPacket data;
// need copy to prevent corruption by strtok call in LineFromMessage original string
char* buf = strdup(str);
char* pos = buf;
while (char* line = LineFromMessage(pos))
for (std::string_view line : Acore::Tokenize(str, '\n', true))
{
BuildChatPacket(data, CHAT_MSG_SYSTEM, LANG_UNIVERSAL, nullptr, nullptr, line);
m_session->SendPacket(&data);
}
free(buf);
}
void ChatHandler::SendGlobalSysMessage(const char* str)
{
// Chat output
WorldPacket data;
// need copy to prevent corruption by strtok call in LineFromMessage original string
char* buf = strdup(str);
char* pos = buf;
while (char* line = LineFromMessage(pos))
for (std::string_view line : Acore::Tokenize(str, '\n', true))
{
BuildChatPacket(data, CHAT_MSG_SYSTEM, LANG_UNIVERSAL, nullptr, nullptr, line);
sWorld->SendGlobalMessage(&data);
}
free(buf);
}
void ChatHandler::SendGlobalGMSysMessage(const char* str)
{
// Chat output
WorldPacket data;
// need copy to prevent corruption by strtok call in LineFromMessage original string
char* buf = strdup(str);
char* pos = buf;
while (char* line = LineFromMessage(pos))
for (std::string_view line : Acore::Tokenize(str, '\n', true))
{
BuildChatPacket(data, CHAT_MSG_SYSTEM, LANG_UNIVERSAL, nullptr, nullptr, line);
sWorld->SendGlobalGMMessage(&data);
}
free(buf);
}
void ChatHandler::SendSysMessage(uint32 entry)
@@ -225,388 +156,42 @@ void ChatHandler::SendSysMessage(uint32 entry)
SendSysMessage(GetAcoreString(entry));
}
void ChatHandler::PSendSysMessage(uint32 entry, ...)
bool ChatHandler::_ParseCommands(std::string_view text)
{
const char* format = GetAcoreString(entry);
va_list ap;
char str [2048];
va_start(ap, entry);
vsnprintf(str, 2048, format, ap);
va_end(ap);
SendSysMessage(str);
}
void ChatHandler::PSendSysMessage(const char* format, ...)
{
va_list ap;
char str [2048];
va_start(ap, format);
vsnprintf(str, 2048, format, ap);
va_end(ap);
SendSysMessage(str);
}
bool ChatHandler::ExecuteCommandInTable(std::vector<ChatCommand> const& table, const char* text, std::string const& fullcmd)
{
char const* oldtext = text;
std::string cmd = "";
while (*text != ' ' && *text != '\0')
{
cmd += *text;
++text;
}
while (*text == ' ') ++text;
for (uint32 i = 0; i < table.size(); ++i)
{
if (!hasStringAbbr(table[i].Name, cmd.c_str()))
continue;
bool match = false;
if (strlen(table[i].Name) > cmd.length())
{
for (uint32 j = 0; j < table.size(); ++j)
{
if (!hasStringAbbr(table[j].Name, cmd.c_str()))
continue;
if (strcmp(table[j].Name, cmd.c_str()) == 0)
{
match = true;
break;
}
}
}
if (match)
continue;
// select subcommand from child commands list
if (!table[i].ChildCommands.empty())
{
if (!ExecuteCommandInTable(table[i].ChildCommands, text, fullcmd.c_str()))
{
#ifdef ELUNA
if (!sEluna->OnCommand(GetSession() ? GetSession()->GetPlayer() : nullptr, oldtext))
return true;
#endif
if (text[0] != '\0')
SendSysMessage(LANG_NO_SUBCMD);
else
SendSysMessage(LANG_CMD_SYNTAX);
ShowHelpForCommand(table[i].ChildCommands, text);
}
return true;
}
// must be available and have handler
if (!table[i].Handler || !isAvailable(table[i]))
continue;
SetSentErrorMessage(false);
// table[i].Name == "" is special case: send original command to handler
if ((table[i].Handler)(this, table[i].Name[0] != '\0' ? text : oldtext))
{
if (!m_session) // ignore console
return true;
Player* player = m_session->GetPlayer();
if (!AccountMgr::IsPlayerAccount(m_session->GetSecurity()))
{
ObjectGuid guid = player->GetTarget();
uint32 areaId = player->GetAreaId();
uint32 zoneId = player->GetZoneId();
std::string areaName = "Unknown";
std::string zoneName = "Unknown";
int locale = GetSessionDbcLocale();
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(areaId))
{
areaName = area->area_name[locale];
}
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(zoneId))
{
zoneName = zone->area_name[locale];
}
LOG_GM(m_session->GetAccountId(), "Command: %s [Player: %s (%s) (Account: %u) X: %f Y: %f Z: %f Map: %u (%s) Area: %u (%s) Zone: %u (%s) Selected: %s (%s)]",
fullcmd.c_str(), player->GetName().c_str(), player->GetGUID().ToString().c_str(),
m_session->GetAccountId(), player->GetPositionX(), player->GetPositionY(),
player->GetPositionZ(), player->GetMapId(),
player->GetMap()->GetMapName(),
areaId, areaName.c_str(), zoneId, zoneName.c_str(),
(player->GetSelectedUnit()) ? player->GetSelectedUnit()->GetName().c_str() : "",
guid.ToString().c_str());
}
}
// some commands have custom error messages. Don't send the default one in these cases.
else if (!HasSentErrorMessage())
{
if (!table[i].Help.empty())
SendSysMessage(table[i].Help.c_str());
else
SendSysMessage(LANG_CMD_SYNTAX);
}
if (Acore::ChatCommands::TryExecuteCommand(*this, text))
return true;
}
return false;
}
bool ChatHandler::SetDataForCommandInTable(std::vector<ChatCommand>& table, char const* text, uint32 security, std::string const& help, std::string const& fullcommand)
{
std::string cmd = "";
while (*text != ' ' && *text != '\0')
{
cmd += *text;
++text;
}
while (*text == ' ') ++text;
for (uint32 i = 0; i < table.size(); i++)
{
// for data fill use full explicit command names
if (table[i].Name != cmd)
continue;
// select subcommand from child commands list (including "")
if (!table[i].ChildCommands.empty())
{
if (SetDataForCommandInTable(table[i].ChildCommands, text, security, help, fullcommand))
return true;
else if (*text)
return false;
// fail with "" subcommands, then use normal level up command instead
}
// expected subcommand by full name DB content
else if (*text)
{
LOG_ERROR("sql.sql", "Table `command` have unexpected subcommand '%s' in command '%s', skip.", text, fullcommand.c_str());
return false;
}
table[i].SecurityLevel = security;
table[i].Help = help;
return true;
}
// in case "" command let process by caller
if (!cmd.empty())
{
if (&table == &getCommandTable())
LOG_ERROR("sql.sql", "Table `command` have non-existing command '%s', skip.", cmd.c_str());
else
LOG_ERROR("sql.sql", "Table `command` have non-existing subcommand '%s' in command '%s', skip.", cmd.c_str(), fullcommand.c_str());
}
return false;
}
bool ChatHandler::ParseCommands(char const* text)
{
ASSERT(text);
ASSERT(*text);
std::string fullcmd = text;
// Pretend commands don't exist for regular players
if (m_session && AccountMgr::IsPlayerAccount(m_session->GetSecurity()) && !sWorld->getBoolConfig(CONFIG_ALLOW_PLAYER_COMMANDS))
return false;
/// chat case (.command or !command format)
if (m_session)
{
if (text[0] != '!' && text[0] != '.')
return false;
}
/// ignore single . and ! in line
if (strlen(text) < 2)
return false;
// original `text` can't be used. It content destroyed in command code processing.
/// ignore messages staring from many dots.
if ((text[0] == '.' && text[1] == '.') || (text[0] == '!' && text[1] == '!'))
return false;
/// skip first . or ! (in console allowed use command with . and ! and without its)
if (text[0] == '!' || text[0] == '.')
++text;
if (!ExecuteCommandInTable(getCommandTable(), text, fullcmd))
{
#ifdef ELUNA
if (!sEluna->OnCommand(GetSession() ? GetSession()->GetPlayer() : nullptr, text))
return true;
#endif
if (m_session && AccountMgr::IsPlayerAccount(m_session->GetSecurity()))
return false;
SendSysMessage(LANG_NO_CMD);
}
// Send error message for GMs
PSendSysMessage(LANG_CMD_INVALID, STRING_VIEW_FMT_ARG(text));
SetSentErrorMessage(true);
return true;
}
bool ChatHandler::isValidChatMessage(char const* message)
bool ChatHandler::ParseCommands(std::string_view text)
{
/*
Valid examples:
|cffa335ee|Hitem:812:0:0:0:0:0:0:0:70|h[Glowing Brightwood Staff]|h|r
|cff808080|Hquest:2278:47|h[The Platinum Discs]|h|r
|cffffd000|Htrade:4037:1:150:1:6AAAAAAAAAAAAAAAAAAAAAAOAADAAAAAAAAAAAAAAAAIAAAAAAAAA|h[Engineering]|h|r
|cff4e96f7|Htalent:2232:-1|h[Taste for Blood]|h|r
|cff71d5ff|Hspell:21563|h[Command]|h|r
|cffffd000|Henchant:3919|h[Engineering: Rough Dynamite]|h|r
|cffffff00|Hachievement:546:0000000000000001:0:0:0:-1:0:0:0:0|h[Safe Deposit]|h|r
|cff66bbff|Hglyph:21:762|h[Glyph of Bladestorm]|h|r
ASSERT(!text.empty());
| will be escaped to ||
*/
if (strlen(message) > 255)
// chat case (.command or !command format)
if ((text[0] != '!') && (text[0] != '.'))
return false;
// more simple checks
if (sWorld->getIntConfig(CONFIG_CHAT_STRICT_LINK_CHECKING_SEVERITY) < 3)
{
const char validSequence[6] = "cHhhr";
const char* validSequenceIterator = validSequence;
const std::string validCommands = "cHhr|";
while (*message)
{
// find next pipe command
message = strchr(message, '|');
if (!message)
return true;
++message;
char commandChar = *message;
if (validCommands.find(commandChar) == std::string::npos)
return false;
++message;
// validate sequence
if (sWorld->getIntConfig(CONFIG_CHAT_STRICT_LINK_CHECKING_SEVERITY) == 2)
{
if (commandChar == *validSequenceIterator)
{
if (validSequenceIterator == validSequence + 4)
validSequenceIterator = validSequence;
else
++validSequenceIterator;
}
else
return false;
}
}
return true;
}
return LinkExtractor(message).IsValidMessage();
}
bool ChatHandler::ShowHelpForSubCommands(std::vector<ChatCommand> const& table, char const* cmd, char const* subcmd)
{
std::string list;
for (uint32 i = 0; i < table.size(); ++i)
{
// must be available (ignore handler existence for show command with possible available subcommands)
if (!isAvailable(table[i]))
continue;
// for empty subcmd show all available
if (*subcmd && !hasStringAbbr(table[i].Name, subcmd))
continue;
if (m_session)
list += "\n |- ";
else
list += "\n\r |- ";
list += table[i].Name;
if (!table[i].ChildCommands.empty())
list += " ...";
}
if (list.empty())
// ignore single . and ! in line
if (text.length() < 2)
return false;
if (&table == &getCommandTable())
{
SendSysMessage(LANG_AVIABLE_CMD);
PSendSysMessage("%s", list.c_str());
}
else
PSendSysMessage(LANG_SUBCMDS_LIST, cmd, list.c_str());
// ignore messages staring from many dots.
if (text[1] == text[0])
return false;
return true;
}
// ignore messages with separator after .
if (text[1] == Acore::Impl::ChatCommands::COMMAND_DELIMITER)
return false;
bool ChatHandler::ShowHelpForCommand(std::vector<ChatCommand> const& table, const char* cmd)
{
if (*cmd)
{
for (uint32 i = 0; i < table.size(); ++i)
{
// must be available (ignore handler existence for show command with possible available subcommands)
if (!isAvailable(table[i]))
continue;
if (!hasStringAbbr(table[i].Name, cmd))
continue;
// have subcommand
char const* subcmd = (*cmd) ? strtok(nullptr, " ") : "";
if (!table[i].ChildCommands.empty() && subcmd && *subcmd)
{
if (ShowHelpForCommand(table[i].ChildCommands, subcmd))
return true;
}
if (!table[i].Help.empty())
SendSysMessage(table[i].Help.c_str());
if (!table[i].ChildCommands.empty())
if (ShowHelpForSubCommands(table[i].ChildCommands, table[i].Name, subcmd ? subcmd : ""))
return true;
return !table[i].Help.empty();
}
}
else
{
for (uint32 i = 0; i < table.size(); ++i)
{
// must be available (ignore handler existence for show command with possible available subcommands)
if (!isAvailable(table[i]))
continue;
if (strlen(table[i].Name))
continue;
if (!table[i].Help.empty())
SendSysMessage(table[i].Help.c_str());
if (!table[i].ChildCommands.empty())
if (ShowHelpForSubCommands(table[i].ChildCommands, "", ""))
return true;
return !table[i].Help.empty();
}
}
return ShowHelpForSubCommands(table, "", cmd);
return _ParseCommands(text.substr(1));
}
size_t ChatHandler::BuildChatPacket(WorldPacket& data, ChatMsg chatType, Language language, ObjectGuid senderGUID, ObjectGuid receiverGUID, std::string_view message, uint8 chatTag,
@@ -1009,21 +594,6 @@ uint32 ChatHandler::extractSpellIdFromLink(char* text)
return 0;
}
GameTele const* ChatHandler::extractGameTeleFromLink(char* text)
{
// id, or string, or [name] Shift-click form |color|Htele:id|h[name]|h|r
char* cId = extractKeyFromLink(text, "Htele");
if (!cId)
return nullptr;
// id case (explicit or from shift link)
if (cId[0] >= '0' || cId[0] <= '9')
if (uint32 id = atoi(cId))
return sObjectMgr->GetGameTele(id);
return sObjectMgr->GetGameTele(cId);
}
enum GuidLinkType
{
SPELL_LINK_PLAYER = 0, // must be first for selection in not link case
@@ -1140,10 +710,15 @@ bool ChatHandler::extractPlayerTarget(char* args, Player** player, ObjectGuid* p
}
else
{
// populate strtok buffer to prevent crashes
static char dummy[1] = "";
strtok(dummy, "");
Player* pl = getSelectedPlayer();
// if allowed player pointer
if (player)
*player = pl;
// if allowed player guid (if no then only online players allowed)
if (player_guid)
*player_guid = pl ? pl->GetGUID() : ObjectGuid::Empty;
@@ -1163,24 +738,6 @@ bool ChatHandler::extractPlayerTarget(char* args, Player** player, ObjectGuid* p
return true;
}
void ChatHandler::extractOptFirstArg(char* args, char** arg1, char** arg2)
{
char* p1 = strtok(args, " ");
char* p2 = strtok(nullptr, " ");
if (!p2)
{
p2 = p1;
p1 = nullptr;
}
if (arg1)
*arg1 = p1;
if (arg2)
*arg2 = p2;
}
char* ChatHandler::extractQuotedArg(char* args)
{
if (!*args)
@@ -1247,18 +804,24 @@ char const* CliHandler::GetAcoreString(uint32 entry) const
return sObjectMgr->GetAcoreStringForDBCLocale(entry);
}
bool CliHandler::isAvailable(ChatCommand const& cmd) const
{
// skip non-console commands in console case
return cmd.AllowConsole;
}
void CliHandler::SendSysMessage(const char* str)
void CliHandler::SendSysMessage(std::string_view str, bool /*escapeCharacters*/)
{
m_print(m_callbackArg, str);
m_print(m_callbackArg, "\r\n");
}
bool CliHandler::ParseCommands(std::string_view str)
{
if (str.empty())
return false;
// Console allows using commands both with and without leading indicator
if (str[0] == '.' || str[0] == '!')
str = str.substr(1);
return _ParseCommands(str);
}
std::string CliHandler::GetNameLink() const
{
return GetAcoreString(LANG_CONSOLE_COMMAND);