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