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 | } |