]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/vote.qc
bugfix target_spawn, and change docs to match it
[divverent/nexuiz.git] / data / qcsrc / server / vote.qc
1 float VoteCheckNasty(string cmd)
2 {
3         if(strstrofs(cmd, ";", 0) >= 0)
4                 return TRUE;
5         if(strstrofs(cmd, "\n", 0) >= 0)
6                 return TRUE;
7         if(strstrofs(cmd, "\r", 0) >= 0)
8                 return TRUE;
9         if(strstrofs(cmd, "$", 0) >= 0)
10                 return TRUE;
11         return FALSE;
12 }
13
14 entity GetKickVoteVictim(string vote, string cmd, entity caller)
15 {
16         float tokens;
17         float n, t;
18         string ns;
19         entity e;
20
21         tokens = tokenize_sane(vote);
22         ns = "";
23
24         if(tokens >= 2)
25                 if(substring(argv(1), 0, 1) == "#")
26                 {
27                         ns = substring(argv(1), 1, 999);
28                         t = 2;
29                 }
30
31         if(tokens >= 3)
32                 if(argv(1) == "#")
33                 {
34                         ns = argv(2);
35                         t = 3;
36                 }
37
38         if(ns != "")
39         {
40                 if(t < tokens)
41                         GetKickVoteVictim_reason = substring(vote, argv_start_index(t), argv_end_index(-1) - argv_start_index(t));
42                 else
43                         GetKickVoteVictim_reason = "";
44
45                 n = stof(ns);
46                 if(ns == ftos(n)) if(n >= 1) if(n <= maxclients)
47                 {
48                         e = edict_num(n);
49                         if(clienttype(e) == CLIENTTYPE_REAL)
50                         {
51                                 GetKickVoteVictim_newcommand = strcat(argv(0), " # ", ns);
52                                 return e;
53                         }
54                 }
55         }
56
57         print_to(caller, strcat("Usage: ", cmd, " ", argv(0), " #playernumber (as in \"status\")\n"));
58         return world;
59 }
60
61 string RemapVote_display;
62 string RemapVote_vote;
63 float RemapVote(string vote, string cmd, entity e)
64 {
65         float vote_argc;
66         entity victim;
67         vote_argc = tokenize_sane(vote);
68
69         print(">>", vote, "<<\n");
70
71         if(!VoteAllowed(argv(0)))
72         {
73                 return FALSE;
74         }
75
76         // VoteAllowed tokenizes!
77         vote_argc = tokenize_sane(vote);
78
79         // remap chmap to gotomap (forces intermission)
80         if(vote_argc < 2)
81                 if(argv(0) == "chmap" || argv(0) == "gotomap" || argv(0) == "kick" || argv(0) == "kickban") // won't work without arguments
82                         return FALSE;
83         if(argv(0) == "chmap")
84         {
85                 vote = strcat("gotomap ", substring(vote, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)));
86                 vote_argc = tokenize_sane(vote);
87         }
88         if(argv(0) == "gotomap")
89         {
90                 if(!(vote = ValidateMap(substring(vote, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)), e)))
91                         return FALSE;
92                 vote = strcat("gotomap ", vote);
93                 vote_argc = tokenize_sane(vote); // ValidateMap may have done some stuff to it
94         }
95
96         // make kick and kickban votes a bit nicer (and reject them if formatted badly)
97         if(argv(0) == "kick" || argv(0) == "kickban")
98         {
99                 if(!(victim = GetKickVoteVictim(vote, cmd, e)))
100                         return FALSE;
101                 RemapVote_vote = GetKickVoteVictim_newcommand;
102                 RemapVote_display = strcat("^1", vote, " (^7", victim.netname, "^1): ", GetKickVoteVictim_reason);
103         }
104         else
105         {
106                 RemapVote_vote = vote;
107                 RemapVote_display = strzone(strcat("^1", vote));
108         }
109
110         return TRUE;
111 }
112
113 float GameCommand_Vote(string s, entity e) {
114         float argc;
115         argc = tokenize_sane(s);
116         if(argv(0) == "help") {
117                 print_to(e, "  vote COMMANDS ARGUMENTS. See 'vote help' for more info.");
118                 return TRUE;
119         } else if(argv(0) == "vote") {
120                 if(argv(1) == "") {
121                         print_to(e, "^1You have to supply a vote command. See help for more info.");
122                 } else if(argv(1) == "help") {
123                         VoteHelp(e);
124                 } else if(argv(1) == "status") {
125                         if(votecalled) {
126                                 print_to(e, strcat("^7Vote for ", votecalledvote_display, "^7 called by ^7", VoteNetname(votecaller), "^7."));
127                         } else {
128                                 print_to(e, "^1No vote called.");
129                         }
130                 } else if(argv(1) == "call") {
131                         if(!e || cvar("sv_vote_call")) {
132                                 if(cvar("sv_vote_nospectators") && e.classname != "player") {
133                                         print_to(e, "^1Error: Only players can call a vote."); // TODO invent a cvar name for allowing votes by spectators during warmup anyway
134                                 }
135                                 else if(timeoutStatus) { //don't allow a vote call during a timeout
136                                         print_to(e, "^1Error: You can not call a vote while a timeout is active.");
137                                 }
138                                 else if(votecalled) {
139                                         print_to(e, "^1There is already a vote called.");
140                                 } else {
141                                         local string vote;
142                                         vote = VoteParse(s, argc);
143                                         if(vote == "") {
144                                                 print_to(e, "^1Your vote is empty. See help for more info.");
145                                         } else if(e
146                                                 && time < e.vote_next) {
147                                                         print_to(e, strcat("^1You have to wait ^2", ftos(e.vote_next - time), "^1 seconds before you can again call a vote."));
148                                         } else if(VoteCheckNasty(vote)) {
149                                                 print_to(e, "Syntax error in command. See help for more info.");
150                                         } else if(RemapVote(vote, "vcall", e)) {
151                                                 votecalledvote = strzone(RemapVote_vote);
152                                                 votecalledvote_display = strzone(RemapVote_display);
153                                                 votecalled = TRUE;
154                                                 votecalledmaster = FALSE;
155                                                 votefinished = time + cvar("sv_vote_timeout");
156                                                 votecaller = e; // remember who called the vote
157                                                 if(e) {
158                                                         e.vote_vote = 1; // of course you vote yes
159                                                         e.vote_next = time + cvar("sv_vote_wait");
160                                                 }
161                                                 bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2 calls a vote for ", votecalledvote_display, "\n");
162                                                 if(cvar("sv_eventlog"))
163                                                         GameLogEcho(strcat(":vote:vcall:", ftos(votecaller.playerid), ":", votecalledvote_display));
164                                                 VoteCount(); // needed if you are the only one
165                                                 Nagger_VoteChanged();
166                                         } else {
167                                                 print_to(e, "^1This vote is not ok. See help for more info.");
168                                         }
169                                 }
170                         } else {
171                                 print_to(e, "^1Vote calling is NOT allowed.");
172                         }
173                 } else if(argv(1) == "stop") {
174                         if(!votecalled) {
175                                 print_to(e, "^1No vote called.");
176                         } else if(e == votecaller) { // the votecaller can stop a vote
177                                 VoteStop(e);
178                         } else if(!e) { // server admin / console can too
179                                 VoteStop(e);
180                         } else if(e.vote_master) { // masters can too
181                                 VoteStop(e);
182                         } else {
183                                 print_to(e, "^1You are not allowed to stop that Vote.");
184                         }
185                 } else if(argv(1) == "master") {
186                         if(cvar("sv_vote_master")) {
187                                 if(votecalled) {
188                                         print_to(e, "^1There is already a vote called.");
189                                 } else {
190                                         votecalled = TRUE;
191                                         votecalledmaster = TRUE;
192                                         votecalledvote = strzone("XXX");
193                                         votecalledvote_display = strzone("^3master");
194                                         votefinished = time + cvar("sv_vote_timeout");
195                                         votecaller = e; // remember who called the vote
196                                         if(e) {
197                                                 e.vote_vote = 1; // of course you vote yes
198                                                 e.vote_next = time + cvar("sv_vote_wait");
199                                         }
200                                         bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2 calls a vote to become ^3master^2.\n");
201                                         if(cvar("sv_eventlog"))
202                                                 GameLogEcho(strcat(":vote:vcall:", ftos(votecaller.playerid), ":", votecalledvote_display));
203                                         VoteCount(); // needed if you are the only one
204                                         Nagger_VoteChanged();
205                                 }
206                         } else {
207                                 print_to(e, "^1Vote to become master is NOT allowed.");
208                         }
209                 } else if(argv(1) == "do") {
210                         if(!e || e.vote_master) {
211                                 local string dovote;
212                                 dovote = VoteParse(s, argc);
213                                 if(dovote == "") {
214                                         print_to(e, "^1Your command was empty. See help for more info.");
215                                 } else if(VoteCheckNasty(dovote)) {
216                                         print_to(e, "Syntax error in command. See help for more info.");
217                                 } else if(RemapVote(dovote, "vdo", e)) { // strcat seems to be necessary
218                                         bprint("\{1}^2* ^3", VoteNetname(e), "^2 used his ^3master^2 status to do \"^2", RemapVote_display, "^2\".\n");
219                                         if(cvar("sv_eventlog"))
220                                                 GameLogEcho(strcat(":vote:vdo:", ftos(e.playerid), ":", RemapVote_display));
221                                         localcmd(strcat(RemapVote_vote, "\n"));
222                                 } else {
223                                         print_to(e, "^1This command is not ok. See help for more info.");
224                                 }
225                         } else {
226                                 print_to(e, "^1You are NOT a master.  You might need to login or vote to become master first. See help for more info.");
227                         }
228                 } else if(argv(1) == "login") {
229                         local string masterpwd;
230                         masterpwd = cvar_string("sv_vote_master_password");
231                         if(masterpwd != "") {
232                                 local float granted;
233                                 granted = (masterpwd == argv(2));
234                                 if (e)
235                                         e.vote_master = granted;
236                                 if(granted) {
237                                         print("Accepted master login from ", VoteNetname(e), "\n");
238                                         bprint("\{1}^2* ^3", VoteNetname(e), "^2 logged in as ^3master^2\n");
239                                         if(cvar("sv_eventlog"))
240                                                 GameLogEcho(strcat(":vote:vlogin:", ftos(e.playerid)));
241                                 }
242                                 else
243                                         print("REJECTED master login from ", VoteNetname(e), "\n");
244                         }
245                         else
246                                 print_to(e, "^1Login to become master is NOT allowed.");
247                 } else if(argv(1) == "yes") {
248                         if(!votecalled) {
249                                 print_to(e, "^1No vote called.");
250                         } else if (!e) {
251                                 print_to(e, "^1You can't vote from the server console.");
252                         } else if(e.vote_vote == 0
253                                   || cvar("sv_vote_change")) {
254                                 print_to(e, "^1You accepted the vote.");
255                                 e.vote_vote = 1;
256                                 centerprint_expire(e, CENTERPRIO_VOTE);
257                                 if(!cvar("sv_vote_singlecount")) {
258                                         VoteCount();
259                                 }
260                         } else {
261                                 print_to(e, "^1You have already voted.");
262                         }
263                 } else if(argv(1) == "no") {
264                         if(!votecalled) {
265                                 print_to(e, "^1No vote called.");
266                         } else if (!e) {
267                                 print_to(e, "^1You can't vote from the server console.");
268                         } else if(e.vote_vote == 0
269                                   || cvar("sv_vote_change")) {
270                                 print_to(e, "^1You rejected the vote.");
271                                 e.vote_vote = -1;
272                                 centerprint_expire(e, CENTERPRIO_VOTE);
273                                 if(!cvar("sv_vote_singlecount")) {
274                                         VoteCount();
275                                 }
276                         } else {
277                                 print_to(e, "^1You have already voted.");
278                         }
279                 } else if(argv(1) == "abstain" || argv(1) == "dontcare") {
280                         if(!votecalled) {
281                                 print_to(e, "^1No vote called.");
282                         } else if (!e) {
283                                 print_to(e, "^1You can't vote from the server console.");
284                         } else if(e.vote_vote == 0
285                                   || cvar("sv_vote_change")) {
286                                 print_to(e, "^1You abstained from your vote.");
287                                 e.vote_vote = -2;
288                                 centerprint_expire(e, CENTERPRIO_VOTE);
289                                 if(!cvar("sv_vote_singlecount")) {
290                                         VoteCount();
291                                 }
292                         } else {
293                                 print_to(e, "^1You have already voted.");
294                         }
295                 } else {
296                         // ignore this?
297                         print_to(e, "^1Unknown vote command.");
298                 }
299                 return TRUE;
300         }
301         return FALSE;
302 }
303
304 void VoteHelp(entity e) {
305         local string vmasterdis;
306         if(!cvar("sv_vote_master")) {
307                 vmasterdis = " ^1(disabled)";
308         }
309
310         local string vlogindis;
311         if("" == cvar_string("sv_vote_master_password")) {
312                 vlogindis = " ^1(disabled)";
313         }
314
315         local string vcalldis;
316         if(!cvar("sv_vote_call")) {
317                 vcalldis = " ^1(disabled)";
318         }
319
320         print_to(e, "^7You can use voting with \"^2cmd vote help^7\" \"^2cmd vote status^7\" \"^2cmd vote call ^3COMMAND ARGUMENTS^7\" \"^2cmd vote stop^7\" \"^2cmd vote master^7\" \"^2cmd vote login^7\" \"^2cmd vote do ^3COMMAND ARGUMENTS^7\" \"^2cmd vote yes^7\" \"^2cmd vote no^7\" \"^2cmd vote abstain^7\" \"^2cmd vote dontcare^7\".");
321         print_to(e, "^7Or if your version is up to date you can use these aliases \"^2vhelp^7\" \"^2vstatus^7\" \"^2vcall ^3COMMAND ARGUMENTS^7\" \"^2vstop^7\" \"^2vmaster^7\" \"^2vlogin^7\" \"^2vdo ^3COMMAND ARGUMENTS^7\" \"^2vyes^7\" \"^2vno^7\" \"^2abstain^7\" \"^2vdontcare^7\".");
322         print_to(e, "^7\"^2help^7\" shows this info.");
323         print_to(e, "^7\"^2status^7\" shows if there is a vote called and who called it.");
324         print_to(e, strcat("^7\"^2call^7\" is used to call a vote. See the list of allowed commands.", vcalldis, "^7"));
325         print_to(e, "^7\"^2stop^7\" can be used by the vote caller or an admin to stop a vote and maybe correct it.");
326         print_to(e, strcat("^7\"^2master^7\" call a vote to become master who can execute commands without a vote", vmasterdis, "^7"));
327         print_to(e, strcat("^7\"^2login^7\" login to become master who can execute commands without a vote.", vlogindis, "^7"));
328         print_to(e, "^7\"^2do^7\" executes a command if you are a master. See the list of allowed commands.");
329         print_to(e, "^7\"^2yes^7\", \"^2no^7\", \"^2abstain^7\" and \"^2dontcare^7\" to make your vote.");
330         print_to(e, "^7If enough of the players vote yes the vote is accepted.");
331         print_to(e, "^7If enough of the players vote no the vote is rejected.");
332         print_to(e, strcat("^7If neither the vote will timeout after ", cvar_string("sv_vote_timeout"), "^7 seconds."));
333         print_to(e, "^7You can call a vote for or execute these commands:");
334         print_to(e, strcat("^3", cvar_string("sv_vote_commands"), "^7 and maybe further ^3arguments^7"));
335 }
336
337 string VoteNetname(entity e)
338 {
339         if(e) {
340                 return e.netname;
341         } else {
342                 if(cvar_string("sv_adminnick") != "") {
343                         return cvar_string("sv_adminnick");
344                 } else {
345                         return cvar_string("hostname");
346                 }
347         }
348 }
349
350 string ValidateMap(string m, entity e)
351 {
352         m = MapInfo_FixName(m);
353         if(!m)
354         {
355                 print_to(e, "This map is not available on this server.");
356                 return string_null;
357         }
358         if(!cvar("sv_vote_override_mostrecent"))
359                 if(Map_IsRecent(m))
360                 {
361                         print_to(e, "This server does not allow for recent maps to be played again. Please be patient for some rounds.");
362                         return string_null;
363                 }
364         if(!MapInfo_CheckMap(m))
365         {
366                 print_to(e, strcat("^1Invalid mapname, \"^3", m, "^1\" does not support the current game mode."));
367                 return string_null;
368         }
369
370         return m;
371 }
372
373
374 void VoteThink() {
375         if(votefinished > 0) // a vote was called
376         if(time > votefinished) // time is up
377         {
378                 VoteCount();
379         }
380 }
381
382 string VoteParse(string all, float argc) {
383         if(argc < 3)
384                 return "";
385         return substring(all, argv_start_index(2), argv_end_index(-1) - argv_start_index(2));
386 }
387
388 float VoteAllowed(string votecommand) {
389         tokenize_sane(cvar_string("sv_vote_commands"));
390         local float index;
391         index = 0;
392         while(argv(index) != "") {
393                 if(votecommand == argv(index)) {
394                         return TRUE;
395                 }
396                 ++index;
397         }
398         return FALSE;
399 }
400
401 void VoteReset() {
402         local entity player;
403
404         FOR_EACH_CLIENT(player)
405         {
406                 player.vote_vote = 0;
407                 centerprint_expire(player, CENTERPRIO_VOTE);
408         }
409
410         if(votecalled)
411         {
412                 strunzone(votecalledvote);
413                 strunzone(votecalledvote_display);
414         }
415
416         votecalled = FALSE;
417         votecalledmaster = FALSE;
418         votefinished = 0;
419         Nagger_VoteChanged();
420 }
421
422 void VoteAccept() {
423         bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ^1", votecalledvote_display, "^2 was accepted\n");
424         if(votecalledmaster)
425         {
426                 if(votecaller) {
427                         votecaller.vote_master = 1;
428                 }
429         } else {
430                 localcmd(strcat(votecalledvote, "\n"));
431         }
432         if(votecaller) {
433                 votecaller.vote_next = 0; // people like your votes,
434                                           // no wait for next vote
435         }
436         VoteReset();
437 }
438
439 void VoteReject() {
440         bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ", votecalledvote_display, "^2 was rejected\n");
441         VoteReset();
442 }
443
444 void VoteTimeout() {
445         bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ", votecalledvote_display, "^2 timed out\n");
446         VoteReset();
447 }
448
449 void VoteStop(entity stopper) {
450         bprint("\{1}^2* ^3", VoteNetname(stopper), "^2 stopped ^3", VoteNetname(votecaller), "^2's vote\n");
451         if(cvar("sv_eventlog"))
452                 GameLogEcho(strcat(":vote:vstop:", ftos(stopper.playerid)));
453         if(stopper == votecaller) {
454                 // no wait for next vote so you can correct your vote
455                 if(votecaller) {
456                         votecaller.vote_next = 0;
457                 }
458         }
459         VoteReset();
460 }
461
462 void VoteSpam(float yescount, float nocount, float abstaincount, float notvoters, float mincount, string result)
463 {
464         string s;
465         if(mincount >= 0)
466         {
467                 s = strcat("\{1}^2* vote results: ^1", ftos(yescount), "^2:^1");
468                 s = strcat(s, ftos(nocount), "^2 (^1");
469                 s = strcat(s, ftos(mincount), "^2 needed), ^1");
470                 s = strcat(s, ftos(abstaincount), "^2 didn't care, ^1");
471                 s = strcat(s, ftos(notvoters), "^2 didn't vote\n");
472         }
473         else
474         {
475                 s = strcat("\{1}^2* vote results: ^1", ftos(yescount), "^2:^1");
476                 s = strcat(s, ftos(nocount), "^2, ^1");
477                 s = strcat(s, ftos(abstaincount), "^2 didn't care, ^1");
478                 s = strcat(s, ftos(notvoters), "^2 didn't have to vote\n");
479         }
480         bprint(s);
481         if(cvar("sv_eventlog"))
482         {
483                 s = strcat(":vote:v", result, ":", ftos(yescount));
484                 s = strcat(s, ":", ftos(nocount));
485                 s = strcat(s, ":", ftos(abstaincount));
486                 s = strcat(s, ":", ftos(notvoters));
487                 s = strcat(s, ":", ftos(mincount));
488                 GameLogEcho(s);
489         }
490 }
491
492 void VoteCount() {
493         local float playercount;
494         playercount = 0;
495         local float yescount;
496         yescount = 0;
497         local float nocount;
498         nocount = 0;
499         local float abstaincount;
500         abstaincount = 0;
501         local entity player;
502         //same for real players
503         local float realplayercount;
504         local float realplayeryescount;
505         local float realplayernocount;
506         local float realplayerabstaincount;
507         realplayercount = realplayernocount = realplayerabstaincount = realplayeryescount = 0;
508
509         FOR_EACH_REALCLIENT(player)
510         {
511                 if(player.vote_vote == -1) {
512                         ++nocount;
513                 } else if(player.vote_vote == 1) {
514                         ++yescount;
515                 } else if(player.vote_vote == -2) {
516                         ++abstaincount;
517                 }
518                 ++playercount;
519                 //do the same for real players
520                 if(player.classname == "player") {
521                         if(player.vote_vote == -1) {
522                                 ++realplayernocount;
523                         } else if(player.vote_vote == 1) {
524                                 ++realplayeryescount;
525                         } else if(player.vote_vote == -2) {
526                                 ++realplayerabstaincount;
527                         }
528                         ++realplayercount;
529                 }
530         }
531
532         //in tournament mode, if we have at least one player then don't make the vote dependent on spectators (so specs don't have to press F1)
533         if(cvar("sv_vote_nospectators"))
534         if(realplayercount > 0) {
535                 yescount = realplayeryescount;
536                 nocount = realplayernocount;
537                 abstaincount = realplayerabstaincount;
538                 playercount = realplayercount;
539         }
540
541
542         if(votecalledmaster
543            && playercount == 1) {
544                 // if only one player is on the server becoming vote
545                 // master is not allowed.  This could be used for
546                 // trolling or worse. 'self' is the user who has
547                 // called the vote because this function is called
548                 // by SV_ParseClientCommand. Maybe all voting should
549                 // be disabled for a single player?
550                 print_to(votecaller, "^1You are the only player on this server so you can not become vote master.");
551                 if(votecaller) {
552                         votecaller.vote_next = 0;
553                 }
554                 VoteReset();
555         } else {
556                 float votefactor;
557                 votefactor = bound(0.5, cvar("sv_vote_majority_factor"), 0.999);
558                 if(yescount > (playercount - abstaincount) * votefactor)
559                 {
560                         VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, -1, "yes");
561                         VoteAccept();
562                 }
563                 else if(nocount >= (playercount - abstaincount) * (1 - votefactor)) // that means, yescount cannot reach minyes any more
564                 {
565                         VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, -1, "no");
566                         VoteReject();
567                 }
568                 else if(time > votefinished)
569                 {
570                         if(cvar("sv_vote_simple_majority"))
571                         {
572                                 string result;
573                                 if(yescount > (yescount + nocount) * votefactor)
574                                         result = "yes";
575                                 else if(yescount + nocount > 0)
576                                         result = "no";
577                                 else
578                                         result = "timeout";
579                                 VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, floor((yescount + nocount) * votefactor) + 1, result);
580                                 if(result == "yes")
581                                         VoteAccept();
582                                 else if(result == "no")
583                                         VoteReject();
584                                 else
585                                         VoteTimeout();
586                         }
587                         else
588                         {
589                                 VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, floor((playercount - abstaincount) * votefactor) + 1, "timeout");
590                                 VoteTimeout();
591                         }
592                 }
593         }
594
595         Nagger_VoteCountChanged();
596 }