1 // =============================================
2 // Server side voting code, reworked by Samual
3 // Last updated: December 4th, 2011
4 // =============================================
6 #define VC_REQUEST_COMMAND 1
7 #define VC_REQUEST_USAGE 2
9 #define VC_ASGNMNT_BOTH 1
10 #define VC_ASGNMNT_CLIENTONLY 2
11 #define VC_ASGNMNT_SERVERONLY 3
14 // ============================
15 // Misc. Supporting Functions
16 // ============================
18 float Votecommand_check_assignment(entity caller, float assignment)
20 float from_server = (!caller);
22 if((assignment == VC_ASGNMNT_BOTH)
23 || ((!from_server && assignment == VC_ASGNMNT_CLIENTONLY)
24 || (from_server && assignment == VC_ASGNMNT_SERVERONLY)))
26 print("check_assignment returned true\n");
30 print("check_assignment returned false\n");
34 string VoteCommand_getprefix(entity caller)
42 float Nagger_SendEntity(entity to, float sendflags)
46 WriteByte(MSG_ENTITY, ENT_CLIENT_NAGGER);
50 // 2 = player needs to ready up
52 // 8 = player needs to vote
80 if(!(nags & 4)) // no vote called? send no string
83 WriteByte(MSG_ENTITY, nags);
87 WriteByte(MSG_ENTITY, vote_yescount);
88 WriteByte(MSG_ENTITY, vote_nocount);
89 WriteByte(MSG_ENTITY, vote_needed_absolute);
90 WriteChar(MSG_ENTITY, to.vote_vote);
94 WriteString(MSG_ENTITY, votecalledvote_display);
98 for(i = 1; i <= maxclients; i += 8)
100 for(f = 0, e = edict_num(i), b = 1; b < 256; b *= 2, e = nextent(e))
101 if(clienttype(e) != CLIENTTYPE_REAL || e.ready)
103 WriteByte(MSG_ENTITY, f);
112 Net_LinkEntity(nagger = spawn(), FALSE, 0, Nagger_SendEntity);
115 void Nagger_VoteChanged()
118 nagger.SendFlags |= 128;
121 void Nagger_VoteCountChanged()
124 nagger.SendFlags |= 64;
127 void Nagger_ReadyCounted()
130 nagger.SendFlags |= 1;
133 void ReadyRestartForce()
137 bprint("^1Server is restarting...\n");
142 if (checkrules_overtimesadded > 0 && g_race_qualifying != 2) {
143 //we have to decrease timelimit to its original value again!!
145 newTL = autocvar_timelimit;
146 newTL -= checkrules_overtimesadded * autocvar_timelimit_overtime;
147 cvar_set("timelimit", ftos(newTL));
150 checkrules_suddendeathend = checkrules_overtimesadded = checkrules_suddendeathwarning = 0;
153 readyrestart_happened = 1;
154 game_starttime = time;
155 if(!g_ca && !g_arena)
156 game_starttime += RESTART_COUNTDOWN;
157 restart_mapalreadyrestarted = 0; //reset this var, needed when cvar sv_ready_restart_repeatable is in use
159 inWarmupStage = 0; //once the game is restarted the game is in match stage
161 //reset the .ready status of all players (also spectators)
162 FOR_EACH_CLIENTSLOT(e)
165 Nagger_ReadyCounted(); // NOTE: this causes a resend of that entity, and will also turn off warmup state on the client
167 if(autocvar_teamplay_lockonrestart && teamplay) {
169 bprint("^1The teams are now locked.\n");
172 //initiate the restart-countdown-announcer entity
173 if(autocvar_sv_ready_restart_after_countdown && !g_ca && !g_arena)
175 restartTimer = spawn();
176 restartTimer.think = restartTimer_Think;
177 restartTimer.nextthink = game_starttime;
180 //after a restart every players number of allowed timeouts gets reset, too
181 if(autocvar_sv_timeout)
183 FOR_EACH_REALPLAYER(e)
184 e.allowedTimeouts = autocvar_sv_timeout_number;
187 //reset map immediately if this cvar is not set
188 if (!autocvar_sv_ready_restart_after_countdown)
191 if(autocvar_sv_eventlog)
192 GameLogEcho(":restart");
197 // no arena, assault support yet...
198 if(g_arena | g_assault | gameover | intermission_running | race_completing)
199 localcmd("restart\n");
201 localcmd("\nsv_hook_gamerestart\n");
205 // reset ALL scores, but only do that at the beginning
206 //of the countdown if sv_ready_restart_after_countdown is off!
207 //Otherwise scores could be manipulated during the countdown!
208 if (!autocvar_sv_ready_restart_after_countdown)
213 * Counts how many players are ready. If not enough players are ready, the function
214 * does nothing. If all players are ready, the timelimit will be extended and the
215 * restart_countdown variable is set to allow other functions like PlayerPostThink
216 * to detect that the countdown is now active. If the cvar sv_ready_restart_after_countdown
217 * is not set the map will be resetted.
219 * Function is called after the server receives a 'ready' sign from a player.
228 FOR_EACH_REALPLAYER(e)
237 Nagger_ReadyCounted();
239 if(r) // at least one is ready
240 if(r == p) // and, everyone is ready
245 * Restarts the map after the countdown is over (and cvar sv_ready_restart_after_countdown
248 void restartTimer_Think() {
249 restart_mapalreadyrestarted = 1;
256 float VoteCheckNasty(string cmd)
258 if(strstrofs(cmd, ";", 0) >= 0)
260 if(strstrofs(cmd, "\n", 0) >= 0)
262 if(strstrofs(cmd, "\r", 0) >= 0)
264 if(strstrofs(cmd, "$", 0) >= 0)
269 string GetKickVoteVictim_newcommand;
270 string GetKickVoteVictim_reason;
272 entity GetKickVoteVictim(string vote, string cmd, entity caller)
279 tokens = tokenize_console(vote);
282 e = GetCommandPlayerSlotTargetFromTokenizedCommand(tokens, 1);
285 if(ParseCommandPlayerSlotTarget_firsttoken < tokens)
286 GetKickVoteVictim_reason = substring(vote, argv_start_index(ParseCommandPlayerSlotTarget_firsttoken), argv_end_index(-1) - argv_start_index(ParseCommandPlayerSlotTarget_firsttoken));
288 GetKickVoteVictim_reason = "";
291 if(cmd != "vdo" || GetKickVoteVictim_reason == "")
292 reason = "~"; // by convention, ~ prefixes a "unverified" kickban which will not be networked
294 if(substring(GetKickVoteVictim_reason, 0, 1) == "~")
297 GetKickVoteVictim_reason = substring(GetKickVoteVictim_reason, 1, strlen(GetKickVoteVictim_reason) - 1);
301 reason = strcat(reason, "player ", strdecolorize(caller.netname));
303 reason = strcat(reason, "console vote");
304 if(GetKickVoteVictim_reason != "")
305 reason = strcat(reason, ": ", strdecolorize(GetKickVoteVictim_reason));
307 if not(cvar_value_issafe(reason))
308 reason = uri_escape(reason);
310 GetKickVoteVictim_newcommand = strcat(argv(0), " # ", ftos(num_for_edict(e)));
311 if(argv(0) == "kickban")
313 GetKickVoteVictim_newcommand = strcat(GetKickVoteVictim_newcommand, " ", ftos(autocvar_g_ban_default_bantime), " ", ftos(autocvar_g_ban_default_masksize), " ", reason);
315 else if(argv(0) == "kick")
317 GetKickVoteVictim_newcommand = strcat(GetKickVoteVictim_newcommand, " ", reason);
322 print_to(caller, strcat("Usage: ", cmd, " ", argv(0), " #playernumber (as in \"status\")\n"));
326 string RemapVote_display;
327 string RemapVote_vote;
328 float RemapVote(string vote, string cmd, entity e)
332 vote_argc = tokenize_console(vote);
334 if(!VoteAllowed(argv(0), cmd))
337 // VoteAllowed tokenizes!
338 vote_argc = tokenize_console(vote);
340 // remap chmap to gotomap (forces intermission)
342 if(argv(0) == "chmap" || argv(0) == "gotomap" || argv(0) == "kick" || argv(0) == "kickban") // won't work without arguments
344 if(argv(0) == "chmap")
346 vote = strcat("gotomap ", substring(vote, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)));
347 vote_argc = tokenize_console(vote);
349 if(argv(0) == "gotomap")
351 if(!(vote = ValidateMap(substring(vote, argv_start_index(1), argv_end_index(-1) - argv_start_index(1)), e)))
353 vote = strcat("gotomap ", vote);
354 vote_argc = tokenize_console(vote); // ValidateMap may have done some stuff to it
357 // make kick and kickban votes a bit nicer (and reject them if formatted badly)
358 if(argv(0) == "kick" || argv(0) == "kickban")
360 if(!(victim = GetKickVoteVictim(vote, cmd, e)))
362 RemapVote_vote = GetKickVoteVictim_newcommand;
363 RemapVote_display = strcat("^1", vote, " (^7", victim.netname, "^1): ", GetKickVoteVictim_reason);
367 RemapVote_vote = vote;
368 RemapVote_display = strzone(strcat("^1", vote));
375 // =======================
376 // Command Sub-Functions
377 // =======================
379 void VoteCommand_abstain(float request, entity caller)
383 case VC_REQUEST_COMMAND:
390 case VC_REQUEST_USAGE:
392 print("\nUsage:^3 vote \n");
393 print(" No arguments required.\n");
399 void VoteCommand_stop(float request, entity caller)
403 case VC_REQUEST_COMMAND:
410 case VC_REQUEST_USAGE:
412 print("\nUsage:^3 vote \n");
413 print(" No arguments required.\n");
419 /* use this when creating a new command, making sure to place it in alphabetical order.
420 void VoteCommand_(float request)
424 case VC_REQUEST_COMMAND:
431 case VC_REQUEST_USAGE:
433 print("\nUsage:^3 vote \n");
434 print(" No arguments required.\n");
442 // ==================================
443 // Macro system for server commands
444 // ==================================
446 // Do not hard code aliases for these, instead create them in commands.cfg... also: keep in alphabetical order, please ;)
447 #define VOTE_COMMANDS(request,caller,arguments) \
448 VOTE_COMMAND("abstain", VoteCommand_abstain(request, caller), "Abstain your vote in current vote", VC_ASGNMNT_CLIENTONLY) \
449 VOTE_COMMAND("call", VoteCommand_call(request, caller, arguments), "Create a new vote for players to decide on", VC_ASGNMNT_BOTH) \
450 VOTE_COMMAND("force", VoteCommand_force(request, caller), "Force a result of a vote", VC_ASGNMNT_SERVERONLY) \
451 VOTE_COMMAND("help", VoteCommand_macro_help(caller), "Shows this information", VC_ASGNMNT_BOTH) \
452 VOTE_COMMAND("master", VoteCommand_master(request, caller, arguments), "", VC_ASGNMNT_CLIENTONLY) \
453 VOTE_COMMAND("no", VoteCommand_no(request, caller), "Vote no in current poll", VC_ASGNMNT_CLIENTONLY) \
454 VOTE_COMMAND("status", VoteCommand_status(request, caller), "Prints information about current poll", VC_ASGNMNT_BOTH) \
455 VOTE_COMMAND("stop", VoteCommand_stop(request, caller), "Immediately end a vote", VC_ASGNMNT_BOTH) \
456 VOTE_COMMAND("yes", VoteCommand_yes(request, caller), "Vote yes in current poll", VC_ASGNMNT_CLIENTONLY) \
459 void VoteCommand_macro_help(entity caller)
461 print("\nUsage:^3 ", VoteCommand_getprefix(caller), " vote COMMAND...^7, where possible commands are:\n");
463 #define VOTE_COMMAND(name,function,description,assignment) \
464 { if(Votecommand_check_assignment(caller, assignment)) { print(" ^2", name, "^7: ", description, "\n"); } }
466 VOTE_COMMANDS(0, caller, 0)
472 float VoteCommand_macro_command(entity caller, float argc)
474 #define VOTE_COMMAND(name,function,description,assignment) \
475 { if(Votecommand_check_assignment(caller, assignment)) { if(name == strtolower(argv(0))) { function; return TRUE; } } }
477 VOTE_COMMANDS(VC_REQUEST_COMMAND, caller, argc)
483 float VoteCommand_macro_usage(entity caller, float argc)
485 #define VOTE_COMMAND(name,function,description,assignment) \
486 { if(Votecommand_check_assignment(caller, assignment)) { if(name == strtolower(argv(1))) { function; return TRUE; } } }
488 VOTE_COMMANDS(VC_REQUEST_USAGE, caller, argc)
495 // ======================================
496 // Main function handling vote commands
497 // ======================================
499 void VoteCommand(entity caller, float argc)
501 if(strtolower(argv(0)) == "help")
505 VoteCommand_macro_help(caller);
508 else if(VoteCommand_macro_usage(caller, argc))
513 else if(VoteCommand_macro_command(caller, argc))
518 // nothing above caught the command, must be invalid
519 //print("Unknown server command", ((command != "") ? strcat(" \"", command, "\"") : ""), ". For a list of supported commands, try sv_cmd help.\n");
522 void VoteHelp(entity e) {
524 if(!autocvar_sv_vote_master) {
525 vmasterdis = " ^1(disabled)";
529 if("" == autocvar_sv_vote_master_password) {
530 vlogindis = " ^1(disabled)";
534 if(!autocvar_sv_vote_call) {
535 vcalldis = " ^1(disabled)";
538 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\".");
539 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\".");
540 print_to(e, "^7\"^2help^7\" shows this info.");
541 print_to(e, "^7\"^2status^7\" shows if there is a vote called and who called it.");
542 print_to(e, strcat("^7\"^2call^7\" is used to call a vote. See the list of allowed commands.", vcalldis, "^7"));
543 print_to(e, "^7\"^2stop^7\" can be used by the vote caller or an admin to stop a vote and maybe correct it.");
544 print_to(e, strcat("^7\"^2master^7\" call a vote to become master who can execute commands without a vote", vmasterdis, "^7"));
545 print_to(e, strcat("^7\"^2login^7\" login to become master who can execute commands without a vote.", vlogindis, "^7"));
546 print_to(e, "^7\"^2do^7\" executes a command if you are a master. See the list of allowed commands.");
547 print_to(e, "^7\"^2yes^7\", \"^2no^7\", \"^2abstain^7\" and \"^2dontcare^7\" to make your vote.");
548 print_to(e, "^7If enough of the players vote yes the vote is accepted.");
549 print_to(e, "^7If enough of the players vote no the vote is rejected.");
550 print_to(e, strcat("^7If neither the vote will timeout after ", ftos(autocvar_sv_vote_timeout), "^7 seconds."));
551 print_to(e, "^7You can call a vote for or execute these commands:");
552 print_to(e, strcat("^3", autocvar_sv_vote_commands, "^7 and maybe further ^3arguments^7"));
555 string VoteNetname(entity e)
560 if(autocvar_sv_adminnick != "") {
561 return autocvar_sv_adminnick;
563 return autocvar_hostname;
568 string ValidateMap(string m, entity e)
570 m = MapInfo_FixName(m);
573 print_to(e, "This map is not available on this server.");
576 if(!autocvar_sv_vote_override_mostrecent)
579 print_to(e, "This server does not allow for recent maps to be played again. Please be patient for some rounds.");
582 if(!MapInfo_CheckMap(m))
584 print_to(e, strcat("^1Invalid mapname, \"^3", m, "^1\" does not support the current game mode."));
593 if(votefinished > 0) // a vote was called
594 if(time > votefinished) // time is up
600 string VoteParse(string all, float argc) {
603 return substring(all, argv_start_index(2), argv_end_index(-1) - argv_start_index(2));
606 float VoteCommandInList(string votecommand, string list)
609 l = strcat(" ", list, " ");
611 if(strstrofs(l, strcat(" ", votecommand, " "), 0) >= 0)
614 // if gotomap is allowed, chmap is too, and vice versa
615 if(votecommand == "gotomap")
616 if(strstrofs(l, " chmap ", 0) >= 0)
618 if(votecommand == "chmap")
619 if(strstrofs(l, " gotomap ", 0) >= 0)
625 float VoteAllowed(string votecommand, string cmd) {
626 if(VoteCommandInList(votecommand, autocvar_sv_vote_commands))
631 if(VoteCommandInList(votecommand, autocvar_sv_vote_master_commands))
636 if(VoteCommandInList(votecommand, autocvar_sv_vote_only_commands))
646 FOR_EACH_CLIENT(player)
648 player.vote_vote = 0;
653 strunzone(votecalledvote);
654 strunzone(votecalledvote_display);
658 votecalledmaster = FALSE;
660 votecalledvote = string_null;
661 votecalledvote_display = string_null;
663 Nagger_VoteChanged();
667 bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ^1", votecalledvote_display, "^2 was accepted\n");
671 votecaller.vote_master = 1;
674 localcmd(strcat(votecalledvote, "\n"));
677 votecaller.vote_next = 0; // people like your votes,
678 // no wait for next vote
681 Announce("voteaccept");
685 bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ", votecalledvote_display, "^2 was rejected\n");
687 Announce("votefail");
691 bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ", votecalledvote_display, "^2 timed out\n");
693 Announce("votefail");
696 void VoteStop(entity stopper) {
697 bprint("\{1}^2* ^3", VoteNetname(stopper), "^2 stopped ^3", VoteNetname(votecaller), "^2's vote\n");
698 if(autocvar_sv_eventlog)
699 GameLogEcho(strcat(":vote:vstop:", ftos(stopper.playerid)));
700 if(stopper == votecaller) {
701 // no wait for next vote so you can correct your vote
703 votecaller.vote_next = time + autocvar_sv_vote_stop;
709 void VoteSpam(float notvoters, float mincount, string result)
714 s = strcat("\{1}^2* vote results: ^1", ftos(vote_yescount), "^2:^1");
715 s = strcat(s, ftos(vote_nocount), "^2 (^1");
716 s = strcat(s, ftos(mincount), "^2 needed), ^1");
717 s = strcat(s, ftos(vote_abstaincount), "^2 didn't care, ^1");
718 s = strcat(s, ftos(notvoters), "^2 didn't vote\n");
722 s = strcat("\{1}^2* vote results: ^1", ftos(vote_yescount), "^2:^1");
723 s = strcat(s, ftos(vote_nocount), "^2, ^1");
724 s = strcat(s, ftos(vote_abstaincount), "^2 didn't care, ^1");
725 s = strcat(s, ftos(notvoters), "^2 didn't have to vote\n");
728 if(autocvar_sv_eventlog)
730 s = strcat(":vote:v", result, ":", ftos(vote_yescount));
731 s = strcat(s, ":", ftos(vote_nocount));
732 s = strcat(s, ":", ftos(vote_abstaincount));
733 s = strcat(s, ":", ftos(notvoters));
734 s = strcat(s, ":", ftos(mincount));
744 vote_abstaincount = 0;
746 //same for real players
747 float realplayercount;
748 float realplayeryescount;
749 float realplayernocount;
750 float realplayerabstaincount;
751 realplayercount = realplayernocount = realplayerabstaincount = realplayeryescount = 0;
753 Nagger_VoteCountChanged();
755 FOR_EACH_REALCLIENT(player)
757 if(player.vote_vote == -1) {
759 } else if(player.vote_vote == 1) {
761 } else if(player.vote_vote == -2) {
765 //do the same for real players
766 if(player.classname == "player") {
767 if(player.vote_vote == -1) {
769 } else if(player.vote_vote == 1) {
770 ++realplayeryescount;
771 } else if(player.vote_vote == -2) {
772 ++realplayerabstaincount;
778 //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)
779 if(autocvar_sv_vote_nospectators)
780 if(realplayercount > 0) {
781 vote_yescount = realplayeryescount;
782 vote_nocount = realplayernocount;
783 vote_abstaincount = realplayerabstaincount;
784 playercount = realplayercount;
787 float votefactor, simplevotefactor;
788 votefactor = bound(0.5, autocvar_sv_vote_majority_factor, 0.999);
789 simplevotefactor = autocvar_sv_vote_simple_majority_factor;
791 // FIXME this number is a guess
792 vote_needed_absolute = floor((playercount - vote_abstaincount) * votefactor) + 1;
795 simplevotefactor = bound(votefactor, simplevotefactor, 0.999);
796 vote_needed_simple = floor((vote_yescount + vote_nocount) * simplevotefactor) + 1;
799 vote_needed_simple = 0;
802 && playercount == 1) {
803 // if only one player is on the server becoming vote
804 // master is not allowed. This could be used for
805 // trolling or worse. 'self' is the user who has
806 // called the vote because this function is called
807 // by SV_ParseClientCommand. Maybe all voting should
808 // be disabled for a single player?
809 print_to(votecaller, "^1You are the only player on this server so you can not become vote master.");
811 votecaller.vote_next = 0;
815 if(vote_yescount >= vote_needed_absolute)
817 VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, -1, "yes");
820 else if(vote_nocount > playercount - vote_abstaincount - vote_needed_absolute) // that means, vote_yescount cannot reach vote_needed_absolute any more
822 VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, -1, "no");
825 else if(time > votefinished)
830 if(vote_yescount >= vote_needed_simple)
832 else if(vote_yescount + vote_nocount > 0)
836 VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, min(vote_needed_absolute, vote_needed_simple), result);
839 else if(result == "no")
846 VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, vote_needed_absolute, "timeout");