wesnoth/src/map_command_handler.hpp
Gunter Labes 403fc54790
Leave chat open on failed command execution
Also remove unused set_help_on_unknown function.
2025-03-21 09:43:56 +01:00

455 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2006 - 2025
by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
Copyright (C) 2003 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
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.
See the COPYING file for more details.
*/
#pragma once
#include "config.hpp"
#include "serialization/string_utils.hpp"
#include "formula/string_utils.hpp"
#include "gettext.hpp"
#include <boost/algorithm/string.hpp>
namespace events {
//simple command args parser, separated from command_handler for clarity.
//a word begins with a nonspace
//n-th arg is n-th word up to the next space
//n-th data is n-th word up to the end
//cmd is 0-th arg, begins at 0 always.
class cmd_arg_parser
{
public:
cmd_arg_parser() :
str_(""),
args(1, 0),
args_end(false)
{
}
explicit cmd_arg_parser(const std::string& str) :
str_(str),
args(1, 0),
args_end(false)
{
}
void parse(const std::string& str)
{
str_ = str;
args.clear();
args.push_back(0);
args_end = false;
}
const std::string& get_str() const
{
return str_;
}
std::string get_arg(unsigned n) const
{
advance_to_arg(n);
if (n < args.size()) {
return std::string(str_, args[n], str_.find(' ', args[n]) - args[n]);
}
else {
return "";
}
}
std::string get_data(unsigned n) const
{
advance_to_arg(n);
if (n < args.size()) {
std::string data(str_, args[n]);
boost::trim(data);
return data;
}
else {
return "";
}
}
std::string get_cmd() const
{
return get_arg(0);
}
private:
cmd_arg_parser& operator=(const cmd_arg_parser&);
cmd_arg_parser(const cmd_arg_parser&);
void advance_to_arg(unsigned n) const
{
while (n < args.size() && !args_end) {
std::size_t first_space = str_.find_first_of(' ', args.back());
std::size_t next_arg_begin = str_.find_first_not_of(' ', first_space);
if (next_arg_begin != std::string::npos) {
args.push_back(next_arg_begin);
}
else {
args_end = true;
}
}
}
std::string str_;
mutable std::vector<std::size_t> args;
mutable bool args_end;
};
//A helper class template with a slim public interface
//This represents a map of strings to void()-member-function-of-Worker-pointers
//with all the common functionality like general help, command help and aliases
//Usage (of a derived class): Derived(specific-arguments) d; d.dispatch(command);
//Derived classes should override virtual functions where noted.
//The template parameter currently must be the dervived class itself,
//i.e. class X : public map_command_handler<X>
//To add a new command in a derived class:
// * add a new private void function() to the derived class
// * add it to the function map in init_map there, setting flags like
// "D" for debug only (checking the flag is also done in the derived class)
// * remember to add some help and/or usage information in init_map()
template <class Worker>
class map_command_handler
{
public:
typedef void (Worker::*command_handler)();
struct command
{
command_handler handler;
std::string help; //long help text
std::string usage; //only args info
std::string flags; //< see implementation of get_command_flags_description() for meaning of flags
explicit command(command_handler h, const std::string& help = "",
const std::string& usage = "", const std::string& flags = "")
: handler(h), help(help), usage(usage), flags(flags)
{
}
bool has_flag(const char f) const
{
return flags.find(f) != flags.npos;
}
command& add_flag(const char f)
{
flags += f;
return *this;
}
};
typedef std::map<std::string, command> command_map;
typedef std::map<std::string, std::string> command_alias_map;
map_command_handler() : cap_("")
{
}
virtual ~map_command_handler() {}
bool empty() const
{
return command_map_.empty();
}
//actual work function
bool dispatch(std::string cmd)
{
if (empty()) {
init_map_default();
init_map();
}
// We recursively resolve alias (100 max to avoid infinite recursion)
for (int i = 0; i < 100; ++i) {
parse_cmd(cmd);
std::string actual_cmd = get_actual_cmd(get_cmd());
if (actual_cmd == get_cmd())
break;
std::string data = get_data(1);
// translate the command and add space + data if any
cmd = actual_cmd + (data.empty() ? "" : " ") + data;
}
if (get_cmd().empty()) {
return false;
}
if (const command* c = get_command(get_cmd())) {
if (is_enabled(*c)) {
(static_cast<Worker*>(this)->*(c->handler))();
return true;
}
else {
print(get_cmd(), _("This command is currently unavailable."));
return false;
}
}
utils::string_map symbols;
if(!cmd_flag_) {
symbols["help_command"] = cmd_prefix_ + "help";
symbols["command"] = cmd_prefix_ + get_cmd();
}
else {
symbols["help_command"] = "help";
symbols["command"] = get_cmd();
}
std::string string_user = get_cmd();
int distance = 0;
// Minimum length of the two compared strings.
int len_min = 0;
bool has_command_proposal = false;
// Compare the input with every command (excluding alias).
for(const auto& [key, index] : command_map_) {
// No need to test commands that are not enabled.
if(is_enabled(index)) {
distance = edit_distance_approx(string_user, key);
len_min = std::min(string_user.length(), key.length());
// Maximum of a third of the letters are wrong. The ratio
// between the edit distance and the minimum length, multiplied
// by a hundred gives us the percentage of errors.
if(distance * 100 / len_min < 34) {
symbols["command_proposal"] = key;
has_command_proposal = true;
// If a good enough candidate is found, exit the loop.
break;
}
}
}
// If a proposal for a command is found, print it
if(has_command_proposal) {
print("help", VGETTEXT("Unknown command $command, did you mean $command_proposal? try $help_command "
"for a list of available commands.", symbols));
}
else {
print("help", VGETTEXT("Unknown command $command, try $help_command "
"for a list of available commands.", symbols));
}
return false;
}
std::vector<std::string> get_commands_list() const
{
std::vector<std::string> res;
for (typename command_map::value_type i : command_map_) {
res.push_back(i.first);
}
return res;
}
//command error reporting shorthands
void command_failed(const std::string& message, bool = false)
{
print(get_cmd(), _("Error:") + std::string(" ") + message);
}
protected:
void init_map_default()
{
register_command("help", &map_command_handler<Worker>::help,
_("Available commands list and command-specific help. "
"Use \"help all\" to include currently unavailable commands."),
// TRANSLATORS: These are the arguments accepted by the "help" command,
// which are either "all" or the name of another command.
// As with the command's name, "all" is hardcoded, and shouldn't change in the translation.
_("[all|<command>]\n“all” = overview of all commands, <command> = name of a specific command (provides more detail)"));
}
//derived classes initialize the map overriding this function
virtual void init_map() = 0;
//overridden in derived classes to actually print the messages somwehere
virtual void print(const std::string& title, const std::string& message) = 0;
//should be overridden in derived classes if the commands have flags
//this should return a string describing what all the flags mean
virtual std::string get_flags_description() const
{
return "";
}
//this should return a string describing the flags of the given command
virtual std::string get_command_flags_description(const command& /*c*/) const
{
return "";
}
//this should be overridden if e.g. flags are used to control command
//availability. Return false if the command should not be executed by dispatch()
virtual bool is_enabled(const command& /*c*/) const
{
return true;
}
virtual void parse_cmd(const std::string& cmd_string)
{
cap_.parse(cmd_string);
}
//safe n-th argunment getter
virtual std::string get_arg(unsigned argn) const
{
return cap_.get_arg(argn);
}
//"data" is n-th arg and everything after it
virtual std::string get_data(unsigned argn = 1) const
{
return cap_.get_data(argn);
}
virtual std::string get_cmd() const
{
return cap_.get_cmd();
}
void command_failed_need_arg(int argn)
{
utils::string_map symbols;
symbols["arg_id"] = std::to_string(argn);
command_failed(VGETTEXT("Missing argument $arg_id", symbols));
}
void print_usage()
{
help_command(get_cmd());
}
//take aliases into account
std::string get_actual_cmd(const std::string& cmd) const
{
command_alias_map::const_iterator i = command_alias_map_.find(cmd);
return i != command_alias_map_.end() ? i->second : cmd;
}
const command* get_command(const std::string& cmd) const
{
typename command_map::const_iterator i = command_map_.find(cmd);
return i != command_map_.end() ? &i->second : 0;
}
command* get_command(const std::string& cmd)
{
typename command_map::iterator i = command_map_.find(cmd);
return i != command_map_.end() ? &i->second : 0;
}
void help()
{
//print command-specific help if available, otherwise list commands
if (help_command(get_arg(1))) {
return;
}
std::stringstream ss;
bool show_unavail = show_unavailable_ || get_arg(1) == "all";
for (typename command_map::value_type i : command_map_) {
if (show_unavail || is_enabled(i.second)) {
ss << i.first;
//if (!i.second.usage.empty()) {
// ss << " " << i.second.usage;
//}
//uncomment the above to display usage information in command list
//which might clutter it somewhat
if (!i.second.flags.empty()) {
ss << " (" << i.second.flags << ") ";
}
ss << "; ";
}
}
utils::string_map symbols;
symbols["flags_description"] = get_flags_description();
symbols["list_of_commands"] = ss.str();
if(!cmd_flag_) {
symbols["help_command"] = cmd_prefix_ + "help";
}
else {
symbols["help_command"] = "help";
}
print(_("help"), VGETTEXT("Available commands $flags_description:\n$list_of_commands", symbols));
print(_("help"), VGETTEXT("Type $help_command <command> for more info.", symbols));
}
//returns true if the command exists.
bool help_command(const std::string& acmd)
{
std::string cmd = get_actual_cmd(acmd);
const command* c = get_command(cmd);
if (c) {
std::stringstream ss;
if(!cmd_flag_) {
ss << cmd_prefix_ << cmd;
}
else {
ss << cmd;
}
if (c->help.empty() && c->usage.empty()) {
ss << _(" No help available.");
}
else {
ss << " - " << c->help << "\n";
}
if (!c->usage.empty()) {
if(!cmd_flag_) {
ss << _("Usage:") << " " << cmd_prefix_ << cmd << " " << c->usage << "\n";
}
else {
ss << _("Usage:") << " " << cmd << " " << c->usage << "\n";
}
}
const auto flags_description = get_command_flags_description(*c);
if (!flags_description.empty()) {
// This shares the objectives dialog's translation of "Notes:"
ss << _("Notes:") << " " << get_command_flags_description(*c) << "\n";
}
const std::vector<std::string> l = get_aliases(cmd);
if (!l.empty()) {
// TRANSLATORS: alternative names for command-line commands, only shown if
// there is at least one of them.
ss << _n("command^Alias:", "Aliases:", l.size()) << " " << utils::join(l, " ") << "\n";
}
print(_("help"), ss.str());
}
return c != 0;
}
cmd_arg_parser cap_;
protected:
//this is display-only
static void set_cmd_prefix(const std::string& value)
{
cmd_prefix_ = value;
}
//sets the "cmd_flag_" flag as "true" when "cmd_prefix_" is in command
//line mode and to "false" when "cmd_prefix_" is in message line mode.
//The "help" message's symbols depend on the flag's value.
static void set_cmd_flag(bool value)
{
cmd_flag_ = value;
}
virtual void register_command(const std::string& cmd,
command_handler h, const std::string& help = "",
const std::string& usage = "", const std::string& flags = "")
{
command c = command(h, help, usage, flags);
std::pair<typename command_map::iterator, bool> r;
r = command_map_.insert(typename command_map::value_type(cmd, c));
if (!r.second) { //overwrite if exists
r.first->second = c;
}
}
virtual void register_alias(const std::string& to_cmd,
const std::string& cmd)
{
command_alias_map_[cmd] = to_cmd;
}
//get all aliases of a command.
static const std::vector<std::string> get_aliases(const std::string& cmd)
{
std::vector<std::string> aliases;
typedef command_alias_map::value_type p;
for (p i : command_alias_map_) {
if (i.second == cmd) {
aliases.push_back(i.first);
}
}
return aliases;
}
private:
static inline command_map command_map_ {};
static inline command_alias_map command_alias_map_ {};
static inline bool show_unavailable_ = false;
static inline std::string cmd_prefix_ {};
static inline bool cmd_flag_ = false;
};
}