/* Copyright (C) 1996-2001 Id Software, Inc. Copyright (C) 2002-2009 John Fitzgibbons and others Copyright (C) 2007-2008 Kristian Duske Copyright (C) 2010-2014 QuakeSpasm developers 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, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // cmd.c -- Quake script command processing module #include #include #include #include #include "quakedef.hpp" constexpr int CMDLINE_LENGTH = 256; constexpr int MAX_ARGS = 80; extern convar cmdline; namespace command { source last_source; namespace { constexpr int MAX_ALIAS_LENGTH = 32; bool waiting{false}; std::map > REFS{}; std::map > ALIASES{}; std::vector _argv{}; std::string _args{}; /** * Causes execution of the remainder of the command buffer to be delayed until * next frame. This allows commands like: * * ``` * bind g "impulse 5 ; +attack ; wait ; -attack ; impulse 2" * ``` */ void _set_wait() { waiting = true; } void stop_waiting() { waiting = false; } bool is_waiting() { return waiting; } void _list() { std::optional partial{std::nullopt}; if (argc() > 1) { partial = argv(1); } auto count = 0; for (auto &[name, ref]: std::ranges::views::values(REFS)) { if (partial.has_value() && !name.starts_with(partial.value())) { continue; } console::safe_print(" %s\n", name.c_str()); count++; } console::safe_print("%i commands", count); if (partial) { console::safe_print(" beginning with \"%s\"", partial->c_str()); } console::safe_print("\n"); } void _unalias() { switch (argc()) { default: case 1: console::info("unalias : delete alias\n"); break; case 2: { const auto name = argv(1).value(); if (ALIASES.contains(name)) { ALIASES.erase(name); } else { console::info("No alias named %s\n", name.c_str()); } break; } } } void _alias() { switch (argc()) { case 1: { //list all aliases auto count = 0; for (auto &[name, alias]: ALIASES) { console::safe_print(" %s: %s", name.c_str(), alias.value.c_str()); count++; } if (count != 0) console::safe_print("%i alias command(s)\n", count); else console::safe_print("no alias commands found\n"); break; } case 2: { //output current alias string const auto lookup = argv(1).value(); for (auto &[name, alias]: ALIASES) if (name.starts_with(lookup)) console::info(" %s: %s", name.c_str(), alias.value.c_str()); break; } default: { //set alias string const auto new_name = argv(1).value(); if (new_name.length() >= MAX_ALIAS_LENGTH) { console::info("Alias name is too long\n"); return; } std::optional new_alias = std::nullopt; // if the alias already exists, reuse it for (auto &[name, alias]: ALIASES) { if (name == new_name) { new_alias = alias; alias.value.clear(); break; } } if (!new_alias.has_value()) { new_alias = alias{}; } new_alias->name = new_name; // copy the rest of the command line auto cmd = std::string{}; const auto count = argc(); for (auto i = 2; i < count; i++) { cmd += argv(i).value(); if (i != count - 1) { cmd += ' '; } } cmd += '\n'; new_alias->value = cmd; break; } } } void _unalias_all() { ALIASES.clear(); } /* =============== johnfitz -- rewritten to read the "cmdline" cvar, for use with dynamic mod loading Adds command line parameters as script statements Commands lead with a +, and continue until a - or another + quake +prog jctest.qp +cmd amlev1 quake -nosound +cmd amlev1 =============== */ void _stuffcmds() { char cmds[CMDLINE_LENGTH]; int i, j; auto plus = false; // On Unix, argv[0] is command name for (i = 0, j = 0; cmdline.string[i]; i++) { if (cmdline.string[i] == '+') { plus = true; if (j > 0) { cmds[j - 1] = ';'; cmds[j++] = ' '; } } else if (cmdline.string[i] == '-' && (i == 0 || cmdline.string[i - 1] == ' ')) //johnfitz -- allow hypenated map names with +map plus = false; else if (plus) cmds[j++] = cmdline.string[i]; } cmds[j] = 0; buffer::insert_text(cmds); } #include "default_cfg.hpp" void _exec() { if (argc() != 2) { console::info("exec : execute a script file\n"); return; } const auto mark = Hunk_LowMark(); const auto filename = argv(1); auto f = reinterpret_cast(COM_LoadHunkFile(filename->c_str(), nullptr)); if (f == nullptr && argv(1) == "default.cfg") { f = default_cfg; /* see above.. */ } if (f == nullptr) { console::info("couldn't exec %s\n", argv(1)->c_str()); return; } console::info("execing %s\n", argv(1)->c_str()); buffer::insert_text(f); if (f != default_cfg) { Hunk_FreeToLowMark(mark); } } void _echo() { for (auto i = 1; i < argc(); i++) console::info("%s ", argv(i)->c_str()); console::info("\n"); } char *tint_substring(const char *in, const char *substr, char *out, const size_t outsize) { int l; char *m; q_strlcpy(out, in, outsize); while ((m = q_strcasestr(out, substr))) { l = strlen(substr); while (l-- > 0) if (*m >= ' ' && *m < 127) *m++ |= 0x80; } return out; } /* ============ scans through each command and cvar names+descriptions for the given substring we don't support descriptions, so this isn't really all that useful, but even without the sake of consistency it still combines cvars+commands under a single command. ============ */ void _apropos() { char tmpbuf[256]; int hits = 0; const auto substr = argv(1); if (!substr.has_value()) { console::safe_print("%s : search through commands and cvars for the given substring\n", argv(0).value().c_str()); return; } for (const auto &name: REFS | std::views::keys) { if (q_strcasestr(name.c_str(), substr.value().c_str())) { hits++; console::safe_print( "%s\n", tint_substring(name.c_str(), substr.value().c_str(), tmpbuf, sizeof(tmpbuf))); } } for (const auto &var: convar::get_variables()) { if (q_strcasestr(var->name.c_str(), substr.value().c_str())) { hits++; console::safe_print("%s (current value: \"%s\")\n", tint_substring(var->name.c_str(), substr.value().c_str(), tmpbuf, sizeof(tmpbuf)), var->string); } } if (!hits) console::safe_print("no cvars nor commands contain that substring\n"); } } void init() { add("cmdlist", _list); //johnfitz add("unalias", _unalias); //johnfitz add("unaliasall", _unalias_all); //johnfitz add("stuffcmds", _stuffcmds); add("exec", _exec); add("echo", _echo); add("alias", _alias); add("cmd", forward_to_server); add("wait", _set_wait); add("apropos", _apropos); add("find", _apropos); } std::optional complete(const std::string &partial) { if (partial.empty()) return std::nullopt; for (const auto &name: REFS | std::views::keys) { if (name.compare(0, partial.length(), partial) == 0) return name; } return std::nullopt; } void add(const std::string &name, const xcommand_t cmd) { if (host_initialized) // because hunk allocation would get stomped Sys_Error("Cmd_AddCommand after host_initialized"); // fail if the command is a variable name if (convar::variable_string(name).has_value()) { console::info("Cmd_AddCommand: %s already defined as a var\n", name.c_str()); return; } // fail if the command already exists for (auto &[other_name, func]: std::ranges::views::values(REFS)) { if (name == other_name) { console::info("Cmd_AddCommand: %s already defined\n", name.c_str()); return; } } REFS.emplace(name, definition{name, cmd}); } bool exists(const std::string_view lookup) { return std::ranges::any_of(REFS.begin(), REFS.end(), [lookup](const auto &pair) { return pair.first == lookup; }); } int argc() { return static_cast(_argv.size()); } std::optional argv(const int arg) { if (arg < 0 || arg >= _argv.size()) { return std::nullopt; } return _argv[arg]; } std::string args() { return _args; } int check_parm(const std::string &parm) { if (parm.empty()) Sys_Error("Cmd_CheckParm: null input\n"); for (auto i = 1; i < argc(); i++) if (std::is_eq(common::strcasecmp(parm, *argv(i)))) return i; return 0; } /** * Parses the given string into command line tokens. * @param text */ void tokenize_string(const std::string &text) { _argv.clear(); _args = std::string{}; std::istringstream ss{text}; while (true) { while (!ss.eof() && ss.peek() <= ' ' && ss.peek() != '\n') { ss.get(); } if (ss.peek() == '\n') break; if (ss.eof()) return; if (_argv.size() == 1) _args = ss.str().substr(ss.tellg()); if (_argv.size() >= MAX_ARGS) { return; } auto token = common::parse_token(ss); if (!token.has_value()) { return; } _argv.emplace_back(token.value()); } } void execute_string(const std::string &text, const source src) { last_source = src; tokenize_string(text); // execute the command line if (argc() == 0) return; // no tokens const auto needle = argv(0).value(); // check functions for (const auto &[name, ref]: REFS) { if (std::is_eq(common::strcasecmp(needle, name.c_str()))) { ref.func(); return; } } // check alias for (const auto &[name, alias]: ALIASES) { if (std::is_eq(common::strcasecmp(argv(0).value().c_str(), name.c_str()))) { buffer::insert_text(alias.value); return; } } // check cvars if (!Cvar_Command()) console::info("Unknown command \"%s\"\n", argv(0).value().c_str()); } void forward_to_server() { if (cls.state != ca_connected) { console::info("Can't \"%s\", not connected\n", argv(0).value_or("").c_str()); return; } if (cls.demoplayback) return; // not really connected cls.message.write_byte(clc_stringcmd); if (std::is_neq(common::strcasecmp(argv(0).value_or(""), "cmd"))) { if (argc() > 1) { cls.message.write_string(std::format("{} {}", argv(0).value(), args())); } else { cls.message.write_string(std::format("{} \n", argv(0).value())); } } else { if (argc() > 1) { cls.message.write_string(std::format("{}", args())); } else { cls.message.write_string("\n"); } } } definition_view get_commands() { return std::ranges::views::values(REFS); } alias_view get_aliases() { return std::ranges::views::values(ALIASES); } } //============================================================================= /* ============================================================================= COMMAND BUFFER ============================================================================= */ namespace command::buffer { namespace { constexpr std::size_t MAX_BUFFER_SIZE = 1 << 18; std::string _buffer; } void init() { _buffer = std::string{}; _buffer.reserve(MAX_BUFFER_SIZE); } void add_text(const std::string &text) { if (_buffer.length() + text.length() >= MAX_BUFFER_SIZE) { console::info("command::buffer::add_text(): overflow\n"); return; } _buffer.append(text); } void insert_text(const std::string &text) { if (_buffer.length() + text.length() >= MAX_BUFFER_SIZE) { console::info("command::buffer::insert_text(): overflow\n"); return; } _buffer = text + _buffer; } void execute() { while (!_buffer.empty()) { std::string line{}; auto quotes = 0; for (const auto c: _buffer) { if (c == '"') { quotes++; } if (!(quotes & 1) && c == ';') { break; // don't break if inside a quoted string } if (c == '\n') { break; } line += c; } // delete the text from the command buffer and move remaining commands down // this is necessary because commands (exec, alias) can insert data at the // beginning of the text buffer if (line.length() + 1 == _buffer.length()) { _buffer.clear(); } else { _buffer = _buffer.substr(line.length() + 1); } // execute the command line execute_string(line, source::command); if (is_waiting()) { // skip out while text still remains in buffer, leaving it // for next frame stop_waiting(); break; } } } }