691 lines
19 KiB
C++
691 lines
19 KiB
C++
/*
|
|
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 <algorithm>
|
|
#include <cstring>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
#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<std::string, definition, std::less<> > REFS{};
|
|
std::map<std::string, alias, std::less<> > ALIASES{};
|
|
std::vector<std::string> _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<std::string> 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 <name> : 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<command::alias> 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 <filename> : 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 <substring> : 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<std::string> 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<int>(_argv.size());
|
|
}
|
|
|
|
std::optional<std::string> 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) {
|
|
}
|