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