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

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

View File

@@ -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 = &map;
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); }

View 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

View 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;
}

View 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

View 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);
}

View 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

View 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;
}

View 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