/* 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" void Cmd_ForwardToServer(void); 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; } Con_SafePrintf(" %s\n", name.c_str()); count++; } Con_SafePrintf("%i commands", count); if (partial) { Con_SafePrintf(" beginning with \"%s\"", partial->c_str()); } Con_SafePrintf("\n"); } void _unalias() { switch (argc()) { default: case 1: Con_Printf("unalias : delete alias\n"); break; case 2: { const auto name = argv(1).value(); if (ALIASES.contains(name)) { ALIASES.erase(name); } else { Con_Printf("No alias named %s\n", name.c_str()); } break; } } } void _alias() { char cmd[1024]; switch (argc()) { case 1: { //list all aliases auto count = 0; for (auto &[name, alias]: command::ALIASES) { Con_SafePrintf(" %s: %s", name.c_str(), alias.value.c_str()); count++; } if (count != 0) Con_SafePrintf("%i alias command(s)\n", count); else Con_SafePrintf("no alias commands found\n"); break; } case 2: { //output current alias string const auto lookup = argv(1).value(); for (auto &[name, alias]: command::ALIASES) if (name.starts_with(lookup)) Con_Printf(" %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() >= command::MAX_ALIAS_LENGTH) { Con_Printf("Alias name is too long\n"); return; } std::optional new_alias = std::nullopt; // if the alias already exists, reuse it for (auto &[name, alias]: command::ALIASES) { if (name == new_name) { new_alias = alias; alias.value.clear(); break; } } if (!new_alias.has_value()) { new_alias = command::alias{}; } new_alias->name = new_name; // copy the rest of the command line cmd[0] = 0; // start out with a null string auto c = argc(); // TODO: ew for (auto i = 2; i < c; i++) { q_strlcat(cmd, argv(i).value().c_str(), sizeof(cmd)); if (i != c - 1) q_strlcat(cmd, " ", sizeof(cmd)); } if (q_strlcat(cmd, "\n", sizeof(cmd)) >= sizeof(cmd)) { Con_Printf("alias value too long!\n"); cmd[0] = '\n'; // nullify the string cmd[1] = 0; } new_alias->value = Z_Strdup(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, plus; 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; Cbuf_InsertText(cmds); } #include "default_cfg.hpp" void _exec() { const char *f; int mark; if (argc() != 2) { Con_Printf("exec : execute a script file\n"); return; } mark = Hunk_LowMark(); auto filename = argv(1); f = (const char *) COM_LoadHunkFile(filename->c_str(), NULL); if (!f && !strcmp(argv(1)->c_str(), "default.cfg")) { f = default_cfg; /* see above.. */ } if (!f) { Con_Printf("couldn't exec %s\n", argv(1)->c_str()); return; } Con_Printf("execing %s\n", argv(1)->c_str()); Cbuf_InsertText(f); if (f != default_cfg) { Hunk_FreeToLowMark(mark); } } void _echo() { for (auto i = 1; i < argc(); i++) Con_Printf("%s ", argv(i)->c_str()); Con_Printf("\n"); } char *tint_substring(const char *in, const char *substr, char *out, 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()) { Con_SafePrintf("%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++; Con_SafePrintf( "%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++; Con_SafePrintf("%s (current value: \"%s\")\n", tint_substring(var->name.c_str(), substr.value().c_str(), tmpbuf, sizeof(tmpbuf)), var->string); } } if (!hits) Con_SafePrintf("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", Cmd_ForwardToServer); 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()) { Con_Printf("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) { Con_Printf("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 (!q_strcasecmp(parm.c_str(), argv(i)->c_str())) 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 auto needle = argv(0).value(); // check functions for (const auto &[name, ref]: REFS) { if (!q_strcasecmp(needle.c_str(), name.c_str())) { ref.func(); return; } } // check alias for (const auto &[name, alias]: ALIASES) { if (!q_strcasecmp(argv(0).value().c_str(), name.c_str())) { Cbuf_InsertText(alias.value.c_str()); return; } } // check cvars if (!Cvar_Command()) Con_Printf("Unknown command \"%s\"\n", argv(0).value().c_str()); } void forward_to_server() { if (cls.state != ca_connected) { Con_Printf("Can't \"%s\", not connected\n", argv(0).value_or("").c_str()); return; } if (cls.demoplayback) return; // not really connected MSG_WriteByte(&cls.message, clc_stringcmd); if (q_strcasecmp(argv(0).value_or("").c_str(), "cmd") != 0) { SZ_Print(&cls.message, argv(0).value_or("").c_str()); SZ_Print(&cls.message, " "); } if (argc() > 1) SZ_Print(&cls.message, args().c_str()); else SZ_Print(&cls.message, "\n"); } definition_view get_commands() { return std::ranges::views::values(REFS); } alias_view get_aliases() { return std::ranges::views::values(ALIASES); } void print(std::string text) { } } //============================================================================= /* ============================================================================= COMMAND BUFFER ============================================================================= */ sizebuf_t cmd_text; /* ============ Cbuf_Init ============ */ void Cbuf_Init(void) { SZ_Alloc(&cmd_text, 1 << 18); // space for commands and script files. spike -- was 8192, but modern configs can be _HUGE_, at least if they contain lots of comments/docs for things. } /* ============ Cbuf_AddText Adds command text at the end of the buffer ============ */ void Cbuf_AddText(const char *text) { int l; l = Q_strlen(text); if (cmd_text.cursize + l >= cmd_text.maxsize) { Con_Printf("Cbuf_AddText: overflow\n"); return; } SZ_Write(&cmd_text, text, Q_strlen(text)); } /* ============ Cbuf_InsertText Adds command text immediately after the current command Adds a \n to the text FIXME: actually change the command buffer to do less copying ============ */ void Cbuf_InsertText(const char *text) { char *temp; int templen; // copy off any commands still remaining in the exec buffer templen = cmd_text.cursize; if (templen) { temp = (char *) Z_Malloc(templen); Q_memcpy(temp, cmd_text.data, templen); SZ_Clear(&cmd_text); } else temp = NULL; // shut up compiler // add the entire text of the file Cbuf_AddText(text); SZ_Write(&cmd_text, "\n", 1); // add the copied off data if (templen) { SZ_Write(&cmd_text, temp, templen); Z_Free(temp); } } /* ============ Cbuf_Execute ============ */ void Cbuf_Execute(void) { int i; char *text; char line[1024]; int quotes; while (cmd_text.cursize) { // find a \n or ; line break text = (char *) cmd_text.data; quotes = 0; for (i = 0; i < cmd_text.cursize; i++) { if (text[i] == '"') quotes++; if (!(quotes & 1) && text[i] == ';') break; // don't break if inside a quoted string if (text[i] == '\n') break; } if (i > (int) sizeof(line) - 1) { memcpy(line, text, sizeof(line) - 1); line[sizeof(line) - 1] = 0; } else { memcpy(line, text, i); line[i] = 0; } // 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 (i == cmd_text.cursize) cmd_text.cursize = 0; else { i++; cmd_text.cursize -= i; memmove(text, text + i, cmd_text.cursize); } // execute the command line command::execute_string(line, command::source::command); if (command::is_waiting()) { // skip out while text still remains in buffer, leaving it // for next frame command::stop_waiting(); break; } } } /* ============================================================================== SCRIPT COMMANDS ============================================================================== */ /* id1/pak0.pak from 2021 re-release doesn't have a default.cfg * embedding Quakespasm's customized default.cfg for that... */ /* ============================================================================= COMMAND EXECUTION ============================================================================= */ /* ============ Cmd_AddCommand ============ */ /* ============ Cmd_Exists ============ */ /* ============ Cmd_CompleteCommand ============ */ /* ============ Cmd_ExecuteString A complete command line has been parsed, so try to execute it FIXME: lookupnoadd the token to speed search? ============ */ /* =================== Cmd_ForwardToServer Sends the entire command line over to the server =================== */ void Cmd_ForwardToServer(void) { } /* ================ Cmd_CheckParm Returns the position (1 to argc-1) in the command's argument list where the given parameter apears, or 0 if not present ================ */ int Cmd_CheckParm(const char *parm) { }