]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/clientcommands.qc
Many new commands, plus disable the help/usage functionality by default
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / clientcommands.qc
1 // =======================================================
2 //  Server side client commands code, reworked by Samual
3 //  Last updated: July 23rd, 2011
4 // =======================================================
5
6 #define CC_REQUEST_HELP 1
7 #define CC_REQUEST_COMMAND 2
8 #define CC_REQUEST_USAGE 3
9
10 entity nagger;
11
12 .float cmd_floodtime;
13 .float cmd_floodcount;
14 .float checkfail;
15
16 float readyrestart_happened;
17 float readycount;
18
19 string MapVote_Suggest(string m);
20
21 void ReadyCount();
22
23
24 // ============================
25 //  Misc. Supporting Functions
26 // ============================
27
28 float SV_ParseClientCommand_floodcheck()
29 {
30         if (timeoutStatus != 2) // why?
31         {
32                 if(time == self.cmd_floodtime) // todo: add buffer time as well, ONLY one second is a short amount of time for someone to be spamming. 
33                 {
34                         self.cmd_floodcount += 1;
35                         if(self.cmd_floodcount > 8) // todo: replace constant 8 with a cvar for the server to control
36                                 return FALSE; // too much spam, halt
37                 }
38                 else
39                 {
40                         self.cmd_floodtime = time;
41                         self.cmd_floodcount = 1;
42                 }
43         }
44         return TRUE; // continue, as we're not flooding yet
45 }
46
47 float Nagger_SendEntity(entity to, float sendflags)
48 {
49         float nags, i, f, b;
50         entity e;
51         WriteByte(MSG_ENTITY, ENT_CLIENT_NAGGER);
52
53         nags = 0;
54         if(readycount)
55         {
56                 nags |= 1;
57                 if(to.ready == 0)
58                         nags |= 2;
59         }
60         if(votecalled)
61         {
62                 nags |= 4;
63                 if(to.vote_vote == 0)
64                         nags |= 8;
65         }
66         if(inWarmupStage)
67                 nags |= 16;
68
69         if(sendflags & 128)
70                 nags |= 128;
71
72         if(!(nags & 4)) // no vote called? send no string
73                 nags &~= 128;
74
75         WriteByte(MSG_ENTITY, nags);
76
77         if(nags & 128)
78         {
79                 WriteString(MSG_ENTITY, votecalledvote_display);
80         }
81
82         if(nags & 1)
83         {
84                 for(i = 1; i <= maxclients; i += 8)
85                 {
86                         for(f = 0, e = edict_num(i), b = 1; b < 256; b *= 2, e = nextent(e))
87                                 if(clienttype(e) != CLIENTTYPE_REAL || e.ready)
88                                         f |= b;
89                         WriteByte(MSG_ENTITY, f);
90                 }
91         }
92
93         return TRUE;
94 }
95 void Nagger_Init()
96 {
97         Net_LinkEntity(nagger = spawn(), FALSE, 0, Nagger_SendEntity);
98 }
99 void Nagger_VoteChanged()
100 {
101         if(nagger)
102                 nagger.SendFlags |= 128;
103 }
104 void Nagger_VoteCountChanged()
105 {
106         if(nagger)
107                 nagger.SendFlags |= 1;
108 }
109 void Nagger_ReadyCounted()
110 {
111         if(nagger)
112                 nagger.SendFlags |= 1;
113 }
114
115 void ReadyRestartForce()
116 {
117         local entity e;
118
119         bprint("^1Server is restarting...\n");
120
121         VoteReset();
122
123         // clear overtime
124         if (checkrules_overtimesadded > 0 && g_race_qualifying != 2) {
125                 //we have to decrease timelimit to its original value again!!
126                 float newTL;
127                 newTL = autocvar_timelimit;
128                 newTL -= checkrules_overtimesadded * autocvar_timelimit_overtime;
129                 cvar_set("timelimit", ftos(newTL));
130         }
131
132         checkrules_suddendeathend = checkrules_overtimesadded = checkrules_suddendeathwarning = 0;
133
134
135         readyrestart_happened = 1;
136         game_starttime = time;
137         if(!g_ca && !g_arena)
138                 game_starttime += RESTART_COUNTDOWN;
139         restart_mapalreadyrestarted = 0; //reset this var, needed when cvar sv_ready_restart_repeatable is in use
140
141         inWarmupStage = 0; //once the game is restarted the game is in match stage
142
143         //reset the .ready status of all players (also spectators)
144         FOR_EACH_CLIENTSLOT(e)
145                 e.ready = 0;
146         readycount = 0;
147         Nagger_ReadyCounted(); // NOTE: this causes a resend of that entity, and will also turn off warmup state on the client
148
149         if(autocvar_teamplay_lockonrestart && teamplay) {
150                 lockteams = 1;
151                 bprint("^1The teams are now locked.\n");
152         }
153
154         //initiate the restart-countdown-announcer entity
155         if(autocvar_sv_ready_restart_after_countdown && !g_ca && !g_arena)
156         {
157                 restartTimer = spawn();
158                 restartTimer.think = restartTimer_Think;
159                 restartTimer.nextthink = game_starttime;
160         }
161
162         //after a restart every players number of allowed timeouts gets reset, too
163         if(autocvar_sv_timeout)
164         {
165                 FOR_EACH_REALPLAYER(e)
166                         e.allowedTimeouts = autocvar_sv_timeout_number;
167         }
168
169         //reset map immediately if this cvar is not set
170         if (!autocvar_sv_ready_restart_after_countdown)
171                 reset_map(TRUE);
172
173         if(autocvar_sv_eventlog)
174                 GameLogEcho(":restart");
175 }
176
177 void ReadyRestart()
178 {
179         // no arena, assault support yet...
180         if(g_arena | g_assault | gameover | intermission_running | race_completing)
181                 localcmd("restart\n");
182         else
183                 localcmd("\nsv_hook_gamerestart\n");
184
185         ReadyRestartForce();
186
187         // reset ALL scores, but only do that at the beginning
188         //of the countdown if sv_ready_restart_after_countdown is off!
189         //Otherwise scores could be manipulated during the countdown!
190         if (!autocvar_sv_ready_restart_after_countdown)
191                 Score_ClearAll();
192 }
193
194 /**
195  * Counts how many players are ready. If not enough players are ready, the function
196  * does nothing. If all players are ready, the timelimit will be extended and the
197  * restart_countdown variable is set to allow other functions like PlayerPostThink
198  * to detect that the countdown is now active. If the cvar sv_ready_restart_after_countdown
199  * is not set the map will be resetted.
200  *
201  * Function is called after the server receives a 'ready' sign from a player.
202  */
203 void ReadyCount()
204 {
205         local entity e;
206         local float r, p;
207
208         r = p = 0;
209
210         FOR_EACH_REALPLAYER(e)
211         {
212                 p += 1;
213                 if(e.ready)
214                         r += 1;
215         }
216
217         readycount = r;
218
219         Nagger_ReadyCounted();
220
221         if(r) // at least one is ready
222         if(r == p) // and, everyone is ready
223                 ReadyRestart();
224 }
225
226 /**
227  * Restarts the map after the countdown is over (and cvar sv_ready_restart_after_countdown
228  * is set)
229  */
230 void restartTimer_Think() {
231         restart_mapalreadyrestarted = 1;
232         reset_map(TRUE);
233         Score_ClearAll();
234         remove(self);
235         return;
236 }
237
238 /**
239  * Checks whether the player who calls the timeout is allowed to do so.
240  * If so, it initializes the timeout countdown. It also checks whether another
241  * timeout was already running at this time and reacts correspondingly.
242  *
243  * affected globals/fields: .allowedTimeouts, remainingTimeoutTime, remainingLeadTime,
244  *                          timeoutInitiator, timeoutStatus, timeoutHandler
245  *
246  * This function is called when a player issues the calltimeout command.
247  */
248 void evaluateTimeout() {
249         if (inWarmupStage && !g_warmup_allow_timeout)
250                 return sprint(self, "^7Error: You can not call a timeout in warmup-stage!\n");
251         if (time < game_starttime )
252                 return sprint(self, "^7Error: You can not call a timeout while the map is being restarted!\n");
253         if (timeoutStatus != 2) {
254                 //if the map uses a timelimit make sure that timeout cannot be called right before the map ends
255                 if (autocvar_timelimit) {
256                         //a timelimit was used
257                         local float myTl;
258                         myTl = autocvar_timelimit;
259
260                         local float lastPossibleTimeout;
261                         lastPossibleTimeout = (myTl*60) - autocvar_sv_timeout_leadtime - 1;
262
263                         if (lastPossibleTimeout < time - game_starttime)
264                                 return sprint(self, "^7Error: It is too late to call a timeout now!\n");
265                 }
266         }
267         //player may not call a timeout if he has no calls left
268         if (self.allowedTimeouts < 1)
269                 return sprint(self, "^7Error: You already used all your timeout calls for this map!\n");
270         //now all required checks are passed
271         self.allowedTimeouts -= 1;
272         bprint(self.netname, " ^7called a timeout (", ftos(self.allowedTimeouts), " timeouts left)!\n"); //write a bprint who started the timeout (and how many he has left)
273         remainingTimeoutTime = autocvar_sv_timeout_length;
274         remainingLeadTime = autocvar_sv_timeout_leadtime;
275         timeoutInitiator = self;
276         if (timeoutStatus == 0) { //if another timeout was already active, don't change its status (which was 1 or 2) to 1, only change it to 1 if no timeout was active yet
277                 timeoutStatus = 1;
278                 //create the timeout indicator which centerprints the information to all players and takes care of pausing/unpausing
279                 timeoutHandler = spawn();
280                 timeoutHandler.think = timeoutHandler_Think;
281         }
282         timeoutHandler.nextthink = time; //always let the entity think asap
283
284         //inform all connected clients about the timeout call
285         Announce("timeoutcalled");
286 }
287
288 /**
289  * Checks whether a player is allowed to resume the game. If he is allowed to do it,
290  * and the lead time for the timeout is still active, this countdown just will be aborted (the
291  * game will never be paused). Otherwise the remainingTimeoutTime will be set to the corresponding
292  * value of the cvar sv_timeout_resumetime.
293  *
294  * This function is called when a player issues the resumegame command.
295  */
296 void evaluateTimein() {
297         if (!timeoutStatus)
298                 return sprint(self, "^7Error: There is no active timeout which could be aborted!\n");
299         if (self != timeoutInitiator)
300                 return sprint(self, "^7Error: You may not abort the active timeout. Only the player who called it can do that!\n");
301         if (timeoutStatus == 1) {
302                 remainingTimeoutTime = timeoutStatus = 0;
303                 timeoutHandler.nextthink = time; //timeoutHandler has to take care of it immediately
304                 bprint(strcat("^7The timeout was aborted by ", self.netname, " !\n"));
305         }
306         else if (timeoutStatus == 2) {
307                 //only shorten the remainingTimeoutTime if it makes sense
308                 if( remainingTimeoutTime > (autocvar_sv_timeout_resumetime + 1) ) {
309                         bprint(strcat("^1Attention: ^7", self.netname, " resumed the game! Prepare for battle!\n"));
310                         remainingTimeoutTime = autocvar_sv_timeout_resumetime;
311                         timeoutHandler.nextthink = time; //timeoutHandler has to take care of it immediately
312                 }
313                 else
314                         sprint(self, "^7Error: Your resumegame call was discarded!\n");
315
316         }
317 }
318
319
320 // =======================
321 //  Command Sub-Functions
322 // =======================
323
324 void ClientCommand_autoswitch(float request, entity client, float argc)
325 {
326         switch(request)
327         {
328                 case CC_REQUEST_HELP:
329                         sprint(client, "  ^2autoswitch^7: Whether or not to switch automatically when getting a better weapon\n");
330                         return;
331                         
332                 case CC_REQUEST_COMMAND:
333                         client.autoswitch = ("0" != argv(1));
334                         sprint(client, strcat("^1autoswitch is currently turned ", (client.autoswitch ? "on" : "off"), ".\n"));
335                         return; // never fall through to usage
336                         
337                 default:
338                 case CC_REQUEST_USAGE:
339                         sprint(client, "\nUsage:^3 cmd autoswitch selection\n");
340                         sprint(client, "  Where 'selection' is 1 or 0 for on or off.\n"); 
341                         return;
342         }
343 }
344
345 void ClientCommand_checkfail(float request, entity client, string command) // used only by client side code
346 {
347         switch(request)
348         {
349                 case CC_REQUEST_HELP:
350                         sprint(client, "  ^2checkfail^7: Report if a client-side check failed\n");
351                         return;
352                         
353                 case CC_REQUEST_COMMAND:
354                         print(sprintf("CHECKFAIL: %s (%s) epically failed check %s\n", client.netname, client.netaddress, substring(command, argv_start_index(1), argv_end_index(-1) - argv_start_index(1))));
355                         client.checkfail = 1;
356                         return; // never fall through to usage
357                         
358                 default:
359                 case CC_REQUEST_USAGE:
360                         sprint(client, "\nUsage:^3 cmd checkfail message\n");
361                         sprint(client, "  Where 'message' is the message reported by client about the fail.\n");
362                         return;
363         }
364 }
365
366 void ClientCommand_clientversion(float request, entity client, float argc) // used only by client side code
367 {
368         switch(request)
369         {
370                 case CC_REQUEST_HELP:
371                         sprint(client, "  ^2clientversion^7: Release version of the game\n");
372                         return;
373                         
374                 case CC_REQUEST_COMMAND:
375                         if (client.flags & FL_CLIENT)
376                         {
377                                 client.version = ((argv(1) == "$gameversion") ? 1 : stof(argv(1)));
378                                 
379                                 if(client.version < autocvar_gameversion_min || client.version > autocvar_gameversion_max)
380                                 {
381                                         client.version_mismatch = 1;
382                                         ClientKill_TeamChange(-2); // observe
383                                 } 
384                                 else if(autocvar_g_campaign || autocvar_g_balance_teams || autocvar_g_balance_teams_force) 
385                                 {
386                                         //JoinBestTeam(self, FALSE, TRUE);
387                                 } 
388                                 else if(teamplay && !autocvar_sv_spectate && !(client.team_forced > 0)) 
389                                 {
390                                         client.classname = "observer"; // really?
391                                         stuffcmd(client, "menu_showteamselect\n");
392                                 }
393                         }
394                         return; // never fall through to usage
395                         
396                 default:
397                 case CC_REQUEST_USAGE:
398                         sprint(client, "\nUsage:^3 cmd clientversion version\n");
399                         sprint(client, "  Where 'version' is the game version reported by client.\n");
400                         return;
401         }
402 }
403
404 void ClientCommand_cvar_changes(float request, entity client)
405 {
406         switch(request)
407         {
408                 case CC_REQUEST_HELP:
409                         sprint(client, "  ^2cvar_changes^7: Prints a list of all changed server cvars\n");
410                         return;
411                         
412                 case CC_REQUEST_COMMAND:
413                         sprint(client, cvar_changes);
414                         return;
415                         
416                 default:
417                 case CC_REQUEST_USAGE:
418                         sprint(client, "\nUsage:^3 sv_cmd cvar_changes\n");
419                         sprint(client, "  No arguments required.\n");
420                         //sprint(client, "See also: ^2cvar_purechanges^7\n");
421                         return;
422         }
423 }
424
425 void ClientCommand_cvar_purechanges(float request, entity client)
426 {
427         switch(request)
428         {
429                 case CC_REQUEST_HELP:
430                         sprint(client, "  ^2cvar_purechanges^7: Prints a list of all changed gameplay cvars\n");
431                         return;
432                         
433                 case CC_REQUEST_COMMAND:
434                         sprint(client, cvar_purechanges);
435                         return;
436                         
437                 default:
438                 case CC_REQUEST_USAGE:
439                         sprint(client, "\nUsage:^3 sv_cmd cvar_purechanges\n");
440                         sprint(client, "  No arguments required.\n");
441                         //sprint(client, "See also: ^2cvar_changes^7\n");
442                         return;
443         }
444 }
445
446
447 // ======================================
448 //  Main Function Called By Engine (cmd)
449 // ======================================
450 // If this function exists, server game code parses clientcommand before the engine code gets it.
451
452 void SV_ParseClientCommand(string command)
453 {
454         float search_request_type;
455         float argc = tokenize_console(command);
456         
457         // for floodcheck
458         switch(strtolower(argv(0)))
459         {
460                 // exempt commands which are not subject to floodcheck
461                 case "begin": break; // handled by engine in host_cmd.c
462                 case "pause": break; // handled by engine in host_cmd.c
463                 case "prespawn": break; // handled by engine in host_cmd.c
464                 case "reportcvar": break; // handled by server in this file
465                 case "sentcvar": break; // handled by server in this file
466                 case "spawn": break; // handled by engine in host_cmd.c
467                 
468                 default: 
469                         if(SV_ParseClientCommand_floodcheck())
470                                 break; // "TRUE": continue, as we're not flooding yet
471                         else
472                                 return; // "FALSE": not allowed to continue, halt
473         }
474         
475         /* NOTE: totally disabled for now, however the functionality and descriptions are there if we ever want it.
476         if(argv(0) == "help") 
477         {
478                 if(argc == 1) 
479                 {
480                         sprint(self, "\nUsage:^3 cmd COMMAND...^7, where possible commands are:\n");
481                         ClientCommand_autoswitch(CC_REQUEST_HELP, self, 0);
482                         ClientCommand_checkfail(CC_REQUEST_HELP, self, "");
483                         clientCommand_clientversion(CC_REQUEST_HELP, self, 0);
484                         ClientCommand_cvar_changes(CC_REQUEST_HELP, self);
485                         ClientCommand_cvar_purechanges(CC_REQUEST_HELP, self);
486                         sprint(self, "For help about specific commands, type cmd help COMMAND\n");
487                         return;
488                 } 
489                 else
490                         search_request_type = CC_REQUEST_USAGE; // Instead of trying to call a command, we're going to see detailed information about it
491         } 
492         else*/ if(GameCommand_Vote(command, self)) 
493         {
494                 return; // handled by server/vote.qc 
495         }
496         else if(GameCommand_MapVote(argv(0))) 
497         {
498                 return; // handled by server/g_world.qc
499         }
500         else if(CheatCommand(argc)) 
501         {
502                 return; // handled by server/cheats.qc
503         }
504         else
505                 search_request_type = CC_REQUEST_COMMAND; // continue as usual and scan for normal commands
506         
507         switch(strtolower((search_request_type == CC_REQUEST_USAGE) ? argv(1) : argv(0)))
508         {
509                 // Do not hard code aliases for these, instead create them in defaultXonotic.cfg
510                 // also: keep in alphabetical order, please ;)
511                 
512                 case "autoswitch": ClientCommand_autoswitch(search_request_type, self, argc); break;
513                 case "checkfail": ClientCommand_checkfail(search_request_type, self, command); break;
514                 case "clientversion": ClientCommand_clientversion(search_request_type, self, argc); break;
515                 case "cvar_changes": ClientCommand_cvar_changes(search_request_type, self); break;
516                 case "cvar_purechanges": ClientCommand_cvar_purechanges(search_request_type, self); break;
517                 
518                 default:
519                         clientcommand(self, command); //print("Invalid command. For a list of supported commands, try cmd help.\n");
520         }
521 }