/* * 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); }