From 98ca45be32fac5252e9bec9c24d3d12e2f1ae541 Mon Sep 17 00:00:00 2001 From: Samual Date: Mon, 5 Dec 2011 13:36:26 -0500 Subject: [PATCH] Finished more refactoring of the rest of the game logic of voting -- next up comes re-writing the votecount function --- qcsrc/server/assault.qc | 2 +- qcsrc/server/defs.qh | 5 - qcsrc/server/vote.qc | 1072 +++++++++++++++++++-------------------- qcsrc/server/vote.qh | 7 + 4 files changed, 542 insertions(+), 544 deletions(-) diff --git a/qcsrc/server/assault.qc b/qcsrc/server/assault.qc index 425bbf779..2562dca3d 100644 --- a/qcsrc/server/assault.qc +++ b/qcsrc/server/assault.qc @@ -372,5 +372,5 @@ void assault_new_round() // reset the level with a countdown cvar_set("timelimit", ftos(ceil(time - game_starttime) / 60)); - ReadyRestartForce(); // sets game_starttime + ReadyRestart_force(); // sets game_starttime } diff --git a/qcsrc/server/defs.qh b/qcsrc/server/defs.qh index 430bf369a..6cae6732b 100644 --- a/qcsrc/server/defs.qh +++ b/qcsrc/server/defs.qh @@ -265,11 +265,6 @@ float alreadychangedlevel; // footstep interval .float nextstep; -.float ready; -#define RESTART_COUNTDOWN 10 -float restart_mapalreadyrestarted; //bool, indicates whether reset_map() was already executed -entity restartTimer; -void restartTimer_Think(); float blockSpectators; //if set, new or existing spectators or observers will be removed unless they become a player within g_maxplayers_spectator_blocktime seconds .float spectatortime; //point in time since the client is spectating or observing void checkSpectatorBlock(); diff --git a/qcsrc/server/vote.qc b/qcsrc/server/vote.qc index af78b6daf..f5d43a5f3 100644 --- a/qcsrc/server/vote.qc +++ b/qcsrc/server/vote.qc @@ -1,6 +1,6 @@ // ============================================= // Server side voting code, reworked by Samual -// Last updated: December 4th, 2011 +// Last updated: December 6th, 2011 // ============================================= #define VC_REQUEST_COMMAND 1 @@ -15,9 +15,6 @@ #define VOTE_SELECT_NULL 0 #define VOTE_SELECT_ACCEPT 1 -string GetKickVoteVictim_newcommand; -string GetKickVoteVictim_reason; - string vote_parsed_command; string vote_parsed_display; @@ -122,228 +119,552 @@ void Nagger_ReadyCounted() // Game logic for voting // ======================= +void VoteReset() +{ + entity tmp_player; + + FOR_EACH_CLIENT(tmp_player) { tmp_player.vote_selection = 0; } + + if(votecalled) + { + strunzone(votecalledvote); + strunzone(votecalledvote_display); + } + + votecalled = FALSE; + votecalledmaster = FALSE; + votefinished = 0; + votecalledvote = string_null; + votecalledvote_display = string_null; + + vote_parsed_command = string_null; + vote_parsed_display = string_null; + + Nagger_VoteChanged(); +} + void VoteStop(entity stopper) { bprint("\{1}^2* ^3", VoteCommand_getname(stopper), "^2 stopped ^3", VoteCommand_getname(votecaller), "^2's vote\n"); if(autocvar_sv_eventlog) { GameLogEcho(strcat(":vote:vstop:", ftos(stopper.playerid))); } // Don't force them to wait for next vote, this way they can e.g. correct their vote. - if(votecaller) - if(stopper == votecaller) - votecaller.vote_next = time + autocvar_sv_vote_stop; + if((votecaller) && (stopper == votecaller)) { votecaller.vote_next = time + autocvar_sv_vote_stop; } VoteReset(); } - -// ====================================== -// Supporting functions for VoteCommand -// ====================================== - -float Votecommand_check_assignment(entity caller, float assignment) +void VoteThink() { - float from_server = (!caller); - - if((assignment == VC_ASGNMNT_BOTH) - || ((!from_server && assignment == VC_ASGNMNT_CLIENTONLY) - || (from_server && assignment == VC_ASGNMNT_SERVERONLY))) + if(votefinished > 0) // a vote was called + if(time > votefinished) // time is up { - return TRUE; + VoteCount(); } - - return FALSE; -} - -string VoteCommand_getprefix(entity caller) -{ - if(caller) - return "cmd"; - else - return "sv_cmd"; -} - -string VoteCommand_getname(entity caller) -{ - if(caller) - return caller.netname; - else - return ((autocvar_sv_adminnick != "") ? autocvar_sv_adminnick : autocvar_hostname); + + return; } -string VoteCommand_extractcommand(string input, float startpos, float argc) +void VoteAccept() { - string output; + bprint("\{1}^2* ^3", VoteCommand_getname(votecaller), "^2's vote for ^1", votecalledvote_display, "^2 was accepted\n"); - if((argc - 1) < startpos) - output = ""; + if(votecalledmaster && votecaller) + votecaller.vote_master = 1; else - output = substring(input, argv_start_index(startpos), argv_end_index(-1) - argv_start_index(startpos)); - - print("VoteCommand_parse: '", output, "'. \n"); - return output; + localcmd(strcat(votecalledvote, "\n")); + + if(votecaller) { votecaller.vote_next = 0; } // people like your votes, you don't need to wait to vote again // todo separate anti-spam even for succeeded votes + + VoteReset(); + Announce("voteaccept"); } -float VoteCommand_checknasty(string vote_command) +void VoteReject() { - if((strstrofs(vote_command, ";", 0) >= 0) - || (strstrofs(vote_command, "\n", 0) >= 0) - || (strstrofs(vote_command, "\r", 0) >= 0) - || (strstrofs(vote_command, "$", 0) >= 0)) - return TRUE; - - return FALSE; + bprint("\{1}^2* ^3", VoteCommand_getname(votecaller), "^2's vote for ", votecalledvote_display, "^2 was rejected\n"); + VoteReset(); + Announce("votefail"); } -float VoteCommand_checkinlist(string vote_command, string list) +void VoteTimeout() { - string l = strcat(" ", list, " "); - - if(strstrofs(l, strcat(" ", vote_command, " "), 0) >= 0) - return TRUE; - - // if gotomap is allowed, chmap is too, and vice versa - if(vote_command == "gotomap") - if(strstrofs(l, " chmap ", 0) >= 0) - return TRUE; - - if(vote_command == "chmap") - if(strstrofs(l, " gotomap ", 0) >= 0) - return TRUE; - - return FALSE; + bprint("\{1}^2* ^3", VoteCommand_getname(votecaller), "^2's vote for ", votecalledvote_display, "^2 timed out\n"); + VoteReset(); + Announce("votefail"); } -string ValidateMap(string m, entity e) +void VoteSpam(float notvoters, float mincount, string result) { - m = MapInfo_FixName(m); - if(!m) + string s; + if(mincount >= 0) { - print_to(e, "This map is not available on this server."); - return string_null; + s = strcat("\{1}^2* vote results: ^1", ftos(vote_yescount), "^2:^1"); + s = strcat(s, ftos(vote_nocount), "^2 (^1"); + s = strcat(s, ftos(mincount), "^2 needed), ^1"); + s = strcat(s, ftos(vote_abstaincount), "^2 didn't care, ^1"); + s = strcat(s, ftos(notvoters), "^2 didn't vote\n"); } - if(!autocvar_sv_vote_override_mostrecent) - if(Map_IsRecent(m)) - { - print_to(e, "This server does not allow for recent maps to be played again. Please be patient for some rounds."); - return string_null; - } - if(!MapInfo_CheckMap(m)) + else { - print_to(e, strcat("^1Invalid mapname, \"^3", m, "^1\" does not support the current game mode.")); - return string_null; + s = strcat("\{1}^2* vote results: ^1", ftos(vote_yescount), "^2:^1"); + s = strcat(s, ftos(vote_nocount), "^2, ^1"); + s = strcat(s, ftos(vote_abstaincount), "^2 didn't care, ^1"); + s = strcat(s, ftos(notvoters), "^2 didn't have to vote\n"); } - - return m; -} - -float VoteCommand_parse(entity caller, string vote_command, string vote_list, float startpos, float argc) -{ - string first_command; - entity victim; - first_command = argv(startpos); - - if not(VoteCommand_checkinlist(vote_command, vote_list)) - return FALSE; - - if((argc - 1) < startpos) // These commands won't work without arguments - { - switch(first_command) - { - case "map": - case "chmap": - case "gotomap": - case "kick": - case "kickban": - return FALSE; - - default: { break; } - } - } + bprint(s); - switch(first_command) // now go through and parse the proper commands to adjust as needed. + if(autocvar_sv_eventlog) { - case "kick": - case "kickban": // catch all kick/kickban commands - { - victim = edict_num(GetFilteredNumber(substring(vote_command, argv_start_index(startpos + 1), argv_end_index(-1) - argv_start_index(startpos + 1)))); - if not(victim) { return FALSE; } - // TODO: figure out how kick/kickban/ban commands work and re-write this to fit around them - vote_parsed_command = vote_command; - vote_parsed_display = strcat("^1", vote_command, " (^7", victim.netname, "^1): ", "todo"); - - break; - } - - case "map": - case "chmap": - case "gotomap": // re-direct all map selection commands to gotomap - { - vote_command = ValidateMap(substring(vote_command, argv_start_index(startpos + 1), argv_end_index(-1) - argv_start_index(startpos + 1)), caller); - if not(vote_command) { return FALSE; } - vote_parsed_command = strcat("gotomap ", vote_command); - vote_parsed_display = strzone(strcat("^1", vote_parsed_command)); - - break; - } - - default: - { - vote_parsed_command = vote_command; - vote_parsed_display = strzone(strcat("^1", vote_command)); - break; - } + s = strcat(":vote:v", result, ":", ftos(vote_yescount)); + s = strcat(s, ":", ftos(vote_nocount)); + s = strcat(s, ":", ftos(vote_abstaincount)); + s = strcat(s, ":", ftos(notvoters)); + s = strcat(s, ":", ftos(mincount)); + GameLogEcho(s); } - - return TRUE; } +void VoteCount() +{ + float playercount; + playercount = 0; + vote_yescount = 0; + vote_nocount = 0; + vote_abstaincount = 0; + entity player; + //same for real players + float realplayercount; + float realplayeryescount; + float realplayernocount; + float realplayerabstaincount; + realplayercount = realplayernocount = realplayerabstaincount = realplayeryescount = 0; -// ======================= -// Command Sub-Functions -// ======================= + Nagger_VoteCountChanged(); -void VoteCommand_abstain(float request, entity caller) // CLIENT ONLY -{ - switch(request) + FOR_EACH_REALCLIENT(player) { - case VC_REQUEST_COMMAND: - { - if not(votecalled) { print_to(caller, "^1No vote called."); } - else if not(caller.vote_selection == VOTE_SELECT_NULL || autocvar_sv_vote_change) { print_to(caller, "^1You have already voted."); } - - else // everything went okay, continue changing vote - { - print_to(caller, "^1You abstained from your vote."); - caller.vote_selection = VOTE_SELECT_ABSTAIN; - msg_entity = caller; - if(!autocvar_sv_vote_singlecount) { VoteCount(); } - } - - return; + if(player.vote_selection == -1) { + ++vote_nocount; + } else if(player.vote_selection == 1) { + ++vote_yescount; + } else if(player.vote_selection == -2) { + ++vote_abstaincount; } - - default: - case VC_REQUEST_USAGE: - { - print("\nUsage:^3 vote abstain\n"); - print(" No arguments required.\n"); - return; + ++playercount; + //do the same for real players + if(player.classname == "player") { + if(player.vote_selection == -1) { + ++realplayernocount; + } else if(player.vote_selection == 1) { + ++realplayeryescount; + } else if(player.vote_selection == -2) { + ++realplayerabstaincount; + } + ++realplayercount; } } -} -void VoteCommand_call(float request, entity caller, float argc, string vote_command) // BOTH -{ - switch(request) - { - case VC_REQUEST_COMMAND: - { - float spectators_allowed = ((autocvar_sv_vote_nospectators != 2) || ((autocvar_sv_vote_nospectators == 1) && inWarmupStage)); - float tmp_playercount; - entity tmp_player; - - vote_command = VoteCommand_extractcommand(vote_command, 2, argc); + //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) + if(autocvar_sv_vote_nospectators) + if(realplayercount > 0) { + vote_yescount = realplayeryescount; + vote_nocount = realplayernocount; + vote_abstaincount = realplayerabstaincount; + playercount = realplayercount; + } + + float votefactor, simplevotefactor; + votefactor = bound(0.5, autocvar_sv_vote_majority_factor, 0.999); + simplevotefactor = autocvar_sv_vote_simple_majority_factor; + + // FIXME this number is a guess + vote_needed_absolute = floor((playercount - vote_abstaincount) * votefactor) + 1; + if(simplevotefactor) + { + simplevotefactor = bound(votefactor, simplevotefactor, 0.999); + vote_needed_simple = floor((vote_yescount + vote_nocount) * simplevotefactor) + 1; + } + else + vote_needed_simple = 0; + + if(votecalledmaster + && playercount == 1) { + // if only one player is on the server becoming vote + // master is not allowed. This could be used for + // trolling or worse. 'self' is the user who has + // called the vote because this function is called + // by SV_ParseClientCommand. Maybe all voting should + // be disabled for a single player? + print_to(votecaller, "^1You are the only player on this server so you can not become vote master."); + if(votecaller) { + votecaller.vote_next = 0; + } + VoteReset(); + } else { + if(vote_yescount >= vote_needed_absolute) + { + VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, -1, "yes"); + VoteAccept(); + } + else if(vote_nocount > playercount - vote_abstaincount - vote_needed_absolute) // that means, vote_yescount cannot reach vote_needed_absolute any more + { + VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, -1, "no"); + VoteReject(); + } + else if(time > votefinished) + { + if(simplevotefactor) + { + string result; + if(vote_yescount >= vote_needed_simple) + result = "yes"; + else if(vote_yescount + vote_nocount > 0) + result = "no"; + else + result = "timeout"; + VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, min(vote_needed_absolute, vote_needed_simple), result); + if(result == "yes") + VoteAccept(); + else if(result == "no") + VoteReject(); + else + VoteTimeout(); + } + else + { + VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, vote_needed_absolute, "timeout"); + VoteTimeout(); + } + } + } +} + + +// ======================= +// Game logic for warmup +// ======================= + +// Restarts the map after the countdown is over (and cvar sv_ready_restart_after_countdown is set) +void ReadyRestart_think() +{ + restart_mapalreadyrestarted = 1; + reset_map(TRUE); + Score_ClearAll(); + remove(self); + + return; +} + +// Forces a restart of the game without actually reloading the map // this is a mess... +void ReadyRestart_force() +{ + entity tmp_player, restart_timer; + + bprint("^1Server is restarting...\n"); + + VoteReset(); + + // clear overtime, we have to decrease timelimit to its original value again. + if (checkrules_overtimesadded > 0 && g_race_qualifying != 2) { cvar_set("timelimit", ftos(autocvar_timelimit - (checkrules_overtimesadded * autocvar_timelimit_overtime))); } + + checkrules_suddendeathend = checkrules_overtimesadded = checkrules_suddendeathwarning = 0; + + readyrestart_happened = 1; + game_starttime = time; + if(!g_ca && !g_arena) { game_starttime += RESTART_COUNTDOWN; } + + restart_mapalreadyrestarted = 0; // reset this var, needed when cvar sv_ready_restart_repeatable is in use + + // disable the warmup global for the server + inWarmupStage = 0; // once the game is restarted the game is in match stage + + // reset the .ready status of all players (also spectators) + FOR_EACH_CLIENTSLOT(tmp_player) { tmp_player.ready = 0; } + readycount = 0; + Nagger_ReadyCounted(); // NOTE: this causes a resend of that entity, and will also turn off warmup state on the client + + // lock teams with lockonrestart + if(autocvar_teamplay_lockonrestart && teamplay) + { + lockteams = 1; + bprint("^1The teams are now locked.\n"); + } + + //initiate the restart-countdown-announcer entity + if(autocvar_sv_ready_restart_after_countdown && !g_ca && !g_arena) + { + restart_timer = spawn(); + restart_timer.think = ReadyRestart_think; + restart_timer.nextthink = game_starttime; + } + + // after a restart every players number of allowed timeouts gets reset, too + if(autocvar_sv_timeout) { FOR_EACH_REALPLAYER(tmp_player) { tmp_player.allowedTimeouts = autocvar_sv_timeout_number; } } + + //reset map immediately if this cvar is not set + if not(autocvar_sv_ready_restart_after_countdown) { reset_map(TRUE); } + + if(autocvar_sv_eventlog) { GameLogEcho(":restart"); } +} + +void ReadyRestart() +{ + // no arena, assault support yet... + if(g_arena | g_assault | gameover | intermission_running | race_completing) + localcmd("restart\n"); + else + localcmd("\nsv_hook_gamerestart\n"); + + // Reset ALL scores, but only do that at the beginning of the countdown if sv_ready_restart_after_countdown is off! + // Otherwise scores could be manipulated during the countdown. + if not(autocvar_sv_ready_restart_after_countdown) { Score_ClearAll(); } + + ReadyRestart_force(); + + return; +} + +// Count the players who are ready and determine whether or not to restart the match +void ReadyCount() +{ + entity tmp_player; + float t_ready, t_players; + + FOR_EACH_REALPLAYER(tmp_player) + { + ++t_players; + if(tmp_player.ready) { ++t_ready; } + } + + readycount = t_ready; + + Nagger_ReadyCounted(); + + // TODO: check percentage of ready players + if(t_ready) // at least one is ready + if(t_ready == t_players) // and, everyone is ready + ReadyRestart(); + + return; +} + + +// ====================================== +// Supporting functions for VoteCommand +// ====================================== + +float Votecommand_check_assignment(entity caller, float assignment) +{ + float from_server = (!caller); + + if((assignment == VC_ASGNMNT_BOTH) + || ((!from_server && assignment == VC_ASGNMNT_CLIENTONLY) + || (from_server && assignment == VC_ASGNMNT_SERVERONLY))) + { + return TRUE; + } + + return FALSE; +} + +string VoteCommand_getprefix(entity caller) +{ + if(caller) + return "cmd"; + else + return "sv_cmd"; +} + +string VoteCommand_getname(entity caller) +{ + if(caller) + return caller.netname; + else + return ((autocvar_sv_adminnick != "") ? autocvar_sv_adminnick : autocvar_hostname); +} + +string VoteCommand_extractcommand(string input, float startpos, float argc) +{ + string output; + + if((argc - 1) < startpos) + output = ""; + else + output = substring(input, argv_start_index(startpos), argv_end_index(-1) - argv_start_index(startpos)); + + print("VoteCommand_parse: '", output, "'. \n"); + return output; +} + +float VoteCommand_checknasty(string vote_command) +{ + if((strstrofs(vote_command, ";", 0) >= 0) + || (strstrofs(vote_command, "\n", 0) >= 0) + || (strstrofs(vote_command, "\r", 0) >= 0) + || (strstrofs(vote_command, "$", 0) >= 0)) + return TRUE; + + return FALSE; +} + +float VoteCommand_checkinlist(string vote_command, string list) +{ + string l = strcat(" ", list, " "); + + if(strstrofs(l, strcat(" ", vote_command, " "), 0) >= 0) + return TRUE; + + // if gotomap is allowed, chmap is too, and vice versa + if(vote_command == "gotomap") + if(strstrofs(l, " chmap ", 0) >= 0) + return TRUE; + + if(vote_command == "chmap") + if(strstrofs(l, " gotomap ", 0) >= 0) + return TRUE; + + return FALSE; +} + +string ValidateMap(string validated_map, entity caller) +{ + validated_map = MapInfo_FixName(validated_map); + + if(!validated_map) + { + print_to(caller, "This map is not available on this server."); + return string_null; + } + + if(!autocvar_sv_vote_override_mostrecent && caller) + { + if(Map_IsRecent(validated_map)) + { + print_to(caller, "This server does not allow for recent maps to be played again. Please be patient for some rounds."); + return string_null; + } + } + + if(!MapInfo_CheckMap(validated_map)) + { + print_to(caller, strcat("^1Invalid mapname, \"^3", validated_map, "^1\" does not support the current game mode.")); + return string_null; + } + + return validated_map; +} + +float VoteCommand_parse(entity caller, string vote_command, string vote_list, float startpos, float argc) +{ + string first_command; + entity victim; + + first_command = argv(startpos); + + if not(VoteCommand_checkinlist(vote_command, vote_list)) + return FALSE; + + if((argc - 1) < startpos) // These commands won't work without arguments + { + switch(first_command) + { + case "map": + case "chmap": + case "gotomap": + case "kick": + case "kickban": + return FALSE; + + default: { break; } + } + } + + switch(first_command) // now go through and parse the proper commands to adjust as needed. + { + case "kick": + case "kickban": // catch all kick/kickban commands + { + victim = edict_num(GetFilteredNumber(substring(vote_command, argv_start_index(startpos + 1), argv_end_index(-1) - argv_start_index(startpos + 1)))); + if not(victim) { return FALSE; } + // TODO: figure out how kick/kickban/ban commands work and re-write this to fit around them + vote_parsed_command = vote_command; + vote_parsed_display = strcat("^1", vote_command, " (^7", victim.netname, "^1): ", "todo"); + + break; + } + + case "map": + case "chmap": + case "gotomap": // re-direct all map selection commands to gotomap + { + vote_command = ValidateMap(substring(vote_command, argv_start_index(startpos + 1), argv_end_index(-1) - argv_start_index(startpos + 1)), caller); + if not(vote_command) { return FALSE; } + vote_parsed_command = strcat("gotomap ", vote_command); + vote_parsed_display = strzone(strcat("^1", vote_parsed_command)); + + break; + } + + default: + { + vote_parsed_command = vote_command; + vote_parsed_display = strzone(strcat("^1", vote_command)); + + break; + } + } + + return TRUE; +} + + +// ======================= +// Command Sub-Functions +// ======================= + +void VoteCommand_abstain(float request, entity caller) // CLIENT ONLY +{ + switch(request) + { + case VC_REQUEST_COMMAND: + { + if not(votecalled) { print_to(caller, "^1No vote called."); } + else if not(caller.vote_selection == VOTE_SELECT_NULL || autocvar_sv_vote_change) { print_to(caller, "^1You have already voted."); } + + else // everything went okay, continue changing vote + { + print_to(caller, "^1You abstained from your vote."); + caller.vote_selection = VOTE_SELECT_ABSTAIN; + msg_entity = caller; + if(!autocvar_sv_vote_singlecount) { VoteCount(); } + } + + return; + } + + default: + case VC_REQUEST_USAGE: + { + print("\nUsage:^3 vote abstain\n"); + print(" No arguments required.\n"); + return; + } + } +} + +void VoteCommand_call(float request, entity caller, float argc, string vote_command) // BOTH +{ + switch(request) + { + case VC_REQUEST_COMMAND: + { + float spectators_allowed = ((autocvar_sv_vote_nospectators != 2) || ((autocvar_sv_vote_nospectators == 1) && inWarmupStage)); + float tmp_playercount; + entity tmp_player; + + vote_command = VoteCommand_extractcommand(vote_command, 2, argc); if not(autocvar_sv_vote_call || !caller) { print_to(caller, "^1Vote calling is not allowed."); } else if(votecalled) { print_to(caller, "^1There is already a vote called."); } @@ -658,165 +979,45 @@ float VoteCommand_macro_command(entity caller, float argc, string vote_command) #define VOTE_COMMAND(name,function,description,assignment) \ { if(Votecommand_check_assignment(caller, assignment)) { if(name == strtolower(argv(1))) { function; return TRUE; } } } - VOTE_COMMANDS(VC_REQUEST_COMMAND, caller, argc, vote_command) - #undef VOTE_COMMAND - - return FALSE; -} - - -// ====================================== -// Main function handling vote commands -// ====================================== - -void VoteCommand(float request, entity caller, float argc, string vote_command) -{ - // Guide for working with argc arguments by example: - // argc: 1 - 2 - 3 - 4 - // argv: 0 - 1 - 2 - 3 - // cmd vote - master - login - password - - switch(request) - { - case VC_REQUEST_COMMAND: - { - if(VoteCommand_macro_command(caller, argc, vote_command)) - return; - } - - default: - print_to(caller, strcat("Unknown vote command", ((argv(1) != "") ? strcat(" \"", argv(1), "\"") : ""), ". For a list of supported commands, try ", VoteCommand_getprefix(caller), " help.\n")); - case VC_REQUEST_USAGE: - { - VoteCommand_macro_help(caller, argc); - return; - } - } -} - -// ======================= -// Game logic for voting -// ======================= - -void ReadyRestartForce() -{ - local entity e; - - bprint("^1Server is restarting...\n"); - - VoteReset(); - - // clear overtime - if (checkrules_overtimesadded > 0 && g_race_qualifying != 2) { - //we have to decrease timelimit to its original value again!! - float newTL; - newTL = autocvar_timelimit; - newTL -= checkrules_overtimesadded * autocvar_timelimit_overtime; - cvar_set("timelimit", ftos(newTL)); - } - - checkrules_suddendeathend = checkrules_overtimesadded = checkrules_suddendeathwarning = 0; - - - readyrestart_happened = 1; - game_starttime = time; - if(!g_ca && !g_arena) - game_starttime += RESTART_COUNTDOWN; - restart_mapalreadyrestarted = 0; //reset this var, needed when cvar sv_ready_restart_repeatable is in use - - inWarmupStage = 0; //once the game is restarted the game is in match stage - - //reset the .ready status of all players (also spectators) - FOR_EACH_CLIENTSLOT(e) - e.ready = 0; - readycount = 0; - Nagger_ReadyCounted(); // NOTE: this causes a resend of that entity, and will also turn off warmup state on the client - - if(autocvar_teamplay_lockonrestart && teamplay) { - lockteams = 1; - bprint("^1The teams are now locked.\n"); - } - - //initiate the restart-countdown-announcer entity - if(autocvar_sv_ready_restart_after_countdown && !g_ca && !g_arena) - { - restartTimer = spawn(); - restartTimer.think = restartTimer_Think; - restartTimer.nextthink = game_starttime; - } - - //after a restart every players number of allowed timeouts gets reset, too - if(autocvar_sv_timeout) - { - FOR_EACH_REALPLAYER(e) - e.allowedTimeouts = autocvar_sv_timeout_number; - } - - //reset map immediately if this cvar is not set - if (!autocvar_sv_ready_restart_after_countdown) - reset_map(TRUE); - - if(autocvar_sv_eventlog) - GameLogEcho(":restart"); + VOTE_COMMANDS(VC_REQUEST_COMMAND, caller, argc, vote_command) + #undef VOTE_COMMAND + + return FALSE; } -void ReadyRestart() -{ - // no arena, assault support yet... - if(g_arena | g_assault | gameover | intermission_running | race_completing) - localcmd("restart\n"); - else - localcmd("\nsv_hook_gamerestart\n"); - - ReadyRestartForce(); - // reset ALL scores, but only do that at the beginning - //of the countdown if sv_ready_restart_after_countdown is off! - //Otherwise scores could be manipulated during the countdown! - if (!autocvar_sv_ready_restart_after_countdown) - Score_ClearAll(); -} +// ====================================== +// Main function handling vote commands +// ====================================== -/** - * Counts how many players are ready. If not enough players are ready, the function - * does nothing. If all players are ready, the timelimit will be extended and the - * restart_countdown variable is set to allow other functions like PlayerPostThink - * to detect that the countdown is now active. If the cvar sv_ready_restart_after_countdown - * is not set the map will be resetted. - * - * Function is called after the server receives a 'ready' sign from a player. - */ -void ReadyCount() +void VoteCommand(float request, entity caller, float argc, string vote_command) { - entity tmp_player; - float t_ready, t_players; - - FOR_EACH_REALPLAYER(tmp_player) + // Guide for working with argc arguments by example: + // argc: 1 - 2 - 3 - 4 + // argv: 0 - 1 - 2 - 3 + // cmd vote - master - login - password + + switch(request) { - ++t_players; - if(tmp_player.ready) { ++t_ready; } + case VC_REQUEST_COMMAND: + { + if(VoteCommand_macro_command(caller, argc, vote_command)) + return; + } + + default: + print_to(caller, strcat("Unknown vote command", ((argv(1) != "") ? strcat(" \"", argv(1), "\"") : ""), ". For a list of supported commands, try ", VoteCommand_getprefix(caller), " help.\n")); + case VC_REQUEST_USAGE: + { + VoteCommand_macro_help(caller, argc); + return; + } } - - readycount = t_ready; - - Nagger_ReadyCounted(); - - // TODO: Add ability to - if(t_ready) // at least one is ready - if(t_ready == t_players) // and, everyone is ready - ReadyRestart(); } - -// Restarts the map after the countdown is over (and cvar sv_ready_restart_after_countdown is set) -void restartTimer_Think() -{ - restart_mapalreadyrestarted = 1; - reset_map(TRUE); - Score_ClearAll(); - remove(self); - return; -} +// ======================= +// Game logic for voting +// ======================= void VoteHelp(entity e) { string vmasterdis; @@ -849,209 +1050,4 @@ void VoteHelp(entity e) { print_to(e, strcat("^7If neither the vote will timeout after ", ftos(autocvar_sv_vote_timeout), "^7 seconds.")); print_to(e, "^7You can call a vote for or execute these commands:"); print_to(e, strcat("^3", autocvar_sv_vote_commands, "^7 and maybe further ^3arguments^7")); -} - -void VoteThink() { - if(votefinished > 0) // a vote was called - if(time > votefinished) // time is up - { - VoteCount(); - } -} - -void VoteReset() { - entity player; - - FOR_EACH_CLIENT(player) - { - player.vote_selection = 0; - } - - if(votecalled) - { - strunzone(votecalledvote); - strunzone(votecalledvote_display); - } - - votecalled = FALSE; - votecalledmaster = FALSE; - votefinished = 0; - votecalledvote = string_null; - votecalledvote_display = string_null; - - Nagger_VoteChanged(); -} - -void VoteAccept() { - bprint("\{1}^2* ^3", VoteCommand_getname(votecaller), "^2's vote for ^1", votecalledvote_display, "^2 was accepted\n"); - if(votecalledmaster) - { - if(votecaller) { - votecaller.vote_master = 1; - } - } else { - localcmd(strcat(votecalledvote, "\n")); - } - if(votecaller) { - votecaller.vote_next = 0; // people like your votes, - // no wait for next vote - } - VoteReset(); - Announce("voteaccept"); -} - -void VoteReject() { - bprint("\{1}^2* ^3", VoteCommand_getname(votecaller), "^2's vote for ", votecalledvote_display, "^2 was rejected\n"); - VoteReset(); - Announce("votefail"); -} - -void VoteTimeout() { - bprint("\{1}^2* ^3", VoteCommand_getname(votecaller), "^2's vote for ", votecalledvote_display, "^2 timed out\n"); - VoteReset(); - Announce("votefail"); -} - -void VoteSpam(float notvoters, float mincount, string result) -{ - string s; - if(mincount >= 0) - { - s = strcat("\{1}^2* vote results: ^1", ftos(vote_yescount), "^2:^1"); - s = strcat(s, ftos(vote_nocount), "^2 (^1"); - s = strcat(s, ftos(mincount), "^2 needed), ^1"); - s = strcat(s, ftos(vote_abstaincount), "^2 didn't care, ^1"); - s = strcat(s, ftos(notvoters), "^2 didn't vote\n"); - } - else - { - s = strcat("\{1}^2* vote results: ^1", ftos(vote_yescount), "^2:^1"); - s = strcat(s, ftos(vote_nocount), "^2, ^1"); - s = strcat(s, ftos(vote_abstaincount), "^2 didn't care, ^1"); - s = strcat(s, ftos(notvoters), "^2 didn't have to vote\n"); - } - bprint(s); - if(autocvar_sv_eventlog) - { - s = strcat(":vote:v", result, ":", ftos(vote_yescount)); - s = strcat(s, ":", ftos(vote_nocount)); - s = strcat(s, ":", ftos(vote_abstaincount)); - s = strcat(s, ":", ftos(notvoters)); - s = strcat(s, ":", ftos(mincount)); - GameLogEcho(s); - } -} - -void VoteCount() { - float playercount; - playercount = 0; - vote_yescount = 0; - vote_nocount = 0; - vote_abstaincount = 0; - entity player; - //same for real players - float realplayercount; - float realplayeryescount; - float realplayernocount; - float realplayerabstaincount; - realplayercount = realplayernocount = realplayerabstaincount = realplayeryescount = 0; - - Nagger_VoteCountChanged(); - - FOR_EACH_REALCLIENT(player) - { - if(player.vote_selection == -1) { - ++vote_nocount; - } else if(player.vote_selection == 1) { - ++vote_yescount; - } else if(player.vote_selection == -2) { - ++vote_abstaincount; - } - ++playercount; - //do the same for real players - if(player.classname == "player") { - if(player.vote_selection == -1) { - ++realplayernocount; - } else if(player.vote_selection == 1) { - ++realplayeryescount; - } else if(player.vote_selection == -2) { - ++realplayerabstaincount; - } - ++realplayercount; - } - } - - //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) - if(autocvar_sv_vote_nospectators) - if(realplayercount > 0) { - vote_yescount = realplayeryescount; - vote_nocount = realplayernocount; - vote_abstaincount = realplayerabstaincount; - playercount = realplayercount; - } - - float votefactor, simplevotefactor; - votefactor = bound(0.5, autocvar_sv_vote_majority_factor, 0.999); - simplevotefactor = autocvar_sv_vote_simple_majority_factor; - - // FIXME this number is a guess - vote_needed_absolute = floor((playercount - vote_abstaincount) * votefactor) + 1; - if(simplevotefactor) - { - simplevotefactor = bound(votefactor, simplevotefactor, 0.999); - vote_needed_simple = floor((vote_yescount + vote_nocount) * simplevotefactor) + 1; - } - else - vote_needed_simple = 0; - - if(votecalledmaster - && playercount == 1) { - // if only one player is on the server becoming vote - // master is not allowed. This could be used for - // trolling or worse. 'self' is the user who has - // called the vote because this function is called - // by SV_ParseClientCommand. Maybe all voting should - // be disabled for a single player? - print_to(votecaller, "^1You are the only player on this server so you can not become vote master."); - if(votecaller) { - votecaller.vote_next = 0; - } - VoteReset(); - } else { - if(vote_yescount >= vote_needed_absolute) - { - VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, -1, "yes"); - VoteAccept(); - } - else if(vote_nocount > playercount - vote_abstaincount - vote_needed_absolute) // that means, vote_yescount cannot reach vote_needed_absolute any more - { - VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, -1, "no"); - VoteReject(); - } - else if(time > votefinished) - { - if(simplevotefactor) - { - string result; - if(vote_yescount >= vote_needed_simple) - result = "yes"; - else if(vote_yescount + vote_nocount > 0) - result = "no"; - else - result = "timeout"; - VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, min(vote_needed_absolute, vote_needed_simple), result); - if(result == "yes") - VoteAccept(); - else if(result == "no") - VoteReject(); - else - VoteTimeout(); - } - else - { - VoteSpam(playercount - vote_yescount - vote_nocount - vote_abstaincount, vote_needed_absolute, "timeout"); - VoteTimeout(); - } - } - } -} +} \ No newline at end of file diff --git a/qcsrc/server/vote.qh b/qcsrc/server/vote.qh index e5233b074..1dc639d17 100644 --- a/qcsrc/server/vote.qh +++ b/qcsrc/server/vote.qh @@ -25,7 +25,14 @@ void VoteStop(entity stopper); void VoteSpam(float notvoters, float mincount, string result); void VoteCount(); +// ========================= +// warmup and nagger stuff +// ========================= + +#define RESTART_COUNTDOWN 10 entity nagger; +.float ready; float readycount; float readyrestart_happened; +float restart_mapalreadyrestarted; // bool, indicates whether reset_map() was already executed void ReadyCount(); \ No newline at end of file -- 2.39.2