/*
* This file is part of the TrinityCore 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 General Public License as published by the
* Free Software Foundation; either version 2 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 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 .
*/
#include "ChatCommand.h"
#include "AccountMgr.h"
#include "Chat.h"
#include "DatabaseEnv.h"
#include "DB2Stores.h"
#include "Log.h"
#include "Map.h"
#include "Player.h"
#include "ScriptMgr.h"
#include "WorldSession.h"
using ChatSubCommandMap = std::map;
void Trinity::Impl::ChatCommands::ChatCommandNode::LoadFromBuilder(ChatCommandBuilder const& builder)
{
if (ChatCommandBuilder::InvokerEntry const* invokerEntry = std::get_if(&builder._data))
{
ASSERT(!_invoker, "Duplicate blank sub-command.");
_invoker = invokerEntry->_invoker;
if (invokerEntry->_help)
_help.emplace(invokerEntry->_help);
_permission = invokerEntry->_permissions;
}
else
LoadCommandsIntoMap(this, _subCommands, std::get(builder._data));
}
/*static*/ void Trinity::Impl::ChatCommands::ChatCommandNode::LoadCommandsIntoMap(ChatCommandNode* blank, ChatSubCommandMap& map, Trinity::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 const tokens = Trinity::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& Trinity::Impl::ChatCommands::ChatCommandNode::GetTopLevelMap()
{
if (COMMAND_MAP.empty())
LoadCommandMap();
return COMMAND_MAP;
}
/*static*/ void Trinity::Impl::ChatCommands::ChatCommandNode::InvalidateCommandMap() { COMMAND_MAP.clear(); }
/*static*/ void Trinity::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[1].GetStringView();
ChatCommandNode* cmd = nullptr;
ChatSubCommandMap* map = &COMMAND_MAP;
for (std::string_view key : Trinity::Tokenize(name, COMMAND_DELIMITER, false))
{
auto it = map->find(key);
if (it != map->end())
{
cmd = &it->second;
map = &cmd->_subCommands;
}
else
{
TC_LOG_ERROR("sql.sql", "Table `command` contains data for non-existant command '{}'. Skipped.", name);
cmd = nullptr;
break;
}
}
if (!cmd)
continue;
if (std::holds_alternative(cmd->_help))
TC_LOG_ERROR("sql.sql", "Table `command` contains duplicate data for command '{}'. Skipped.", name);
if (std::holds_alternative(cmd->_help))
cmd->_help.emplace(help);
else
TC_LOG_ERROR("sql.sql", "Table `command` contains legacy help text for command '{}', which uses `trinity_string`. Skipped.", name);
} while (result->NextRow());
}
for (auto& [name, cmd] : COMMAND_MAP)
cmd.ResolveNames(std::string(name));
}
void Trinity::Impl::ChatCommands::ChatCommandNode::ResolveNames(std::string name)
{
if (_invoker && std::holds_alternative(_help))
TC_LOG_WARN("sql.sql", "Table `command` is missing help text for command '{}'.", 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, uint32 permission, std::string_view cmdStr)
{
if (AccountMgr::IsPlayerAccount(session.GetSecurity()))
return;
if (sAccountMgr->GetRBACPermission(rbac::RBAC_ROLE_PLAYER)->GetLinkedPermissions().count(permission))
return;
Player* player = session.GetPlayer();
ObjectGuid targetGuid = player->GetTarget();
uint32 areaId = player->GetAreaId();
std::string areaName = "Unknown";
std::string zoneName = "Unknown";
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(areaId))
{
LocaleConstant locale = session.GetSessionDbcLocale();
areaName = area->AreaName[locale];
if (area->GetFlags().HasFlag(AreaFlags::IsSubzone))
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(area->ParentAreaID))
zoneName = zone->AreaName[locale];
}
sLog->OutCommand(session.GetAccountId(), "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, zoneName,
player->GetSelectedUnit() ? player->GetSelectedUnit()->GetName().c_str() : "",
targetGuid.ToString());
}
void Trinity::Impl::ChatCommands::ChatCommandNode::SendCommandHelp(ChatHandler& handler) const
{
bool const hasInvoker = IsInvokerVisible(handler);
if (hasInvoker)
{
if (std::holds_alternative(_help))
handler.SendSysMessage(std::get(_help));
else if (std::holds_alternative(_help))
handler.SendSysMessage(std::get(_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 Trinity::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(*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 Trinity::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(), cmd->_permission.RequiredPermission, cmdStr);
}
else if (!handler.HasSentErrorMessage())
{ /* invocation failed, we should show usage */
cmd->SendCommandHelp(handler);
handler.SetSentErrorMessage(true);
}
return true;
}
return false;
}
/*static*/ void Trinity::Impl::ChatCommands::ChatCommandNode::SendCommandHelpFor(ChatHandler& handler, std::string_view cmdStr)
{
ChatCommandNode const* cmd = nullptr;
ChatSubCommandMap const* map = &GetTopLevelMap();
for (std::string_view token : Trinity::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 Trinity::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 vec;
auto possibility = ([prefix = std::string_view(path), suffix = std::string_view(newTail)](std::string_view match)
{
if (prefix.empty())
return Trinity::StringFormat("{}{}{}", match, COMMAND_DELIMITER, suffix);
else
return Trinity::StringFormat("{}{}{}{}{}", prefix, COMMAND_DELIMITER, match, COMMAND_DELIMITER, 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 = Trinity::StringFormat("{}{}{}", path, COMMAND_DELIMITER, 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 {
Trinity::StringFormat("{}{}{}", path, COMMAND_DELIMITER, 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 Trinity::StringFormat("{}{}{}", prefix, COMMAND_DELIMITER, match);
}
});
std::vector vec;
for (FilteredCommandListIterator it(*map, handler, ""); it; ++it)
vec.emplace_back(possibility(it->first));
return vec;
}
}
bool Trinity::Impl::ChatCommands::ChatCommandNode::IsInvokerVisible(ChatHandler const& who) const
{
if (!_invoker)
return false;
if (who.IsConsole() && (_permission.AllowConsole == Trinity::ChatCommands::Console::No))
return false;
return who.HasPermission(_permission.RequiredPermission);
}
bool Trinity::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 Trinity::ChatCommands::LoadCommandMap() { Trinity::Impl::ChatCommands::ChatCommandNode::LoadCommandMap(); }
void Trinity::ChatCommands::InvalidateCommandMap() { Trinity::Impl::ChatCommands::ChatCommandNode::InvalidateCommandMap(); }
bool Trinity::ChatCommands::TryExecuteCommand(ChatHandler& handler, std::string_view cmd) { return Trinity::Impl::ChatCommands::ChatCommandNode::TryExecuteCommand(handler, cmd); }
void Trinity::ChatCommands::SendCommandHelpFor(ChatHandler& handler, std::string_view cmd) { Trinity::Impl::ChatCommands::ChatCommandNode::SendCommandHelpFor(handler, cmd); }
std::vector Trinity::ChatCommands::GetAutoCompletionsFor(ChatHandler const& handler, std::string_view cmd) { return Trinity::Impl::ChatCommands::ChatCommandNode::GetAutoCompletionsFor(handler, cmd); }