/* * 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 . */ #include "ChatCommand.h" #include "AccountMgr.h" #include "Chat.h" #include "DBCStores.h" #include "DatabaseEnv.h" #include "Log.h" #include "Player.h" #include "ScriptMgr.h" #include "StringFormat.h" #include "Tokenize.h" #include "WorldSession.h" using ChatSubCommandMap = std::map; void Acore::Impl::ChatCommands::ChatCommandNode::LoadFromBuilder(ChatCommandBuilder const& builder) { if (std::holds_alternative(builder._data)) { ASSERT(!_invoker, "Duplicate blank sub-command."); AcoreStrings help; std::tie(_invoker, help, _permission) = *(std::get(builder._data)); if (help) _help.emplace(help); } else LoadCommandsIntoMap(this, _subCommands, std::get(builder._data)); } /*static*/ void Acore::Impl::ChatCommands::ChatCommandNode::LoadCommandsIntoMap(ChatCommandNode* blank, std::map& 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 const tokens = Acore::Tokenize(builder._name, COMMAND_DELIMITER, false); ASSERT(!tokens.empty(), "Invalid command name '{}'.", builder._name); ChatSubCommandMap* subMap = ↦ for (std::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].Get(); std::string_view const help = fields[2].Get(); uint32 const secLevel = fields[1].Get(); 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 '{}'. Skipped.", name); cmd = nullptr; break; } } if (!cmd) continue; if (cmd->_invoker && (cmd->_permission.RequiredLevel != secLevel)) { LOG_WARN("sql.sql", "Table `command` has permission {} for '{}' which does not match the core ({}). Overriding.", secLevel, name, cmd->_permission.RequiredLevel); cmd->_permission.RequiredLevel = secLevel; } if (std::holds_alternative(cmd->_help)) LOG_ERROR("sql.sql", "Table `command` contains duplicate data for command '{}'. Skipped.", name); if (std::holds_alternative(cmd->_help)) cmd->_help.emplace(help); else LOG_ERROR("sql.sql", "Table `command` contains legacy help text for command '{}', which uses `acore_string`. Skipped.", 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(_help)) 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, 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::StringFormat("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(_help)) handler.SendSysMessage(std::get(_help)); else if (std::holds_alternative(_help)) handler.SendSysMessage(std::get(_help)); else { handler.PSendSysMessage(LANG_CMD_HELP_GENERIC, _name); handler.PSendSysMessage(LANG_CMD_NO_HELP_AVAILABLE, _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, _name); } handler.SendSysMessage(LANG_SUBCMDS_LIST); header = true; } handler.PSendSysMessage(subCommandHasSubCommand ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, 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(*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, cmd->_name, COMMAND_DELIMITER, token); else handler.PSendSysMessage(LANG_CMD_AMBIGUOUS, token); handler.PSendSysMessage(it1->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, it1->first); do { handler.PSendSysMessage(it2->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, 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 (!sScriptMgr->OnTryExecuteCommand(handler, cmdStr)) return true; /* if we matched a command at some point, invoke it */ if (cmd) { 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, cmd->_name, COMMAND_DELIMITER, token); } else handler.PSendSysMessage(LANG_CMD_INVALID, 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, cmd->_name, COMMAND_DELIMITER, token); else handler.PSendSysMessage(LANG_CMD_AMBIGUOUS, token); handler.PSendSysMessage(it1->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, it1->first); do { handler.PSendSysMessage(it2->second.HasVisibleSubCommands(handler) ? LANG_SUBCMDS_LIST_ENTRY_ELLIPSIS : LANG_SUBCMDS_LIST_ENTRY, 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, it->second._name); } while (++it); } else handler.PSendSysMessage(LANG_CMD_INVALID, cmdStr); } /*static*/ std::vector 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 vec; auto possibility = ([prefix = std::string_view(path), suffix = std::string_view(newTail)](std::string_view match) { if (prefix.empty()) { return Acore::StringFormat("{}{}{}", match, COMMAND_DELIMITER, suffix); } else { return Acore::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 = Acore::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 { Acore::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 Acore::StringFormat("{}{}{}", prefix, COMMAND_DELIMITER, match); } }); std::vector 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 Acore::ChatCommands::GetAutoCompletionsFor(ChatHandler const& handler, std::string_view cmd) { return Acore::Impl::ChatCommands::ChatCommandNode::GetAutoCompletionsFor(handler, cmd); }