]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/clientcommands.qc
ready status indicator in scoreboard
[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                         //if(!restartAnnouncer)
223                         {
224                                 if(!readyrestart_happened || cvar("sv_ready_restart_repeatable"))
225                                 {
226                                         self.ready = TRUE;
227                                         bprint(self.netname, "^2 is ready\n");
228                                         ReadyCount();
229                                 } else {
230                                         sprint(self, "^1game has already been restarted\n");
231                                 }
232                         }
233                 }
234         } else if(argv(0) == "maplist") {
235                 sprint(self, maplist_reply);
236         } else if(argv(0) == "lsmaps") {
237                 sprint(self, lsmaps_reply);
238         } else if(argv(0) == "records") {
239                 sprint(self, records_reply);
240         } else if(argv(0) == "voice") {
241                 if(tokens >= 3)
242                         VoiceMessage(argv(1), substring(s, argv_start_index(2), argv_end_index(-1) - argv_start_index(2)));
243                 else
244                         VoiceMessage(argv(1), "");
245         } else if(argv(0) == "say") {
246                 if(tokens >= 2)
247                         Say(self, FALSE, substring(s, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)), 1);
248                 //clientcommand(self, formatmessage(s));
249         } else if(argv(0) == "say_team") {
250                 if(tokens >= 2)
251                         Say(self, TRUE, substring(s, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)), 1);
252                 //clientcommand(self, formatmessage(s));
253         } else if(argv(0) == "info") {
254                 cmd = cvar_string(strcat("sv_info_", argv(1)));
255                 if(cmd == "")
256                         sprint(self, "ERROR: unsupported info command\n");
257                 else
258                         wordwrap_sprint(cmd, 1111);
259         } else if(argv(0) == "suggestmap") {
260                 sprint(self, strcat(MapVote_Suggest(argv(1)), "\n"));
261         } else if(argv(0) == "timeout") {
262                 if not(self.flags & FL_CLIENT)
263                         return;
264                 if(cvar("sv_timeout")) {
265                         if(self.classname == "player") {
266                                 if(votecalled)
267                                         sprint(self, "^7Error: you can not call a timeout while a vote is active!\n");
268                                 else
269                                         evaluateTimeout();
270                         }
271                         else
272                                 sprint(self, "^7Error: only players can call a timeout!\n");
273                 }
274         } else if(argv(0) == "timein") {
275                 if not(self.flags & FL_CLIENT)
276                         return;
277                 if(cvar("sv_timeout")) {
278                         evaluateTimein();
279                 }
280         } else if(argv(0) == "teamstatus") {
281                 Score_NicePrint(self);
282         } else if(argv(0) == "cvar_changes") {
283                 sprint(self, cvar_changes);
284         } else if(argv(0) == "pointparticles") {
285                 if(sv_cheats && tokens == 5)
286                 {
287                         // arguments:
288                         //   effectname
289                         //   origin (0..1, on crosshair line)
290                         //   velocity
291                         //   howmany
292                         effectnum = particleeffectnum(argv(1));
293                         f = stof(argv(2));
294                         start = (1-f) * self.origin + f * self.cursor_trace_endpos;
295                         end = stov(argv(3));
296                         f = stof(argv(4));
297                         pointparticles(effectnum, start, end, f);
298                 }
299                 else
300                         sprint(self, "Usage: sv_cheats 1; restart; cmd pointparticles effectname position(0..1) velocityvector multiplier\n");
301         } else if(argv(0) == "trailparticles") {
302                 if(sv_cheats && tokens == 3)
303                 {
304                         // arguments:
305                         //   effectname
306                         //   shot origin vector
307                         effectnum = particleeffectnum(argv(1));
308                         W_SetupShot(self, stov(argv(2)), FALSE, FALSE, "");
309                         traceline(w_shotorg, w_shotorg + w_shotdir * MAX_SHOT_DISTANCE, MOVE_NORMAL, self);
310                         trailparticles(self, effectnum, w_shotorg, trace_endpos);
311                 }
312                 else
313                         sprint(self, "Usage: sv_cheats 1; restart; cmd trailparticles effectname shotorigin\n");
314         } else {
315                 //if(ctf_clientcommand())
316                 //      return;
317                 cmd = argv(0);
318                 // grep for Cmd_AddCommand_WithClientCommand to find them all
319                 if(cmd != "status")
320                 if(cmd != "max")
321                 if(cmd != "monster")
322                 if(cmd != "scrag")
323                 if(cmd != "wraith")
324                 if(cmd != "gimme")
325                 if(cmd != "god")
326                 if(cmd != "notarget")
327                 if(cmd != "fly")
328                 if(cmd != "noclip")
329                 if(cmd != "give")
330                 //if(cmd != "say") // handled above
331                 //if(cmd != "say_team") // handled above
332                 if(cmd != "tell")
333                 if(cmd != "kill")
334                 if(cmd != "pause")
335                 if(cmd != "ping")
336                 if(cmd != "name")
337                 if(cmd != "color")
338                 if(cmd != "rate")
339                 if(cmd != "pmodel")
340                 if(cmd != "playermodel")
341                 if(cmd != "playerskin")
342                 if(cmd != "prespawn")
343                 if(cmd != "spawn")
344                 if(cmd != "begin")
345                 if(cmd != "pings")
346                 if(cmd != "sv_startdownload")
347                 if(cmd != "download")
348                 {
349                         print("WARNING: Invalid clientcommand by ", self.netname, ": ", s, "\n");
350                         return;
351                 }
352                 clientcommand(self,s);
353         }
354 }
355
356 void ReadyRestartForce()
357 {
358         local entity e;
359
360         bprint("^1Server is restarting...\n");
361
362         VoteReset();
363
364         // clear overtime
365         if(checkrules_overtimeend)
366                 checkrules_overtimeend = 0;
367
368         readyrestart_happened = 1;
369         game_starttime = time + RESTART_COUNTDOWN;
370         restart_mapalreadyrestarted = 0; //reset this var, needed when cvar sv_ready_restart_repeatable is in use
371
372         inWarmupStage = 0; //once the game is restarted the game is in match stage
373
374         //reset the .ready status of all players (also spectators)
375         FOR_EACH_CLIENTSLOT(e)
376                 e.ready = 0;
377         readycount = 0;
378         Nagger_ReadyCounted(); // NOTE: this causes a resend of that entity, and will also turn off warmup state on the client
379
380         if(cvar("teamplay_lockonrestart") && teams_matter) {
381                 lockteams = 1;
382                 bprint("^1The teams are now locked.\n");
383         }
384         
385         //initiate the restart-countdown-announcer entity
386         restartAnnouncer = spawn();
387         restartAnnouncer.think = restartAnnouncer_Think;
388         restartAnnouncer.nextthink = time;
389         restartAnnouncer.spawnflags = !!cvar("sv_ready_restart_after_countdown");
390         
391         //after a restart every players number of allowed timeouts gets reset, too
392         if(cvar("sv_timeout"))
393         {
394                 FOR_EACH_REALPLAYER(e)
395                         e.allowedTimeouts = cvar("sv_timeout_number");
396         }
397
398         //play the prepareforbattle sound to everyone
399         play2all("announcer/robotic/prepareforbattle.wav");
400
401         //reset map immediately if this cvar is not set
402         if (!cvar("sv_ready_restart_after_countdown"))
403                 reset_map(TRUE);
404         
405         if(cvar("sv_eventlog"))
406                 GameLogEcho(":restart");
407 }
408
409 void ReadyRestart()
410 {
411         // no arena, assault support yet...
412         if(g_arena | g_assault | gameover | intermission_running | race_completing)
413                 localcmd("restart\n");
414
415         // reset ALL scores, but only do that at the beginning 
416         //of the countdown if sv_ready_restart_after_countdown is off!
417         //Otherwise scores could be manipulated during the countdown!
418         if (!cvar("sv_ready_restart_after_countdown"))
419                 Score_ClearAll();
420
421         ReadyRestartForce();
422 }
423
424 /**
425  * Counts how many players are ready. If not enough players are ready, the function
426  * does nothing. If all players are ready, the timelimit will be extended and the
427  * restart_countdown variable is set to allow other functions like PlayerPostThink
428  * to detect that the countdown is now active. If the cvar sv_ready_restart_after_countdown
429  * is not set the map will be resetted.
430  * 
431  * Function is called after the server receives a 'ready' sign from a player.
432  */
433 void ReadyCount()
434 {
435         local entity e;
436         local float r, p;
437
438         r = p = 0;
439
440         FOR_EACH_REALPLAYER(e)
441         {
442                 p += 1;
443                 if(e.ready)
444                         r += 1;
445         }
446
447         readycount = r;
448
449         Nagger_ReadyCounted();
450
451         if(r) // at least one is ready
452         if(r == p) // and, everyone is ready
453                 ReadyRestart();
454 }
455
456 /**
457  * Shows the restart countdown for all players.
458  * Plays the countdown sounds for the seconds 3, 2 1, begin for everyone.
459  * Restarts the map after the countdown is over (and cvar sv_ready_restart_after_countdown
460  * is set to 1).
461  */
462 void restartAnnouncer_Think() {
463         local entity plr;
464         local string s;
465         float f, c;
466         c = game_starttime - time;
467         f = floor(0.5 + c);
468         if(c <= 0) { //show the "Begin" message
469                 if(self.spawnflags & 1) // if sv_ready_restart_after_countdown is set
470                 {
471                         if (!g_assault) {
472                                 //don't clear scores for assault mode, which uses ReadyRestartForce (and thus 
473                                 //restartAnnouncer_Think) to start a new round
474                                 Score_ClearAll();
475                         }
476                         restart_mapalreadyrestarted = 1;
477                         reset_map(TRUE);
478                 }
479
480                 FOR_EACH_REALCLIENT(plr) {
481                         if(plr.classname == "player") {
482                                 s = strcat(NEWLINES, "^1Begin!");
483                                 centerprint(plr, s);
484                         }
485                 }
486                 play2all("announcer/robotic/begin.wav");
487
488                 remove(self);
489                 return;
490         }
491         else {
492                 FOR_EACH_REALCLIENT(plr) {
493                         if(plr.classname == "player") {
494                                 s = strcat(NEWLINES, "^1Game starts in ", ftos(f), " seconds");
495                                 centerprint(plr, s);
496                         }
497                 }
498
499                 if(f <= 3) {
500                         play2all(strcat("announcer/robotic/", ftos(f), ".wav"));
501                 }
502                 self.nextthink = game_starttime - (f - 1);
503         }
504 }
505
506 /**
507  * Checks whether the player who calls the timeout is allowed to do so.
508  * If so, it initializes the timeout countdown. It also checks whether another
509  * timeout was already running at this time and reacts correspondingly.
510  *
511  * affected globals/fields: .allowedTimeouts, remainingTimeoutTime, remainingLeadTime,
512  *                          timeoutInitiator, timeoutStatus, timeoutHandler
513  *
514  * This function is called when a player issues the calltimeout command.
515  */
516 void evaluateTimeout() {
517         if (inWarmupStage && !g_warmup_allow_timeout)
518                 return sprint(self, "^7Error: You can not call a timeout in warmup-stage!\n");
519         if (time < game_starttime )
520                 return sprint(self, "^7Error: You can not call a timeout while the map is being restarted!\n");
521         if (timeoutStatus != 2) {
522                 //if the map uses a timelimit make sure that timeout cannot be called right before the map ends
523                 if (cvar("timelimit")) {
524                         //a timelimit was used
525                         local float myTl;
526                         myTl = cvar("timelimit");
527
528                         local float lastPossibleTimeout;
529                         lastPossibleTimeout = (myTl*60) - cvar("sv_timeout_leadtime") - 1;
530
531                         if (lastPossibleTimeout < time - game_starttime)
532                                 return sprint(self, "^7Error: It is too late to call a timeout now!\n");
533                 }
534         }
535         //player may not call a timeout if he has no calls left
536         if (self.allowedTimeouts < 1)
537                 return sprint(self, "^7Error: You already used all your timeout calls for this map!\n");
538         //now all required checks are passed
539         self.allowedTimeouts -= 1;
540         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)
541         remainingTimeoutTime = cvar("sv_timeout_length");
542         remainingLeadTime = cvar("sv_timeout_leadtime");
543         timeoutInitiator = self;
544         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
545                 timeoutStatus = 1;
546                 //create the timeout indicator which centerprints the information to all players and takes care of pausing/unpausing
547                 timeoutHandler = spawn();
548                 timeoutHandler.think = timeoutHandler_Think;
549         }
550         timeoutHandler.nextthink = time; //always let the entity think asap
551
552         //inform all connected clients about the timeout call
553         play2all("announcer/robotic/timeoutcalled.wav");
554 }
555
556 /**
557  * Checks whether a player is allowed to resume the game. If he is allowed to do it,
558  * and the lead time for the timeout is still active, this countdown just will be aborted (the
559  * game will never be paused). Otherwise the remainingTimeoutTime will be set to the corresponding
560  * value of the cvar sv_timeout_resumetime.
561  *
562  * This function is called when a player issues the resumegame command.
563  */
564 void evaluateTimein() {
565         if (!timeoutStatus)
566                 return sprint(self, "^7Error: There is no active timeout which could be aborted!\n");
567         if (self != timeoutInitiator)
568                 return sprint(self, "^7Error: You may not abort the active timeout. Only the player who called it can do that!\n");
569         if (timeoutStatus == 1) {
570                 remainingTimeoutTime = timeoutStatus = 0;
571                 timeoutHandler.nextthink = time; //timeoutHandler has to take care of it immediately
572                 bprint(strcat("^7The timeout was aborted by ", self.netname, " !\n"));
573         }
574         else if (timeoutStatus == 2) {
575                 //only shorten the remainingTimeoutTime if it makes sense
576                 if( remainingTimeoutTime > (cvar("sv_timeout_resumetime") + 1) ) {
577                         bprint(strcat("^1Attention: ^7", self.netname, " resumed the game! Prepare for battle!\n"));
578                         remainingTimeoutTime = cvar("sv_timeout_resumetime");
579                         timeoutHandler.nextthink = time; //timeoutHandler has to take care of it immediately
580                 }
581                 else
582                         sprint(self, "^7Error: Your resumegame call was discarded!\n");
583
584         }
585 }