Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 1 | pmbaty | 1 | /* |
| 2 | * This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>. |
||
| 3 | * It is copyright by its individual contributors, as recorded in the |
||
| 4 | * project's Git history. See COPYING.txt at the top level for license |
||
| 5 | * terms and a link to the Git history. |
||
| 6 | * |
||
| 7 | */ |
||
| 8 | /* |
||
| 9 | * |
||
| 10 | * Command parsing and processing |
||
| 11 | * |
||
| 12 | */ |
||
| 13 | |||
| 14 | #include <cstdarg> |
||
| 15 | #include <map> |
||
| 16 | #include <forward_list> |
||
| 17 | #include <stdio.h> |
||
| 18 | #include <stdlib.h> |
||
| 19 | #include <string.h> |
||
| 20 | #include <ctype.h> |
||
| 21 | |||
| 22 | #include "dxxerror.h" |
||
| 23 | #include "u_mem.h" |
||
| 24 | #include "strutil.h" |
||
| 25 | #include "inferno.h" |
||
| 26 | #include "console.h" |
||
| 27 | #include "cvar.h" |
||
| 28 | #include "physfsx.h" |
||
| 29 | |||
| 30 | #include "compiler-range_for.h" |
||
| 31 | #include <memory> |
||
| 32 | |||
| 33 | namespace { |
||
| 34 | |||
| 35 | struct cmd_t |
||
| 36 | { |
||
| 37 | const char *name; |
||
| 38 | cmd_handler_t function; |
||
| 39 | const char *help_text; |
||
| 40 | }; |
||
| 41 | |||
| 42 | /* The list of cmds */ |
||
| 43 | static std::map<const char *, std::unique_ptr<cmd_t>> cmd_list; |
||
| 44 | |||
| 45 | #define ALIAS_NAME_MAX 32 |
||
| 46 | struct cmd_alias_t |
||
| 47 | { |
||
| 48 | char name[ALIAS_NAME_MAX]; |
||
| 49 | RAIIdmem<char[]> value; |
||
| 50 | }; |
||
| 51 | |||
| 52 | } |
||
| 53 | |||
| 54 | #define CMD_MAX_ALIASES 1024 |
||
| 55 | |||
| 56 | /* The list of aliases */ |
||
| 57 | static std::map<std::string, std::unique_ptr<cmd_alias_t>> cmd_alias_list; |
||
| 58 | |||
| 59 | static cmd_t *cmd_findcommand(const char *cmd_name) |
||
| 60 | { |
||
| 61 | const auto i = cmd_list.find(cmd_name); |
||
| 62 | return i == cmd_list.end() ? nullptr : i->second.get(); |
||
| 63 | } |
||
| 64 | |||
| 65 | |||
| 66 | static cmd_alias_t *cmd_findalias(const char *alias_name ) |
||
| 67 | { |
||
| 68 | const auto i = cmd_alias_list.find(alias_name); |
||
| 69 | return i == cmd_alias_list.end() ? nullptr : i->second.get(); |
||
| 70 | } |
||
| 71 | |||
| 72 | |||
| 73 | /* add a new console command */ |
||
| 74 | void cmd_addcommand(const char *cmd_name, cmd_handler_t cmd_func, const char *cmd_help_text) |
||
| 75 | { |
||
| 76 | const auto i = cmd_list.insert(std::make_pair(cmd_name, std::unique_ptr<cmd_t>{})); |
||
| 77 | if (!i.second) |
||
| 78 | { |
||
| 79 | Int3(); |
||
| 80 | con_printf(CON_NORMAL, "command %s already exists, not adding", cmd_name); |
||
| 81 | return; |
||
| 82 | } |
||
| 83 | auto cmd = (i.first->second = std::make_unique<cmd_t>()).get(); |
||
| 84 | /* create command, insert to hashtable */ |
||
| 85 | cmd->name = cmd_name; |
||
| 86 | cmd->function = cmd_func; |
||
| 87 | cmd->help_text = cmd_help_text; |
||
| 88 | |||
| 89 | con_printf(CON_DEBUG, "cmd_addcommand: added %s", cmd->name); |
||
| 90 | } |
||
| 91 | |||
| 92 | namespace { |
||
| 93 | |||
| 94 | struct cmd_queue_t |
||
| 95 | { |
||
| 96 | RAIIdmem<char[]> command_line; |
||
| 97 | explicit cmd_queue_t(char *p) : |
||
| 98 | command_line(p) |
||
| 99 | { |
||
| 100 | } |
||
| 101 | }; |
||
| 102 | |||
| 103 | } |
||
| 104 | |||
| 105 | /* The list of commands to be executed */ |
||
| 106 | static std::forward_list<cmd_queue_t> cmd_queue; |
||
| 107 | |||
| 108 | /* execute a parsed command */ |
||
| 109 | static void cmd_execute(unsigned long argc, const char *const *const argv) |
||
| 110 | { |
||
| 111 | cmd_t *cmd; |
||
| 112 | cmd_alias_t *alias; |
||
| 113 | |||
| 114 | if ( (cmd = cmd_findcommand(argv[0])) ) |
||
| 115 | { |
||
| 116 | con_printf(CON_DEBUG, "cmd_execute: executing %s", argv[0]); |
||
| 117 | cmd->function(argc, argv); |
||
| 118 | return; |
||
| 119 | } |
||
| 120 | |||
| 121 | if ( (alias = cmd_findalias(argv[0])) && alias->value ) |
||
| 122 | { |
||
| 123 | con_printf(CON_DEBUG, "cmd_execute: pushing alias \"%s\": %s", alias->name, alias->value.get()); |
||
| 124 | cmd_insert(alias->value.get()); |
||
| 125 | return; |
||
| 126 | } |
||
| 127 | |||
| 128 | /* Otherwise */ |
||
| 129 | if (argc < 31) |
||
| 130 | { // set value of cvar |
||
| 131 | const char *new_argv[32]; |
||
| 132 | int i; |
||
| 133 | |||
| 134 | new_argv[0] = "set"; |
||
| 135 | for (i = 0; i < argc; i++) |
||
| 136 | new_argv[i+1] = argv[i]; |
||
| 137 | cvar_cmd_set(argc + 1, new_argv); |
||
| 138 | } |
||
| 139 | } |
||
| 140 | |||
| 141 | |||
| 142 | /* Parse an input string */ |
||
| 143 | static void cmd_parse(char *input) |
||
| 144 | { |
||
| 145 | char buffer[CMD_MAX_LENGTH]; |
||
| 146 | char *tokens[CMD_MAX_TOKENS]; |
||
| 147 | int num_tokens; |
||
| 148 | uint_fast32_t i, l; |
||
| 149 | |||
| 150 | Assert(input != NULL); |
||
| 151 | |||
| 152 | /* Strip leading spaces */ |
||
| 153 | while( isspace(*input) ) { ++input; } |
||
| 154 | buffer[sizeof(buffer) - 1] = 0; |
||
| 155 | strncpy(buffer, input, sizeof(buffer) - 1); |
||
| 156 | |||
| 157 | //printf("lead strip \"%s\"\n",buffer); |
||
| 158 | l = strlen(buffer); |
||
| 159 | /* If command is empty, give up */ |
||
| 160 | if (l==0) return; |
||
| 161 | |||
| 162 | /* Strip trailing spaces */ |
||
| 163 | for (i=l-1; i>0 && isspace(buffer[i]); i--) ; |
||
| 164 | buffer[i+1] = 0; |
||
| 165 | //printf("trail strip \"%s\"\n",buffer); |
||
| 166 | |||
| 167 | /* Split into tokens */ |
||
| 168 | l = strlen(buffer); |
||
| 169 | num_tokens = 1; |
||
| 170 | |||
| 171 | tokens[0] = buffer; |
||
| 172 | for (i=1; i<l; i++) { |
||
| 173 | if (buffer[i] == '"') { |
||
| 174 | tokens[num_tokens - 1] = &buffer[++i]; |
||
| 175 | while (i < l && buffer[i] != '"') |
||
| 176 | i++; |
||
| 177 | buffer[i] = 0; |
||
| 178 | continue; |
||
| 179 | } |
||
| 180 | if (isspace(buffer[i]) || buffer[i] == '=') { |
||
| 181 | buffer[i] = 0; |
||
| 182 | while (isspace(buffer[i+1]) && (i+1 < l)) i++; |
||
| 183 | tokens[num_tokens++] = &buffer[i+1]; |
||
| 184 | } |
||
| 185 | } |
||
| 186 | |||
| 187 | /* Check for matching commands */ |
||
| 188 | cmd_execute(num_tokens, tokens); |
||
| 189 | } |
||
| 190 | |||
| 191 | |||
| 192 | static int cmd_queue_wait; |
||
| 193 | |||
| 194 | int cmd_queue_process(void) |
||
| 195 | { |
||
| 196 | for (;;) |
||
| 197 | { |
||
| 198 | if (cmd_queue_wait) |
||
| 199 | break; |
||
| 200 | auto cmd = cmd_queue.begin(); |
||
| 201 | if (cmd == cmd_queue.end()) |
||
| 202 | break; |
||
| 203 | auto command_line = std::move(cmd->command_line); |
||
| 204 | cmd_queue.pop_front(); |
||
| 205 | con_printf(CON_DEBUG, "cmd_queue_process: processing %s", command_line.get()); |
||
| 206 | cmd_parse(command_line.get()); // Note, this may change the queue |
||
| 207 | } |
||
| 208 | |||
| 209 | if (cmd_queue_wait > 0) { |
||
| 210 | cmd_queue_wait--; |
||
| 211 | con_puts(CON_DEBUG, "cmd_queue_process: waiting"); |
||
| 212 | return 1; |
||
| 213 | } |
||
| 214 | |||
| 215 | return 0; |
||
| 216 | } |
||
| 217 | |||
| 218 | |||
| 219 | /* execute until there are no commands left */ |
||
| 220 | void cmd_queue_flush(void) |
||
| 221 | { |
||
| 222 | while (cmd_queue_process()) { |
||
| 223 | } |
||
| 224 | } |
||
| 225 | |||
| 226 | |||
| 227 | /* Add some commands to the queue to be executed */ |
||
| 228 | void cmd_enqueue(int insert, const char *input) |
||
| 229 | { |
||
| 230 | std::forward_list<cmd_queue_t> l; |
||
| 231 | char output[CMD_MAX_LENGTH]; |
||
| 232 | char *optr; |
||
| 233 | auto before_end = [](std::forward_list<cmd_queue_t> &f) { |
||
| 234 | for (auto i = f.before_begin();;) |
||
| 235 | { |
||
| 236 | auto j = i; |
||
| 237 | if (++ i == f.end()) |
||
| 238 | return j; |
||
| 239 | } |
||
| 240 | }; |
||
| 241 | auto iter = before_end(l); |
||
| 242 | while (*input) { |
||
| 243 | optr = output; |
||
| 244 | int quoted = 0; |
||
| 245 | |||
| 246 | /* Strip leading spaces */ |
||
| 247 | while(isspace(*input) || *input == ';') |
||
| 248 | input++; |
||
| 249 | |||
| 250 | /* If command is empty, give up */ |
||
| 251 | if (! *input) |
||
| 252 | continue; |
||
| 253 | |||
| 254 | /* Find the end of this line (\n, ;, or nul) */ |
||
| 255 | do { |
||
| 256 | if (!*input) |
||
| 257 | break; |
||
| 258 | if (*input == '"') { |
||
| 259 | quoted = 1 - quoted; |
||
| 260 | continue; |
||
| 261 | } else if ( *input == '\n' || (!quoted && *input == ';') ) { |
||
| 262 | input++; |
||
| 263 | break; |
||
| 264 | } |
||
| 265 | } while ((*optr++ = *input++)); |
||
| 266 | *optr = 0; |
||
| 267 | |||
| 268 | /* make a new queue item, add it to list */ |
||
| 269 | iter = l.emplace_after(iter, cmd_queue_t{d_strdup(output)}); |
||
| 270 | con_printf(CON_DEBUG, "cmd_enqueue: adding %s", output); |
||
| 271 | } |
||
| 272 | auto after = insert |
||
| 273 | /* add our list to the head of the main list */ |
||
| 274 | ? (con_puts(CON_DEBUG, "cmd_enqueue: added to front of list"), cmd_queue.before_begin()) |
||
| 275 | /* add our list to the tail of the main list */ |
||
| 276 | : (con_puts(CON_DEBUG, "cmd_enqueue: added to back of list"), before_end(cmd_queue)) |
||
| 277 | ; |
||
| 278 | cmd_queue.splice_after(after, std::move(l)); |
||
| 279 | } |
||
| 280 | |||
| 281 | void cmd_enqueuef(int insert, const char *fmt, ...) |
||
| 282 | { |
||
| 283 | va_list arglist; |
||
| 284 | char buf[CMD_MAX_LENGTH]; |
||
| 285 | |||
| 286 | va_start (arglist, fmt); |
||
| 287 | vsnprintf(buf, sizeof(buf), fmt, arglist); |
||
| 288 | va_end (arglist); |
||
| 289 | |||
| 290 | cmd_enqueue(insert, buf); |
||
| 291 | } |
||
| 292 | |||
| 293 | |||
| 294 | /* Attempt to autocomplete an input string */ |
||
| 295 | const char *cmd_complete(const char *input) |
||
| 296 | { |
||
| 297 | uint_fast32_t len = strlen(input); |
||
| 298 | |||
| 299 | if (!len) |
||
| 300 | return NULL; |
||
| 301 | |||
| 302 | range_for (const auto &i, cmd_list) |
||
| 303 | if (!d_strnicmp(input, i.first, len)) |
||
| 304 | return i.first; |
||
| 305 | |||
| 306 | range_for (const auto &i, cmd_alias_list) |
||
| 307 | if (!d_strnicmp(input, i.first.c_str(), len)) |
||
| 308 | return i.first.c_str(); |
||
| 309 | |||
| 310 | return cvar_complete(input); |
||
| 311 | } |
||
| 312 | |||
| 313 | |||
| 314 | /* alias */ |
||
| 315 | static void cmd_alias(unsigned long argc, const char *const *const argv) |
||
| 316 | { |
||
| 317 | char buf[CMD_MAX_LENGTH] = ""; |
||
| 318 | if (argc < 2) { |
||
| 319 | con_puts(CON_NORMAL, "aliases:"); |
||
| 320 | range_for (const auto &i, cmd_alias_list) |
||
| 321 | con_printf(CON_NORMAL, "%s: %s", i.first.c_str(), i.second->value.get()); |
||
| 322 | return; |
||
| 323 | } |
||
| 324 | |||
| 325 | if (argc == 2) { |
||
| 326 | cmd_alias_t *alias; |
||
| 327 | if ( (alias = cmd_findalias(argv[1])) && alias->value ) |
||
| 328 | { |
||
| 329 | con_printf(CON_NORMAL, "%s: %s", alias->name, alias->value.get()); |
||
| 330 | return; |
||
| 331 | } |
||
| 332 | |||
| 333 | con_printf(CON_NORMAL, "alias: %s not found", argv[1]); |
||
| 334 | return; |
||
| 335 | } |
||
| 336 | |||
| 337 | for (int i = 2; i < argc; i++) { |
||
| 338 | if (i > 2) |
||
| 339 | strncat(buf, " ", sizeof(buf) - strlen(buf) - 1); |
||
| 340 | strncat(buf, argv[i], sizeof(buf) - strlen(buf) - 1); |
||
| 341 | } |
||
| 342 | const auto i = cmd_alias_list.insert(std::make_pair(argv[1], std::unique_ptr<cmd_alias_t>{})); |
||
| 343 | auto alias = i.first->second.get(); |
||
| 344 | if (i.second) |
||
| 345 | { |
||
| 346 | alias = (i.first->second = std::make_unique<cmd_alias_t>()).get(); |
||
| 347 | alias->name[sizeof(alias->name) - 1] = 0; |
||
| 348 | strncpy(alias->name, argv[1], sizeof(alias->name) - 1); |
||
| 349 | } |
||
| 350 | alias->value.reset(d_strdup(buf)); |
||
| 351 | } |
||
| 352 | |||
| 353 | |||
| 354 | /* unalias */ |
||
| 355 | static void cmd_unalias(unsigned long argc, const char *const *const argv) |
||
| 356 | { |
||
| 357 | if (argc < 2 || argc > 2) { |
||
| 358 | cmd_insertf("help %s", argv[0]); |
||
| 359 | return; |
||
| 360 | } |
||
| 361 | |||
| 362 | const char *alias_name = argv[1]; |
||
| 363 | const auto alias = cmd_alias_list.find(alias_name); |
||
| 364 | if (alias == cmd_alias_list.end()) |
||
| 365 | { |
||
| 366 | con_printf(CON_NORMAL, "unalias: %s not found", alias_name); |
||
| 367 | return; |
||
| 368 | } |
||
| 369 | cmd_alias_list.erase(alias); |
||
| 370 | } |
||
| 371 | |||
| 372 | /* echo to console */ |
||
| 373 | static void cmd_echo(unsigned long argc, const char *const *const argv) |
||
| 374 | { |
||
| 375 | char buf[CMD_MAX_LENGTH] = ""; |
||
| 376 | int i; |
||
| 377 | |||
| 378 | for (i = 1; i < argc; i++) { |
||
| 379 | if (i > 1) |
||
| 380 | strncat(buf, " ", sizeof(buf) - strlen(buf) - 1); |
||
| 381 | strncat(buf, argv[i], sizeof(buf) - strlen(buf) - 1); |
||
| 382 | } |
||
| 383 | con_puts(CON_NORMAL, buf); |
||
| 384 | } |
||
| 385 | |||
| 386 | /* execute script */ |
||
| 387 | static void cmd_exec(unsigned long argc, const char *const *const argv) |
||
| 388 | { |
||
| 389 | PHYSFSX_gets_line_t<CMD_MAX_LENGTH> line; |
||
| 390 | |||
| 391 | if (argc < 2 || argc > 2) { |
||
| 392 | cmd_insertf("help %s", argv[0]); |
||
| 393 | return; |
||
| 394 | } |
||
| 395 | auto f = PHYSFSX_openReadBuffered(argv[1]); |
||
| 396 | if (!f) { |
||
| 397 | con_printf(CON_CRITICAL, "exec: %s not found", argv[1]); |
||
| 398 | return; |
||
| 399 | } |
||
| 400 | std::forward_list<cmd_queue_t> l; |
||
| 401 | auto i = l.before_begin(); |
||
| 402 | while (PHYSFSX_fgets(line, f)) { |
||
| 403 | /* make a new queue item, add it to list */ |
||
| 404 | i = l.emplace_after(i, cmd_queue_t{d_strdup(line)}); |
||
| 405 | con_printf(CON_DEBUG, "cmd_exec: adding %s", static_cast<const char *>(line)); |
||
| 406 | } |
||
| 407 | |||
| 408 | /* add our list to the head of the main list */ |
||
| 409 | con_puts(CON_DEBUG, "cmd_exec: added to front of list"); |
||
| 410 | cmd_queue.splice_after(cmd_queue.before_begin(), std::move(l)); |
||
| 411 | } |
||
| 412 | |||
| 413 | /* get help */ |
||
| 414 | static void cmd_help(unsigned long argc, const char *const *const argv) |
||
| 415 | { |
||
| 416 | cmd_t *cmd; |
||
| 417 | |||
| 418 | if (argc > 2) { |
||
| 419 | cmd_insertf("help %s", argv[0]); |
||
| 420 | return; |
||
| 421 | } |
||
| 422 | |||
| 423 | if (argc < 2) { |
||
| 424 | con_puts(CON_NORMAL, "Available commands:"); |
||
| 425 | range_for (const auto &i, cmd_list) |
||
| 426 | con_printf(CON_NORMAL, " %s", i.first); |
||
| 427 | |||
| 428 | return; |
||
| 429 | } |
||
| 430 | |||
| 431 | cmd = cmd_findcommand(argv[1]); |
||
| 432 | |||
| 433 | if (!cmd) { |
||
| 434 | con_printf(CON_URGENT, "Command %s not found", argv[1]); |
||
| 435 | return; |
||
| 436 | } |
||
| 437 | |||
| 438 | if (!cmd->help_text) { |
||
| 439 | con_printf(CON_NORMAL, "%s: no help found", argv[1]); |
||
| 440 | return; |
||
| 441 | } |
||
| 442 | |||
| 443 | con_puts(CON_NORMAL, cmd->help_text); |
||
| 444 | } |
||
| 445 | |||
| 446 | /* execute script */ |
||
| 447 | static void cmd_wait(unsigned long argc, const char *const *const argv) |
||
| 448 | { |
||
| 449 | if (argc > 2) { |
||
| 450 | cmd_insertf("help %s", argv[0]); |
||
| 451 | return; |
||
| 452 | } |
||
| 453 | |||
| 454 | if (argc < 2) |
||
| 455 | cmd_queue_wait = 1; |
||
| 456 | else |
||
| 457 | cmd_queue_wait = atoi(argv[1]); |
||
| 458 | } |
||
| 459 | |||
| 460 | void cmd_init(void) |
||
| 461 | { |
||
| 462 | cmd_addcommand("alias", cmd_alias, "alias <name> <commands>\n" " define <name> as an alias for <commands>\n" |
||
| 463 | "alias <name>\n" " show the current definition of <name>\n" |
||
| 464 | "alias\n" " show all defined aliases"); |
||
| 465 | cmd_addcommand("unalias", cmd_unalias, "unalias <name>\n" " undefine the alias <name>"); |
||
| 466 | cmd_addcommand("echo", cmd_echo, "echo [text]\n" " write <text> to the console"); |
||
| 467 | cmd_addcommand("exec", cmd_exec, "exec <file>\n" " execute <file>"); |
||
| 468 | cmd_addcommand("help", cmd_help, "help [command]\n" " get help for <command>, or list all commands if not specified."); |
||
| 469 | cmd_addcommand("wait", cmd_wait, "usage: wait [n]\n" " stop processing commands, resume in <n> cycles (default 1)"); |
||
| 470 | } |