]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/command/cmd.qc
Don't make use of impulses to trigger waypoint editor commands
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / command / cmd.qc
1 #include "cmd.qh"
2
3 #include <server/defs.qh>
4 #include <server/miscfunctions.qh>
5
6 #include <common/command/_mod.qh>
7
8 #include "common.qh"
9 #include "vote.qh"
10
11 #include "../bot/api.qh"
12
13 #include "../campaign.qh"
14 #include "../cheats.qh"
15 #include "../client.qh"
16 #include "../player.qh"
17 #include "../ipban.qh"
18 #include "../mapvoting.qh"
19 #include "../scores.qh"
20 #include "../teamplay.qh"
21
22 #include <server/mutators/_mod.qh>
23 #include <common/gamemodes/_mod.qh>
24
25 #ifdef SVQC
26         #include <common/vehicles/all.qh>
27 #endif
28
29 #include <common/constants.qh>
30 #include <common/deathtypes/all.qh>
31 #include <common/effects/all.qh>
32 #include <common/mapinfo.qh>
33 #include <common/notifications/all.qh>
34 #include <common/physics/player.qh>
35 #include <common/teams.qh>
36 #include <common/util.qh>
37 #include <common/mapobjects/triggers.qh>
38
39 #include <common/minigames/sv_minigames.qh>
40
41 #include <common/monsters/_mod.qh>
42 #include <common/monsters/sv_spawn.qh>
43 #include <common/monsters/sv_monsters.qh>
44
45 #include <lib/warpzone/common.qh>
46
47 // =========================================================
48 //  Server side networked commands code, reworked by Samual
49 //  Last updated: December 28th, 2011
50 // =========================================================
51
52 bool SV_ParseClientCommand_floodcheck(entity this)
53 {
54         entity store = IS_CLIENT(this) ? CS(this) : this; // unfortunately, we need to store these on the client initially
55
56         if (!timeout_status)  // not while paused
57         {
58                 if (time <= (store.cmd_floodtime + autocvar_sv_clientcommand_antispam_time))
59                 {
60                         store.cmd_floodcount += 1;
61                         if (store.cmd_floodcount > autocvar_sv_clientcommand_antispam_count)   return false;  // too much spam, halt
62                 }
63                 else
64                 {
65                         store.cmd_floodtime = time;
66                         store.cmd_floodcount = 1;
67                 }
68         }
69         return true;  // continue, as we're not flooding yet
70 }
71
72
73 // =======================
74 //  Command Sub-Functions
75 // =======================
76
77 void ClientCommand_autoswitch(entity caller, int request, int argc)
78 {
79         switch (request)
80         {
81                 case CMD_REQUEST_COMMAND:
82                 {
83                         if (argv(1) != "")
84                         {
85                                 CS(caller).autoswitch = InterpretBoolean(argv(1));
86                                 sprint(caller, strcat("^1autoswitch is currently turned ", (CS(caller).autoswitch ? "on" : "off"), ".\n"));
87                                 return;
88                         }
89                 }
90
91                 default:
92                         sprint(caller, "Incorrect parameters for ^2autoswitch^7\n");
93                 case CMD_REQUEST_USAGE:
94                 {
95                         sprint(caller, "\nUsage:^3 cmd autoswitch selection\n");
96                         sprint(caller, "  Where 'selection' controls if autoswitch is on or off.\n");
97                         return;
98                 }
99         }
100 }
101
102 void ClientCommand_clientversion(entity caller, int request, int argc)  // internal command, used only by code
103 {
104         switch (request)
105         {
106                 case CMD_REQUEST_COMMAND:
107                 {
108                         if (argv(1) != "")
109                         {
110                                 if (IS_CLIENT(caller))
111                                 {
112                                         CS(caller).version = ((argv(1) == "$gameversion") ? 1 : stof(argv(1)));
113
114                                         if (CS(caller).version < autocvar_gameversion_min || CS(caller).version > autocvar_gameversion_max)
115                                         {
116                                                 CS(caller).version_mismatch = true;
117                                                 ClientKill_TeamChange(caller, -2);  // observe
118                                         }
119                                         else if (autocvar_g_campaign || autocvar_g_balance_teams)
120                                         {
121                                                 // JoinBestTeam(caller, false, true);
122                                         }
123                                         else if (teamplay && !autocvar_sv_spectate && !(caller.team_forced > 0))
124                                         {
125                                                 TRANSMUTE(Observer, caller);  // really?
126                                                 stuffcmd(caller, "menu_showteamselect\n");
127                                         }
128                                 }
129
130                                 return;
131                         }
132                 }
133
134                 default:
135                         sprint(caller, "Incorrect parameters for ^2clientversion^7\n");
136                 case CMD_REQUEST_USAGE:
137                 {
138                         sprint(caller, "\nUsage:^3 cmd clientversion version\n");
139                         sprint(caller, "  Where 'version' is the game version reported by caller.\n");
140                         return;
141                 }
142         }
143 }
144
145 void ClientCommand_mv_getpicture(entity caller, int request, int argc)  // internal command, used only by code
146 {
147         switch (request)
148         {
149                 case CMD_REQUEST_COMMAND:
150                 {
151                         if (argv(1) != "")
152                         {
153                                 if (intermission_running) MapVote_SendPicture(caller, stof(argv(1)));
154
155                                 return;
156                         }
157                 }
158
159                 default:
160                         sprint(caller, "Incorrect parameters for ^2mv_getpicture^7\n");
161                 case CMD_REQUEST_USAGE:
162                 {
163                         sprint(caller, "\nUsage:^3 cmd mv_getpicture mapid\n");
164                         sprint(caller, "  Where 'mapid' is the id number of the map to request an image of on the map vote selection menu.\n");
165                         return;
166                 }
167         }
168 }
169
170 void ClientCommand_wpeditor(entity caller, int request, int argc)
171 {
172         switch (request)
173         {
174                 case CMD_REQUEST_COMMAND:
175                 {
176                         if (!autocvar_g_waypointeditor)
177                         {
178                                 sprint(caller, "ERROR: this command works only if the waypoint editor is on\n");
179                                 return;
180                         }
181
182                         if (argv(1) != "")
183                         {
184                                 if (argv(1) == "spawn")
185                                 {
186                                         if (!IS_PLAYER(caller))
187                                                 sprint(caller, "ERROR: this command works only if you are player\n");
188                                         else
189                                                 waypoint_spawn_fromeditor(caller);
190                                 }
191                                 else if (argv(1) == "remove")
192                                 {
193                                         if (!IS_PLAYER(caller))
194                                                 sprint(caller, "ERROR: this command works only if you are player\n");
195                                         else
196                                                 waypoint_remove_fromeditor(caller);
197                                 }
198                                 else if (argv(1) == "unreachable")
199                                 {
200                                         if (!IS_PLAYER(caller))
201                                                 sprint(caller, "ERROR: this command works only if you are player\n");
202                                         else
203                                                 waypoint_unreachable(caller);
204                                 }
205                                 else if (argv(1) == "saveall")
206                                         waypoint_saveall();
207                                 else if (argv(1) == "relinkall")
208                                         waypoint_schedulerelinkall();
209
210                                 return;
211                         }
212                 }
213
214                 default:
215                         sprint(caller, "Incorrect parameters for ^2wpeditor^7\n");
216                 case CMD_REQUEST_USAGE:
217                 {
218                         sprint(caller, "\nUsage:^3 cmd wpeditor action\n");
219                         sprint(caller, "  Where 'action' can be: spawn, remove, unreachable, saveall, relinkall\n");
220                         return;
221                 }
222         }
223 }
224
225 void ClientCommand_join(entity caller, int request)
226 {
227         switch (request)
228         {
229                 case CMD_REQUEST_COMMAND:
230                 {
231                         if (!game_stopped)
232                         if (IS_CLIENT(caller) && !IS_PLAYER(caller))
233                         if (joinAllowed(caller))
234                                 Join(caller);
235
236                         return;  // never fall through to usage
237                 }
238
239                 default:
240                 case CMD_REQUEST_USAGE:
241                 {
242                         sprint(caller, "\nUsage:^3 cmd join\n");
243                         sprint(caller, "  No arguments required.\n");
244                         return;
245                 }
246         }
247 }
248
249 void ClientCommand_physics(entity caller, int request, int argc)
250 {
251         switch (request)
252         {
253                 case CMD_REQUEST_COMMAND:
254                 {
255                         string command = strtolower(argv(1));
256
257                         if (!autocvar_g_physics_clientselect)
258                         {
259                                 sprint(caller, "Client physics selection is currently disabled.\n");
260                                 return;
261                         }
262
263                         if (command == "list" || command == "help")
264                         {
265                                 sprint(caller, strcat("Available physics sets: \n\n", autocvar_g_physics_clientselect_options, " default\n"));
266                                 return;
267                         }
268
269                         if (Physics_Valid(command) || command == "default")
270                         {
271                                 stuffcmd(caller, strcat("\nseta cl_physics ", command, "\nsendcvar cl_physics\n"));
272                                 sprint(caller, strcat("^2Physics set successfully changed to ^3", command, "\n"));
273                                 return;
274                         }
275                 }
276
277                 default:
278                         sprint(caller, strcat("Current physics set: ^3", CS(caller).cvar_cl_physics, "\n"));
279                 case CMD_REQUEST_USAGE:
280                 {
281                         sprint(caller, "\nUsage:^3 cmd physics <physics>\n");
282                         sprint(caller, "  See 'cmd physics list' for available physics sets.\n");
283                         sprint(caller, "  Argument 'default' resets to standard physics.\n");
284                         return;
285                 }
286         }
287 }
288
289 void ClientCommand_ready(entity caller, int request)  // todo: anti-spam for toggling readyness
290 {
291         switch (request)
292         {
293                 case CMD_REQUEST_COMMAND:
294                 {
295                         if (IS_CLIENT(caller))
296                         {
297                                 if (warmup_stage || sv_ready_restart || g_race_qualifying == 2)
298                                 {
299                                         if (!readyrestart_happened || sv_ready_restart_repeatable)
300                                         {
301                                                 if (time < game_starttime) // game is already restarting
302                                                         return;
303                                                 if (caller.ready)            // toggle
304                                                 {
305                                                         caller.ready = false;
306                                                         if(IS_PLAYER(caller) || caller.caplayer == 1)
307                                                                 bprint(playername(caller, false), "^2 is ^1NOT^2 ready\n");
308                                                 }
309                                                 else
310                                                 {
311                                                         caller.ready = true;
312                                                         if(IS_PLAYER(caller) || caller.caplayer == 1)
313                                                                 bprint(playername(caller, false), "^2 is ready\n");
314                                                 }
315
316                                                 // cannot reset the game while a timeout is active!
317                                                 if (!timeout_status) ReadyCount();
318                                         }
319                                         else
320                                         {
321                                                 sprint(caller, "^1Game has already been restarted\n");
322                                         }
323                                 }
324                         }
325                         return;  // never fall through to usage
326                 }
327
328                 default:
329                 case CMD_REQUEST_USAGE:
330                 {
331                         sprint(caller, "\nUsage:^3 cmd ready\n");
332                         sprint(caller, "  No arguments required.\n");
333                         return;
334                 }
335         }
336 }
337
338 void ClientCommand_say(entity caller, int request, int argc, string command)
339 {
340         switch (request)
341         {
342                 case CMD_REQUEST_COMMAND:
343                 {
344                         if (argc >= 2)   Say(caller, false, NULL, substring(command, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)), 1);
345                         return;  // never fall through to usage
346                 }
347
348                 default:
349                 case CMD_REQUEST_USAGE:
350                 {
351                         sprint(caller, "\nUsage:^3 cmd say <message>\n");
352                         sprint(caller, "  Where 'message' is the string of text to say.\n");
353                         return;
354                 }
355         }
356 }
357
358 void ClientCommand_say_team(entity caller, int request, int argc, string command)
359 {
360         switch (request)
361         {
362                 case CMD_REQUEST_COMMAND:
363                 {
364                         if (argc >= 2)
365                         {
366                                 string _classname = string_null;
367                                 if (!IS_PLAYER(caller) && caller.caplayer)
368                                 {
369                                         // CA: make work team chat for killed players
370                                         _classname = caller.classname;
371                                         caller.classname = STR_PLAYER;
372                                 }
373
374                                 Say(caller, true, NULL, substring(command, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)), 1);
375                                 if (_classname)
376                                         caller.classname = _classname;
377                         }
378                         return;  // never fall through to usage
379                 }
380
381                 default:
382                 case CMD_REQUEST_USAGE:
383                 {
384                         sprint(caller, "\nUsage:^3 cmd say_team <message>\n");
385                         sprint(caller, "  Where 'message' is the string of text to say.\n");
386                         return;
387                 }
388         }
389 }
390
391 .bool team_selected;
392 void ClientCommand_selectteam(entity caller, int request, int argc)
393 {
394         switch (request)
395         {
396                 case CMD_REQUEST_COMMAND:
397                 {
398                         if (argv(1) == "")
399                         {
400                                 return;
401                         }
402                         if (!IS_CLIENT(caller))
403                         {
404                                 return;
405                         }
406                         if (!teamplay)
407                         {
408                                 sprint(caller, "^7selectteam can only be used in teamgames\n");
409                                 return;
410                         }
411                         if (caller.team_forced > 0)
412                         {
413                                 sprint(caller, "^7selectteam can not be used as your team is forced\n");
414                                 return;
415                         }
416                         if (lockteams)
417                         {
418                                 sprint(caller, "^7The game has already begun, you must wait until the next map to be able to join a team.\n");
419                                 return;
420                         }
421                         float selection;
422                         switch (argv(1))
423                         {
424                                 case "red":
425                                 {
426                                         selection = NUM_TEAM_1;
427                                         break;
428                                 }
429                                 case "blue":
430                                 {
431                                         selection = NUM_TEAM_2;
432                                         break;
433                                 }
434                                 case "yellow":
435                                 {
436                                         selection = NUM_TEAM_3;
437                                         break;
438                                 }
439                                 case "pink":
440                                 {
441                                         selection = NUM_TEAM_4;
442                                         break;
443                                 }
444                                 case "auto":
445                                 {
446                                         selection = (-1);
447                                         break;
448                                 }
449                                 default:
450                                 {
451                                         return;
452                                 }
453                         }
454                         if (caller.team == selection && selection != -1 && !IS_DEAD(caller))
455                         {
456                                 sprint(caller, "^7You already are on that team.\n");
457                                 return;
458                         }
459                         if (CS(caller).wasplayer && autocvar_g_changeteam_banned)
460                         {
461                                 sprint(caller, "^1You cannot change team, forbidden by the server.\n");
462                                 return;
463                         }
464                         if ((selection != -1) && autocvar_g_balance_teams &&
465                                 autocvar_g_balance_teams_prevent_imbalance)
466                         {
467                                 entity balance = TeamBalance_CheckAllowedTeams(caller);
468                                 TeamBalance_GetTeamCounts(balance, caller);
469                                 if ((Team_IndexToBit(Team_TeamToIndex(selection)) &
470                                         TeamBalance_FindBestTeams(balance, caller, false)) == 0)
471                                 {
472                                         Send_Notification(NOTIF_ONE, caller, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
473                                         TeamBalance_Destroy(balance);
474                                         return;
475                                 }
476                                 TeamBalance_Destroy(balance);
477                         }
478                         ClientKill_TeamChange(caller, selection);
479                         if (!IS_PLAYER(caller))
480                         {
481                                 caller.team_selected = true; // avoids asking again for team selection on join
482                         }
483                         return;
484                 }
485                 default:
486                         sprint(caller, "Incorrect parameters for ^2selectteam^7\n");
487                 case CMD_REQUEST_USAGE:
488                 {
489                         sprint(caller, "\nUsage:^3 cmd selectteam team\n");
490                         sprint(caller, "  Where 'team' is the prefered team to try and join.\n");
491                         sprint(caller, "  Full list of options here: \"red, blue, yellow, pink, auto\"\n");
492                         return;
493                 }
494         }
495 }
496
497 void ClientCommand_selfstuff(entity caller, int request, string command)
498 {
499         switch (request)
500         {
501                 case CMD_REQUEST_COMMAND:
502                 {
503                         if (argv(1) != "")
504                         {
505                                 stuffcmd(caller, substring(command, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)));
506                                 return;
507                         }
508                 }
509
510                 default:
511                         sprint(caller, "Incorrect parameters for ^2selfstuff^7\n");
512                 case CMD_REQUEST_USAGE:
513                 {
514                         sprint(caller, "\nUsage:^3 cmd selfstuff <command>\n");
515                         sprint(caller, "  Where 'command' is the string to be stuffed to your client.\n");
516                         return;
517                 }
518         }
519 }
520
521 void ClientCommand_sentcvar(entity caller, int request, int argc, string command)
522 {
523         switch (request)
524         {
525                 case CMD_REQUEST_COMMAND:
526                 {
527                         if (argv(1) != "")
528                         {
529                                 // float tokens;
530                                 string s;
531
532                                 if (argc == 2)  // undefined cvar: use the default value on the server then
533                                 {
534                                         s = strcat(substring(command, argv_start_index(0), argv_end_index(1) - argv_start_index(0)), " \"", cvar_defstring(argv(1)), "\"");
535                                         tokenize_console(s);
536                                 }
537
538                                 GetCvars(caller, CS(caller), 1);
539
540                                 return;
541                         }
542                 }
543
544                 default:
545                         sprint(caller, "Incorrect parameters for ^2sentcvar^7\n");
546                 case CMD_REQUEST_USAGE:
547                 {
548                         sprint(caller, "\nUsage:^3 cmd sentcvar <cvar>\n");
549                         sprint(caller, "  Where 'cvar' is the cvar plus arguments to send to the server.\n");
550                         return;
551                 }
552         }
553 }
554
555 void ClientCommand_spectate(entity caller, int request)
556 {
557         switch (request)
558         {
559                 case CMD_REQUEST_COMMAND:
560                 {
561                         if (!intermission_running && IS_CLIENT(caller))
562                         {
563                                 if((IS_SPEC(caller) || IS_OBSERVER(caller)) && argv(1) != "")
564                                 {
565                                         entity client = GetFilteredEntity(argv(1));
566                                         int spec_accepted = VerifyClientEntity(client, false, false);
567                                         if(spec_accepted > 0 && IS_PLAYER(client))
568                                         {
569                                                 if(Spectate(caller, client))
570                                                         return; // fall back to regular handling
571                                         }
572                                 }
573
574                                 int mutator_returnvalue = MUTATOR_CALLHOOK(ClientCommand_Spectate, caller);
575
576                                 if (mutator_returnvalue == MUT_SPECCMD_RETURN) return;
577
578                                 if ((IS_PLAYER(caller) || mutator_returnvalue == MUT_SPECCMD_FORCE))
579                                 if (autocvar_sv_spectate == 1)
580                                         ClientKill_TeamChange(caller, -2); // observe
581                         }
582                         return; // never fall through to usage
583                 }
584
585                 default:
586                 case CMD_REQUEST_USAGE:
587                 {
588                         sprint(caller, "\nUsage:^3 cmd spectate <client>\n");
589                         sprint(caller, "  Where 'client' can be the player to spectate.\n");
590                         return;
591                 }
592         }
593 }
594
595 void ClientCommand_suggestmap(entity caller, int request, int argc)
596 {
597         switch (request)
598         {
599                 case CMD_REQUEST_COMMAND:
600                 {
601                         if (argv(1) != "")
602                         {
603                                 sprint(caller, strcat(MapVote_Suggest(caller, argv(1)), "\n"));
604                                 return;
605                         }
606                 }
607
608                 default:
609                         sprint(caller, "Incorrect parameters for ^2suggestmap^7\n");
610                 case CMD_REQUEST_USAGE:
611                 {
612                         sprint(caller, "\nUsage:^3 cmd suggestmap map\n");
613                         sprint(caller, "  Where 'map' is the name of the map to suggest.\n");
614                         return;
615                 }
616         }
617 }
618
619 void ClientCommand_tell(entity caller, int request, int argc, string command)
620 {
621         switch (request)
622         {
623                 case CMD_REQUEST_COMMAND:
624                 {
625                         if (argc >= 3)
626                         {
627                                 if(!IS_CLIENT(caller) && IS_REAL_CLIENT(caller)) // connecting
628                                 {
629                                         print_to(caller, "You can't ^2tell^7 a message while connecting.");
630                                         return;
631                                 }
632
633                                 entity tell_to = GetIndexedEntity(argc, 1);
634                                 float tell_accepted = VerifyClientEntity(tell_to, true, false);
635
636                                 if (tell_accepted > 0)   // the target is a real client
637                                 {
638                                         if (tell_to != caller) // and we're allowed to send to them :D
639                                         {
640                                                 // workaround for argv indexes indexing ascii chars instead of utf8 chars
641                                                 // In this case when the player name contains utf8 chars
642                                                 // the message gets partially trimmed in the beginning.
643                                                 // Potentially this bug affects any substring call that uses
644                                                 // argv_start_index and argv_end_index.
645
646                                                 string utf8_enable_save = cvar_string("utf8_enable");
647                                                 cvar_set("utf8_enable", "0");
648                                                 string msg = substring(command, argv_start_index(next_token), argv_end_index(-1) - argv_start_index(next_token));
649                                                 cvar_set("utf8_enable", utf8_enable_save);
650
651                                                 Say(caller, false, tell_to, msg, true);
652                                                 return;
653                                         }
654                                         else { print_to(caller, "You can't ^2tell^7 a message to yourself."); return; }
655                                 }
656                                 else if (argv(1) == "#0")
657                                 {
658                                         trigger_magicear_processmessage_forallears(caller, -1, NULL, substring(command, argv_start_index(next_token), argv_end_index(-1) - argv_start_index(next_token)));
659                                         return;
660                                 }
661                                 else { print_to(caller, strcat("tell: ", GetClientErrorString(tell_accepted, argv(1)), ".")); return; }
662                         }
663                 }
664
665                 default:
666                         sprint(caller, "Incorrect parameters for ^2tell^7\n");
667                 case CMD_REQUEST_USAGE:
668                 {
669                         sprint(caller, "\nUsage:^3 cmd tell client <message>\n");
670                         sprint(caller, "  Where 'client' is the entity number or name of the player to send 'message' to.\n");
671                         return;
672                 }
673         }
674 }
675
676 void ClientCommand_voice(entity caller, int request, int argc, string command)
677 {
678         switch (request)
679         {
680                 case CMD_REQUEST_COMMAND:
681                 {
682                         if (argv(1) != "")
683                         {
684                                 entity e = GetVoiceMessage(argv(1));
685                                 if (!e)
686                                 {
687                                         sprint(caller, sprintf("Invalid voice. Use one of: %s\n", allvoicesamples));
688                                         return;
689                                 }
690                                 string msg = "";
691                                 if (argc >= 3)
692                                         msg = substring(command, argv_start_index(2), argv_end_index(-1) - argv_start_index(2));
693                                 VoiceMessage(caller, e, msg);
694
695                                 return;
696                         }
697                 }
698
699                 default:
700                         sprint(caller, "Incorrect parameters for ^2voice^7\n");
701                 case CMD_REQUEST_USAGE:
702                 {
703                         sprint(caller, "\nUsage:^3 cmd voice messagetype <soundname>\n");
704                         sprint(caller, "  'messagetype' is the type of broadcast to do, like team only or such,\n");
705                         sprint(caller, "  and 'soundname' is the string/filename of the sound/voice message to play.\n");
706                         return;
707                 }
708         }
709 }
710
711 /* use this when creating a new command, making sure to place it in alphabetical order... also,
712 ** ADD ALL NEW COMMANDS TO commands.cfg WITH PROPER ALIASES IN THE SAME FASHION!
713 void ClientCommand_(entity caller, int request)
714 {
715     switch(request)
716     {
717         case CMD_REQUEST_COMMAND:
718         {
719
720             return; // never fall through to usage
721         }
722
723         default:
724         case CMD_REQUEST_USAGE:
725         {
726             sprint(caller, "\nUsage:^3 cmd \n");
727             sprint(caller, "  No arguments required.\n");
728             return;
729         }
730     }
731 }
732 */
733
734
735 // =====================================
736 //  Macro system for networked commands
737 // =====================================
738
739 // Do not hard code aliases for these, instead create them in commands.cfg... also: keep in alphabetical order, please ;)
740 #define CLIENT_COMMANDS(ent, request, arguments, command) \
741         CLIENT_COMMAND("autoswitch", ClientCommand_autoswitch(ent, request, arguments), "Whether or not to switch automatically when getting a better weapon") \
742         CLIENT_COMMAND("clientversion", ClientCommand_clientversion(ent, request, arguments), "Release version of the game") \
743         CLIENT_COMMAND("join", ClientCommand_join(ent, request), "Become a player in the game") \
744         CLIENT_COMMAND("minigame", ClientCommand_minigame(ent, request, arguments, command), "Start a minigame") \
745         CLIENT_COMMAND("mv_getpicture", ClientCommand_mv_getpicture(ent, request, arguments), "Retrieve mapshot picture from the server") \
746         CLIENT_COMMAND("physics", ClientCommand_physics(ent, request, arguments), "Change physics set") \
747         CLIENT_COMMAND("ready", ClientCommand_ready(ent, request), "Qualify as ready to end warmup stage (or restart server if allowed)") \
748         CLIENT_COMMAND("say", ClientCommand_say(ent, request, arguments, command), "Print a message to chat to all players") \
749         CLIENT_COMMAND("say_team", ClientCommand_say_team(ent, request, arguments, command), "Print a message to chat to all team mates") \
750         CLIENT_COMMAND("selectteam", ClientCommand_selectteam(ent, request, arguments), "Attempt to choose a team to join into") \
751         CLIENT_COMMAND("selfstuff", ClientCommand_selfstuff(ent, request, command), "Stuffcmd a command to your own client") \
752         CLIENT_COMMAND("sentcvar", ClientCommand_sentcvar(ent, request, arguments, command), "New system for sending a client cvar to the server") \
753         CLIENT_COMMAND("spectate", ClientCommand_spectate(ent, request), "Become an observer") \
754         CLIENT_COMMAND("suggestmap", ClientCommand_suggestmap(ent, request, arguments), "Suggest a map to the mapvote at match end") \
755         CLIENT_COMMAND("tell", ClientCommand_tell(ent, request, arguments, command), "Send a message directly to a player") \
756         CLIENT_COMMAND("voice", ClientCommand_voice(ent, request, arguments, command), "Send voice message via sound") \
757         CLIENT_COMMAND("wpeditor", ClientCommand_wpeditor(ent, request, arguments), "Waypoint editor commands") \
758         /* nothing */
759
760 void ClientCommand_macro_help(entity caller)
761 {
762         #define CLIENT_COMMAND(name, function, description) \
763                 { sprint(caller, "  ^2", name, "^7: ", description, "\n"); }
764
765         CLIENT_COMMANDS(NULL, 0, 0, "");
766 #undef CLIENT_COMMAND
767 }
768
769 float ClientCommand_macro_command(int argc, entity caller, string command)
770 {
771         #define CLIENT_COMMAND(name, function, description) \
772                 { if (name == strtolower(argv(0))) { function; return true; } }
773
774         CLIENT_COMMANDS(caller, CMD_REQUEST_COMMAND, argc, command);
775 #undef CLIENT_COMMAND
776
777         return false;
778 }
779
780 float ClientCommand_macro_usage(int argc, entity caller)
781 {
782         #define CLIENT_COMMAND(name, function, description) \
783                 { if (name == strtolower(argv(1))) { function; return true; } }
784
785         CLIENT_COMMANDS(caller, CMD_REQUEST_USAGE, argc, "");
786 #undef CLIENT_COMMAND
787
788         return false;
789 }
790
791 void ClientCommand_macro_write_aliases(float fh)
792 {
793         #define CLIENT_COMMAND(name, function, description) \
794                 { CMD_Write_Alias("qc_cmd_cmd", name, description); }
795
796         CLIENT_COMMANDS(NULL, 0, 0, "");
797 #undef CLIENT_COMMAND
798 }
799
800 // ======================================
801 //  Main Function Called By Engine (cmd)
802 // ======================================
803 // If this function exists, server game code parses clientcommand before the engine code gets it.
804
805 void SV_ParseClientCommand(entity this, string command)
806 {
807         // If invalid UTF-8, don't even parse it
808         string command2 = "";
809         float len = strlen(command);
810         float i;
811         for (i = 0; i < len; ++i)
812                 command2 = strcat(command2, chr2str(str2chr(command, i)));
813         if (command != command2) return;
814
815         // if we're banned, don't even parse the command
816         if (Ban_MaybeEnforceBanOnce(this)) return;
817
818         int argc = tokenize_console(command);
819
820         // Guide for working with argc arguments by example:
821         // argc:   1    - 2      - 3     - 4
822         // argv:   0    - 1      - 2     - 3
823         // cmd     vote - master - login - password
824
825         // for floodcheck
826         switch (strtolower(argv(0)))
827         {
828                 // exempt commands which are not subject to floodcheck
829                 case "begin": break;                               // handled by engine in host_cmd.c
830                 case "download": break;                            // handled by engine in cl_parse.c
831                 case "mv_getpicture": break;                       // handled by server in this file
832                 case "wpeditor": break;                            // handled by server in this file
833                 case "pause": break;                               // handled by engine in host_cmd.c
834                 case "prespawn": break;                            // handled by engine in host_cmd.c
835                 case "sentcvar": break;                            // handled by server in this file
836                 case "spawn": break;                               // handled by engine in host_cmd.c
837                 case "c2s": Net_ClientCommand(this, command); return; // handled by net.qh
838
839                 default:
840                         if (SV_ParseClientCommand_floodcheck(this)) break; // "true": continue, as we're not flooding yet
841                         else return;                                   // "false": not allowed to continue, halt // print("^1ERROR: ^7ANTISPAM CAUGHT: ", command, ".\n");
842         }
843
844         /* NOTE: should this be disabled? It can be spammy perhaps, but hopefully it's okay for now */
845         if (argv(0) == "help")
846         {
847                 if (argc == 1)
848                 {
849                         sprint(this, "\nClient networked commands:\n");
850                         ClientCommand_macro_help(this);
851
852                         sprint(this, "\nCommon networked commands:\n");
853                         CommonCommand_macro_help(this);
854
855                         sprint(this, "\nUsage:^3 cmd COMMAND...^7, where possible commands are listed above.\n");
856                         sprint(this, "For help about a specific command, type cmd help COMMAND\n");
857                         return;
858                 }
859                 else if (CommonCommand_macro_usage(argc, this))  // Instead of trying to call a command, we're going to see detailed information about it
860                 {
861                         return;
862                 }
863                 else if (ClientCommand_macro_usage(argc, this))  // same, but for normal commands now
864                 {
865                         return;
866                 }
867         }
868         else if (MUTATOR_CALLHOOK(SV_ParseClientCommand, this, strtolower(argv(0)), argc, command))
869         {
870                 return;  // handled by a mutator
871         }
872         else if (CheatCommand(this, argc))
873         {
874                 return;  // handled by server/cheats.qc
875         }
876         else if (CommonCommand_macro_command(argc, this, command))
877         {
878                 return;                                          // handled by server/command/common.qc
879         }
880         else if (ClientCommand_macro_command(argc, this, command)) // continue as usual and scan for normal commands
881         {
882                 return;                                          // handled by one of the above ClientCommand_* functions
883         }
884         else
885         {
886                 clientcommand(this, command);
887         }
888 }