Files
quakespasm/Quake/cmd.cpp
T

550 lines
17 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"
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;
}
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 <name> : 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<alias> 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 <filename> : execute a script file\n");
return;
}
const auto mark = Hunk_LowMark();
const auto filename = argv(1);
auto f = reinterpret_cast<const char *>(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 <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++;
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<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()) {
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<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 (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_eq(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 {
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;
}
}
}
}