1 /*
2 * Crossfire -- cooperative multi-player graphical RPG and adventure game
3 *
4 * Copyright (c) 1999-2013 Mark Wedel and the Crossfire Development Team
5 * Copyright (c) 1992 Frank Tore Johansen
6 *
7 * Crossfire is free software and comes with ABSOLUTELY NO WARRANTY. You are
8 * welcome to redistribute it under certain conditions. For details, please
9 * see COPYING and LICENSE.
10 *
11 * The authors can be reached via e-mail at <crossfire@metalforge.org>.
12 */
13
14 /**
15 * @file
16 * Contains a lot about the commands typed into the client.
17 */
18
19 #include <ctype.h>
20
21 #include "client.h"
22 #include "external.h"
23 #include "p_cmd.h"
24 #include "script.h"
25
26 /**
27 * @defgroup PCmdHelpCommands Common client player commands.
28 * @{
29 */
30
31 #define H1(a) draw_ext_info(NDI_BLACK, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE, a)
32 #define H2(a) draw_ext_info(NDI_BLACK, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE, a)
33 #define LINE(a) draw_ext_info(NDI_BLACK, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE, a)
34
35 /* TODO Help topics other than commands? Refer to other documents? */
36
37 static int get_num_commands(void);
38
do_clienthelp_list()39 static void do_clienthelp_list() {
40 ConsoleCommand **sorted_cmds = get_cat_sorted_commands();
41 CommCat category = COMM_CAT_MISC;
42 GString *line = g_string_new(NULL);
43
44 H1("Client commands:");
45 for (int i = 0; i < get_num_commands(); i++) {
46 ConsoleCommand *cmd = sorted_cmds[i];
47 if (cmd->cat != category) {
48 // If moving on to next category, dump line_buf and print header.
49 char buf[MAX_BUF];
50 snprintf(buf, sizeof(buf), "%s commands:",
51 get_category_name(cmd->cat));
52 LINE(line->str);
53 H2(buf);
54
55 category = cmd->cat;
56 g_string_free(line, true);
57 line = g_string_new(NULL);
58 }
59 g_string_append_printf(line, "%s ", cmd->name);
60 }
61
62 LINE(line->str);
63 g_string_free(line, true);
64 }
65
show_help(const ConsoleCommand * cc)66 static void show_help(const ConsoleCommand *cc) {
67 char buf[MAX_BUF];
68 if (cc->desc != NULL) {
69 snprintf(buf, MAX_BUF - 1, "%s - %s", cc->name, cc->desc);
70 } else {
71 snprintf(buf, MAX_BUF - 1, "Help for '%s':", cc->name);
72 }
73 H2(buf);
74
75 if (cc->helpfunc != NULL) {
76 const char *long_help = NULL;
77 long_help = cc->helpfunc();
78
79 if (long_help != NULL) {
80 LINE(long_help);
81 } else {
82 LINE("Extended help for this command is broken.");
83 }
84 } else {
85 LINE("No extended help is available for this command.");
86 }
87 }
88
command_help(const char * cpnext)89 static void command_help(const char *cpnext) {
90 if (cpnext) {
91 const ConsoleCommand * cc;
92 char buf[MAX_BUF];
93
94 cc = find_command(cpnext);
95 if (cc != NULL) {
96 show_help(cc);
97 } else {
98 snprintf(buf, sizeof(buf), "help %s", cpnext);
99 /* maybe not a must send, but we probably don't want to drop it */
100 send_command(buf, -1, 1);
101 }
102 } else {
103 do_clienthelp_list();
104 /* Now fetch (in theory) command list from the server.
105 TODO Protocol command - feed it to the tab completer.
106
107 Nope! It effectivey fetches '/help commands for commands'.
108 */
109 send_command("help", -1, 1); /* TODO make install in server branch doesn't install def_help. */
110 }
111 }
112
help_help(void)113 static const char * help_help(void) {
114 return
115 "Syntax:\n"
116 "\n"
117 " help\n"
118 " help <topic>\n"
119 "\n"
120 "Without any arguments, displays a list of client-side "
121 "commands, and fetches the without-arguments help from "
122 "the server.\n"
123 "\n"
124 "With arguments, first checks if there's a client command "
125 "named <topic>. If there is, display it's help. If there "
126 "isn't, send the topic to the server.";
127 }
128
129 /**
130 * @} */ /* EndOf PCmdHelpCommands
131 */
132
set_command_window(const char * cpnext)133 static void set_command_window(const char *cpnext) {
134 if (!cpnext) {
135 draw_ext_info(NDI_BLACK, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE,
136 "cwindow command requires a number parameter");
137 } else {
138 want_config[CONFIG_CWINDOW] = atoi(cpnext);
139 if (want_config[CONFIG_CWINDOW]<1 || want_config[CONFIG_CWINDOW]>127) {
140 want_config[CONFIG_CWINDOW]=COMMAND_WINDOW;
141 } else {
142 use_config[CONFIG_CWINDOW] = want_config[CONFIG_CWINDOW];
143 }
144 }
145 }
146
command_foodbeep()147 static void command_foodbeep() {
148 if (want_config[CONFIG_FOODBEEP]) {
149 want_config[CONFIG_FOODBEEP] = 0;
150 draw_ext_info(NDI_BLACK, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE,
151 "Warning bell when low on food disabled");
152 } else {
153 want_config[CONFIG_FOODBEEP] = 1;
154 draw_ext_info(NDI_BLACK, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE,
155 "Warning bell when low on food enabled");
156 }
157 use_config[CONFIG_FOODBEEP] = want_config[CONFIG_FOODBEEP];
158 }
159
get_category_name(CommCat cat)160 const char * get_category_name(CommCat cat) {
161 const char * cat_name;
162
163 /* HACK Need to keep this in sync. with player.h */
164 switch(cat) {
165 case COMM_CAT_MISC:
166 cat_name = "Miscellaneous";
167 break;
168 case COMM_CAT_INFO:
169 cat_name = "Informational";
170 break;
171 case COMM_CAT_SETUP:
172 cat_name = "Configuration";
173 break;
174 case COMM_CAT_SCRIPT:
175 cat_name = "Scripting";
176 break;
177 case COMM_CAT_DEBUG:
178 cat_name = "Debugging";
179 break;
180 default:
181 cat_name = "PROGRAMMER ERROR";
182 break;
183 }
184
185 return cat_name;
186 }
187
188 /*
189 * Command table.
190 *
191 * Implementation basically stolen verbatim from the server.
192 */
193
do_script_list()194 static void do_script_list() { script_list(); }
195
do_clearinfo()196 static void do_clearinfo() { menu_clear(); }
197
do_disconnect()198 static void do_disconnect() { client_disconnect(); }
199
200
do_inv()201 static void do_inv() { print_inventory(cpl.ob); }
202
do_magicmap()203 static void do_magicmap() {
204 draw_magic_map();
205 }
206
do_savedefaults()207 static void do_savedefaults() { save_defaults(); }
208
do_savewinpos()209 static void do_savewinpos() { save_winpos(); }
210
do_take(const char * used)211 static void do_take(const char *used) {
212 command_take("take", used); /* I dunno why they want it. */
213 }
214
215 /* Help "typecasters". */
216 #include "chelp.h"
217
help_bind(void)218 static const char * help_bind(void) {
219 return HELP_BIND_LONG;
220 }
221
help_unbind(void)222 static const char * help_unbind(void) {
223 return HELP_UNBIND_LONG;
224 }
225
help_magicmap(void)226 static const char * help_magicmap(void) {
227 return HELP_MAGICMAP_LONG;
228 }
229
help_inv(void)230 static const char * help_inv(void) {
231 return HELP_INV_LONG;
232 }
233
help_cwindow(void)234 static const char * help_cwindow(void) {
235 return
236 "Syntax:\n"
237 "\n"
238 " cwindow <val>\n"
239 "\n"
240 "set size of command"
241 "window (if val is exceeded"
242 "client won't send new"
243 "commands to server\n\n"
244 "(What does this mean, 'put a lid on it'?) TODO";
245 }
246
help_script(void)247 static const char * help_script(void) {
248 return
249 "Syntax: script <path>\n\n"
250 "Start an executable client script located at <path>. For details on "
251 "client-side scripting, please see the Crossfire Wiki.";
252 }
253
help_scripttell(void)254 static const char * help_scripttell(void) {
255 return
256 "Syntax:\n"
257 "\n"
258 " scripttell <yourname> <data>\n"
259 "\n"
260 "?";
261 }
262
263 /* Toolkit-dependent. */
264
help_savewinpos(void)265 static const char * help_savewinpos(void) {
266 return
267 "Syntax:\n"
268 "\n"
269 " savewinpos\n"
270 "\n"
271 "save window positions - split windows mode only.";
272 }
273
help_scriptkill(void)274 static const char * help_scriptkill(void) {
275 return
276 "Syntax:\n"
277 "\n"
278 " scriptkill <name>\n"
279 "\n"
280 "Stop scripts named <name>.\n"
281 "(Not guaranteed to work?)";
282 }
283
cmd_raw(const char * cmd)284 static void cmd_raw(const char *cmd) {
285 cs_print_string(csocket.fd, "%s", cmd);
286 }
287
288 static ConsoleCommand CommonCommands[] = {
289 {"cmd", COMM_CAT_DEBUG, cmd_raw, NULL, "Send a raw command to the server"},
290
291 {"bind", COMM_CAT_SETUP, bind_key, help_bind, HELP_BIND_SHORT},
292
293 {"script", COMM_CAT_SCRIPT, script_init, help_script, NULL},
294 #ifdef HAVE_LUA
295 {"lua_load", COMM_CAT_SCRIPT, script_lua_load, NULL, NULL},
296
297 {"lua_list", COMM_CAT_SCRIPT, script_lua_list, NULL, NULL},
298
299 {"lua_kill", COMM_CAT_SCRIPT, script_lua_kill, NULL, NULL},
300 #endif
301 {"scripts", COMM_CAT_SCRIPT, do_script_list, NULL, "List running scripts"},
302
303 {"scriptkill", COMM_CAT_SCRIPT, script_kill, help_scriptkill, NULL},
304
305 {"scripttell", COMM_CAT_SCRIPT, script_tell, help_scripttell, NULL},
306
307 {"clearinfo", COMM_CAT_MISC, do_clearinfo, NULL, "Clear message window"},
308
309 {"cwindow", COMM_CAT_SETUP, set_command_window, help_cwindow, NULL},
310
311 {"disconnect", COMM_CAT_MISC, do_disconnect, NULL, NULL},
312
313
314 {"foodbeep", COMM_CAT_SETUP, command_foodbeep, NULL,
315 "toggle audible low on food warning"},
316
317 {"help", COMM_CAT_MISC, command_help, help_help, NULL},
318
319 {"inv", COMM_CAT_DEBUG, do_inv, help_inv, HELP_INV_SHORT},
320
321 {"magicmap", COMM_CAT_MISC, do_magicmap, help_magicmap,
322 HELP_MAGICMAP_SHORT},
323
324 {"savedefaults", COMM_CAT_SETUP, do_savedefaults, NULL,
325 HELP_SAVEDEFAULTS_SHORT},
326
327 {
328 "savewinpos", COMM_CAT_SETUP, do_savewinpos, help_savewinpos,
329 "Saves the position and sizes of windows." /* Panes? */
330 },
331
332 {"take", COMM_CAT_MISC, do_take, NULL, NULL},
333
334 {"unbind", COMM_CAT_SETUP, unbind_key, help_unbind, NULL},
335
336 {"show", COMM_CAT_SETUP, command_show, NULL,
337 "Change what items to show in inventory"},
338 };
339
340 const size_t num_commands = sizeof(CommonCommands) / sizeof(ConsoleCommand);
get_num_commands()341 static int get_num_commands() {
342 return num_commands;
343 }
344
345 static ConsoleCommand ** name_sorted_commands;
346
sort_by_name(const void * a_,const void * b_)347 static int sort_by_name(const void * a_, const void * b_) {
348 ConsoleCommand * a = *((ConsoleCommand **)a_);
349 ConsoleCommand * b = *((ConsoleCommand **)b_);
350 return strcmp(a->name, b->name);
351 }
352
353 static ConsoleCommand ** cat_sorted_commands;
354
355 /* Sort by category, then by name. */
356
sort_by_category(const void * a_,const void * b_)357 static int sort_by_category(const void *a_, const void *b_) {
358 /* Typecasts, so it goes. */
359 ConsoleCommand * a = *((ConsoleCommand **)a_);
360 ConsoleCommand * b = *((ConsoleCommand **)b_);
361
362 if (a->cat == b->cat) {
363 return strcmp(a->name, b->name);
364 }
365
366 return a->cat - b->cat;
367 }
368
init_commands()369 void init_commands() {
370 /* XXX Leak! */
371 name_sorted_commands = g_malloc(sizeof(ConsoleCommand *) * num_commands);
372
373 for (size_t i = 0; i < num_commands; i++) {
374 name_sorted_commands[i] = &CommonCommands[i];
375 }
376
377 /* Sort them. */
378 qsort(name_sorted_commands, num_commands, sizeof(ConsoleCommand *), sort_by_name);
379
380 /* Copy the list, then sort it by category. */
381 cat_sorted_commands = g_malloc(sizeof(ConsoleCommand *) * num_commands);
382
383 memcpy(cat_sorted_commands, name_sorted_commands, sizeof(ConsoleCommand *) * num_commands);
384
385 qsort(cat_sorted_commands, num_commands, sizeof(ConsoleCommand *), sort_by_category);
386
387 /* TODO Add to the list of tab-completion items. */
388 }
389
find_command(const char * cmd)390 const ConsoleCommand * find_command(const char *cmd) {
391 ConsoleCommand ** asp_p = NULL, dummy;
392 ConsoleCommand * dummy_p;
393 ConsoleCommand * asp;
394 char *cp, *cmd_cpy;
395 cmd_cpy = g_strdup(cmd);
396
397 for (cp=cmd_cpy; *cp; cp++) {
398 *cp =tolower(*cp);
399 }
400
401 dummy.name = cmd_cpy;
402 dummy_p = &dummy;
403 asp_p = bsearch(
404 (void *)&dummy_p,
405 (void *)name_sorted_commands,
406 num_commands,
407 sizeof(ConsoleCommand *),
408 sort_by_name);
409
410 if (asp_p == NULL) {
411 free(cmd_cpy);
412 return NULL;
413 }
414
415 asp = *asp_p;
416
417 /* TODO The server's find_command() searches first the commands,
418 then the emotes. We might have to do something similar someday, too. */
419 /* if (asp == NULL) search something else? */
420
421 free(cmd_cpy);
422
423 return asp;
424 }
425
426 /**
427 * Returns a pointer to the head of an array of ConsoleCommands sorted by
428 * category, then by name. It's num_commands long.
429 */
get_cat_sorted_commands(void)430 ConsoleCommand ** get_cat_sorted_commands(void) {
431 return cat_sorted_commands;
432 }
433
434 /**
435 * Tries to handle command cp (with optional params in cpnext, which may be
436 * null) as a local command. If this was a local command, returns true to
437 * indicate command was handled. This code was moved from extended_command so
438 * scripts ca issue local commands to handle keybindings or anything else.
439 *
440 * @param cp
441 * @param cpnext
442 */
handle_local_command(const char * cp,const char * cpnext)443 int handle_local_command(const char* cp, const char *cpnext) {
444 const ConsoleCommand * cc = NULL;
445
446 cc = find_command(cp);
447
448 if (cc == NULL) {
449 return FALSE;
450 }
451
452 if (cc->dofunc == NULL) {
453 char buf[MAX_BUF];
454
455 snprintf(buf, MAX_BUF - 1, "Client command %s has no implementation!", cc->name);
456 draw_ext_info(NDI_RED, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE, buf);
457
458 return FALSE;
459 }
460
461 cc->dofunc(cpnext);
462
463 return TRUE;
464 }
465
466 /**
467 * This is an extended command (ie, 'who, 'whatever, etc). In general, we
468 * just send the command to the server, but there are a few that we care about
469 * (bind, unbind)
470 *
471 * The command passed to us can not be modified - if it is a keybinding, we
472 * get passed the string that is that binding - modifying it effectively
473 * changes the binding.
474 *
475 * @param ocommand
476 */
extended_command(const char * ocommand)477 void extended_command(const char *ocommand) {
478 const char *cp = ocommand;
479 char *cpnext, command[MAX_BUF];
480
481 if ((cpnext = strchr(cp, ' '))!=NULL) {
482 int len = cpnext - ocommand;
483 if (len > (MAX_BUF -1 )) {
484 len = MAX_BUF-1;
485 }
486
487 strncpy(command, ocommand, len);
488 command[len] = '\0';
489 cp = command;
490 while (*cpnext == ' ') {
491 cpnext++;
492 }
493 if (*cpnext == 0) {
494 cpnext = NULL;
495 }
496 }
497 /*
498 * Try to prevent potential client hang by trying to delete a
499 * character when there is no character to delete.
500 * Thus, only send quit command if there is a player to delete.
501 */
502 if (cpl.title[0] == '\0' && strcmp(cp, "quit") == 0){
503 // Bail here, there isn't anything this should be doing.
504 return;
505 }
506
507 /* cp now contains the command (everything before first space),
508 * and cpnext contains everything after that first space. cpnext
509 * could be NULL.
510 */
511 #ifdef HAVE_LUA
512 if ( script_lua_command(cp, cpnext) ) {
513 return;
514 }
515 #endif
516
517 /* If this isn't a client-side command, send it to the server. */
518 if (!handle_local_command(cp, cpnext)) {
519 /* just send the command(s) (if `ocommand' is a compound command */
520 /* then split it and send each part seperately */
521 /* TODO Remove this from the server; end of commands.c. */
522 strncpy(command, ocommand, MAX_BUF-1);
523 command[MAX_BUF-1]=0;
524 cp = strtok(command, ";");
525 while ( cp ) {
526 while( *cp == ' ' ) {
527 cp++;
528 } /* throw out leading spaces; server
529 does not like them */
530 send_command(cp, cpl.count, 0);
531 cp = strtok(NULL, ";");
532 }
533 }
534 }
535
536 /* ------------------------------------------------------------------ */
537
538 /* This list is used for the 'tab' completion, and nothing else.
539 * Therefore, if it is out of date, it isn't that terrible, but
540 * ideally it should stay somewhat up to date with regards to
541 * the commands the server supports.
542 */
543
544 /* TODO Dynamically generate. */
545
546 static const char *const commands[] = {
547 "accuse", "afk", "apply", "applymode", "archs", "beg", "bleed", "blush",
548 "body", "bounce", "bow", "bowmode", "brace", "build", "burp", "cackle", "cast",
549 "chat", "chuckle", "clap", "cointoss", "cough", "cringe", "cry", "dance",
550 "disarm", "dm", "dmhide", "drop", "dropall", "east", "examine", "explore",
551 "fire", "fire_stop", "fix_me", "flip", "frown", "gasp", "get", "giggle",
552 "glare", "grin", "groan", "growl", "gsay", "help", "hiccup", "hiscore", "hug",
553 "inventory", "invoke", "killpets", "kiss", "laugh", "lick", "listen", "logs",
554 "mapinfo", "maps", "mark", "me", "motd", "nod", "north", "northeast",
555 "northwest", "orcknuckle", "output-count", "output-sync", "party", "peaceful",
556 "petmode", "pickup", "players", "poke", "pout", "prepare", "printlos", "puke",
557 "quests", "quit", "ready_skill", "rename", "reply", "resistances",
558 "rotateshoottype", "run", "run_stop", "save", "say", "scream", "search",
559 "search-items", "shake", "shiver", "shout", "showpets", "shrug", "shutdown",
560 "sigh", "skills", "slap", "smile", "smirk", "snap", "sneeze", "snicker",
561 "sniff", "snore", "sound", "south", "southeast", "southwest", "spit",
562 "statistics", "stay", "strings", "strut", "sulk", "take", "tell", "thank",
563 "think", "throw", "time", "title", "twiddle", "use_skill", "usekeys",
564 "version", "wave", "weather", "west", "whereabouts", "whereami", "whistle",
565 "who", "wimpy", "wink", "yawn",
566 };
567 const size_t num_server_commands = sizeof(commands) / sizeof(char *);
568
569 /**
570 * Player has entered 'command' and hit tab to complete it. See if we can
571 * find a completion. Returns matching command. Returns NULL if no command
572 * matches.
573 *
574 * @param command
575 */
complete_command(const char * command)576 const char * complete_command(const char *command) {
577 int len, display = 0;
578 const char *match;
579 static char result[64];
580 char list[500];
581
582 len = strlen(command);
583
584 if (len == 0) {
585 return NULL;
586 }
587
588 strcpy(list, "Matching commands:");
589
590 /* TODO Partial match, e.g.:
591 If the completion list was:
592 wear
593 wet #?
594
595 If we type 'w' then hit tab, put in the e.
596
597 Basically part of bash (readline?)'s behaviour.
598 */
599
600 match = NULL;
601
602 /* check server side commands */
603 for (size_t i = 0; i < num_server_commands; i++) {
604 if (!strncmp(command, commands[i], len)) {
605 if (display) {
606 snprintf(list + strlen(list), 499 - strlen(list), " %s", commands[i]);
607 } else if (match != NULL) {
608 display = 1;
609 snprintf(list + strlen(list), 499 - strlen(list), " %s %s", match, commands[i]);
610 match = NULL;
611 } else {
612 match = commands[i];
613 }
614 }
615 }
616
617 /* check client side commands */
618 for (size_t i = 0; i < num_commands; i++) {
619 if (!strncmp(command, CommonCommands[i].name, len)) {
620 if (display) {
621 snprintf(list + strlen(list), 499 - strlen(list), " %s", CommonCommands[i].name);
622 } else if (match != NULL) {
623 display = 1;
624 snprintf(list + strlen(list), 499 - strlen(list), " %s %s", match, CommonCommands[i].name);
625 match = NULL;
626 } else {
627 match = CommonCommands[i].name;
628 }
629 }
630 }
631
632 if (match == NULL) {
633 if (display) {
634 strncat(list, "\n", 499 - strlen(list));
635 draw_ext_info(
636 NDI_BLACK, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE, list);
637 } else
638 draw_ext_info(NDI_BLACK, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_NOTICE,
639 "No matching command.\n");
640 /* No match. */
641 return NULL;
642 }
643
644 /*
645 * Append a space to allow typing arguments. For commands without arguments
646 * the excess space should be stripped off automatically.
647 */
648 snprintf(result, sizeof(result), "%s ", match);
649 return result;
650 }
651