]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/clientcommands.qc
[ 2555894 ] Ready restart countdown clientside
[divverent/nexuiz.git] / data / qcsrc / server / clientcommands.qc
1 entity nagger;
2 float readycount;
3 float Nagger_SendEntity(entity to, float sendflags)
4 {
5         float nags, i, f, b;
6         entity e;
7         WriteByte(MSG_ENTITY, ENT_CLIENT_NAGGER);
8
9         nags = 0;
10         if(readycount)
11         {
12                 nags |= 1;
13                 if(to.ready == 0)
14                         nags |= 2;
15         }
16         if(votecalled)
17         {
18                 nags |= 4;
19                 if(to.vote_vote == 0)
20                         nags |= 8;
21         }
22         if(inWarmupStage)
23                 nags |= 16;
24
25         if(sendflags & 128)
26                 nags |= 128;
27
28         WriteByte(MSG_ENTITY, nags);
29
30         if(nags & 128)
31         {
32                 if(votecalled)
33                         WriteString(MSG_ENTITY, votecalledvote_display);
34                 else
35                         WriteString(MSG_ENTITY, "");
36         }
37         
38         if(nags & 1)
39         {
40                 for(i = 1; i <= maxclients; i += 8)
41                 {
42                         for(f = 0, e = edict_num(i), b = 1; b < 256; b *= 2, e = nextent(e))
43                                 if(clienttype(e) != CLIENTTYPE_REAL || e.ready)
44                                         f |= b;
45                         WriteByte(MSG_ENTITY, f);
46                 }
47         }
48
49         return TRUE;
50 }
51 void Nagger_Init()
52 {
53         Net_LinkEntity(nagger = spawn(), FALSE, 0, Nagger_SendEntity);
54 }
55 void Nagger_VoteChanged()
56 {
57         if(nagger)
58                 nagger.SendFlags |= 128;
59 }
60 void Nagger_VoteCountChanged()
61 {
62         if(nagger)
63                 nagger.SendFlags |= 1;
64 }
65 void Nagger_ReadyCounted()
66 {
67         if(nagger)
68                 nagger.SendFlags |= 1;
69 }
70
71 void ReadyCount();
72 string MapVote_Suggest(string m);
73
74 entity GetPlayer(string name)
75 {
76         float num;
77         entity e;
78         string ns;
79
80         if(substring(name, 0, 1) == "#") {
81                 num = stof(substring(name, 1, 999));
82                 if(num >= 1 && num <= maxclients) {
83                         for((e = world); num > 0; --num, (e = nextent(e)))
84                                 ;
85                         //if(clienttype(e) == CLIENTTYPE_REAL)
86                         if(e.classname == "player")
87                                 return e;
88                 }
89         } else {
90                 ns = strdecolorize(name);
91                 FOR_EACH_REALPLAYER(e) {
92                         if(!strcasecmp(strdecolorize(e.netname), ns)) {
93                                 return e;
94                         }
95                 }
96         }
97         return world;
98 }
99
100 //float ctf_clientcommand();
101 float readyrestart_happened;
102 void SV_ParseClientCommand(string s) {
103         local string cmd;
104         local float tokens, f, effectnum;
105         local vector start, end;
106
107         tokens = tokenize_sane(s);
108
109         if(GameCommand_Vote(s, self)) {
110                 return;
111         } else if(GameCommand_MapVote(argv(0))) {
112                 return;
113         } else if(argv(0) == "autoswitch") {
114                 // be backwards compatible with older clients (enabled)
115                 self.autoswitch = ("0" != argv(1));
116                 local string autoswitchmsg;
117                 if (self.autoswitch) {
118                         autoswitchmsg = "on";
119                 } else {
120                         autoswitchmsg = "off";
121                 }
122                 sprint(self, strcat("^1autoswitch turned ", autoswitchmsg, "\n"));
123         } else if(argv(0) == "clientversion") {
124                 if not(self.flags & FL_CLIENT)
125                         return;
126                 if (argv(1) == "$gameversion") {
127                         //versionmsg = "^1client is too old to get versioninfo.\nUPDATE!!! (http://www.nexuiz.com)^8";
128                         // either that or someone wants to be funny
129                         self.version = 1;
130                 } else {
131                         self.version = stof(argv(1));
132                 }
133                 if(self.version != cvar("gameversion"))
134                 {
135                         self.classname = "observer";
136                         self.version_mismatch = 1;
137                         PutClientInServer();
138                 } else if(cvar("g_campaign") || cvar("g_balance_teams") || cvar("g_balance_teams_force")) {
139                         //JoinBestTeam(self, FALSE, TRUE);
140                 } else if(cvar("teamplay") && !cvar("sv_spectate")) {
141                         self.classname = "observer";
142                         stuffcmd(self,"menu_showteamselect\n");
143                 }
144         } else if(argv(0) == "reportcvar") { // old system
145                 if(substring(argv(2), 0, 1) == "$") // undefined cvar: use the default value on the server then
146                 {
147                         s = strcat(substring(s, argv_start_index(0), argv_end_index(1)), " \"", cvar_defstring(argv(1)), "\"");
148                         tokens = tokenize_sane(s);
149                 }
150                 GetCvars(1);
151         } else if(argv(0) == "sentcvar") { // new system
152                 if(tokens == 2) // undefined cvar: use the default value on the server then
153                 {
154                         s = strcat(substring(s, argv_start_index(0), argv_end_index(1)), " \"", cvar_defstring(argv(1)), "\"");
155                         tokens = tokenize_sane(s);
156                 }
157                 GetCvars(1);
158         } else if(argv(0) == "spectate") {
159                 if not(self.flags & FL_CLIENT)
160                         return;
161                 if(g_lms || g_arena)
162                         return; // don't allow spectating in lms, unless player runs out of lives
163                 if(self.classname == "player" && cvar("sv_spectate") == 1) {
164                         if(self.flagcarried)
165                                 DropFlag(self.flagcarried, world, world);
166                         kh_Key_DropAll(self, TRUE);
167                         WaypointSprite_PlayerDead();
168                         self.classname = "observer";
169                         if(blockSpectators)
170                                 sprint(self, strcat("^7You have to become a player within the next ", ftos(cvar("g_maxplayers_spectator_blocktime")), " seconds, otherwise you will be kicked, because spectators aren't allowed at this time!\n"));
171                         PutClientInServer();
172                 }
173         } else if(argv(0) == "join") {
174                 if not(self.flags & FL_CLIENT)
175                         return;
176                 if(!g_arena)
177                 if (self.classname != "player" && !lockteams)
178                 {
179                         if(isJoinAllowed()) {
180                                 self.classname = "player";
181                                 PlayerScore_Clear(self);
182                                 bprint ("^4", self.netname, "^4 is playing now\n");
183                                 PutClientInServer();
184                         }
185                         else {
186                                 //player may not join because of g_maxplayers is set
187                                 centerprint_atprio(self, CENTERPRIO_MAPVOTE, PREVENT_JOIN_TEXT);
188                         }
189                 }
190         } else if( argv(0) == "selectteam" ) {
191                 if not(self.flags & FL_CLIENT)
192                         return;
193                 if( !cvar("teamplay") ) {
194                         sprint( self, "selecteam can only be used in teamgames\n");
195                 } else if(cvar("g_campaign")) {
196                         //JoinBestTeam(self, 0);
197                 } else if(lockteams) {
198                         sprint( self, "^7The game has already begun, you must wait until the next map to be able to join a team.\n");
199                 } else if( argv(1) == "red" ) {
200                         DoTeamChange(COLOR_TEAM1);
201                 } else if( argv(1) == "blue" ) {
202                         DoTeamChange(COLOR_TEAM2);
203                 } else if( argv(1) == "yellow" ) {
204                         DoTeamChange(COLOR_TEAM3);
205                 } else if( argv(1) == "pink" ) {
206                         DoTeamChange(COLOR_TEAM4);
207                 } else if( argv(1) == "auto" ) {
208                         DoTeamChange(-1);
209                 } else {
210                         sprint( self, strcat( "selectteam none/red/blue/yellow/pink/auto - \"", argv(1), "\" not recognised\n" ) );
211                 }
212         } else if(argv(0) == "ready") {
213                 if not(self.flags & FL_CLIENT)
214                         return;
215                 if((inWarmupStage && 0 <= g_warmup_limit) // with unlimited warmup players have to be able to restart
216                    || cvar("sv_ready_restart"))
217                 {
218                         if(timeoutStatus) {
219                                 return sprint(self, "^1You cannot reset the game while a timeout is active!\n");
220                         }
221
222                         {
223                                 if(!readyrestart_happened || cvar("sv_ready_restart_repeatable"))
224                                 {
225                                         self.ready = TRUE;
226                                         bprint(self.netname, "^2 is ready\n");
227                                         ReadyCount();
228                                 } else {
229                                         sprint(self, "^1game has already been restarted\n");
230                                 }
231                         }
232                 }
233         } else if(argv(0) == "maplist") {
234                 sprint(self, maplist_reply);
235         } else if(argv(0) == "lsmaps") {
236                 sprint(self, lsmaps_reply);
237         } else if(argv(0) == "records") {
238                 sprint(self, records_reply);
239         } else if(argv(0) == "voice") {
240                 if(tokens >= 3)
241                         VoiceMessage(argv(1), substring(s, argv_start_index(2), argv_end_index(-1) - argv_start_index(2)));
242                 else
243                         VoiceMessage(argv(1), "");
244         } else if(argv(0) == "say") {
245                 if(tokens >= 2)
246                         Say(self, FALSE, substring(s, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)), 1);
247                 //clientcommand(self, formatmessage(s));
248         } else if(argv(0) == "say_team") {
249                 if(tokens >= 2)
250                         Say(self, TRUE, substring(s, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)), 1);
251                 //clientcommand(self, formatmessage(s));
252         } else if(argv(0) == "info") {
253                 cmd = cvar_string(strcat("sv_info_", argv(1)));
254                 if(cmd == "")
255                         sprint(self, "ERROR: unsupported info command\n");
256                 else
257                         wordwrap_sprint(cmd, 1111);
258         } else if(argv(0) == "suggestmap") {
259                 sprint(self, strcat(MapVote_Suggest(argv(1)), "\n"));
260         } else if(argv(0) == "timeout") {
261                 if not(self.flags & FL_CLIENT)
262                         return;
263                 if(cvar("sv_timeout")) {
264                         if(self.classname == "player") {
265                                 if(votecalled)
266                                         sprint(self, "^7Error: you can not call a timeout while a vote is active!\n");
267                                 else
268                                         evaluateTimeout();
269                         }
270                         else
271                                 sprint(self, "^7Error: only players can call a timeout!\n");
272                 }
273         } else if(argv(0) == "timein") {
274                 if not(self.flags & FL_CLIENT)
275                         return;
276                 if(cvar("sv_timeout")) {
277                         evaluateTimein();
278                 }
279         } else if(argv(0) == "teamstatus") {
280                 Score_NicePrint(self);
281         } else if(argv(0) == "cvar_changes") {
282                 sprint(self, cvar_changes);
283         } else if(argv(0) == "pointparticles") {
284                 if(sv_cheats && tokens == 5)
285                 {
286                         // arguments:
287                         //   effectname
288                         //   origin (0..1, on crosshair line)
289                         //   velocity
290                         //   howmany
291                         effectnum = particleeffectnum(argv(1));
292                         f = stof(argv(2));
293                         start = (1-f) * self.origin + f * self.cursor_trace_endpos;
294                         end = stov(argv(3));
295                         f = stof(argv(4));
296                         pointparticles(effectnum, start, end, f);
297                 }
298                 else
299                         sprint(self, "Usage: sv_cheats 1; restart; cmd pointparticles effectname position(0..1) velocityvector multiplier\n");
300         } else if(argv(0) == "trailparticles") {
301                 if(sv_cheats && tokens == 3)
302                 {
303                         // arguments:
304                         //   effectname
305                         //   shot origin vector
306                         effectnum = particleeffectnum(argv(1));
307                         W_SetupShot(self, stov(argv(2)), FALSE, FALSE, "");
308                         traceline(w_shotorg, w_shotorg + w_shotdir * MAX_SHOT_DISTANCE, MOVE_NORMAL, self);
309                         trailparticles(self, effectnum, w_shotorg, trace_endpos);
310                 }
311                 else
312                         sprint(self, "Usage: sv_cheats 1; restart; cmd trailparticles effectname shotorigin\n");
313         } else {
314                 //if(ctf_clientcommand())
315                 //      return;
316                 cmd = argv(0);
317                 // grep for Cmd_AddCommand_WithClientCommand to find them all
318                 if(cmd != "status")
319                 if(cmd != "max")
320                 if(cmd != "monster")
321                 if(cmd != "scrag")
322                 if(cmd != "wraith")
323                 if(cmd != "gimme")
324                 if(cmd != "god")
325                 if(cmd != "notarget")
326                 if(cmd != "fly")
327                 if(cmd != "noclip")
328                 if(cmd != "give")
329                 //if(cmd != "say") // handled above
330                 //if(cmd != "say_team") // handled above
331                 if(cmd != "tell")
332                 if(cmd != "kill")
333                 if(cmd != "pause")
334                 if(cmd != "ping")
335                 if(cmd != "name")
336                 if(cmd != "color")
337                 if(cmd != "rate")
338                 if(cmd != "pmodel")
339                 if(cmd != "playermodel")
340                 if(cmd != "playerskin")
341                 if(cmd != "prespawn")
342                 if(cmd != "spawn")
343                 if(cmd != "begin")
344                 if(cmd != "pings")
345                 if(cmd != "sv_startdownload")
346                 if(cmd != "download")
347                 {
348                         print("WARNING: Invalid clientcommand by ", self.netname, ": ", s, "\n");
349                         return;
350                 }
351                 clientcommand(self,s);
352         }
353 }
354
355 void ReadyRestartForce()
356 {
357         local entity e;
358
359         bprint("^1Server is restarting...\n");
360
361         VoteReset();
362
363         // clear overtime
364         if(checkrules_overtimeend)
365                 checkrules_overtimeend = 0;
366
367         readyrestart_happened = 1;
368         game_starttime = time + RESTART_COUNTDOWN;
369         restart_mapalreadyrestarted = 0; //reset this var, needed when cvar sv_ready_restart_repeatable is in use
370
371         inWarmupStage = 0; //once the game is restarted the game is in match stage
372
373         //reset the .ready status of all players (also spectators)
374         FOR_EACH_CLIENTSLOT(e)
375                 e.ready = 0;
376         readycount = 0;
377         Nagger_ReadyCounted(); // NOTE: this causes a resend of that entity, and will also turn off warmup state on the client
378
379         if(cvar("teamplay_lockonrestart") && teams_matter) {
380                 lockteams = 1;
381                 bprint("^1The teams are now locked.\n");
382         }
383
384         //initiate the restart-countdown-announcer entity
385         if(cvar("sv_ready_restart_after_countdown"))
386         {
387                 restartTimer = spawn();
388                 restartTimer.think = restartTimer_Think;
389                 restartTimer.nextthink = game_starttime;
390         }
391
392         //after a restart every players number of allowed timeouts gets reset, too
393         if(cvar("sv_timeout"))
394         {
395                 FOR_EACH_REALPLAYER(e)
396                         e.allowedTimeouts = cvar("sv_timeout_number");
397         }
398
399         //reset map immediately if this cvar is not set
400         if (!cvar("sv_ready_restart_after_countdown"))
401                 reset_map(TRUE);
402
403         if(cvar("sv_eventlog"))
404                 GameLogEcho(":restart");
405 }
406
407 void ReadyRestart()
408 {
409         // no arena, assault support yet...
410         if(g_arena | g_assault | gameover | intermission_running | race_completing)
411                 localcmd("restart\n");
412
413         // reset ALL scores, but only do that at the beginning 
414         //of the countdown if sv_ready_restart_after_countdown is off!
415         //Otherwise scores could be manipulated during the countdown!
416         if (!cvar("sv_ready_restart_after_countdown"))
417                 Score_ClearAll();
418
419         ReadyRestartForce();
420 }
421
422 /**
423  * Counts how many players are ready. If not enough players are ready, the function
424  * does nothing. If all players are ready, the timelimit will be extended and the
425  * restart_countdown variable is set to allow other functions like PlayerPostThink
426  * to detect that the countdown is now active. If the cvar sv_ready_restart_after_countdown
427  * is not set the map will be resetted.
428  *
429  * Function is called after the server receives a 'ready' sign from a player.
430  */
431 void ReadyCount()
432 {
433         local entity e;
434         local float r, p;
435
436         r = p = 0;
437
438         FOR_EACH_REALPLAYER(e)
439         {
440                 p += 1;
441                 if(e.ready)
442                         r += 1;
443         }
444
445         readycount = r;
446
447         Nagger_ReadyCounted();
448
449         if(r) // at least one is ready
450         if(r == p) // and, everyone is ready
451                 ReadyRestart();
452 }
453
454 /**
455  * Restarts the map after the countdown is over (and cvar sv_ready_restart_after_countdown
456  * is set)
457  */
458 void restartTimer_Think() {
459         restart_mapalreadyrestarted = 1;
460         reset_map(TRUE);
461         remove(self);
462         return;
463 }
464
465 /**
466  * Checks whether the player who calls the timeout is allowed to do so.
467  * If so, it initializes the timeout countdown. It also checks whether another
468  * timeout was already running at this time and reacts correspondingly.
469  *
470  * affected globals/fields: .allowedTimeouts, remainingTimeoutTime, remainingLeadTime,
471  *                          timeoutInitiator, timeoutStatus, timeoutHandler
472  *
473  * This function is called when a player issues the calltimeout command.
474  */
475 void evaluateTimeout() {
476         if (inWarmupStage && !g_warmup_allow_timeout)
477                 return sprint(self, "^7Error: You can not call a timeout in warmup-stage!\n");
478         if (time < game_starttime )
479                 return sprint(self, "^7Error: You can not call a timeout while the map is being restarted!\n");
480         if (timeoutStatus != 2) {
481                 //if the map uses a timelimit make sure that timeout cannot be called right before the map ends
482                 if (cvar("timelimit")) {
483                         //a timelimit was used
484                         local float myTl;
485                         myTl = cvar("timelimit");
486
487                         local float lastPossibleTimeout;
488                         lastPossibleTimeout = (myTl*60) - cvar("sv_timeout_leadtime") - 1;
489
490                         if (lastPossibleTimeout < time - game_starttime)
491                                 return sprint(self, "^7Error: It is too late to call a timeout now!\n");
492                 }
493         }
494         //player may not call a timeout if he has no calls left
495         if (self.allowedTimeouts < 1)
496                 return sprint(self, "^7Error: You already used all your timeout calls for this map!\n");
497         //now all required checks are passed
498         self.allowedTimeouts -= 1;
499         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)
500         remainingTimeoutTime = cvar("sv_timeout_length");
501         remainingLeadTime = cvar("sv_timeout_leadtime");
502         timeoutInitiator = self;
503         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
504                 timeoutStatus = 1;
505                 //create the timeout indicator which centerprints the information to all players and takes care of pausing/unpausing
506                 timeoutHandler = spawn();
507                 timeoutHandler.think = timeoutHandler_Think;
508         }
509         timeoutHandler.nextthink = time; //always let the entity think asap
510
511         //inform all connected clients about the timeout call
512         play2all("announcer/robotic/timeoutcalled.wav");
513 }
514
515 /**
516  * Checks whether a player is allowed to resume the game. If he is allowed to do it,
517  * and the lead time for the timeout is still active, this countdown just will be aborted (the
518  * game will never be paused). Otherwise the remainingTimeoutTime will be set to the corresponding
519  * value of the cvar sv_timeout_resumetime.
520  *
521  * This function is called when a player issues the resumegame command.
522  */
523 void evaluateTimein() {
524         if (!timeoutStatus)
525                 return sprint(self, "^7Error: There is no active timeout which could be aborted!\n");
526         if (self != timeoutInitiator)
527                 return sprint(self, "^7Error: You may not abort the active timeout. Only the player who called it can do that!\n");
528         if (timeoutStatus == 1) {
529                 remainingTimeoutTime = timeoutStatus = 0;
530                 timeoutHandler.nextthink = time; //timeoutHandler has to take care of it immediately
531                 bprint(strcat("^7The timeout was aborted by ", self.netname, " !\n"));
532         }
533         else if (timeoutStatus == 2) {
534                 //only shorten the remainingTimeoutTime if it makes sense
535                 if( remainingTimeoutTime > (cvar("sv_timeout_resumetime") + 1) ) {
536                         bprint(strcat("^1Attention: ^7", self.netname, " resumed the game! Prepare for battle!\n"));
537                         remainingTimeoutTime = cvar("sv_timeout_resumetime");
538                         timeoutHandler.nextthink = time; //timeoutHandler has to take care of it immediately
539                 }
540                 else
541                         sprint(self, "^7Error: Your resumegame call was discarded!\n");
542
543         }
544 }