mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-01-22 21:26:23 +00:00
feat(Core/Chat): new argument parsing and unify chat hyperlink parsing (#6243)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#ifndef AZEROTHCORE_CHAT_H
|
||||
#define AZEROTHCORE_CHAT_H
|
||||
|
||||
#include "ChatCommand.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "Errors.h"
|
||||
#include "WorldSession.h"
|
||||
@@ -33,26 +34,9 @@ class WorldObject;
|
||||
|
||||
struct GameTele;
|
||||
|
||||
class ChatCommand
|
||||
{
|
||||
typedef bool(*pHandler)(ChatHandler*, char const*);
|
||||
|
||||
public:
|
||||
ChatCommand(char const* name, uint32 securityLevel, bool allowConsole, pHandler handler, std::string help, std::vector<ChatCommand> childCommands = std::vector<ChatCommand>())
|
||||
: Name(ASSERT_NOTNULL(name)), SecurityLevel(securityLevel), AllowConsole(allowConsole), Handler(handler), Help(std::move(help)), ChildCommands(std::move(childCommands)) { }
|
||||
|
||||
char const* Name;
|
||||
uint32 SecurityLevel;
|
||||
bool AllowConsole;
|
||||
pHandler Handler;
|
||||
std::string Help;
|
||||
std::vector<ChatCommand> ChildCommands;
|
||||
};
|
||||
|
||||
class ChatHandler
|
||||
class AC_GAME_API ChatHandler
|
||||
{
|
||||
public:
|
||||
WorldSession* GetSession() { return m_session; }
|
||||
explicit ChatHandler(WorldSession* session) : m_session(session), sentErrorMessage(false) {}
|
||||
virtual ~ChatHandler() { }
|
||||
|
||||
@@ -68,24 +52,35 @@ public:
|
||||
|
||||
// function with different implementation for chat/console
|
||||
virtual char const* GetAcoreString(uint32 entry) const;
|
||||
virtual void SendSysMessage(char const* str);
|
||||
virtual void SendSysMessage(std::string_view str, bool escapeCharacters = false);
|
||||
|
||||
void SendSysMessage(uint32 entry);
|
||||
void PSendSysMessage(char const* format, ...) ATTR_PRINTF(2, 3);
|
||||
void PSendSysMessage(uint32 entry, ...);
|
||||
std::string PGetParseString(uint32 entry, ...) const;
|
||||
|
||||
bool ParseCommands(const char* text);
|
||||
template<typename... Args>
|
||||
void PSendSysMessage(char const* fmt, Args&&... args)
|
||||
{
|
||||
SendSysMessage(Acore::StringFormat(fmt, std::forward<Args>(args)...).c_str());
|
||||
}
|
||||
|
||||
static std::vector<ChatCommand> const& getCommandTable();
|
||||
template<typename... Args>
|
||||
void PSendSysMessage(uint32 entry, Args&&... args)
|
||||
{
|
||||
SendSysMessage(PGetParseString(entry, std::forward<Args>(args)...).c_str());
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
std::string PGetParseString(uint32 entry, Args&&... args) const
|
||||
{
|
||||
return Acore::StringFormat(GetAcoreString(entry), std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
bool _ParseCommands(std::string_view text);
|
||||
virtual bool ParseCommands(std::string_view text);
|
||||
|
||||
bool isValidChatMessage(const char* msg);
|
||||
void SendGlobalSysMessage(const char* str);
|
||||
|
||||
bool hasStringAbbr(const char* name, const char* part);
|
||||
|
||||
// function with different implementation for chat/console
|
||||
virtual bool isAvailable(ChatCommand const& cmd) const;
|
||||
virtual bool IsHumanReadable() const { return true; }
|
||||
virtual std::string GetNameLink() const { return GetNameLink(m_session->GetPlayer()); }
|
||||
virtual bool needReportToTarget(Player* chr) const;
|
||||
virtual LocaleConstant GetSessionDbcLocale() const;
|
||||
@@ -102,16 +97,12 @@ public:
|
||||
// Returns either the selected player or self if there is no selected player
|
||||
Player* getSelectedPlayerOrSelf();
|
||||
|
||||
char* extractKeyFromLink(char* text, char const* linkType, char** something1 = nullptr);
|
||||
char* extractKeyFromLink(char* text, char const* const* linkTypes, int* found_idx, char** something1 = nullptr);
|
||||
|
||||
// if args have single value then it return in arg2 and arg1 == nullptr
|
||||
void extractOptFirstArg(char* args, char** arg1, char** arg2);
|
||||
char* extractQuotedArg(char* args);
|
||||
char* extractKeyFromLink(char* text, char const* linkType, char** something1 = nullptr);
|
||||
char* extractKeyFromLink(char* text, char const* const* linkTypes, int* found_idx, char** something1 = nullptr);
|
||||
char* extractQuotedArg(char* args);
|
||||
|
||||
uint32 extractSpellIdFromLink(char* text);
|
||||
ObjectGuid::LowType extractLowGuidFromLink(char* text, HighGuid& guidHigh);
|
||||
GameTele const* extractGameTeleFromLink(char* text);
|
||||
bool GetPlayerGroupAndGUIDByName(const char* cname, Player*& player, Group*& group, ObjectGuid& guid, bool offline = false);
|
||||
std::string extractPlayerNameFromLink(char* text);
|
||||
// select by arg (name/link) or in-game selection online/offline player
|
||||
@@ -125,34 +116,31 @@ public:
|
||||
Creature* GetCreatureFromPlayerMapByDbGuid(ObjectGuid::LowType lowguid);
|
||||
bool HasSentErrorMessage() const { return sentErrorMessage; }
|
||||
void SetSentErrorMessage(bool val) { sentErrorMessage = val; }
|
||||
static bool LoadCommandTable() { return load_command_table; }
|
||||
static void SetLoadCommandTable(bool val) { load_command_table = val; }
|
||||
|
||||
bool ShowHelpForCommand(std::vector<ChatCommand> const& table, const char* cmd);
|
||||
bool IsConsole() const { return (m_session == nullptr); }
|
||||
Player* GetPlayer() const;
|
||||
WorldSession* GetSession() { return m_session; }
|
||||
bool IsAvailable(uint32 securityLevel) const;
|
||||
protected:
|
||||
explicit ChatHandler() : m_session(nullptr), sentErrorMessage(false) {} // for CLI subclass
|
||||
static bool SetDataForCommandInTable(std::vector<ChatCommand>& table, const char* text, uint32 securityLevel, std::string const& help, std::string const& fullcommand);
|
||||
bool ExecuteCommandInTable(std::vector<ChatCommand> const& table, const char* text, std::string const& fullcmd);
|
||||
bool ShowHelpForSubCommands(std::vector<ChatCommand> const& table, char const* cmd, char const* subcmd);
|
||||
|
||||
private:
|
||||
WorldSession* m_session; // != nullptr for chat command call and nullptr for CLI command
|
||||
|
||||
// common global flag
|
||||
static bool load_command_table;
|
||||
bool sentErrorMessage;
|
||||
};
|
||||
|
||||
class CliHandler : public ChatHandler
|
||||
class AC_GAME_API CliHandler : public ChatHandler
|
||||
{
|
||||
public:
|
||||
typedef void Print(void*, char const*);
|
||||
explicit CliHandler(void* callbackArg, Print* zprint) : m_callbackArg(callbackArg), m_print(zprint) {}
|
||||
using Print = void(void*, std::string_view);
|
||||
explicit CliHandler(void* callbackArg, Print* zprint) : m_callbackArg(callbackArg), m_print(zprint) { }
|
||||
|
||||
// overwrite functions
|
||||
char const* GetAcoreString(uint32 entry) const override;
|
||||
bool isAvailable(ChatCommand const& cmd) const override;
|
||||
void SendSysMessage(const char* str) override;
|
||||
void SendSysMessage(std::string_view, bool escapeCharacters) override;
|
||||
bool ParseCommands(std::string_view str) override;
|
||||
std::string GetNameLink() const override;
|
||||
bool needReportToTarget(Player* chr) const override;
|
||||
LocaleConstant GetSessionDbcLocale() const override;
|
||||
|
||||
534
src/server/game/Chat/ChatCommands/ChatCommand.cpp
Normal file
534
src/server/game/Chat/ChatCommands/ChatCommand.cpp
Normal file
@@ -0,0 +1,534 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatCommand.h"
|
||||
#include "AccountMgr.h"
|
||||
#include "Chat.h"
|
||||
#include "DBCStores.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "Player.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "StringFormat.h"
|
||||
#include "Tokenize.h"
|
||||
#include "WorldSession.h"
|
||||
|
||||
using ChatSubCommandMap = std::map<std::string_view, Acore::Impl::ChatCommands::ChatCommandNode, StringCompareLessI_T>;
|
||||
|
||||
void Acore::Impl::ChatCommands::ChatCommandNode::LoadFromBuilder(ChatCommandBuilder const& builder)
|
||||
{
|
||||
if (std::holds_alternative<ChatCommandBuilder::InvokerEntry>(builder._data))
|
||||
{
|
||||
ASSERT(!_invoker, "Duplicate blank sub-command.");
|
||||
AcoreStrings help;
|
||||
std::tie(_invoker, help, _permission) = *(std::get<ChatCommandBuilder::InvokerEntry>(builder._data));
|
||||
if (help)
|
||||
_help.emplace<AcoreStrings>(help);
|
||||
}
|
||||
else
|
||||
LoadCommandsIntoMap(this, _subCommands, std::get<ChatCommandBuilder::SubCommandEntry>(builder._data));
|
||||
}
|
||||
|
||||
/*static*/ void Acore::Impl::ChatCommands::ChatCommandNode::LoadCommandsIntoMap(ChatCommandNode* blank,
|
||||
std::map<std::string_view, Acore::Impl::ChatCommands::ChatCommandNode, StringCompareLessI_T>& map, Acore::ChatCommands::ChatCommandTable const& commands)
|
||||
{
|
||||
for (ChatCommandBuilder const& builder : commands)
|
||||
{
|
||||
if (builder._name.empty())
|
||||
{
|
||||
ASSERT(blank, "Empty name command at top level is not permitted.");
|
||||
blank->LoadFromBuilder(builder);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<std::string_view> const tokens = Acore::Tokenize(builder._name, COMMAND_DELIMITER, false);
|
||||
ASSERT(!tokens.empty(), "Invalid command name '" STRING_VIEW_FMT "'.", STRING_VIEW_FMT_ARG(builder._name));
|
||||
ChatSubCommandMap* subMap = ↦
|
||||
for (size_t i = 0, n = (tokens.size() - 1); i < n; ++i)
|
||||
subMap = &((*subMap)[tokens[i]]._subCommands);
|
||||
((*subMap)[tokens.back()]).LoadFromBuilder(builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static ChatSubCommandMap COMMAND_MAP;
|
||||
/*static*/ ChatSubCommandMap const& Acore::Impl::ChatCommands::ChatCommandNode::GetTopLevelMap()
|
||||
{
|
||||
if (COMMAND_MAP.empty())
|
||||
LoadCommandMap();
|
||||
|
||||
return COMMAND_MAP;
|
||||
}
|
||||
/*static*/ void Acore::Impl::ChatCommands::ChatCommandNode::InvalidateCommandMap()
|
||||
{
|
||||
COMMAND_MAP.clear();
|
||||
}
|
||||
|
||||
/*static*/ void Acore::Impl::ChatCommands::ChatCommandNode::LoadCommandMap()
|
||||
{
|
||||
InvalidateCommandMap();
|
||||
LoadCommandsIntoMap(nullptr, COMMAND_MAP, sScriptMgr->GetChatCommands());
|
||||
|
||||
if (PreparedQueryResult result = WorldDatabase.Query(WorldDatabase.GetPreparedStatement(WORLD_SEL_COMMANDS)))
|
||||
{
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
std::string_view const name = fields[0].GetStringView();
|
||||
std::string_view const help = fields[2].GetStringView();
|
||||
uint32 const secLevel = fields[1].GetUInt8();
|
||||
|
||||
ChatCommandNode* cmd = nullptr;
|
||||
ChatSubCommandMap* map = &COMMAND_MAP;
|
||||
|
||||
for (std::string_view key : Acore::Tokenize(name, COMMAND_DELIMITER, false))
|
||||
{
|
||||
auto it = map->find(key);
|
||||
if (it != map->end())
|
||||
{
|
||||
cmd = &it->second;
|
||||
map = &cmd->_subCommands;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("sql.sql", "Table `command` contains data for non-existant command '" STRING_VIEW_FMT "'. Skipped.", STRING_VIEW_FMT_ARG(name));
|
||||
cmd = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cmd)
|
||||
continue;
|
||||
|
||||
if (cmd->_invoker && (cmd->_permission.RequiredLevel != secLevel))
|
||||
{
|
||||
LOG_WARN("sql.sql", "Table `command` has permission %u for '" STRING_VIEW_FMT "' which does not match the core (%u). Overriding.",
|
||||
secLevel, STRING_VIEW_FMT_ARG(name), cmd->_permission.RequiredLevel);
|
||||
|
||||
cmd->_permission.RequiredLevel = secLevel;
|
||||
}
|
||||
|
||||
if (std::holds_alternative<std::string>(cmd->_help))
|
||||
LOG_ERROR("sql.sql", "Table `command` contains duplicate data for command '" STRING_VIEW_FMT "'. Skipped.", STRING_VIEW_FMT_ARG(name));
|
||||
|
||||
if (std::holds_alternative<std::monostate>(cmd->_help))
|
||||
cmd->_help.emplace<std::string>(help);
|
||||
else
|
||||
LOG_ERROR("sql.sql", "Table `command` contains legacy help text for command '" STRING_VIEW_FMT "', which uses `trinity_string`. Skipped.", STRING_VIEW_FMT_ARG(name));
|
||||
} while (result->NextRow());
|
||||
}
|
||||
|
||||
for (auto& [name, cmd] : COMMAND_MAP)
|
||||
cmd.ResolveNames(std::string(name));
|
||||
}
|
||||
|
||||
void Acore::Impl::ChatCommands::ChatCommandNode::ResolveNames(std::string name)
|
||||
{
|
||||
if (_invoker && std::holds_alternative<std::monostate>(_help))
|
||||
LOG_WARN("sql.sql", "Table `command` is missing help text for command '" STRING_VIEW_FMT "'.", STRING_VIEW_FMT_ARG(name));
|
||||
|
||||
_name = name;
|
||||
|
||||
for (auto& [subToken, cmd] : _subCommands)
|
||||
{
|
||||
std::string subName(name);
|
||||
subName.push_back(COMMAND_DELIMITER);
|
||||
subName.append(subToken);
|
||||
cmd.ResolveNames(subName);
|
||||
}
|
||||
}
|
||||
|
||||
static void LogCommandUsage(WorldSession const& session, std::string_view cmdStr)
|
||||
{
|
||||
if (AccountMgr::IsPlayerAccount(session.GetSecurity()))
|
||||
return;
|
||||
|
||||
Player* player = session.GetPlayer();
|
||||
ObjectGuid targetGuid = player->GetTarget();
|
||||
uint32 areaId = player->GetAreaId();
|
||||
uint32 zoneId = player->GetZoneId();
|
||||
std::string areaName = "Unknown";
|
||||
std::string zoneName = "Unknown";
|
||||
LocaleConstant locale = sWorld->GetDefaultDbcLocale();
|
||||
|
||||
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(areaId))
|
||||
{
|
||||
areaName = area->area_name[locale];
|
||||
}
|
||||
|
||||
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(zoneId))
|
||||
{
|
||||
zoneName = zone->area_name[locale];
|
||||
}
|
||||
|
||||
std::string logMessage = Acore::StringFormatFmt("Command: {} [Player: {} ({}) (Account: {}) X: {} Y: {} Z: {} Map: {} ({}) Area: {} ({}) Zone: {} ({}) Selected: {} ({})]",
|
||||
cmdStr, player->GetName(), player->GetGUID().ToString(),
|
||||
session.GetAccountId(),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(), player->GetMapId(),
|
||||
player->FindMap() ? player->FindMap()->GetMapName() : "Unknown",
|
||||
areaId, areaName, zoneId, zoneName,
|
||||
(player->GetSelectedUnit()) ? player->GetSelectedUnit()->GetName() : "",
|
||||
targetGuid.ToString());
|
||||
|
||||
LOG_GM(session.GetAccountId(), logMessage);
|
||||
}
|
||||
|
||||
void Acore::Impl::ChatCommands::ChatCommandNode::SendCommandHelp(ChatHandler& handler) const
|
||||
{
|
||||
bool const hasInvoker = IsInvokerVisible(handler);
|
||||
if (hasInvoker)
|
||||
{
|
||||
if (std::holds_alternative<AcoreStrings>(_help))
|
||||
handler.SendSysMessage(std::get<AcoreStrings>(_help));
|
||||
else if (std::holds_alternative<std::string>(_help))
|
||||
handler.SendSysMessage(std::get<std::string>(_help));
|
||||
else
|
||||
{
|
||||
handler.PSendSysMessage(LANG_CMD_HELP_GENERIC, STRING_VIEW_FMT_ARG(_name));
|
||||
handler.PSendSysMessage(LANG_CMD_NO_HELP_AVAILABLE, STRING_VIEW_FMT_ARG(_name));
|
||||
}
|
||||
}
|
||||
|
||||
bool header = false;
|
||||
|
||||
for (auto it = _subCommands.begin(); it != _subCommands.end(); ++it)
|
||||
{
|
||||
bool const subCommandHasSubCommand = it->second.HasVisibleSubCommands(handler);
|
||||
|
||||
if (!subCommandHasSubCommand && !it->second.IsInvokerVisible(handler))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!header)
|
||||
{
|
||||
if (!hasInvoker)
|
||||
{
|
||||
handler.PSendSysMessage(LANG_CMD_HELP_GENERIC, STRING_VIEW_FMT_ARG(_name));
|
||||
}
|
||||
|
||||
handler.SendSysMessage(LANG_SUBCMDS_LIST);
|
||||
header = true;
|
||||
}
|
||||
|
||||
handler.PSendSysMessage(subCommandHasSubCommand ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, STRING_VIEW_FMT_ARG(it->second._name));
|
||||
}
|
||||
}
|
||||
|
||||
namespace Acore::Impl::ChatCommands
|
||||
{
|
||||
struct FilteredCommandListIterator
|
||||
{
|
||||
public:
|
||||
FilteredCommandListIterator(ChatSubCommandMap const& map, ChatHandler const& handler, std::string_view token)
|
||||
: _handler{ handler }, _token{ token }, _it{ map.lower_bound(token) }, _end{ map.end() }
|
||||
{
|
||||
_skip();
|
||||
}
|
||||
|
||||
decltype(auto) operator*() const { return _it.operator*(); }
|
||||
decltype(auto) operator->() const { return _it.operator->(); }
|
||||
FilteredCommandListIterator& operator++()
|
||||
{
|
||||
++_it;
|
||||
_skip();
|
||||
return *this;
|
||||
}
|
||||
explicit operator bool() const { return (_it != _end); }
|
||||
bool operator!() const { return !static_cast<bool>(*this); }
|
||||
|
||||
private:
|
||||
void _skip()
|
||||
{
|
||||
if ((_it != _end) && !StringStartsWithI(_it->first, _token))
|
||||
_it = _end;
|
||||
while ((_it != _end) && !_it->second.IsVisible(_handler))
|
||||
{
|
||||
++_it;
|
||||
if ((_it != _end) && !StringStartsWithI(_it->first, _token))
|
||||
_it = _end;
|
||||
}
|
||||
}
|
||||
|
||||
ChatHandler const& _handler;
|
||||
std::string_view const _token;
|
||||
ChatSubCommandMap::const_iterator _it, _end;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/*static*/ bool Acore::Impl::ChatCommands::ChatCommandNode::TryExecuteCommand(ChatHandler& handler, std::string_view cmdStr)
|
||||
{
|
||||
ChatCommandNode const* cmd = nullptr;
|
||||
ChatSubCommandMap const* map = &GetTopLevelMap();
|
||||
|
||||
while (!cmdStr.empty() && (cmdStr.front() == COMMAND_DELIMITER))
|
||||
cmdStr.remove_prefix(1);
|
||||
|
||||
while (!cmdStr.empty() && (cmdStr.back() == COMMAND_DELIMITER))
|
||||
cmdStr.remove_suffix(1);
|
||||
|
||||
std::string_view oldTail = cmdStr;
|
||||
while (!oldTail.empty())
|
||||
{
|
||||
/* oldTail = token DELIMITER newTail */
|
||||
auto [token, newTail] = tokenize(oldTail);
|
||||
ASSERT(!token.empty());
|
||||
|
||||
FilteredCommandListIterator it1(*map, handler, token);
|
||||
if (!it1)
|
||||
break; /* no matching subcommands found */
|
||||
|
||||
if (!StringEqualI(it1->first, token))
|
||||
{ /* ok, so it1 points at a partially matching subcommand - let's see if there are others */
|
||||
auto it2 = it1;
|
||||
++it2;
|
||||
|
||||
if (it2)
|
||||
{ /* there are multiple matching subcommands - print possibilities and return */
|
||||
if (cmd)
|
||||
handler.PSendSysMessage(LANG_SUBCMD_AMBIGUOUS, STRING_VIEW_FMT_ARG(cmd->_name), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(token));
|
||||
else
|
||||
handler.PSendSysMessage(LANG_CMD_AMBIGUOUS, STRING_VIEW_FMT_ARG(token));
|
||||
|
||||
handler.PSendSysMessage(it1->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, STRING_VIEW_FMT_ARG(it1->first));
|
||||
do
|
||||
{
|
||||
handler.PSendSysMessage(it2->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, STRING_VIEW_FMT_ARG(it2->first));
|
||||
} while (++it2);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* now we matched exactly one subcommand, and it1 points to it; go down the rabbit hole */
|
||||
cmd = &it1->second;
|
||||
map = &cmd->_subCommands;
|
||||
|
||||
oldTail = newTail;
|
||||
}
|
||||
|
||||
if (cmd)
|
||||
{ /* if we matched a command at some point, invoke it */
|
||||
handler.SetSentErrorMessage(false);
|
||||
if (cmd->IsInvokerVisible(handler) && cmd->_invoker(&handler, oldTail))
|
||||
{ /* invocation succeeded, log this */
|
||||
if (!handler.IsConsole())
|
||||
LogCommandUsage(*handler.GetSession(), cmdStr);
|
||||
}
|
||||
else if (!handler.HasSentErrorMessage())
|
||||
{ /* invocation failed, we should show usage */
|
||||
cmd->SendCommandHelp(handler);
|
||||
handler.SetSentErrorMessage(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*static*/ void Acore::Impl::ChatCommands::ChatCommandNode::SendCommandHelpFor(ChatHandler& handler, std::string_view cmdStr)
|
||||
{
|
||||
ChatCommandNode const* cmd = nullptr;
|
||||
ChatSubCommandMap const* map = &GetTopLevelMap();
|
||||
|
||||
for (std::string_view token : Acore::Tokenize(cmdStr, COMMAND_DELIMITER, false))
|
||||
{
|
||||
FilteredCommandListIterator it1(*map, handler, token);
|
||||
if (!it1)
|
||||
{ /* no matching subcommands found */
|
||||
if (cmd)
|
||||
{
|
||||
cmd->SendCommandHelp(handler);
|
||||
handler.PSendSysMessage(LANG_SUBCMD_INVALID, STRING_VIEW_FMT_ARG(cmd->_name), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(token));
|
||||
}
|
||||
else
|
||||
handler.PSendSysMessage(LANG_CMD_INVALID, STRING_VIEW_FMT_ARG(token));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!StringEqualI(it1->first, token))
|
||||
{ /* ok, so it1 points at a partially matching subcommand - let's see if there are others */
|
||||
auto it2 = it1;
|
||||
++it2;
|
||||
|
||||
if (it2)
|
||||
{ /* there are multiple matching subcommands - print possibilities and return */
|
||||
if (cmd)
|
||||
handler.PSendSysMessage(LANG_SUBCMD_AMBIGUOUS, STRING_VIEW_FMT_ARG(cmd->_name), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(token));
|
||||
else
|
||||
handler.PSendSysMessage(LANG_CMD_AMBIGUOUS, STRING_VIEW_FMT_ARG(token));
|
||||
|
||||
handler.PSendSysMessage(it1->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, STRING_VIEW_FMT_ARG(it1->first));
|
||||
do
|
||||
{
|
||||
handler.PSendSysMessage(it2->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, STRING_VIEW_FMT_ARG(it2->first));
|
||||
} while (++it2);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cmd = &it1->second;
|
||||
map = &cmd->_subCommands;
|
||||
}
|
||||
|
||||
if (cmd)
|
||||
cmd->SendCommandHelp(handler);
|
||||
else if (cmdStr.empty())
|
||||
{
|
||||
FilteredCommandListIterator it(*map, handler, "");
|
||||
if (!it)
|
||||
return;
|
||||
handler.SendSysMessage(LANG_AVAILABLE_CMDS);
|
||||
do
|
||||
{
|
||||
handler.PSendSysMessage(it->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, STRING_VIEW_FMT_ARG(it->second._name));
|
||||
} while (++it);
|
||||
}
|
||||
else
|
||||
handler.PSendSysMessage(LANG_CMD_INVALID, STRING_VIEW_FMT_ARG(cmdStr));
|
||||
}
|
||||
|
||||
/*static*/ std::vector<std::string> Acore::Impl::ChatCommands::ChatCommandNode::GetAutoCompletionsFor(ChatHandler const& handler, std::string_view cmdStr)
|
||||
{
|
||||
std::string path;
|
||||
ChatCommandNode const* cmd = nullptr;
|
||||
ChatSubCommandMap const* map = &GetTopLevelMap();
|
||||
|
||||
while (!cmdStr.empty() && (cmdStr.front() == COMMAND_DELIMITER))
|
||||
cmdStr.remove_prefix(1);
|
||||
|
||||
while (!cmdStr.empty() && (cmdStr.back() == COMMAND_DELIMITER))
|
||||
cmdStr.remove_suffix(1);
|
||||
|
||||
std::string_view oldTail = cmdStr;
|
||||
while (!oldTail.empty())
|
||||
{
|
||||
/* oldTail = token DELIMITER newTail */
|
||||
auto [token, newTail] = tokenize(oldTail);
|
||||
ASSERT(!token.empty());
|
||||
FilteredCommandListIterator it1(*map, handler, token);
|
||||
if (!it1)
|
||||
break; /* no matching subcommands found */
|
||||
|
||||
if (!StringEqualI(it1->first, token))
|
||||
{ /* ok, so it1 points at a partially matching subcommand - let's see if there are others */
|
||||
auto it2 = it1;
|
||||
++it2;
|
||||
|
||||
if (it2)
|
||||
{ /* there are multiple matching subcommands - terminate here and show possibilities */
|
||||
std::vector<std::string> vec;
|
||||
auto possibility = ([prefix = std::string_view(path), suffix = std::string_view(newTail)](std::string_view match)
|
||||
{
|
||||
if (prefix.empty())
|
||||
{
|
||||
return Acore::StringFormat(STRING_VIEW_FMT "%c" STRING_VIEW_FMT,
|
||||
STRING_VIEW_FMT_ARG(match), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(suffix));
|
||||
}
|
||||
else
|
||||
{
|
||||
return Acore::StringFormat(STRING_VIEW_FMT "%c" STRING_VIEW_FMT "%c" STRING_VIEW_FMT,
|
||||
STRING_VIEW_FMT_ARG(prefix), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(match), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(suffix));
|
||||
}
|
||||
});
|
||||
|
||||
vec.emplace_back(possibility(it1->first));
|
||||
|
||||
do vec.emplace_back(possibility(it2->first));
|
||||
while (++it2);
|
||||
|
||||
return vec;
|
||||
}
|
||||
}
|
||||
|
||||
/* now we matched exactly one subcommand, and it1 points to it; go down the rabbit hole */
|
||||
if (path.empty())
|
||||
path.assign(it1->first);
|
||||
else
|
||||
{
|
||||
path = Acore::StringFormat(STRING_VIEW_FMT "%c" STRING_VIEW_FMT,
|
||||
STRING_VIEW_FMT_ARG(path), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(it1->first));
|
||||
}
|
||||
cmd = &it1->second;
|
||||
map = &cmd->_subCommands;
|
||||
|
||||
oldTail = newTail;
|
||||
}
|
||||
|
||||
if (!oldTail.empty())
|
||||
{ /* there is some trailing text, leave it as is */
|
||||
if (cmd)
|
||||
{ /* if we matched a command at some point, auto-complete it */
|
||||
return {
|
||||
Acore::StringFormat(STRING_VIEW_FMT "%c" STRING_VIEW_FMT,
|
||||
STRING_VIEW_FMT_ARG(path), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(oldTail))
|
||||
};
|
||||
}
|
||||
else
|
||||
return {};
|
||||
}
|
||||
else
|
||||
{ /* offer all subcommands */
|
||||
auto possibility = ([prefix = std::string_view(path)](std::string_view match)
|
||||
{
|
||||
if (prefix.empty())
|
||||
return std::string(match);
|
||||
else
|
||||
{
|
||||
return Acore::StringFormat(STRING_VIEW_FMT "%c" STRING_VIEW_FMT,
|
||||
STRING_VIEW_FMT_ARG(prefix), COMMAND_DELIMITER, STRING_VIEW_FMT_ARG(match));
|
||||
}
|
||||
});
|
||||
|
||||
std::vector<std::string> vec;
|
||||
for (FilteredCommandListIterator it(*map, handler, ""); it; ++it)
|
||||
vec.emplace_back(possibility(it->first));
|
||||
return vec;
|
||||
}
|
||||
}
|
||||
|
||||
bool Acore::Impl::ChatCommands::ChatCommandNode::IsInvokerVisible(ChatHandler const& who) const
|
||||
{
|
||||
if (!_invoker)
|
||||
return false;
|
||||
|
||||
if (who.IsConsole() && (_permission.AllowConsole == Acore::ChatCommands::Console::No))
|
||||
return false;
|
||||
|
||||
if (who.IsConsole() && (_permission.AllowConsole == Acore::ChatCommands::Console::Yes))
|
||||
return true;
|
||||
|
||||
return !who.IsConsole() && who.IsAvailable(_permission.RequiredLevel);
|
||||
}
|
||||
|
||||
bool Acore::Impl::ChatCommands::ChatCommandNode::HasVisibleSubCommands(ChatHandler const& who) const
|
||||
{
|
||||
for (auto it = _subCommands.begin(); it != _subCommands.end(); ++it)
|
||||
if (it->second.IsVisible(who))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Acore::ChatCommands::LoadCommandMap() { Acore::Impl::ChatCommands::ChatCommandNode::LoadCommandMap(); }
|
||||
void Acore::ChatCommands::InvalidateCommandMap() { Acore::Impl::ChatCommands::ChatCommandNode::InvalidateCommandMap(); }
|
||||
bool Acore::ChatCommands::TryExecuteCommand(ChatHandler& handler, std::string_view cmd) { return Acore::Impl::ChatCommands::ChatCommandNode::TryExecuteCommand(handler, cmd); }
|
||||
void Acore::ChatCommands::SendCommandHelpFor(ChatHandler& handler, std::string_view cmd) { Acore::Impl::ChatCommands::ChatCommandNode::SendCommandHelpFor(handler, cmd); }
|
||||
std::vector<std::string> Acore::ChatCommands::GetAutoCompletionsFor(ChatHandler const& handler, std::string_view cmd) { return Acore::Impl::ChatCommands::ChatCommandNode::GetAutoCompletionsFor(handler, cmd); }
|
||||
280
src/server/game/Chat/ChatCommands/ChatCommand.h
Normal file
280
src/server/game/Chat/ChatCommands/ChatCommand.h
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _CHATCOMMAND_H
|
||||
#define _CHATCOMMAND_H
|
||||
|
||||
#include "advstd.h"
|
||||
#include "ChatCommandArgs.h"
|
||||
#include "ChatCommandTags.h"
|
||||
#include "Define.h"
|
||||
#include "Errors.h"
|
||||
#include "Language.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "Optional.h"
|
||||
#include "StringFormat.h"
|
||||
#include "Util.h"
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
class ChatHandler;
|
||||
|
||||
namespace Acore::ChatCommands
|
||||
{
|
||||
enum class Console : bool
|
||||
{
|
||||
No = false,
|
||||
Yes = true
|
||||
};
|
||||
|
||||
struct ChatCommandBuilder;
|
||||
using ChatCommandTable = std::vector<ChatCommandBuilder>;
|
||||
}
|
||||
|
||||
namespace Acore::Impl::ChatCommands
|
||||
{
|
||||
// forward declaration
|
||||
// ConsumeFromOffset contains the bounds check for offset, then hands off to MultiConsumer
|
||||
// the call stack is MultiConsumer -> ConsumeFromOffset -> MultiConsumer -> ConsumeFromOffset etc
|
||||
// MultiConsumer goes into ArgInfo for parsing on each iteration
|
||||
template <typename Tuple, size_t offset>
|
||||
ChatCommandResult ConsumeFromOffset(Tuple&, ChatHandler const* handler, std::string_view args);
|
||||
|
||||
template <typename Tuple, typename NextType, size_t offset>
|
||||
struct MultiConsumer
|
||||
{
|
||||
static ChatCommandResult TryConsumeTo(Tuple& tuple, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
ChatCommandResult next = ArgInfo<NextType>::TryConsume(std::get<offset>(tuple), handler, args);
|
||||
if (next)
|
||||
return ConsumeFromOffset<Tuple, offset + 1>(tuple, handler, *next);
|
||||
else
|
||||
return next;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Tuple, typename NestedNextType, size_t offset>
|
||||
struct MultiConsumer<Tuple, Optional<NestedNextType>, offset>
|
||||
{
|
||||
static ChatCommandResult TryConsumeTo(Tuple& tuple, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
// try with the argument
|
||||
auto& myArg = std::get<offset>(tuple);
|
||||
myArg.emplace();
|
||||
|
||||
ChatCommandResult result1 = ArgInfo<NestedNextType>::TryConsume(myArg.value(), handler, args);
|
||||
if (result1)
|
||||
if ((result1 = ConsumeFromOffset<Tuple, offset + 1>(tuple, handler, *result1)))
|
||||
return result1;
|
||||
// try again omitting the argument
|
||||
myArg = std::nullopt;
|
||||
ChatCommandResult result2 = ConsumeFromOffset<Tuple, offset + 1>(tuple, handler, args);
|
||||
if (result2)
|
||||
return result2;
|
||||
if (result1.HasErrorMessage() && result2.HasErrorMessage())
|
||||
{
|
||||
return Acore::StringFormat("%s \"%s\"\n%s \"%s\"",
|
||||
GetAcoreString(handler, LANG_CMDPARSER_EITHER), result2.GetErrorMessage().c_str(),
|
||||
GetAcoreString(handler, LANG_CMDPARSER_OR), result1.GetErrorMessage().c_str());
|
||||
}
|
||||
else if (result1.HasErrorMessage())
|
||||
return result1;
|
||||
else
|
||||
return result2;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Tuple, size_t offset>
|
||||
ChatCommandResult ConsumeFromOffset([[maybe_unused]] Tuple& tuple, [[maybe_unused]] ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
if constexpr (offset < std::tuple_size_v<Tuple>)
|
||||
return MultiConsumer<Tuple, std::tuple_element_t<offset, Tuple>, offset>::TryConsumeTo(tuple, handler, args);
|
||||
else if (!args.empty()) /* the entire string must be consumed */
|
||||
return std::nullopt;
|
||||
else
|
||||
return args;
|
||||
}
|
||||
|
||||
template <typename T> struct HandlerToTuple { static_assert(Acore::dependant_false_v<T>, "Invalid command handler signature"); };
|
||||
template <typename... Ts> struct HandlerToTuple<bool(ChatHandler*, Ts...)> { using type = std::tuple<ChatHandler*, advstd::remove_cvref_t<Ts>...>; };
|
||||
template <typename T> using TupleType = typename HandlerToTuple<T>::type;
|
||||
|
||||
struct CommandInvoker
|
||||
{
|
||||
CommandInvoker() : _wrapper(nullptr), _handler(nullptr) {}
|
||||
template <typename TypedHandler>
|
||||
CommandInvoker(TypedHandler& handler)
|
||||
{
|
||||
_wrapper = [](void* handler, ChatHandler* chatHandler, std::string_view argsStr)
|
||||
{
|
||||
using Tuple = TupleType<TypedHandler>;
|
||||
|
||||
Tuple arguments;
|
||||
std::get<0>(arguments) = chatHandler;
|
||||
ChatCommandResult result = ConsumeFromOffset<Tuple, 1>(arguments, chatHandler, argsStr);
|
||||
if (result)
|
||||
return std::apply(reinterpret_cast<TypedHandler*>(handler), std::move(arguments));
|
||||
else
|
||||
{
|
||||
if (result.HasErrorMessage())
|
||||
SendErrorMessageToHandler(chatHandler, result.GetErrorMessage());
|
||||
return false;
|
||||
}
|
||||
};
|
||||
_handler = reinterpret_cast<void*>(handler);
|
||||
}
|
||||
CommandInvoker(bool(&handler)(ChatHandler*, char const*))
|
||||
{
|
||||
_wrapper = [](void* handler, ChatHandler* chatHandler, std::string_view argsStr)
|
||||
{
|
||||
// make a copy of the argument string
|
||||
// legacy handlers can destroy input strings with strtok
|
||||
std::string argsStrCopy(argsStr);
|
||||
return reinterpret_cast<bool(*)(ChatHandler*, char const*)>(handler)(chatHandler, argsStrCopy.c_str());
|
||||
};
|
||||
_handler = reinterpret_cast<void*>(handler);
|
||||
}
|
||||
|
||||
explicit operator bool() const { return (_wrapper != nullptr); }
|
||||
bool operator()(ChatHandler* chatHandler, std::string_view args) const
|
||||
{
|
||||
ASSERT(_wrapper && _handler);
|
||||
return _wrapper(_handler, chatHandler, args);
|
||||
}
|
||||
|
||||
private:
|
||||
using wrapper_func = bool(void*, ChatHandler*, std::string_view);
|
||||
wrapper_func* _wrapper;
|
||||
void* _handler;
|
||||
};
|
||||
|
||||
struct CommandPermissions
|
||||
{
|
||||
CommandPermissions() : RequiredLevel{}, AllowConsole{} { }
|
||||
CommandPermissions(uint32 securityLevel, Acore::ChatCommands::Console console) : RequiredLevel{ securityLevel }, AllowConsole{ console } {}
|
||||
uint32 RequiredLevel;
|
||||
Acore::ChatCommands::Console AllowConsole;
|
||||
};
|
||||
|
||||
class ChatCommandNode
|
||||
{
|
||||
friend struct FilteredCommandListIterator;
|
||||
using ChatCommandBuilder = Acore::ChatCommands::ChatCommandBuilder;
|
||||
|
||||
public:
|
||||
static void LoadCommandMap();
|
||||
static void InvalidateCommandMap();
|
||||
static bool TryExecuteCommand(ChatHandler& handler, std::string_view cmd);
|
||||
static void SendCommandHelpFor(ChatHandler& handler, std::string_view cmd);
|
||||
static std::vector<std::string> GetAutoCompletionsFor(ChatHandler const& handler, std::string_view cmd);
|
||||
|
||||
ChatCommandNode() : _name{}, _invoker {}, _permission{}, _help{}, _subCommands{} { }
|
||||
|
||||
private:
|
||||
static std::map<std::string_view, ChatCommandNode, StringCompareLessI_T> const& GetTopLevelMap();
|
||||
static void LoadCommandsIntoMap(ChatCommandNode* blank, std::map<std::string_view, Acore::Impl::ChatCommands::ChatCommandNode, StringCompareLessI_T>& map, Acore::ChatCommands::ChatCommandTable const& commands);
|
||||
|
||||
void LoadFromBuilder(ChatCommandBuilder const& builder);
|
||||
ChatCommandNode(ChatCommandNode&& other) = default;
|
||||
|
||||
void ResolveNames(std::string name);
|
||||
void SendCommandHelp(ChatHandler& handler) const;
|
||||
|
||||
bool IsVisible(ChatHandler const& who) const { return (IsInvokerVisible(who) || HasVisibleSubCommands(who)); }
|
||||
bool IsInvokerVisible(ChatHandler const& who) const;
|
||||
bool HasVisibleSubCommands(ChatHandler const& who) const;
|
||||
|
||||
std::string _name;
|
||||
CommandInvoker _invoker;
|
||||
CommandPermissions _permission;
|
||||
std::variant<std::monostate, AcoreStrings, std::string> _help;
|
||||
std::map<std::string_view, ChatCommandNode, StringCompareLessI_T> _subCommands;
|
||||
};
|
||||
}
|
||||
|
||||
namespace Acore::ChatCommands
|
||||
{
|
||||
struct ChatCommandBuilder
|
||||
{
|
||||
friend class Acore::Impl::ChatCommands::ChatCommandNode;
|
||||
|
||||
struct InvokerEntry
|
||||
{
|
||||
template <typename T>
|
||||
InvokerEntry(T& handler, AcoreStrings help, uint32 securityLevel, Acore::ChatCommands::Console allowConsole)
|
||||
: _invoker{ handler }, _help{ help }, _permissions{ securityLevel, allowConsole } { }
|
||||
|
||||
InvokerEntry(InvokerEntry const&) = default;
|
||||
InvokerEntry(InvokerEntry&&) = default;
|
||||
|
||||
Acore::Impl::ChatCommands::CommandInvoker _invoker;
|
||||
AcoreStrings _help;
|
||||
Acore::Impl::ChatCommands::CommandPermissions _permissions;
|
||||
|
||||
auto operator*() const { return std::tie(_invoker, _help, _permissions); }
|
||||
};
|
||||
|
||||
using SubCommandEntry = std::reference_wrapper<std::vector<ChatCommandBuilder> const>;
|
||||
|
||||
ChatCommandBuilder(ChatCommandBuilder&&) = default;
|
||||
ChatCommandBuilder(ChatCommandBuilder const&) = default;
|
||||
|
||||
template <typename TypedHandler>
|
||||
ChatCommandBuilder(char const* name, TypedHandler& handler, AcoreStrings help, uint32 securityLevel, Acore::ChatCommands::Console allowConsole)
|
||||
: _name{ ASSERT_NOTNULL(name) }, _data{ std::in_place_type<InvokerEntry>, handler, help, securityLevel, allowConsole } { }
|
||||
|
||||
template <typename TypedHandler>
|
||||
ChatCommandBuilder(char const* name, TypedHandler& handler, uint32 securityLevel, Acore::ChatCommands::Console allowConsole)
|
||||
: ChatCommandBuilder(name, handler, AcoreStrings(), securityLevel, allowConsole) { }
|
||||
|
||||
ChatCommandBuilder(char const* name, std::vector<ChatCommandBuilder> const& subCommands)
|
||||
: _name{ ASSERT_NOTNULL(name) }, _data{ std::in_place_type<SubCommandEntry>, subCommands } { }
|
||||
|
||||
[[deprecated("char const* parameters to command handlers are deprecated; convert this to a typed argument handler instead")]]
|
||||
ChatCommandBuilder(char const* name, bool(&handler)(ChatHandler*, char const*), uint32 securityLevel, Acore::ChatCommands::Console allowConsole)
|
||||
: ChatCommandBuilder(name, handler, AcoreStrings(), securityLevel, allowConsole) { }
|
||||
|
||||
template <typename TypedHandler>
|
||||
[[deprecated("you are using the old-style command format; convert this to the new format ({ name, handler (not a pointer!), permission, Console::(Yes/No) })")]]
|
||||
ChatCommandBuilder(char const* name, uint32 securityLevel, bool console, TypedHandler* handler, char const*)
|
||||
: ChatCommandBuilder(name, *handler, AcoreStrings(), securityLevel, static_cast<Acore::ChatCommands::Console>(console)) { }
|
||||
|
||||
[[deprecated("you are using the old-style command format; convert this to the new format ({ name, subCommands })")]]
|
||||
ChatCommandBuilder(char const* name, uint32, bool, std::nullptr_t, char const*, std::vector <ChatCommandBuilder> const& sub)
|
||||
: ChatCommandBuilder(name, sub) { }
|
||||
|
||||
private:
|
||||
std::string_view _name;
|
||||
std::variant<InvokerEntry, SubCommandEntry> _data;
|
||||
};
|
||||
|
||||
AC_GAME_API void LoadCommandMap();
|
||||
AC_GAME_API void InvalidateCommandMap();
|
||||
AC_GAME_API bool TryExecuteCommand(ChatHandler& handler, std::string_view cmd);
|
||||
AC_GAME_API void SendCommandHelpFor(ChatHandler& handler, std::string_view cmd);
|
||||
AC_GAME_API std::vector<std::string> GetAutoCompletionsFor(ChatHandler const& handler, std::string_view cmd);
|
||||
}
|
||||
|
||||
// backwards compatibility with old patches
|
||||
using ChatCommand [[deprecated("std::vector<ChatCommand> should be ChatCommandTable! (using namespace Acore::ChatCommands)")]] = Acore::ChatCommands::ChatCommandBuilder;
|
||||
|
||||
#endif
|
||||
118
src/server/game/Chat/ChatCommands/ChatCommandArgs.cpp
Normal file
118
src/server/game/Chat/ChatCommands/ChatCommandArgs.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatCommandArgs.h"
|
||||
#include "AchievementMgr.h"
|
||||
#include "ChatCommand.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "Util.h"
|
||||
|
||||
using namespace Acore::ChatCommands;
|
||||
using ChatCommandResult = Acore::Impl::ChatCommands::ChatCommandResult;
|
||||
|
||||
struct AchievementVisitor
|
||||
{
|
||||
using value_type = AchievementEntry const*;
|
||||
value_type operator()(Hyperlink<achievement> achData) const { return achData->Achievement; }
|
||||
value_type operator()(uint32 achId) const { return sAchievementMgr->GetAchievement(achId); }
|
||||
};
|
||||
|
||||
ChatCommandResult Acore::Impl::ChatCommands::ArgInfo<AchievementEntry const*>::TryConsume(AchievementEntry const*& data, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
Variant<Hyperlink<achievement>, uint32> val;
|
||||
ChatCommandResult result = ArgInfo<decltype(val)>::TryConsume(val, handler, args);
|
||||
|
||||
if (!result || (data = val.visit(AchievementVisitor())))
|
||||
return result;
|
||||
|
||||
if (uint32* id = std::get_if<uint32>(&val))
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_ACHIEVEMENT_NO_EXIST, *id);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
struct GameTeleVisitor
|
||||
{
|
||||
using value_type = GameTele const*;
|
||||
value_type operator()(Hyperlink<tele> tele) const { return sObjectMgr->GetGameTele(tele); }
|
||||
value_type operator()(std::string_view tele) const { return sObjectMgr->GetGameTele(tele); }
|
||||
};
|
||||
|
||||
ChatCommandResult Acore::Impl::ChatCommands::ArgInfo<GameTele const*>::TryConsume(GameTele const*& data, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
Variant<Hyperlink<tele>, std::string_view> val;
|
||||
ChatCommandResult result = ArgInfo<decltype(val)>::TryConsume(val, handler, args);
|
||||
|
||||
if (!result || (data = val.visit(GameTeleVisitor())))
|
||||
return result;
|
||||
|
||||
if (val.holds_alternative<Hyperlink<tele>>())
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_GAME_TELE_ID_NO_EXIST, static_cast<uint32>(std::get<Hyperlink<tele>>(val)));
|
||||
else
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_GAME_TELE_NO_EXIST, STRING_VIEW_FMT_ARG(std::get<std::string_view>(val)));
|
||||
}
|
||||
|
||||
struct ItemTemplateVisitor
|
||||
{
|
||||
using value_type = ItemTemplate const*;
|
||||
value_type operator()(Hyperlink<item> item) const { return item->Item; }
|
||||
value_type operator()(uint32 item) { return sObjectMgr->GetItemTemplate(item); }
|
||||
};
|
||||
|
||||
ChatCommandResult Acore::Impl::ChatCommands::ArgInfo<ItemTemplate const*>::TryConsume(ItemTemplate const*& data, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
Variant<Hyperlink<item>, uint32> val;
|
||||
ChatCommandResult result = ArgInfo<decltype(val)>::TryConsume(val, handler, args);
|
||||
|
||||
if (!result || (data = val.visit(ItemTemplateVisitor())))
|
||||
return result;
|
||||
|
||||
if (uint32* id = std::get_if<uint32>(&val))
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_ITEM_NO_EXIST, *id);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
struct SpellInfoVisitor
|
||||
{
|
||||
using value_type = SpellInfo const*;
|
||||
value_type operator()(Hyperlink<enchant> enchant) const { return enchant; };
|
||||
value_type operator()(Hyperlink<glyph> glyph) const { return operator()(glyph->Glyph->SpellId); };
|
||||
value_type operator()(Hyperlink<spell> spell) const { return *spell; }
|
||||
value_type operator()(Hyperlink<talent> talent) const
|
||||
{
|
||||
return operator()(talent->Talent->RankID[talent->Rank - 1]);
|
||||
};
|
||||
value_type operator()(Hyperlink<trade> trade) const { return trade->Spell; };
|
||||
|
||||
value_type operator()(uint32 spellId) const { return sSpellMgr->GetSpellInfo(spellId); }
|
||||
};
|
||||
|
||||
ChatCommandResult Acore::Impl::ChatCommands::ArgInfo<SpellInfo const*>::TryConsume(SpellInfo const*& data, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
Variant<Hyperlink<enchant>, Hyperlink<glyph>, Hyperlink<spell>, Hyperlink<talent>, Hyperlink<trade>, uint32> val;
|
||||
ChatCommandResult result = ArgInfo<decltype(val)>::TryConsume(val, handler, args);
|
||||
|
||||
if (!result || (data = val.visit(SpellInfoVisitor())))
|
||||
return result;
|
||||
|
||||
if (uint32* id = std::get_if<uint32>(&val))
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_SPELL_NO_EXIST, *id);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
324
src/server/game/Chat/ChatCommands/ChatCommandArgs.h
Normal file
324
src/server/game/Chat/ChatCommands/ChatCommandArgs.h
Normal file
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _CHATCOMMANDARGS_H
|
||||
#define _CHATCOMMANDARGS_H
|
||||
|
||||
#include "ChatCommandHelpers.h"
|
||||
#include "ChatCommandTags.h"
|
||||
#include "SmartEnum.h"
|
||||
#include "StringConvert.h"
|
||||
#include "StringFormat.h"
|
||||
#include "Util.h"
|
||||
#include <charconv>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
struct GameTele;
|
||||
|
||||
namespace Acore::Impl::ChatCommands
|
||||
{
|
||||
|
||||
/************************** ARGUMENT HANDLERS *******************************************\
|
||||
|* Define how to extract contents of a certain requested type from a string *|
|
||||
|* Must implement the following: *|
|
||||
|* - TryConsume: T&, ChatHandler const*, std::string_view -> ChatCommandResult *|
|
||||
|* - on match, returns tail of the provided argument string (as std::string_view) *|
|
||||
|* - on specific error, returns error message (as std::string&& or char const*) *|
|
||||
|* - on generic error, returns std::nullopt (this will print command usage) *|
|
||||
|* *|
|
||||
|* - if a match is returned, T& should be initialized to the matched value *|
|
||||
|* - otherwise, the state of T& is indeterminate and caller will not use it *|
|
||||
|* *|
|
||||
\****************************************************************************************/
|
||||
template <typename T, typename = void>
|
||||
struct ArgInfo { static_assert(Acore::dependant_false_v<T>, "Invalid command parameter type - see ChatCommandArgs.h for possible types"); };
|
||||
|
||||
// catch-all for number types
|
||||
template <typename T>
|
||||
struct ArgInfo<T, std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>>>
|
||||
{
|
||||
static ChatCommandResult TryConsume(T& val, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
auto [token, tail] = tokenize(args);
|
||||
if (token.empty())
|
||||
return std::nullopt;
|
||||
|
||||
if (Optional<T> v = StringTo<T>(token, 0))
|
||||
val = *v;
|
||||
else
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_STRING_VALUE_INVALID, STRING_VIEW_FMT_ARG(token), GetTypeName<T>().c_str());
|
||||
|
||||
if constexpr (std::is_floating_point_v<T>)
|
||||
{
|
||||
if (!std::isfinite(val))
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_STRING_VALUE_INVALID, STRING_VIEW_FMT_ARG(token), GetTypeName<T>().c_str());
|
||||
}
|
||||
|
||||
return tail;
|
||||
}
|
||||
};
|
||||
|
||||
// string_view
|
||||
template <>
|
||||
struct ArgInfo<std::string_view, void>
|
||||
{
|
||||
static ChatCommandResult TryConsume(std::string_view& val, ChatHandler const*, std::string_view args)
|
||||
{
|
||||
auto [token, next] = tokenize(args);
|
||||
if (token.empty())
|
||||
return std::nullopt;
|
||||
val = token;
|
||||
return next;
|
||||
}
|
||||
};
|
||||
|
||||
// string
|
||||
template <>
|
||||
struct ArgInfo<std::string, void>
|
||||
{
|
||||
static ChatCommandResult TryConsume(std::string& val, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
std::string_view view;
|
||||
ChatCommandResult next = ArgInfo<std::string_view>::TryConsume(view, handler, args);
|
||||
if (next)
|
||||
val.assign(view);
|
||||
return next;
|
||||
}
|
||||
};
|
||||
|
||||
// wstring
|
||||
template <>
|
||||
struct ArgInfo<std::wstring, void>
|
||||
{
|
||||
static ChatCommandResult TryConsume(std::wstring& val, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
std::string_view utf8view;
|
||||
ChatCommandResult next = ArgInfo<std::string_view>::TryConsume(utf8view, handler, args);
|
||||
|
||||
if (next)
|
||||
{
|
||||
if (Utf8toWStr(utf8view, val))
|
||||
return next;
|
||||
else
|
||||
return GetAcoreString(handler, LANG_CMDPARSER_INVALID_UTF8);
|
||||
}
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
// enum
|
||||
template <typename T>
|
||||
struct ArgInfo<T, std::enable_if_t<std::is_enum_v<T>>>
|
||||
{
|
||||
using SearchMap = std::map<std::string_view, Optional<T>, StringCompareLessI_T>;
|
||||
static SearchMap MakeSearchMap()
|
||||
{
|
||||
SearchMap map;
|
||||
for (T val : EnumUtils::Iterate<T>())
|
||||
{
|
||||
EnumText text = EnumUtils::ToString(val);
|
||||
|
||||
std::string_view title(text.Title);
|
||||
std::string_view constant(text.Constant);
|
||||
|
||||
auto [constantIt, constantNew] = map.try_emplace(title, val);
|
||||
if (!constantNew)
|
||||
constantIt->second = std::nullopt;
|
||||
|
||||
if (title != constant)
|
||||
{
|
||||
auto [titleIt, titleNew] = map.try_emplace(title, val);
|
||||
if (!titleNew)
|
||||
titleIt->second = std::nullopt;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
static inline SearchMap const _map = MakeSearchMap();
|
||||
|
||||
static T const* Match(std::string_view s)
|
||||
{
|
||||
auto it = _map.lower_bound(s);
|
||||
if (it == _map.end() || !StringStartsWithI(it->first, s)) // not a match
|
||||
return nullptr;
|
||||
|
||||
if (!StringEqualI(it->first, s)) // we don't have an exact match - check if it is unique
|
||||
{
|
||||
auto it2 = it;
|
||||
++it2;
|
||||
if ((it2 != _map.end()) && StringStartsWithI(it2->first, s)) // not unique
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (it->second)
|
||||
return &*it->second;
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static ChatCommandResult TryConsume(T& val, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
std::string_view strVal;
|
||||
ChatCommandResult next1 = ArgInfo<std::string_view>::TryConsume(strVal, handler, args);
|
||||
if (next1)
|
||||
{
|
||||
if (T const* match = Match(strVal))
|
||||
{
|
||||
val = *match;
|
||||
return next1;
|
||||
}
|
||||
}
|
||||
|
||||
// Value not found. Try to parse arg as underlying type and cast it to enum type
|
||||
using U = std::underlying_type_t<T>;
|
||||
U uVal = 0;
|
||||
if (ChatCommandResult next2 = ArgInfo<U>::TryConsume(uVal, handler, args))
|
||||
{
|
||||
if (EnumUtils::IsValid<T>(uVal))
|
||||
{
|
||||
val = static_cast<T>(uVal);
|
||||
return next2;
|
||||
}
|
||||
}
|
||||
|
||||
if (next1)
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_STRING_VALUE_INVALID, STRING_VIEW_FMT_ARG(strVal), GetTypeName<T>().c_str());
|
||||
else
|
||||
return next1;
|
||||
}
|
||||
};
|
||||
|
||||
// a container tag
|
||||
template <typename T>
|
||||
struct ArgInfo<T, std::enable_if_t<std::is_base_of_v<ContainerTag, T>>>
|
||||
{
|
||||
static ChatCommandResult TryConsume(T& tag, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
return tag.TryConsume(handler, args);
|
||||
}
|
||||
};
|
||||
|
||||
// non-empty vector
|
||||
template <typename T>
|
||||
struct ArgInfo<std::vector<T>, void>
|
||||
{
|
||||
static ChatCommandResult TryConsume(std::vector<T>& val, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
val.clear();
|
||||
ChatCommandResult next = ArgInfo<T>::TryConsume(val.emplace_back(), handler, args);
|
||||
|
||||
if (!next)
|
||||
return next;
|
||||
|
||||
while (ChatCommandResult next2 = ArgInfo<T>::TryConsume(val.emplace_back(), handler, *next))
|
||||
next = std::move(next2);
|
||||
|
||||
val.pop_back();
|
||||
return next;
|
||||
}
|
||||
};
|
||||
|
||||
// fixed-size array
|
||||
template <typename T, size_t N>
|
||||
struct ArgInfo<std::array<T, N>, void>
|
||||
{
|
||||
static ChatCommandResult TryConsume(std::array<T, N>& val, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
ChatCommandResult next = args;
|
||||
for (T& t : val)
|
||||
if (!(next = ArgInfo<T>::TryConsume(t, handler, *next)))
|
||||
break;
|
||||
return next;
|
||||
}
|
||||
};
|
||||
|
||||
// variant
|
||||
template <typename... Ts>
|
||||
struct ArgInfo<Acore::ChatCommands::Variant<Ts...>>
|
||||
{
|
||||
using V = std::variant<Ts...>;
|
||||
static constexpr size_t N = std::variant_size_v<V>;
|
||||
|
||||
template <size_t I>
|
||||
static ChatCommandResult TryAtIndex([[maybe_unused]] Acore::ChatCommands::Variant<Ts...>& val, [[maybe_unused]] ChatHandler const* handler, [[maybe_unused]] std::string_view args)
|
||||
{
|
||||
if constexpr (I < N)
|
||||
{
|
||||
ChatCommandResult thisResult = ArgInfo<std::variant_alternative_t<I, V>>::TryConsume(val.template emplace<I>(), handler, args);
|
||||
if (thisResult)
|
||||
return thisResult;
|
||||
else
|
||||
{
|
||||
ChatCommandResult nestedResult = TryAtIndex<I + 1>(val, handler, args);
|
||||
if (nestedResult || !thisResult.HasErrorMessage())
|
||||
return nestedResult;
|
||||
if (!nestedResult.HasErrorMessage())
|
||||
return thisResult;
|
||||
if (StringStartsWith(nestedResult.GetErrorMessage(), "\""))
|
||||
return Acore::StringFormat("\"%s\"\n%s %s", thisResult.GetErrorMessage().c_str(), GetAcoreString(handler, LANG_CMDPARSER_OR), nestedResult.GetErrorMessage().c_str());
|
||||
else
|
||||
return Acore::StringFormat("\"%s\"\n%s \"%s\"", thisResult.GetErrorMessage().c_str(), GetAcoreString(handler, LANG_CMDPARSER_OR), nestedResult.GetErrorMessage().c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static ChatCommandResult TryConsume(Acore::ChatCommands::Variant<Ts...>& val, ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
ChatCommandResult result = TryAtIndex<0>(val, handler, args);
|
||||
if (result.HasErrorMessage() && (result.GetErrorMessage().find('\n') != std::string::npos))
|
||||
return Acore::StringFormat("%s %s", GetAcoreString(handler, LANG_CMDPARSER_EITHER), result.GetErrorMessage().c_str());
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// AchievementEntry* from numeric id or link
|
||||
template <>
|
||||
struct AC_GAME_API ArgInfo<AchievementEntry const*>
|
||||
{
|
||||
static ChatCommandResult TryConsume(AchievementEntry const*&, ChatHandler const*, std::string_view);
|
||||
};
|
||||
|
||||
// GameTele* from string name or link
|
||||
template <>
|
||||
struct AC_GAME_API ArgInfo<GameTele const*>
|
||||
{
|
||||
static ChatCommandResult TryConsume(GameTele const*&, ChatHandler const*, std::string_view);
|
||||
};
|
||||
|
||||
// ItemTemplate* from numeric id or link
|
||||
template <>
|
||||
struct AC_GAME_API ArgInfo<ItemTemplate const*>
|
||||
{
|
||||
static ChatCommandResult TryConsume(ItemTemplate const*&, ChatHandler const*, std::string_view);
|
||||
};
|
||||
|
||||
// SpellInfo const* from spell id or link
|
||||
template <>
|
||||
struct AC_GAME_API ArgInfo<SpellInfo const*>
|
||||
{
|
||||
static ChatCommandResult TryConsume(SpellInfo const*&, ChatHandler const*, std::string_view);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
31
src/server/game/Chat/ChatCommands/ChatCommandHelpers.cpp
Normal file
31
src/server/game/Chat/ChatCommands/ChatCommandHelpers.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatCommandHelpers.h"
|
||||
#include "Chat.h"
|
||||
#include "ObjectMgr.h"
|
||||
|
||||
void Acore::Impl::ChatCommands::SendErrorMessageToHandler(ChatHandler* handler, std::string_view str)
|
||||
{
|
||||
handler->SendSysMessage(str);
|
||||
handler->SetSentErrorMessage(true);
|
||||
}
|
||||
|
||||
char const* Acore::Impl::ChatCommands::GetAcoreString(ChatHandler const* handler, AcoreStrings which)
|
||||
{
|
||||
return handler->GetAcoreString(which);
|
||||
}
|
||||
132
src/server/game/Chat/ChatCommands/ChatCommandHelpers.h
Normal file
132
src/server/game/Chat/ChatCommands/ChatCommandHelpers.h
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _CHATCOMMAND_HELPERS_H_
|
||||
#define _CHATCOMMAND_HELPERS_H_
|
||||
|
||||
#include "Define.h"
|
||||
#include "Language.h"
|
||||
#include "StringFormat.h"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
|
||||
class ChatHandler;
|
||||
|
||||
namespace Acore::Impl::ChatCommands
|
||||
{
|
||||
/***************** HELPERS *************************\
|
||||
|* These really aren't for outside use... *|
|
||||
\***************************************************/
|
||||
|
||||
static constexpr char COMMAND_DELIMITER = ' ';
|
||||
|
||||
template <typename T, typename = void>
|
||||
struct tag_base
|
||||
{
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using tag_base_t = typename tag_base<T>::type;
|
||||
|
||||
struct TokenizeResult {
|
||||
explicit operator bool() { return !token.empty(); }
|
||||
std::string_view token;
|
||||
std::string_view tail;
|
||||
};
|
||||
|
||||
inline TokenizeResult tokenize(std::string_view args)
|
||||
{
|
||||
TokenizeResult result;
|
||||
if (size_t delimPos = args.find(COMMAND_DELIMITER); delimPos != std::string_view::npos)
|
||||
{
|
||||
result.token = args.substr(0, delimPos);
|
||||
if (size_t tailPos = args.find_first_not_of(COMMAND_DELIMITER, delimPos); tailPos != std::string_view::npos)
|
||||
result.tail = args.substr(tailPos);
|
||||
}
|
||||
else
|
||||
result.token = args;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename T, typename... Ts>
|
||||
struct are_all_assignable
|
||||
{
|
||||
static constexpr bool value = (std::is_assignable_v<T&, Ts> && ...);
|
||||
};
|
||||
|
||||
template <typename... Ts>
|
||||
struct are_all_assignable<void, Ts...>
|
||||
{
|
||||
static constexpr bool value = false;
|
||||
};
|
||||
|
||||
template <std::size_t index, typename T1, typename... Ts>
|
||||
struct get_nth : get_nth<index-1, Ts...> { };
|
||||
|
||||
template <typename T1, typename... Ts>
|
||||
struct get_nth<0, T1, Ts...>
|
||||
{
|
||||
using type = T1;
|
||||
};
|
||||
|
||||
template <std::size_t index, typename... Ts>
|
||||
using get_nth_t = typename get_nth<index, Ts...>::type;
|
||||
|
||||
// this essentially models std::optional<std::string_view>, except it can also hold an error message
|
||||
// it has std::string_view's bool conversion and dereference operators
|
||||
//
|
||||
// monostate <-> unspecified error, typically end-of-string reached or parsing failed
|
||||
// std::string <-> specified error, typically character-not-found or invalid item link
|
||||
// std::string_view <-> success, string_view is remaining argument string
|
||||
struct ChatCommandResult
|
||||
{
|
||||
ChatCommandResult(std::nullopt_t) : _storage() {}
|
||||
ChatCommandResult(std::string const&) = delete;
|
||||
ChatCommandResult(std::string&& s) : _storage(std::in_place_type<std::string>, std::forward<std::string>(s)) {}
|
||||
ChatCommandResult(char const* c) : _storage(std::in_place_type<std::string>, c) {}
|
||||
ChatCommandResult(std::string_view s) : _storage(std::in_place_type<std::string_view>, s) {}
|
||||
|
||||
ChatCommandResult(ChatCommandResult const&) = delete;
|
||||
ChatCommandResult(ChatCommandResult&&) = default;
|
||||
ChatCommandResult& operator=(ChatCommandResult const&) = delete;
|
||||
ChatCommandResult& operator=(ChatCommandResult&&) = default;
|
||||
|
||||
std::string_view operator*() const { return std::get<std::string_view>(_storage); }
|
||||
bool IsSuccessful() const { return std::holds_alternative<std::string_view>(_storage); }
|
||||
explicit operator bool() const { return IsSuccessful(); }
|
||||
bool HasErrorMessage() const { return std::holds_alternative<std::string>(_storage); }
|
||||
std::string const& GetErrorMessage() const { return std::get<std::string>(_storage); }
|
||||
|
||||
private:
|
||||
std::variant<std::monostate, std::string_view, std::string> _storage;
|
||||
};
|
||||
|
||||
AC_GAME_API void SendErrorMessageToHandler(ChatHandler* handler, std::string_view str);
|
||||
AC_GAME_API char const* GetAcoreString(ChatHandler const* handler, AcoreStrings which);
|
||||
template <typename... Ts>
|
||||
std::string FormatAcoreString(ChatHandler const* handler, AcoreStrings which, Ts&&... args)
|
||||
{
|
||||
return Acore::StringFormat(GetAcoreString(handler, which), std::forward<Ts>(args)...);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
145
src/server/game/Chat/ChatCommands/ChatCommandTags.cpp
Normal file
145
src/server/game/Chat/ChatCommands/ChatCommandTags.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatCommandTags.h"
|
||||
#include "AccountMgr.h"
|
||||
#include "Chat.h"
|
||||
#include "ChatCommandArgs.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
|
||||
using namespace Acore::Impl::ChatCommands;
|
||||
|
||||
ChatCommandResult Acore::ChatCommands::QuotedString::TryConsume(ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
if (args.empty())
|
||||
return std::nullopt;
|
||||
|
||||
if ((args[0] != '"') && (args[0] != '\''))
|
||||
return ArgInfo<std::string>::TryConsume(*this, handler, args);
|
||||
|
||||
char const QUOTE = args[0];
|
||||
for (size_t i = 1; i < args.length(); ++i)
|
||||
{
|
||||
if (args[i] == QUOTE)
|
||||
{
|
||||
auto [remainingToken, tail] = tokenize(args.substr(i + 1));
|
||||
if (remainingToken.empty()) // if this is not empty, then we did not consume the full token
|
||||
return tail;
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (args[i] == '\\')
|
||||
{
|
||||
++i;
|
||||
if (!(i < args.length()))
|
||||
break;
|
||||
}
|
||||
std::string::push_back(args[i]);
|
||||
}
|
||||
|
||||
// if we reach this, we did not find a closing quote
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ChatCommandResult Acore::ChatCommands::AccountIdentifier::TryConsume(ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
std::string_view text;
|
||||
ChatCommandResult next = ArgInfo<std::string_view>::TryConsume(text, handler, args);
|
||||
if (!next)
|
||||
return next;
|
||||
|
||||
// first try parsing as account name
|
||||
_name.assign(text);
|
||||
if (!Utf8ToUpperOnlyLatin(_name))
|
||||
return GetAcoreString(handler, LANG_CMDPARSER_INVALID_UTF8);
|
||||
|
||||
_id = AccountMgr::GetId(_name);
|
||||
if (_id) // account with name exists, we are done
|
||||
return next;
|
||||
|
||||
// try parsing as account id instead
|
||||
Optional<uint32> id = Acore::StringTo<uint32>(text, 10);
|
||||
if (!id)
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_ACCOUNT_NAME_NO_EXIST, STRING_VIEW_FMT_ARG(_name));
|
||||
|
||||
_id = *id;
|
||||
|
||||
if (AccountMgr::GetName(_id, _name))
|
||||
return next;
|
||||
else
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_ACCOUNT_ID_NO_EXIST, _id);
|
||||
}
|
||||
|
||||
ChatCommandResult Acore::ChatCommands::PlayerIdentifier::TryConsume(ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
Variant<Hyperlink<player>, ObjectGuid::LowType, std::string_view> val;
|
||||
ChatCommandResult next = ArgInfo<decltype(val)>::TryConsume(val, handler, args);
|
||||
if (!next)
|
||||
return next;
|
||||
|
||||
if (val.holds_alternative<ObjectGuid::LowType>())
|
||||
{
|
||||
_guid = ObjectGuid::Create<HighGuid::Player>(val.get<ObjectGuid::LowType>());
|
||||
|
||||
if ((_player = ObjectAccessor::FindPlayerByLowGUID(_guid.GetCounter())))
|
||||
_name = _player->GetName();
|
||||
else if (!sObjectMgr->GetPlayerNameByGUID(_guid.GetCounter(), _name))
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_CHAR_GUID_NO_EXIST, _guid.ToString().c_str());
|
||||
|
||||
return next;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (val.holds_alternative<Hyperlink<player>>())
|
||||
_name.assign(static_cast<std::string_view>(val.get<Hyperlink<player>>()));
|
||||
else
|
||||
_name.assign(val.get<std::string_view>());
|
||||
|
||||
if (!normalizePlayerName(_name))
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_CHAR_NAME_INVALID, STRING_VIEW_FMT_ARG(_name));
|
||||
|
||||
if ((_player = ObjectAccessor::FindPlayerByName(_name)))
|
||||
_guid = _player->GetGUID();
|
||||
else if (!(_guid = sObjectMgr->GetPlayerGUIDByName(_name)))
|
||||
return FormatAcoreString(handler, LANG_CMDPARSER_CHAR_NAME_NO_EXIST, STRING_VIEW_FMT_ARG(_name));
|
||||
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
Acore::ChatCommands::PlayerIdentifier::PlayerIdentifier(Player& player)
|
||||
: _name(player.GetName()), _guid(player.GetGUID()), _player(&player) {}
|
||||
|
||||
/*static*/ Optional<Acore::ChatCommands::PlayerIdentifier> Acore::ChatCommands::PlayerIdentifier::FromTarget(ChatHandler* handler)
|
||||
{
|
||||
if (Player* player = handler->GetPlayer())
|
||||
if (Player* target = player->GetSelectedPlayer())
|
||||
return { *target };
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/*static*/ Optional<Acore::ChatCommands::PlayerIdentifier> Acore::ChatCommands::PlayerIdentifier::FromSelf(ChatHandler* handler)
|
||||
{
|
||||
if (Player* player = handler->GetPlayer())
|
||||
return { *player };
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
313
src/server/game/Chat/ChatCommands/ChatCommandTags.h
Normal file
313
src/server/game/Chat/ChatCommands/ChatCommandTags.h
Normal file
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _CHATCOMMANDTAGS_H
|
||||
#define _CHATCOMMANDTAGS_H
|
||||
|
||||
#include "advstd.h"
|
||||
#include "ChatCommandHelpers.h"
|
||||
#include "Hyperlinks.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "Optional.h"
|
||||
#include "Util.h"
|
||||
#include <boost/preprocessor/repetition/repeat.hpp>
|
||||
#include <boost/preprocessor/punctuation/comma_if.hpp>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
class ChatHandler;
|
||||
class Player;
|
||||
|
||||
namespace Acore::Impl::ChatCommands
|
||||
{
|
||||
struct ContainerTag
|
||||
{
|
||||
using ChatCommandResult = Acore::Impl::ChatCommands::ChatCommandResult;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct tag_base<T, std::enable_if_t<std::is_base_of_v<ContainerTag, T>>>
|
||||
{
|
||||
using type = typename T::value_type;
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
inline constexpr char GetChar(char const (&s)[N], size_t i)
|
||||
{
|
||||
static_assert(N <= 25, "The EXACT_SEQUENCE macro can only be used with up to 25 character long literals. Specify them char-by-char (null terminated) as parameters to ExactSequence<> instead.");
|
||||
return i >= N ? '\0' : s[i];
|
||||
}
|
||||
|
||||
#define CHATCOMMANDS_IMPL_SPLIT_LITERAL_EXTRACT_CHAR(z, i, strliteral) \
|
||||
BOOST_PP_COMMA_IF(i) Acore::Impl::ChatCommands::GetChar(strliteral, i)
|
||||
|
||||
#define CHATCOMMANDS_IMPL_SPLIT_LITERAL_CONSTRAINED(maxlen, strliteral) \
|
||||
BOOST_PP_REPEAT(maxlen, CHATCOMMANDS_IMPL_SPLIT_LITERAL_EXTRACT_CHAR, strliteral)
|
||||
|
||||
// this creates always 25 elements - "abc" -> 'a', 'b', 'c', '\0', '\0', ... up to 25
|
||||
#define CHATCOMMANDS_IMPL_SPLIT_LITERAL(strliteral) CHATCOMMANDS_IMPL_SPLIT_LITERAL_CONSTRAINED(25, strliteral)
|
||||
}
|
||||
|
||||
namespace Acore::ChatCommands
|
||||
{
|
||||
/************************** CONTAINER TAGS **********************************************\
|
||||
|* Simple holder classes to differentiate between extraction methods *|
|
||||
|* Must inherit from Acore::Impl::ChatCommands::ContainerTag *|
|
||||
|* Must implement the following: *|
|
||||
|* - TryConsume: ChatHandler const*, std::string_view -> ChatCommandResult *|
|
||||
|* - on match, returns tail of the provided argument string (as std::string_view) *|
|
||||
|* - on specific error, returns error message (as std::string&& or char const*) *|
|
||||
|* - on generic error, returns std::nullopt (this will print command usage) *|
|
||||
|* *|
|
||||
|* - typedef value_type of type that is contained within the tag *|
|
||||
|* - cast operator to value_type *|
|
||||
|* *|
|
||||
\****************************************************************************************/
|
||||
|
||||
template <char... chars>
|
||||
struct ExactSequence : Acore::Impl::ChatCommands::ContainerTag
|
||||
{
|
||||
using value_type = void;
|
||||
|
||||
ChatCommandResult TryConsume(ChatHandler const* handler, std::string_view args) const
|
||||
{
|
||||
if (args.empty())
|
||||
return std::nullopt;
|
||||
std::string_view start = args.substr(0, _string.length());
|
||||
if (StringEqualI(start, _string))
|
||||
{
|
||||
auto [remainingToken, tail] = Acore::Impl::ChatCommands::tokenize(args.substr(_string.length()));
|
||||
if (remainingToken.empty()) // if this is not empty, then we did not consume the full token
|
||||
return tail;
|
||||
start = args.substr(0, _string.length() + remainingToken.length());
|
||||
}
|
||||
return Acore::Impl::ChatCommands::FormatAcoreString(handler, LANG_CMDPARSER_EXACT_SEQ_MISMATCH, STRING_VIEW_FMT_ARG(_string), STRING_VIEW_FMT_ARG(start));
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::array<char, sizeof...(chars)> _storage = { chars... };
|
||||
static_assert(!_storage.empty() && (_storage.back() == '\0'), "ExactSequence parameters must be null terminated! Use the EXACT_SEQUENCE macro to make this easier!");
|
||||
static constexpr std::string_view _string = { _storage.data(), std::string_view::traits_type::length(_storage.data()) };
|
||||
};
|
||||
|
||||
#define EXACT_SEQUENCE(str) Acore::ChatCommands::ExactSequence<CHATCOMMANDS_IMPL_SPLIT_LITERAL(str)>
|
||||
|
||||
struct Tail : std::string_view, Acore::Impl::ChatCommands::ContainerTag
|
||||
{
|
||||
using value_type = std::string_view;
|
||||
|
||||
using std::string_view::operator=;
|
||||
|
||||
ChatCommandResult TryConsume(ChatHandler const*, std::string_view args)
|
||||
{
|
||||
std::string_view::operator=(args);
|
||||
return std::string_view();
|
||||
}
|
||||
};
|
||||
|
||||
struct WTail : std::wstring, Acore::Impl::ChatCommands::ContainerTag
|
||||
{
|
||||
using value_type = std::wstring;
|
||||
|
||||
using std::wstring::operator=;
|
||||
|
||||
ChatCommandResult TryConsume(ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
if (Utf8toWStr(args, *this))
|
||||
return std::string_view();
|
||||
else
|
||||
return Acore::Impl::ChatCommands::GetAcoreString(handler, LANG_CMDPARSER_INVALID_UTF8);
|
||||
}
|
||||
};
|
||||
|
||||
struct QuotedString : std::string, Acore::Impl::ChatCommands::ContainerTag
|
||||
{
|
||||
using value_type = std::string;
|
||||
|
||||
AC_GAME_API ChatCommandResult TryConsume(ChatHandler const* handler, std::string_view args);
|
||||
};
|
||||
|
||||
struct AC_GAME_API AccountIdentifier : Acore::Impl::ChatCommands::ContainerTag
|
||||
{
|
||||
using value_type = uint32;
|
||||
|
||||
operator uint32() const { return _id; }
|
||||
operator std::string const& () const { return _name; }
|
||||
operator std::string_view() const { return { _name }; }
|
||||
|
||||
uint32 GetID() const { return _id; }
|
||||
std::string const& GetName() const { return _name; }
|
||||
|
||||
ChatCommandResult TryConsume(ChatHandler const* handler, std::string_view args);
|
||||
|
||||
private:
|
||||
uint32 _id;
|
||||
std::string _name;
|
||||
};
|
||||
|
||||
struct AC_GAME_API PlayerIdentifier : Acore::Impl::ChatCommands::ContainerTag
|
||||
{
|
||||
using value_type = Player*;
|
||||
|
||||
PlayerIdentifier() : _name(), _guid(), _player(nullptr) {}
|
||||
PlayerIdentifier(Player& player);
|
||||
|
||||
operator ObjectGuid() const { return _guid; }
|
||||
operator std::string const&() const { return _name; }
|
||||
operator std::string_view() const { return _name; }
|
||||
|
||||
std::string const& GetName() const { return _name; }
|
||||
ObjectGuid GetGUID() const { return _guid; }
|
||||
bool IsConnected() const { return (_player != nullptr); }
|
||||
Player* GetConnectedPlayer() const { return _player; }
|
||||
|
||||
ChatCommandResult TryConsume(ChatHandler const* handler, std::string_view args);
|
||||
|
||||
static Optional<PlayerIdentifier> FromTarget(ChatHandler* handler);
|
||||
static Optional<PlayerIdentifier> FromSelf(ChatHandler* handler);
|
||||
static Optional<PlayerIdentifier> FromTargetOrSelf(ChatHandler* handler)
|
||||
{
|
||||
if (Optional<PlayerIdentifier> fromTarget = FromTarget(handler))
|
||||
return fromTarget;
|
||||
else
|
||||
return FromSelf(handler);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _name;
|
||||
ObjectGuid _guid;
|
||||
Player* _player;
|
||||
};
|
||||
|
||||
template <typename linktag>
|
||||
struct Hyperlink : Acore::Impl::ChatCommands::ContainerTag
|
||||
{
|
||||
using value_type = typename linktag::value_type;
|
||||
using storage_type = advstd::remove_cvref_t<value_type>;
|
||||
|
||||
operator value_type() const { return val; }
|
||||
value_type operator*() const { return val; }
|
||||
storage_type const* operator->() const { return &val; }
|
||||
|
||||
ChatCommandResult TryConsume(ChatHandler const* handler, std::string_view args)
|
||||
{
|
||||
Acore::Hyperlinks::HyperlinkInfo info = Acore::Hyperlinks::ParseSingleHyperlink(args);
|
||||
// invalid hyperlinks cannot be consumed
|
||||
if (!info)
|
||||
return std::nullopt;
|
||||
|
||||
// check if we got the right tag
|
||||
if (info.tag != linktag::tag())
|
||||
return std::nullopt;
|
||||
|
||||
// store value
|
||||
if (!linktag::StoreTo(val, info.data))
|
||||
return Acore::Impl::ChatCommands::GetAcoreString(handler, LANG_CMDPARSER_LINKDATA_INVALID);
|
||||
|
||||
// finally, skip any potential delimiters
|
||||
auto [token, next] = Acore::Impl::ChatCommands::tokenize(info.tail);
|
||||
if (token.empty()) /* empty token = first character is delimiter, skip past it */
|
||||
return next;
|
||||
else
|
||||
return info.tail;
|
||||
}
|
||||
|
||||
private:
|
||||
storage_type val;
|
||||
};
|
||||
|
||||
// pull in link tags for user convenience
|
||||
using namespace ::Acore::Hyperlinks::LinkTags;
|
||||
}
|
||||
|
||||
namespace Acore::Impl
|
||||
{
|
||||
template <typename T>
|
||||
struct CastToVisitor
|
||||
{
|
||||
template <typename U>
|
||||
T operator()(U const& v) const { return v; }
|
||||
};
|
||||
}
|
||||
|
||||
namespace Acore::ChatCommands
|
||||
{
|
||||
template <typename T1, typename... Ts>
|
||||
struct Variant : public std::variant<T1, Ts...>
|
||||
{
|
||||
using base = std::variant<T1, Ts...>;
|
||||
|
||||
using first_type = Acore::Impl::ChatCommands::tag_base_t<T1>;
|
||||
static constexpr bool have_operators = Acore::Impl::ChatCommands::are_all_assignable<first_type, Acore::Impl::ChatCommands::tag_base_t<Ts>...>::value;
|
||||
|
||||
template <bool C = have_operators>
|
||||
std::enable_if_t<C, first_type> operator*() const
|
||||
{
|
||||
return visit(Acore::Impl::CastToVisitor<first_type>());
|
||||
}
|
||||
|
||||
template <bool C = have_operators>
|
||||
operator std::enable_if_t<C, first_type>() const
|
||||
{
|
||||
return operator*();
|
||||
}
|
||||
|
||||
template<bool C = have_operators>
|
||||
operator std::enable_if_t<C && !std::is_same_v<first_type, size_t> && std::is_convertible_v<first_type, size_t>, size_t>() const
|
||||
{
|
||||
return operator*();
|
||||
}
|
||||
|
||||
template <bool C = have_operators>
|
||||
std::enable_if_t<C, bool> operator!() const { return !**this; }
|
||||
|
||||
template <typename T>
|
||||
Variant& operator=(T&& arg) { base::operator=(std::forward<T>(arg)); return *this; }
|
||||
|
||||
template <size_t index>
|
||||
constexpr decltype(auto) get() { return std::get<index>(static_cast<base&>(*this)); }
|
||||
template <size_t index>
|
||||
constexpr decltype(auto) get() const { return std::get<index>(static_cast<base const&>(*this)); }
|
||||
template <typename type>
|
||||
constexpr decltype(auto) get() { return std::get<type>(static_cast<base&>(*this)); }
|
||||
template <typename type>
|
||||
constexpr decltype(auto) get() const { return std::get<type>(static_cast<base const&>(*this)); }
|
||||
|
||||
template <typename T>
|
||||
constexpr decltype(auto) visit(T&& arg) { return std::visit(std::forward<T>(arg), static_cast<base&>(*this)); }
|
||||
template <typename T>
|
||||
constexpr decltype(auto) visit(T&& arg) const { return std::visit(std::forward<T>(arg), static_cast<base const&>(*this)); }
|
||||
|
||||
template <typename T>
|
||||
constexpr bool holds_alternative() const { return std::holds_alternative<T>(static_cast<base const&>(*this)); }
|
||||
|
||||
template <bool C = have_operators>
|
||||
friend std::enable_if_t<C, std::ostream&> operator<<(std::ostream& os, Acore::ChatCommands::Variant<T1, Ts...> const& v)
|
||||
{
|
||||
return (os << *v);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,714 +0,0 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatLink.h"
|
||||
#include "DBCStores.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
|
||||
// Supported shift-links (client generated and server side)
|
||||
// |color|Hachievement:achievement_id:player_guid:0:0:0:0:0:0:0:0|h[name]|h|r
|
||||
// - client, item icon shift click, not used in server currently
|
||||
// |color|Harea:area_id|h[name]|h|r
|
||||
// |color|Hcreature:creature_guid|h[name]|h|r
|
||||
// |color|Hcreature_entry:creature_id|h[name]|h|r
|
||||
// |color|Henchant:recipe_spell_id|h[prof_name: recipe_name]|h|r - client, at shift click in recipes list dialog
|
||||
// |color|Hgameevent:id|h[name]|h|r
|
||||
// |color|Hgameobject:go_guid|h[name]|h|r
|
||||
// |color|Hgameobject_entry:go_id|h[name]|h|r
|
||||
// |color|Hglyph:glyph_slot_id:glyph_prop_id|h[%s]|h|r - client, at shift click in glyphs dialog, GlyphSlot.dbc, GlyphProperties.dbc
|
||||
// |color|Hitem:item_id:perm_ench_id:gem1:gem2:gem3:0:0:0:0:reporter_level|h[name]|h|r
|
||||
// - client, item icon shift click
|
||||
// |color|Hitemset:itemset_id|h[name]|h|r
|
||||
// |color|Hplayer:name|h[name]|h|r - client, in some messages, at click copy only name instead link
|
||||
// |color|Hquest:quest_id:quest_level|h[name]|h|r - client, quest list name shift-click
|
||||
// |color|Hskill:skill_id|h[name]|h|r
|
||||
// |color|Hspell:spell_id|h[name]|h|r - client, spellbook spell icon shift-click
|
||||
// |color|Htalent:talent_id, rank|h[name]|h|r - client, talent icon shift-click
|
||||
// |color|Htaxinode:id|h[name]|h|r
|
||||
// |color|Htele:id|h[name]|h|r
|
||||
// |color|Htitle:id|h[name]|h|r
|
||||
// |color|Htrade:spell_id:cur_value:max_value:unk3int:unk3str|h[name]|h|r - client, spellbook profession icon shift-click
|
||||
|
||||
inline bool ReadUInt32(std::istringstream& iss, uint32& res)
|
||||
{
|
||||
iss >> std::dec >> res;
|
||||
return !iss.fail() && !iss.eof();
|
||||
}
|
||||
|
||||
inline bool ReadInt32(std::istringstream& iss, int32& res)
|
||||
{
|
||||
iss >> std::dec >> res;
|
||||
return !iss.fail() && !iss.eof();
|
||||
}
|
||||
|
||||
inline std::string ReadSkip(std::istringstream& iss, char term)
|
||||
{
|
||||
std::string res;
|
||||
char c = iss.peek();
|
||||
while (c != term && c != '\0')
|
||||
{
|
||||
res += c;
|
||||
iss.ignore(1);
|
||||
c = iss.peek();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
inline bool CheckDelimiter(std::istringstream& iss, char delimiter, const char* context)
|
||||
{
|
||||
(void)context; // used only with EXTRA_LOGS
|
||||
char c = iss.peek();
|
||||
if (c != delimiter)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): invalid %s link structure ('%c' expected, '%c' found)", iss.str().c_str(), context, delimiter, c);
|
||||
return false;
|
||||
}
|
||||
iss.ignore(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadHex(std::istringstream& iss, uint32& res, uint32 length)
|
||||
{
|
||||
std::istringstream::pos_type pos = iss.tellg();
|
||||
iss >> std::hex >> res;
|
||||
//uint32 size = uint32(iss.gcount());
|
||||
if (length && uint32(iss.tellg() - pos) != length)
|
||||
return false;
|
||||
return !iss.fail() && !iss.eof();
|
||||
}
|
||||
|
||||
#define DELIMITER ':'
|
||||
#define PIPE_CHAR '|'
|
||||
|
||||
bool ChatLink::ValidateName(char* buffer, const char* /*context*/)
|
||||
{
|
||||
_name = buffer;
|
||||
return true;
|
||||
}
|
||||
|
||||
// |color|Hitem:item_id:perm_ench_id:gem1:gem2:gem3:0:random_property:0:reporter_level|h[name]|h|r
|
||||
// |cffa335ee|Hitem:812:0:0:0:0:0:0:0:70|h[Glowing Brightwood Staff]|h|r
|
||||
bool ItemChatLink::Initialize(std::istringstream& iss)
|
||||
{
|
||||
// Read item entry
|
||||
uint32 itemEntry = 0;
|
||||
if (!ReadUInt32(iss, itemEntry))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading item entry", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate item
|
||||
_item = sObjectMgr->GetItemTemplate(itemEntry);
|
||||
if (!_item)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid itemEntry %u in |item command", iss.str().c_str(), itemEntry);
|
||||
return false;
|
||||
}
|
||||
// Validate item's color
|
||||
if (_color != ItemQualityColors[_item->Quality])
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): linked item has color %u, but user claims %u", iss.str().c_str(), ItemQualityColors[_item->Quality], _color);
|
||||
return false;
|
||||
}
|
||||
// Number of various item properties after item entry
|
||||
const uint8 propsCount = 8;
|
||||
const uint8 randomPropertyPosition = 5;
|
||||
for (uint8 index = 0; index < propsCount; ++index)
|
||||
{
|
||||
if (!CheckDelimiter(iss, DELIMITER, "item"))
|
||||
return false;
|
||||
|
||||
int32 id = 0;
|
||||
if (!ReadInt32(iss, id))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading item property (%u)", iss.str().c_str(), index);
|
||||
return false;
|
||||
}
|
||||
if (id && (index == randomPropertyPosition))
|
||||
{
|
||||
// Validate random property
|
||||
if (id > 0)
|
||||
{
|
||||
_property = sItemRandomPropertiesStore.LookupEntry(id);
|
||||
if (!_property)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid item property id %u in |item command", iss.str().c_str(), id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (id < 0)
|
||||
{
|
||||
_suffix = sItemRandomSuffixStore.LookupEntry(-id);
|
||||
if (!_suffix)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid item suffix id %u in |item command", iss.str().c_str(), -id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
_data[index] = id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string ItemChatLink::FormatName(uint8 index, ItemLocale const* locale, char* const* suffixStrings) const
|
||||
{
|
||||
std::stringstream ss;
|
||||
if (locale == nullptr || index >= locale->Name.size())
|
||||
ss << _item->Name1;
|
||||
else
|
||||
ss << locale->Name[index];
|
||||
if (suffixStrings)
|
||||
ss << ' ' << suffixStrings[index];
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
bool ItemChatLink::ValidateName(char* buffer, const char* context)
|
||||
{
|
||||
ChatLink::ValidateName(buffer, context);
|
||||
|
||||
char* const* suffixStrings = _suffix ? _suffix->nameSuffix : (_property ? _property->nameSuffix : nullptr);
|
||||
|
||||
bool res = (FormatName(LOCALE_enUS, nullptr, suffixStrings) == buffer);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
ItemLocale const* il = sObjectMgr->GetItemLocale(_item->ItemId);
|
||||
for (uint8 index = LOCALE_koKR; index < TOTAL_LOCALES; ++index)
|
||||
{
|
||||
if (FormatName(index, il, suffixStrings) == buffer)
|
||||
{
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!res)
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): linked item (id: %u) name wasn't found in any localization", context, _item->ItemId);
|
||||
return res;
|
||||
}
|
||||
|
||||
// |color|Hquest:quest_id:quest_level|h[name]|h|r
|
||||
// |cff808080|Hquest:2278:47|h[The Platinum Discs]|h|r
|
||||
bool QuestChatLink::Initialize(std::istringstream& iss)
|
||||
{
|
||||
// Read quest id
|
||||
uint32 questId = 0;
|
||||
if (!ReadUInt32(iss, questId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading quest entry", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate quest
|
||||
_quest = sObjectMgr->GetQuestTemplate(questId);
|
||||
if (!_quest)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): quest template %u not found", iss.str().c_str(), questId);
|
||||
return false;
|
||||
}
|
||||
// Check delimiter
|
||||
if (!CheckDelimiter(iss, DELIMITER, "quest"))
|
||||
return false;
|
||||
// Read quest level
|
||||
if (!ReadInt32(iss, _questLevel))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading quest level", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate quest level
|
||||
if (_questLevel >= STRONG_MAX_LEVEL)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): quest level %d is too big", iss.str().c_str(), _questLevel);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QuestChatLink::ValidateName(char* buffer, const char* context)
|
||||
{
|
||||
ChatLink::ValidateName(buffer, context);
|
||||
|
||||
bool res = (_quest->GetTitle() == buffer);
|
||||
if (!res)
|
||||
if (QuestLocale const* ql = sObjectMgr->GetQuestLocale(_quest->GetQuestId()))
|
||||
for (uint8 i = 0; i < ql->Title.size(); i++)
|
||||
if (ql->Title[i] == buffer)
|
||||
{
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!res)
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): linked quest (id: %u) title wasn't found in any localization", context, _quest->GetQuestId());
|
||||
return res;
|
||||
}
|
||||
|
||||
// |color|Hspell:spell_id|h[name]|h|r
|
||||
// |cff71d5ff|Hspell:21563|h[Command]|h|r
|
||||
bool SpellChatLink::Initialize(std::istringstream& iss)
|
||||
{
|
||||
if (_color != CHAT_LINK_COLOR_SPELL)
|
||||
return false;
|
||||
// Read spell id
|
||||
uint32 spellId = 0;
|
||||
if (!ReadUInt32(iss, spellId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading spell entry", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate spell
|
||||
_spell = sSpellMgr->GetSpellInfo(spellId);
|
||||
if (!_spell)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid spell id %u in |spell command", iss.str().c_str(), spellId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellChatLink::ValidateName(char* buffer, const char* context)
|
||||
{
|
||||
ChatLink::ValidateName(buffer, context);
|
||||
|
||||
// spells with that flag have a prefix of "$PROFESSION: "
|
||||
if (_spell->HasAttribute(SPELL_ATTR0_IS_TRADESKILL))
|
||||
{
|
||||
SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(_spell->Id);
|
||||
if (bounds.first == bounds.second)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): skill line not found for spell %u", context, _spell->Id);
|
||||
return false;
|
||||
}
|
||||
SkillLineAbilityEntry const* skillInfo = bounds.first->second;
|
||||
if (!skillInfo)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): skill line ability not found for spell %u", context, _spell->Id);
|
||||
return false;
|
||||
}
|
||||
SkillLineEntry const* skillLine = sSkillLineStore.LookupEntry(skillInfo->SkillLine);
|
||||
if (!skillLine)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): skill line not found for skill %u", context, skillInfo->SkillLine);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
|
||||
{
|
||||
uint32 skillLineNameLength = strlen(skillLine->name[i]);
|
||||
if (skillLineNameLength > 0 && strncmp(skillLine->name[i], buffer, skillLineNameLength) == 0)
|
||||
{
|
||||
// found the prefix, remove it to perform spellname validation below
|
||||
// -2 = strlen(": ")
|
||||
uint32 spellNameLength = (strlen(buffer) <= skillLineNameLength + 2) ? 0 : strlen(buffer) - skillLineNameLength - 2;
|
||||
memmove(buffer, buffer + skillLineNameLength + 2, spellNameLength + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool res = false;
|
||||
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
|
||||
if (*_spell->SpellName[i] && strcmp(_spell->SpellName[i], buffer) == 0)
|
||||
{
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!res)
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): linked spell (id: %u) name wasn't found in any localization", context, _spell->Id);
|
||||
return res;
|
||||
}
|
||||
|
||||
// |color|Hachievement:achievement_id:player_guid:0:0:0:0:0:0:0:0|h[name]|h|r
|
||||
// |cffffff00|Hachievement:546:0000000000000001:0:0:0:-1:0:0:0:0|h[Safe Deposit]|h|r
|
||||
bool AchievementChatLink::Initialize(std::istringstream& iss)
|
||||
{
|
||||
if (_color != CHAT_LINK_COLOR_ACHIEVEMENT)
|
||||
return false;
|
||||
// Read achievemnt Id
|
||||
uint32 achievementId = 0;
|
||||
if (!ReadUInt32(iss, achievementId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading achievement entry", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate achievement
|
||||
_achievement = sAchievementStore.LookupEntry(achievementId);
|
||||
if (!_achievement)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid achivement id %u in |achievement command", iss.str().c_str(), achievementId);
|
||||
return false;
|
||||
}
|
||||
// Check delimiter
|
||||
if (!CheckDelimiter(iss, DELIMITER, "achievement"))
|
||||
return false;
|
||||
// Read HEX
|
||||
if (!ReadHex(iss, _guid, 0))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): invalid hexadecimal number while reading char's guid", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Skip progress
|
||||
const uint8 propsCount = 8;
|
||||
for (uint8 index = 0; index < propsCount; ++index)
|
||||
{
|
||||
if (!CheckDelimiter(iss, DELIMITER, "achievement"))
|
||||
return false;
|
||||
|
||||
if (!ReadUInt32(iss, _data[index]))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading achievement property (%u)", iss.str().c_str(), index);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AchievementChatLink::ValidateName(char* buffer, const char* context)
|
||||
{
|
||||
ChatLink::ValidateName(buffer, context);
|
||||
|
||||
bool res = false;
|
||||
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
|
||||
if (*_achievement->name[i] && strcmp(_achievement->name[i], buffer) == 0)
|
||||
{
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!res)
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): linked achievement (id: %u) name wasn't found in any localization", context, _achievement->ID);
|
||||
return res;
|
||||
}
|
||||
|
||||
// |color|Htrade:spell_id:cur_value:max_value:player_guid:base64_data|h[name]|h|r
|
||||
// |cffffd000|Htrade:4037:1:150:1:6AAAAAAAAAAAAAAAAAAAAAAOAADAAAAAAAAAAAAAAAAIAAAAAAAAA|h[Engineering]|h|r
|
||||
bool TradeChatLink::Initialize(std::istringstream& iss)
|
||||
{
|
||||
if (_color != CHAT_LINK_COLOR_TRADE)
|
||||
return false;
|
||||
// Spell Id
|
||||
uint32 spellId = 0;
|
||||
if (!ReadUInt32(iss, spellId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading achievement entry", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate spell
|
||||
_spell = sSpellMgr->GetSpellInfo(spellId);
|
||||
if (!_spell)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid spell id %u in |trade command", iss.str().c_str(), spellId);
|
||||
return false;
|
||||
}
|
||||
// Check delimiter
|
||||
if (!CheckDelimiter(iss, DELIMITER, "trade"))
|
||||
return false;
|
||||
// Minimum talent level
|
||||
if (!ReadInt32(iss, _minSkillLevel))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading minimum talent level", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Check delimiter
|
||||
if (!CheckDelimiter(iss, DELIMITER, "trade"))
|
||||
return false;
|
||||
// Maximum talent level
|
||||
if (!ReadInt32(iss, _maxSkillLevel))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading maximum talent level", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Check delimiter
|
||||
if (!CheckDelimiter(iss, DELIMITER, "trade"))
|
||||
return false;
|
||||
// Something hexadecimal
|
||||
if (!ReadHex(iss, _guid, 0))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading achievement's owner guid", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Skip base64 encoded stuff
|
||||
_base64 = ReadSkip(iss, PIPE_CHAR);
|
||||
return true;
|
||||
}
|
||||
|
||||
// |color|Htalent:talent_id:rank|h[name]|h|r
|
||||
// |cff4e96f7|Htalent:2232:-1|h[Taste for Blood]|h|r
|
||||
bool TalentChatLink::Initialize(std::istringstream& iss)
|
||||
{
|
||||
if (_color != CHAT_LINK_COLOR_TALENT)
|
||||
return false;
|
||||
// Read talent entry
|
||||
if (!ReadUInt32(iss, _talentId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading talent entry", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate talent
|
||||
TalentEntry const* talentInfo = sTalentStore.LookupEntry(_talentId);
|
||||
if (!talentInfo)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid talent id %u in |talent command", iss.str().c_str(), _talentId);
|
||||
return false;
|
||||
}
|
||||
// Validate talent's spell
|
||||
_spell = sSpellMgr->GetSpellInfo(talentInfo->RankID[0]);
|
||||
if (!_spell)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid spell id %u in |trade command", iss.str().c_str(), talentInfo->RankID[0]);
|
||||
return false;
|
||||
}
|
||||
// Delimiter
|
||||
if (!CheckDelimiter(iss, DELIMITER, "talent"))
|
||||
return false;
|
||||
// Rank
|
||||
if (!ReadInt32(iss, _rankId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading talent rank", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// |color|Henchant:recipe_spell_id|h[prof_name: recipe_name]|h|r
|
||||
// |cffffd000|Henchant:3919|h[Engineering: Rough Dynamite]|h|r
|
||||
bool EnchantmentChatLink::Initialize(std::istringstream& iss)
|
||||
{
|
||||
if (_color != CHAT_LINK_COLOR_ENCHANT)
|
||||
return false;
|
||||
// Spell Id
|
||||
uint32 spellId = 0;
|
||||
if (!ReadUInt32(iss, spellId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading enchantment spell entry", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate spell
|
||||
_spell = sSpellMgr->GetSpellInfo(spellId);
|
||||
if (!_spell)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid spell id %u in |enchant command", iss.str().c_str(), spellId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// |color|Hglyph:glyph_slot_id:glyph_prop_id|h[%s]|h|r
|
||||
// |cff66bbff|Hglyph:21:762|h[Glyph of Bladestorm]|h|r
|
||||
bool GlyphChatLink::Initialize(std::istringstream& iss)
|
||||
{
|
||||
if (_color != CHAT_LINK_COLOR_GLYPH)
|
||||
return false;
|
||||
// Slot
|
||||
if (!ReadUInt32(iss, _slotId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading slot id", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Check delimiter
|
||||
if (!CheckDelimiter(iss, DELIMITER, "glyph"))
|
||||
return false;
|
||||
// Glyph Id
|
||||
uint32 glyphId = 0;
|
||||
if (!ReadUInt32(iss, glyphId))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly while reading glyph entry", iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
// Validate glyph
|
||||
_glyph = sGlyphPropertiesStore.LookupEntry(glyphId);
|
||||
if (!_glyph)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid glyph id %u in |glyph command", iss.str().c_str(), glyphId);
|
||||
return false;
|
||||
}
|
||||
// Validate glyph's spell
|
||||
_spell = sSpellMgr->GetSpellInfo(_glyph->SpellId);
|
||||
if (!_spell)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid spell id %u in |glyph command", iss.str().c_str(), _glyph->SpellId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
LinkExtractor::LinkExtractor(const char* msg) : _iss(msg)
|
||||
{
|
||||
}
|
||||
|
||||
LinkExtractor::~LinkExtractor()
|
||||
{
|
||||
for (Links::iterator itr = _links.begin(); itr != _links.end(); ++itr)
|
||||
delete *itr;
|
||||
_links.clear();
|
||||
}
|
||||
|
||||
bool LinkExtractor::IsValidMessage()
|
||||
{
|
||||
const char validSequence[6] = "cHhhr";
|
||||
const char* validSequenceIterator = validSequence;
|
||||
|
||||
char buffer[256];
|
||||
|
||||
std::istringstream::pos_type startPos = 0;
|
||||
uint32 color = 0;
|
||||
|
||||
ChatLink* link = nullptr;
|
||||
while (!_iss.eof())
|
||||
{
|
||||
if (validSequence == validSequenceIterator)
|
||||
{
|
||||
link = nullptr;
|
||||
_iss.ignore(255, PIPE_CHAR);
|
||||
startPos = _iss.tellg() - std::istringstream::pos_type(1);
|
||||
}
|
||||
else if (_iss.get() != PIPE_CHAR)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence aborted unexpectedly", _iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// pipe has always to be followed by at least one char
|
||||
if (_iss.peek() == '\0')
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): pipe followed by '\\0'", _iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// no further pipe commands
|
||||
if (_iss.eof())
|
||||
break;
|
||||
|
||||
char commandChar;
|
||||
_iss.get(commandChar);
|
||||
|
||||
// | in normal messages is escaped by ||
|
||||
if (commandChar != PIPE_CHAR)
|
||||
{
|
||||
if (commandChar == *validSequenceIterator)
|
||||
{
|
||||
if (validSequenceIterator == validSequence + 4)
|
||||
validSequenceIterator = validSequence;
|
||||
else
|
||||
++validSequenceIterator;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): invalid sequence, expected '%c' but got '%c'", _iss.str().c_str(), *validSequenceIterator, commandChar);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (validSequence != validSequenceIterator)
|
||||
{
|
||||
// no escaped pipes in sequences
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got escaped pipe in sequence", _iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (commandChar)
|
||||
{
|
||||
case 'c':
|
||||
if (!ReadHex(_iss, color, 8))
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): invalid hexadecimal number while reading color", _iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'H':
|
||||
// read chars up to colon = link type
|
||||
_iss.getline(buffer, 256, DELIMITER);
|
||||
if (_iss.eof())
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly", _iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strcmp(buffer, "item") == 0)
|
||||
link = new ItemChatLink();
|
||||
else if (strcmp(buffer, "quest") == 0)
|
||||
link = new QuestChatLink();
|
||||
else if (strcmp(buffer, "trade") == 0)
|
||||
link = new TradeChatLink();
|
||||
else if (strcmp(buffer, "talent") == 0)
|
||||
link = new TalentChatLink();
|
||||
else if (strcmp(buffer, "spell") == 0)
|
||||
link = new SpellChatLink();
|
||||
else if (strcmp(buffer, "enchant") == 0)
|
||||
link = new EnchantmentChatLink();
|
||||
else if (strcmp(buffer, "achievement") == 0)
|
||||
link = new AchievementChatLink();
|
||||
else if (strcmp(buffer, "glyph") == 0)
|
||||
link = new GlyphChatLink();
|
||||
else
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): user sent unsupported link type '%s'", _iss.str().c_str(), buffer);
|
||||
return false;
|
||||
}
|
||||
_links.push_back(link);
|
||||
link->SetColor(color);
|
||||
if (!link->Initialize(_iss))
|
||||
return false;
|
||||
break;
|
||||
case 'h':
|
||||
// if h is next element in sequence, this one must contain the linked text :)
|
||||
if (*validSequenceIterator == 'h')
|
||||
{
|
||||
// links start with '['
|
||||
if (_iss.get() != '[')
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): link caption doesn't start with '['", _iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
_iss.getline(buffer, 256, ']');
|
||||
if (_iss.eof())
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): sequence finished unexpectedly", _iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!link)
|
||||
return false;
|
||||
|
||||
if (!link->ValidateName(buffer, _iss.str().c_str()))
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'r':
|
||||
if (link)
|
||||
link->SetBounds(startPos, _iss.tellg());
|
||||
case '|':
|
||||
// no further payload
|
||||
break;
|
||||
default:
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): got invalid command |%c", _iss.str().c_str(), commandChar);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// check if every opened sequence was also closed properly
|
||||
if (validSequence != validSequenceIterator)
|
||||
{
|
||||
LOG_DEBUG("chat.system", "ChatHandler::isValidChatMessage('%s'): EOF in active sequence", _iss.str().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef AZEROTHCORE_CHATLINK_H
|
||||
#define AZEROTHCORE_CHATLINK_H
|
||||
|
||||
#include "SharedDefines.h"
|
||||
#include <cstring>
|
||||
#include <list>
|
||||
#include <sstream>
|
||||
|
||||
struct ItemLocale;
|
||||
struct ItemTemplate;
|
||||
struct ItemRandomSuffixEntry;
|
||||
struct ItemRandomPropertiesEntry;
|
||||
class SpellInfo;
|
||||
struct AchievementEntry;
|
||||
struct GlyphPropertiesEntry;
|
||||
class Quest;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ChatLink - abstract base class for various links
|
||||
class ChatLink
|
||||
{
|
||||
public:
|
||||
ChatLink() : _color(0), _startPos(0), _endPos(0) { }
|
||||
virtual ~ChatLink() { }
|
||||
void SetColor(uint32 color) { _color = color; }
|
||||
// This will allow to extract the whole link string from the message, if necessary.
|
||||
void SetBounds(std::istringstream::pos_type startPos, std::istringstream::pos_type endPos) { _startPos = startPos; _endPos = endPos; }
|
||||
|
||||
virtual bool Initialize(std::istringstream& iss) = 0;
|
||||
virtual bool ValidateName(char* buffer, const char* context) = 0;
|
||||
|
||||
protected:
|
||||
uint32 _color;
|
||||
std::string _name;
|
||||
std::istringstream::pos_type _startPos;
|
||||
std::istringstream::pos_type _endPos;
|
||||
};
|
||||
|
||||
// ItemChatLink - link to item
|
||||
class ItemChatLink : public ChatLink
|
||||
{
|
||||
public:
|
||||
ItemChatLink() : ChatLink(), _item(nullptr), _suffix(nullptr), _property(nullptr)
|
||||
{
|
||||
memset(_data, 0, sizeof(_data));
|
||||
}
|
||||
bool Initialize(std::istringstream& iss) override;
|
||||
bool ValidateName(char* buffer, const char* context) override;
|
||||
|
||||
protected:
|
||||
std::string FormatName(uint8 index, ItemLocale const* locale, char* const* suffixStrings) const;
|
||||
|
||||
ItemTemplate const* _item;
|
||||
int32 _data[8];
|
||||
ItemRandomSuffixEntry const* _suffix;
|
||||
ItemRandomPropertiesEntry const* _property;
|
||||
};
|
||||
|
||||
// QuestChatLink - link to quest
|
||||
class QuestChatLink : public ChatLink
|
||||
{
|
||||
public:
|
||||
QuestChatLink() : ChatLink(), _quest(nullptr), _questLevel(0) { }
|
||||
bool Initialize(std::istringstream& iss) override;
|
||||
bool ValidateName(char* buffer, const char* context) override;
|
||||
|
||||
protected:
|
||||
Quest const* _quest;
|
||||
int32 _questLevel;
|
||||
};
|
||||
|
||||
// SpellChatLink - link to quest
|
||||
class SpellChatLink : public ChatLink
|
||||
{
|
||||
public:
|
||||
SpellChatLink() : ChatLink(), _spell(nullptr) { }
|
||||
bool Initialize(std::istringstream& iss) override;
|
||||
bool ValidateName(char* buffer, const char* context) override;
|
||||
|
||||
protected:
|
||||
SpellInfo const* _spell;
|
||||
};
|
||||
|
||||
// AchievementChatLink - link to quest
|
||||
class AchievementChatLink : public ChatLink
|
||||
{
|
||||
public:
|
||||
AchievementChatLink() : ChatLink(), _guid(0), _achievement(nullptr)
|
||||
{
|
||||
memset(_data, 0, sizeof(_data));
|
||||
}
|
||||
bool Initialize(std::istringstream& iss) override;
|
||||
bool ValidateName(char* buffer, const char* context) override;
|
||||
|
||||
protected:
|
||||
uint32 _guid;
|
||||
AchievementEntry const* _achievement;
|
||||
uint32 _data[8];
|
||||
};
|
||||
|
||||
// TradeChatLink - link to trade info
|
||||
class TradeChatLink : public SpellChatLink
|
||||
{
|
||||
public:
|
||||
TradeChatLink() : SpellChatLink(), _minSkillLevel(0), _maxSkillLevel(0), _guid(0) { }
|
||||
bool Initialize(std::istringstream& iss) override;
|
||||
private:
|
||||
int32 _minSkillLevel;
|
||||
int32 _maxSkillLevel;
|
||||
uint32 _guid;
|
||||
std::string _base64;
|
||||
};
|
||||
|
||||
// TalentChatLink - link to talent
|
||||
class TalentChatLink : public SpellChatLink
|
||||
{
|
||||
public:
|
||||
TalentChatLink() : SpellChatLink(), _talentId(0), _rankId(0) { }
|
||||
bool Initialize(std::istringstream& iss) override;
|
||||
|
||||
private:
|
||||
uint32 _talentId;
|
||||
int32 _rankId;
|
||||
};
|
||||
|
||||
// EnchantmentChatLink - link to enchantment
|
||||
class EnchantmentChatLink : public SpellChatLink
|
||||
{
|
||||
public:
|
||||
EnchantmentChatLink() : SpellChatLink() { }
|
||||
bool Initialize(std::istringstream& iss) override;
|
||||
};
|
||||
|
||||
// GlyphChatLink - link to glyph
|
||||
class GlyphChatLink : public SpellChatLink
|
||||
{
|
||||
public:
|
||||
GlyphChatLink() : SpellChatLink(), _slotId(0), _glyph(nullptr) { }
|
||||
bool Initialize(std::istringstream& iss) override;
|
||||
private:
|
||||
uint32 _slotId;
|
||||
GlyphPropertiesEntry const* _glyph;
|
||||
};
|
||||
|
||||
class LinkExtractor
|
||||
{
|
||||
public:
|
||||
explicit LinkExtractor(const char* msg);
|
||||
~LinkExtractor();
|
||||
|
||||
bool IsValidMessage();
|
||||
|
||||
private:
|
||||
typedef std::list<ChatLink*> Links;
|
||||
Links _links;
|
||||
std::istringstream _iss;
|
||||
};
|
||||
|
||||
#endif
|
||||
261
src/server/game/Chat/HyperlinkTags.cpp
Normal file
261
src/server/game/Chat/HyperlinkTags.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Hyperlinks.h"
|
||||
#include "AchievementMgr.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
#include <limits>
|
||||
|
||||
static constexpr char HYPERLINK_DATA_DELIMITER = ':';
|
||||
|
||||
class HyperlinkDataTokenizer
|
||||
{
|
||||
public:
|
||||
HyperlinkDataTokenizer(std::string_view str) : _str(str) {}
|
||||
|
||||
template <typename T>
|
||||
bool TryConsumeTo(T& val)
|
||||
{
|
||||
if (IsEmpty())
|
||||
return false;
|
||||
|
||||
if (size_t off = _str.find(HYPERLINK_DATA_DELIMITER); off != std::string_view::npos)
|
||||
{
|
||||
if (!Acore::Hyperlinks::LinkTags::base_tag::StoreTo(val, _str.substr(0, off)))
|
||||
return false;
|
||||
_str = _str.substr(off+1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Acore::Hyperlinks::LinkTags::base_tag::StoreTo(val, _str))
|
||||
return false;
|
||||
_str = std::string_view();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsEmpty() { return _str.empty(); }
|
||||
|
||||
private:
|
||||
std::string_view _str;
|
||||
};
|
||||
|
||||
bool Acore::Hyperlinks::LinkTags::achievement::StoreTo(AchievementLinkData& val, std::string_view text)
|
||||
{
|
||||
HyperlinkDataTokenizer t(text);
|
||||
uint32 achievementId;
|
||||
|
||||
if (!t.TryConsumeTo(achievementId))
|
||||
return false;
|
||||
|
||||
val.Achievement = sAchievementMgr->GetAchievement(achievementId);
|
||||
|
||||
if (!(val.Achievement && t.TryConsumeTo(val.CharacterId) && t.TryConsumeTo(val.IsFinished) && t.TryConsumeTo(val.Month) && t.TryConsumeTo(val.Day)))
|
||||
return false;
|
||||
|
||||
if ((12 < val.Month) || (31 < val.Day))
|
||||
return false;
|
||||
|
||||
int8 year;
|
||||
|
||||
if (!t.TryConsumeTo(year))
|
||||
return false;
|
||||
|
||||
if (val.IsFinished) // if finished, year must be >= 0
|
||||
{
|
||||
if (year < 0)
|
||||
return false;
|
||||
val.Year = static_cast<uint8>(year);
|
||||
}
|
||||
else
|
||||
val.Year = 0;
|
||||
|
||||
return (t.TryConsumeTo(val.Criteria[0]) && t.TryConsumeTo(val.Criteria[1]) && t.TryConsumeTo(val.Criteria[2]) && t.TryConsumeTo(val.Criteria[3]) && t.IsEmpty());
|
||||
}
|
||||
|
||||
bool Acore::Hyperlinks::LinkTags::enchant::StoreTo(SpellInfo const*& val, std::string_view text)
|
||||
{
|
||||
HyperlinkDataTokenizer t(text);
|
||||
uint32 spellId;
|
||||
|
||||
if (!(t.TryConsumeTo(spellId) && t.IsEmpty()))
|
||||
return false;
|
||||
|
||||
return (val = sSpellMgr->GetSpellInfo(spellId)) && val->HasAttribute(SPELL_ATTR0_IS_TRADESKILL);
|
||||
}
|
||||
|
||||
bool Acore::Hyperlinks::LinkTags::glyph::StoreTo(GlyphLinkData& val, std::string_view text)
|
||||
{
|
||||
HyperlinkDataTokenizer t(text);
|
||||
uint32 slot, prop;
|
||||
|
||||
if (!(t.TryConsumeTo(slot) && t.TryConsumeTo(prop) && t.IsEmpty()))
|
||||
return false;
|
||||
|
||||
if (!(val.Slot = sGlyphSlotStore.LookupEntry(slot)))
|
||||
return false;
|
||||
|
||||
if (!(val.Glyph = sGlyphPropertiesStore.LookupEntry(prop)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Acore::Hyperlinks::LinkTags::item::StoreTo(ItemLinkData& val, std::string_view text)
|
||||
{
|
||||
HyperlinkDataTokenizer t(text);
|
||||
uint32 itemId, dummy;
|
||||
|
||||
if (!t.TryConsumeTo(itemId))
|
||||
return false;
|
||||
|
||||
val.Item = sObjectMgr->GetItemTemplate(itemId);
|
||||
val.IsBuggedInspectLink = false;
|
||||
|
||||
// randomPropertyId is actually a int16 in the client
|
||||
// positive values index ItemRandomSuffix.dbc, while negative values index ItemRandomProperties.dbc
|
||||
// however, there is also a client bug in inspect packet handling that causes a int16 to be cast to uint16, then int32 (dropping sign extension along the way)
|
||||
// this results in the wrong value being sent in the link; DBC lookup clientside fails, so it sends the link without suffix
|
||||
// to detect and allow these invalid links, we first read randomPropertyId as a full int32
|
||||
int32 randomPropertyId;
|
||||
if (!(val.Item && t.TryConsumeTo(val.EnchantId) && t.TryConsumeTo(val.GemEnchantId[0]) && t.TryConsumeTo(val.GemEnchantId[1]) &&
|
||||
t.TryConsumeTo(val.GemEnchantId[2]) && t.TryConsumeTo(dummy) && t.TryConsumeTo(randomPropertyId) && t.TryConsumeTo(val.RandomSuffixBaseAmount) &&
|
||||
t.TryConsumeTo(val.RenderLevel) && t.IsEmpty() && !dummy))
|
||||
return false;
|
||||
|
||||
if ((static_cast<int32>(std::numeric_limits<int16>::max()) < randomPropertyId) && (randomPropertyId <= std::numeric_limits<uint16>::max()))
|
||||
{ // this is the bug case, the id we received is actually static_cast<uint16>(i16RandomPropertyId)
|
||||
randomPropertyId = static_cast<int16>(randomPropertyId);
|
||||
val.IsBuggedInspectLink = true;
|
||||
}
|
||||
|
||||
if (randomPropertyId < 0)
|
||||
{
|
||||
if (!val.Item->RandomSuffix)
|
||||
return false;
|
||||
|
||||
if (randomPropertyId < -static_cast<int32>(sItemRandomSuffixStore.GetNumRows()))
|
||||
return false;
|
||||
|
||||
if (ItemRandomSuffixEntry const* suffixEntry = sItemRandomSuffixStore.LookupEntry(-randomPropertyId))
|
||||
{
|
||||
val.RandomSuffix = suffixEntry;
|
||||
val.RandomProperty = nullptr;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
else if (randomPropertyId > 0)
|
||||
{
|
||||
if (!val.Item->RandomProperty)
|
||||
return false;
|
||||
|
||||
if (ItemRandomPropertiesEntry const* propEntry = sItemRandomPropertiesStore.LookupEntry(randomPropertyId))
|
||||
{
|
||||
val.RandomSuffix = nullptr;
|
||||
val.RandomProperty = propEntry;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
val.RandomSuffix = nullptr;
|
||||
val.RandomProperty = nullptr;
|
||||
}
|
||||
|
||||
if ((val.RandomSuffix && !val.RandomSuffixBaseAmount) || (val.RandomSuffixBaseAmount && !val.RandomSuffix))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Acore::Hyperlinks::LinkTags::quest::StoreTo(QuestLinkData& val, std::string_view text)
|
||||
{
|
||||
HyperlinkDataTokenizer t(text);
|
||||
uint32 questId;
|
||||
|
||||
if (!t.TryConsumeTo(questId))
|
||||
return false;
|
||||
|
||||
return (val.Quest = sObjectMgr->GetQuestTemplate(questId)) && t.TryConsumeTo(val.QuestLevel) && (val.QuestLevel >= -1) && t.IsEmpty();
|
||||
}
|
||||
|
||||
bool Acore::Hyperlinks::LinkTags::spell::StoreTo(SpellInfo const*& val, std::string_view text)
|
||||
{
|
||||
HyperlinkDataTokenizer t(text);
|
||||
uint32 spellId;
|
||||
|
||||
if (!(t.TryConsumeTo(spellId) && t.IsEmpty()))
|
||||
return false;
|
||||
|
||||
return !!(val = sSpellMgr->GetSpellInfo(spellId));
|
||||
}
|
||||
|
||||
bool Acore::Hyperlinks::LinkTags::talent::StoreTo(TalentLinkData& val, std::string_view text)
|
||||
{
|
||||
HyperlinkDataTokenizer t(text);
|
||||
uint32 talentId;
|
||||
int8 rank; // talent links contain <learned rank>-1, we store <learned rank>
|
||||
|
||||
if (!(t.TryConsumeTo(talentId) && t.TryConsumeTo(rank) && t.IsEmpty()))
|
||||
return false;
|
||||
|
||||
if (rank < -1 || rank >= MAX_TALENT_RANK)
|
||||
return false;
|
||||
|
||||
val.Talent = sTalentStore.LookupEntry(talentId);
|
||||
val.Rank = rank+1;
|
||||
|
||||
if (!val.Talent)
|
||||
return false;
|
||||
|
||||
if (val.Rank > 0)
|
||||
{
|
||||
uint32 const spellId = val.Talent->RankID[val.Rank - 1];
|
||||
if (!spellId)
|
||||
return false;
|
||||
|
||||
val.Spell = sSpellMgr->GetSpellInfo(spellId);
|
||||
|
||||
if (!val.Spell)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
val.Spell = nullptr;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Acore::Hyperlinks::LinkTags::trade::StoreTo(TradeskillLinkData& val, std::string_view text)
|
||||
{
|
||||
HyperlinkDataTokenizer t(text);
|
||||
uint32 spellId;
|
||||
|
||||
if (!t.TryConsumeTo(spellId))
|
||||
return false;
|
||||
|
||||
val.Spell = sSpellMgr->GetSpellInfo(spellId);
|
||||
|
||||
return (val.Spell && val.Spell->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL && t.TryConsumeTo(val.CurValue) &&
|
||||
t.TryConsumeTo(val.MaxValue) && t.TryConsumeTo(val.Owner) && t.TryConsumeTo(val.KnownRecipes) && t.IsEmpty());
|
||||
}
|
||||
424
src/server/game/Chat/Hyperlinks.cpp
Normal file
424
src/server/game/Chat/Hyperlinks.cpp
Normal file
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Hyperlinks.h"
|
||||
#include "advstd.h"
|
||||
#include "Common.h"
|
||||
#include "DBCStores.h"
|
||||
#include "Errors.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "QuestDef.h"
|
||||
|
||||
using namespace Acore::Hyperlinks;
|
||||
|
||||
inline uint8 toHex(char c) { return (c >= '0' && c <= '9') ? c - '0' + 0x10 : (c >= 'a' && c <= 'f') ? c - 'a' + 0x1a : 0x00; }
|
||||
|
||||
// Validates a single hyperlink
|
||||
HyperlinkInfo Acore::Hyperlinks::ParseSingleHyperlink(std::string_view str)
|
||||
{
|
||||
uint32 color = 0;
|
||||
std::string_view tag;
|
||||
std::string_view data;
|
||||
std::string_view text;
|
||||
|
||||
//color tag
|
||||
if (str.substr(0, 2) != "|c")
|
||||
return {};
|
||||
|
||||
str.remove_prefix(2);
|
||||
|
||||
if (str.length() < 8)
|
||||
return {};
|
||||
|
||||
for (uint8 i = 0; i < 8; ++i)
|
||||
{
|
||||
if (uint8 hex = toHex(str[i]))
|
||||
color = (color << 4) | (hex & 0xf);
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
str.remove_prefix(8);
|
||||
|
||||
if (str.substr(0, 2) != "|H")
|
||||
return {};
|
||||
|
||||
str.remove_prefix(2);
|
||||
|
||||
// tag+data part follows
|
||||
if (size_t delimPos = str.find('|'); delimPos != std::string_view::npos)
|
||||
{
|
||||
tag = str.substr(0, delimPos);
|
||||
str.remove_prefix(delimPos+1);
|
||||
}
|
||||
else
|
||||
return {};
|
||||
|
||||
// split tag if : is present (data separator)
|
||||
if (size_t dataStart = tag.find(':'); dataStart != std::string_view::npos)
|
||||
{
|
||||
data = tag.substr(dataStart+1);
|
||||
tag = tag.substr(0, dataStart);
|
||||
}
|
||||
|
||||
// ok, next should be link data end tag...
|
||||
if (str.substr(0, 1) != "h")
|
||||
return {};
|
||||
str.remove_prefix(1);
|
||||
// skip to final |
|
||||
if (size_t end = str.find('|'); end != std::string_view::npos)
|
||||
{
|
||||
// check end tag
|
||||
if (str.substr(end, 4) != "|h|r")
|
||||
return {};
|
||||
// check text brackets
|
||||
if ((str[0] != '[') || (str[end - 1] != ']'))
|
||||
return {};
|
||||
text = str.substr(1, end - 2);
|
||||
// tail
|
||||
str = str.substr(end + 4);
|
||||
}
|
||||
else
|
||||
return {};
|
||||
|
||||
// ok, valid hyperlink, return info
|
||||
return { str, color, tag, data, text };
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
struct LinkValidator
|
||||
{
|
||||
static bool IsTextValid(typename T::value_type, std::string_view) { return true; }
|
||||
static bool IsColorValid(typename T::value_type, HyperlinkColor) { return true; }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct LinkValidator<LinkTags::achievement>
|
||||
{
|
||||
static bool IsTextValid(AchievementLinkData const& data, std::string_view text)
|
||||
{
|
||||
if (text.empty())
|
||||
return false;
|
||||
|
||||
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
|
||||
if (text == data.Achievement->name[i])
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsColorValid(AchievementLinkData const&, HyperlinkColor c)
|
||||
{
|
||||
return c == CHAT_LINK_COLOR_ACHIEVEMENT;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct LinkValidator<LinkTags::item>
|
||||
{
|
||||
static bool IsTextValid(ItemLinkData const& data, std::string_view text)
|
||||
{
|
||||
ItemLocale const* locale = sObjectMgr->GetItemLocale(data.Item->ItemId);
|
||||
|
||||
std::array<char const*, 16> const* randomSuffixes = nullptr;
|
||||
|
||||
if (data.RandomProperty)
|
||||
randomSuffixes = &data.RandomProperty->Name;
|
||||
else if (data.RandomSuffix)
|
||||
randomSuffixes = &data.RandomSuffix->Name;
|
||||
|
||||
if (data.IsBuggedInspectLink) /* DBC lookup will have failed on the client, so the link should've arrived without suffix */
|
||||
randomSuffixes = nullptr;
|
||||
|
||||
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
|
||||
{
|
||||
if (!locale && i != DEFAULT_LOCALE)
|
||||
continue;
|
||||
|
||||
std::string_view name = (i == DEFAULT_LOCALE) ? data.Item->Name1 : ObjectMgr::GetLocaleString(locale->Name, i);
|
||||
if (name.empty())
|
||||
continue;
|
||||
|
||||
if (randomSuffixes)
|
||||
{
|
||||
std::string_view randomSuffix((*randomSuffixes)[i]);
|
||||
if ((!randomSuffix.empty()) &&
|
||||
(text.length() == (name.length() + 1 + randomSuffix.length())) &&
|
||||
(text.substr(0, name.length()) == name) &&
|
||||
(text[name.length()] == ' ') &&
|
||||
(text.substr(name.length() + 1) == randomSuffix))
|
||||
return true;
|
||||
}
|
||||
else if (text == name)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsColorValid(ItemLinkData const& data, HyperlinkColor c)
|
||||
{
|
||||
return c == ItemQualityColors[data.Item->Quality];
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct LinkValidator<LinkTags::quest>
|
||||
{
|
||||
static bool IsTextValid(QuestLinkData const& data, std::string_view text)
|
||||
{
|
||||
if (text.empty())
|
||||
return false;
|
||||
|
||||
if (text == data.Quest->GetTitle())
|
||||
return true;
|
||||
|
||||
QuestLocale const* locale = sObjectMgr->GetQuestLocale(data.Quest->GetQuestId());
|
||||
if (!locale)
|
||||
return false;
|
||||
|
||||
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
|
||||
{
|
||||
if (i == DEFAULT_LOCALE)
|
||||
continue;
|
||||
|
||||
std::string_view name = ObjectMgr::GetLocaleString(locale->Title, i);
|
||||
if (!name.empty() && (text == name))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsColorValid(QuestLinkData const&, HyperlinkColor c)
|
||||
{
|
||||
for (uint8 i = 0; i < MAX_QUEST_DIFFICULTY; ++i)
|
||||
if (c == QuestDifficultyColors[i])
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct LinkValidator<LinkTags::spell>
|
||||
{
|
||||
static bool IsTextValid(SpellInfo const* info, std::string_view text)
|
||||
{
|
||||
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
|
||||
if (text == info->SpellName[i])
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsColorValid(SpellInfo const*, HyperlinkColor c)
|
||||
{
|
||||
return c == CHAT_LINK_COLOR_SPELL;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct LinkValidator<LinkTags::enchant>
|
||||
{
|
||||
static bool IsTextValid(SpellInfo const* info, std::string_view text)
|
||||
{
|
||||
if (LinkValidator<LinkTags::spell>::IsTextValid(info, text))
|
||||
return true;
|
||||
|
||||
SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(info->Id);
|
||||
if (bounds.first == bounds.second)
|
||||
return false;
|
||||
|
||||
for (auto pair = bounds.first; pair != bounds.second; ++pair)
|
||||
{
|
||||
SkillLineEntry const* skill = sSkillLineStore.LookupEntry(pair->second->SkillLine);
|
||||
if (!skill)
|
||||
return false;
|
||||
|
||||
for (uint8 i = 0; i < TOTAL_LOCALES; ++i)
|
||||
{
|
||||
std::string_view skillName = skill->name[i];
|
||||
std::string_view spellName = info->SpellName[i];
|
||||
// alternate form [Skill Name: Spell Name]
|
||||
if ((text.length() == (skillName.length() + 2 + spellName.length())) &&
|
||||
(text.substr(0, skillName.length()) == skillName) &&
|
||||
(text.substr(skillName.length(), 2) == ": ") &&
|
||||
(text.substr(skillName.length() + 2) == spellName))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsColorValid(SpellInfo const*, HyperlinkColor c)
|
||||
{
|
||||
return c == CHAT_LINK_COLOR_ENCHANT;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct LinkValidator<LinkTags::glyph>
|
||||
{
|
||||
static bool IsTextValid(GlyphLinkData const& data, std::string_view text)
|
||||
{
|
||||
if (SpellInfo const* info = sSpellMgr->GetSpellInfo(data.Glyph->SpellId))
|
||||
return LinkValidator<LinkTags::spell>::IsTextValid(info, text);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsColorValid(GlyphLinkData const&, HyperlinkColor c)
|
||||
{
|
||||
return c == CHAT_LINK_COLOR_GLYPH;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct LinkValidator<LinkTags::talent>
|
||||
{
|
||||
static bool IsTextValid(TalentLinkData const& data, std::string_view text)
|
||||
{
|
||||
SpellInfo const* info = data.Spell;
|
||||
if (!info)
|
||||
info = sSpellMgr->GetSpellInfo(data.Talent->RankID[0]);
|
||||
|
||||
if (!info)
|
||||
return false;
|
||||
|
||||
return LinkValidator<LinkTags::spell>::IsTextValid(info, text);
|
||||
}
|
||||
|
||||
static bool IsColorValid(TalentLinkData const&, HyperlinkColor c)
|
||||
{
|
||||
return c == CHAT_LINK_COLOR_TALENT;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct LinkValidator<LinkTags::trade>
|
||||
{
|
||||
static bool IsTextValid(TradeskillLinkData const& data, std::string_view text)
|
||||
{
|
||||
return LinkValidator<LinkTags::spell>::IsTextValid(data.Spell, text);
|
||||
}
|
||||
|
||||
static bool IsColorValid(TradeskillLinkData const&, HyperlinkColor c)
|
||||
{
|
||||
return c == CHAT_LINK_COLOR_TRADE;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename TAG>
|
||||
static bool ValidateAs(HyperlinkInfo const& info)
|
||||
{
|
||||
std::decay_t<typename TAG::value_type> t;
|
||||
if (!TAG::StoreTo(t, info.data))
|
||||
return false;
|
||||
|
||||
int32 const severity = static_cast<int32>(sWorld->getIntConfig(CONFIG_CHAT_STRICT_LINK_CHECKING_SEVERITY));
|
||||
if (severity >= 0)
|
||||
{
|
||||
if (!LinkValidator<TAG>::IsColorValid(t, info.color))
|
||||
return false;
|
||||
|
||||
if (severity >= 1)
|
||||
{
|
||||
if (!LinkValidator<TAG>::IsTextValid(t, info.text))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#define TryValidateAs(T) do { if (info.tag == T::tag()) return ValidateAs<T>(info); } while (0);
|
||||
|
||||
static bool ValidateLinkInfo(HyperlinkInfo const& info)
|
||||
{
|
||||
using namespace LinkTags;
|
||||
TryValidateAs(achievement);
|
||||
TryValidateAs(area);
|
||||
TryValidateAs(areatrigger);
|
||||
TryValidateAs(creature);
|
||||
TryValidateAs(creature_entry);
|
||||
TryValidateAs(enchant);
|
||||
TryValidateAs(gameevent);
|
||||
TryValidateAs(gameobject);
|
||||
TryValidateAs(gameobject_entry);
|
||||
TryValidateAs(glyph);
|
||||
TryValidateAs(item);
|
||||
TryValidateAs(itemset);
|
||||
TryValidateAs(player);
|
||||
TryValidateAs(quest);
|
||||
TryValidateAs(skill);
|
||||
TryValidateAs(spell);
|
||||
TryValidateAs(talent);
|
||||
TryValidateAs(taxinode);
|
||||
TryValidateAs(tele);
|
||||
TryValidateAs(title);
|
||||
TryValidateAs(trade);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validates all hyperlinks and control sequences contained in str
|
||||
bool Acore::Hyperlinks::CheckAllLinks(std::string_view str)
|
||||
{
|
||||
// Step 1: Disallow all control sequences except ||, |H, |h, |c and |r
|
||||
{
|
||||
std::string_view::size_type pos = 0;
|
||||
while ((pos = str.find('|', pos)) != std::string::npos)
|
||||
{
|
||||
++pos;
|
||||
if (pos == str.length())
|
||||
return false;
|
||||
char next = str[pos];
|
||||
if (next == 'H' || next == 'h' || next == 'c' || next == 'r' || next == '|')
|
||||
++pos;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Parse all link sequences
|
||||
// They look like this: |c<color>|H<linktag>:<linkdata>|h[<linktext>]|h|r
|
||||
// - <color> is 8 hex characters AARRGGBB
|
||||
// - <linktag> is arbitrary length [a-z_]
|
||||
// - <linkdata> is arbitrary length, no | contained
|
||||
// - <linktext> is printable
|
||||
{
|
||||
std::string::size_type pos;
|
||||
while ((pos = str.find('|')) != std::string::npos)
|
||||
{
|
||||
if (str[pos + 1] == '|') // this is an escaped pipe character (||)
|
||||
{
|
||||
str = str.substr(pos + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
HyperlinkInfo info = ParseSingleHyperlink(str.substr(pos));
|
||||
if (!info || !ValidateLinkInfo(info))
|
||||
return false;
|
||||
|
||||
// tag is fine, find the next one
|
||||
str = info.tail;
|
||||
}
|
||||
}
|
||||
|
||||
// all tags are valid
|
||||
return true;
|
||||
}
|
||||
255
src/server/game/Chat/Hyperlinks.h
Normal file
255
src/server/game/Chat/Hyperlinks.h
Normal file
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _HYPERLINKS_H_
|
||||
#define _HYPERLINKS_H_
|
||||
|
||||
#include "ObjectGuid.h"
|
||||
#include "StringConvert.h"
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
struct AchievementEntry;
|
||||
struct GlyphPropertiesEntry;
|
||||
struct GlyphSlotEntry;
|
||||
struct ItemRandomPropertiesEntry;
|
||||
struct ItemRandomSuffixEntry;
|
||||
struct ItemTemplate;
|
||||
class SpellInfo;
|
||||
class Quest;
|
||||
struct TalentEntry;
|
||||
|
||||
namespace Acore::Hyperlinks
|
||||
{
|
||||
|
||||
struct AchievementLinkData
|
||||
{
|
||||
AchievementEntry const* Achievement;
|
||||
ObjectGuid CharacterId;
|
||||
bool IsFinished;
|
||||
uint8 Year;
|
||||
uint8 Month;
|
||||
uint8 Day;
|
||||
std::array<uint32, 4> Criteria;
|
||||
};
|
||||
|
||||
struct GlyphLinkData
|
||||
{
|
||||
GlyphPropertiesEntry const* Glyph;
|
||||
GlyphSlotEntry const* Slot;
|
||||
};
|
||||
|
||||
struct ItemLinkData
|
||||
{
|
||||
ItemTemplate const* Item;
|
||||
uint32 EnchantId;
|
||||
std::array<uint32, 3> GemEnchantId;
|
||||
ItemRandomPropertiesEntry const* RandomProperty;
|
||||
ItemRandomSuffixEntry const* RandomSuffix;
|
||||
uint32 RandomSuffixBaseAmount; /* ITEM_FIELD_PROPERTY_SEED - only nonzero for RandomSuffix items, AllocationPct from DBC are multiplied with this, then floored, to get stat value */
|
||||
uint8 RenderLevel;
|
||||
bool IsBuggedInspectLink;
|
||||
};
|
||||
|
||||
struct QuestLinkData
|
||||
{
|
||||
::Quest const* Quest;
|
||||
int16 QuestLevel;
|
||||
};
|
||||
|
||||
struct TalentLinkData
|
||||
{
|
||||
TalentEntry const* Talent;
|
||||
uint8 Rank;
|
||||
SpellInfo const* Spell;
|
||||
};
|
||||
|
||||
struct TradeskillLinkData
|
||||
{
|
||||
SpellInfo const* Spell;
|
||||
uint16 CurValue;
|
||||
uint16 MaxValue;
|
||||
ObjectGuid Owner;
|
||||
std::string KnownRecipes;
|
||||
};
|
||||
|
||||
namespace LinkTags {
|
||||
|
||||
/************************** LINK TAGS ***************************************************\
|
||||
|* Link tags must abide by the following: *|
|
||||
|* - MUST expose ::value_type typedef *|
|
||||
|* - storage type is remove_cvref_t<value_type> *|
|
||||
|* - MUST expose static ::tag method, void -> std::string_view *|
|
||||
|* - this method SHOULD be constexpr *|
|
||||
|* - returns identifier string for the link ("creature", "creature_entry", "item") *|
|
||||
|* - MUST expose static ::StoreTo method, (storage&, std::string_view) *|
|
||||
|* - assign storage& based on content of std::string_view *|
|
||||
|* - return value indicates success/failure *|
|
||||
|* - for integral/string types this can be achieved by extending base_tag *|
|
||||
\****************************************************************************************/
|
||||
struct base_tag
|
||||
{
|
||||
static bool StoreTo(std::string_view& val, std::string_view data)
|
||||
{
|
||||
val = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool StoreTo(std::string& val, std::string_view data)
|
||||
{
|
||||
val.assign(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static std::enable_if_t<std::is_integral_v<T>, bool> StoreTo(T& val, std::string_view data)
|
||||
{
|
||||
if (Optional<T> res = Acore::StringTo<T>(data))
|
||||
{
|
||||
val = *res;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool StoreTo(ObjectGuid& val, std::string_view data)
|
||||
{
|
||||
if (Optional<uint64> res = Acore::StringTo<uint64>(data, 16))
|
||||
{
|
||||
val.Set(*res);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
#define make_base_tag(ltag, type) struct ltag : public base_tag { using value_type = type; static constexpr std::string_view tag() { return #ltag; } }
|
||||
make_base_tag(area, uint32);
|
||||
make_base_tag(areatrigger, uint32);
|
||||
make_base_tag(creature, ObjectGuid::LowType);
|
||||
make_base_tag(creature_entry, uint32);
|
||||
make_base_tag(gameevent, uint16);
|
||||
make_base_tag(gameobject, ObjectGuid::LowType);
|
||||
make_base_tag(gameobject_entry, uint32);
|
||||
make_base_tag(itemset, uint32);
|
||||
make_base_tag(player, std::string_view);
|
||||
make_base_tag(skill, uint32);
|
||||
make_base_tag(taxinode, uint32);
|
||||
make_base_tag(tele, uint32);
|
||||
make_base_tag(title, uint32);
|
||||
#undef make_base_tag
|
||||
|
||||
struct AC_GAME_API achievement
|
||||
{
|
||||
using value_type = AchievementLinkData const&;
|
||||
static constexpr std::string_view tag() { return "achievement"; }
|
||||
static bool StoreTo(AchievementLinkData& val, std::string_view data);
|
||||
};
|
||||
|
||||
struct AC_GAME_API enchant
|
||||
{
|
||||
using value_type = SpellInfo const*;
|
||||
static constexpr std::string_view tag() { return "enchant"; }
|
||||
static bool StoreTo(SpellInfo const*& val, std::string_view data);
|
||||
};
|
||||
|
||||
struct AC_GAME_API glyph
|
||||
{
|
||||
using value_type = GlyphLinkData const&;
|
||||
static constexpr std::string_view tag() { return "glyph"; };
|
||||
static bool StoreTo(GlyphLinkData& val, std::string_view data);
|
||||
};
|
||||
|
||||
struct AC_GAME_API item
|
||||
{
|
||||
using value_type = ItemLinkData const&;
|
||||
static constexpr std::string_view tag() { return "item"; }
|
||||
static bool StoreTo(ItemLinkData& val, std::string_view data);
|
||||
};
|
||||
|
||||
struct AC_GAME_API quest
|
||||
{
|
||||
using value_type = QuestLinkData const&;
|
||||
static constexpr std::string_view tag() { return "quest"; }
|
||||
static bool StoreTo(QuestLinkData& val, std::string_view data);
|
||||
};
|
||||
|
||||
struct AC_GAME_API spell
|
||||
{
|
||||
using value_type = SpellInfo const*;
|
||||
static constexpr std::string_view tag() { return "spell"; }
|
||||
static bool StoreTo(SpellInfo const*& val, std::string_view data);
|
||||
};
|
||||
|
||||
struct AC_GAME_API talent
|
||||
{
|
||||
using value_type = TalentLinkData const&;
|
||||
static constexpr std::string_view tag() { return "talent"; }
|
||||
static bool StoreTo(TalentLinkData& val, std::string_view data);
|
||||
};
|
||||
|
||||
struct AC_GAME_API trade
|
||||
{
|
||||
using value_type = TradeskillLinkData const&;
|
||||
static constexpr std::string_view tag() { return "trade"; }
|
||||
static bool StoreTo(TradeskillLinkData& val, std::string_view data);
|
||||
};
|
||||
}
|
||||
|
||||
struct HyperlinkColor
|
||||
{
|
||||
HyperlinkColor(uint32 c) : r(c >> 16), g(c >> 8), b(c), a(c >> 24) {}
|
||||
uint8 const r, g, b, a;
|
||||
bool operator==(uint32 c) const
|
||||
{
|
||||
if ((c & 0xff) ^ b)
|
||||
return false;
|
||||
if (((c >>= 8) & 0xff) ^ g)
|
||||
return false;
|
||||
if (((c >>= 8) & 0xff) ^ r)
|
||||
return false;
|
||||
if ((c >>= 8) ^ a)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct HyperlinkInfo
|
||||
{
|
||||
HyperlinkInfo() : ok(false), color(0) {}
|
||||
HyperlinkInfo(std::string_view t, uint32 c, std::string_view ta, std::string_view d, std::string_view te) :
|
||||
ok(true), tail(t), color(c), tag(ta), data(d), text(te) {}
|
||||
|
||||
explicit operator bool() { return ok; }
|
||||
bool const ok;
|
||||
std::string_view const tail;
|
||||
HyperlinkColor const color;
|
||||
std::string_view const tag;
|
||||
std::string_view const data;
|
||||
std::string_view const text;
|
||||
};
|
||||
HyperlinkInfo AC_GAME_API ParseSingleHyperlink(std::string_view str);
|
||||
bool AC_GAME_API CheckAllLinks(std::string_view str);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user