r3668 - in trunk/data: . qcsrc/server
DONOTREPLY at icculus.org
DONOTREPLY at icculus.org
Sun Jun 1 12:58:00 EDT 2008
Author: esteel
Date: 2008-06-01 12:57:58 -0400 (Sun, 01 Jun 2008)
New Revision: 3668
Added:
trunk/data/qcsrc/server/vote.qc
trunk/data/qcsrc/server/vote.qh
Modified:
trunk/data/defaultNexuiz.cfg
trunk/data/qcsrc/server/clientcommands.qc
trunk/data/qcsrc/server/gamecommand.qc
trunk/data/qcsrc/server/progs.src
Log:
- Moved all the voting stuff into a file of its own and made it accessible from the server console via sv_cmd vote.
Now server admins can not only stop votes but use all the vote subcommands like calling a vote.
Can be used to act more 'democratic' or test vote settings
- Added a new alias vlogin and removed the kludge 'vote do login'
Modified: trunk/data/defaultNexuiz.cfg
===================================================================
--- trunk/data/defaultNexuiz.cfg 2008-06-01 13:23:44 UTC (rev 3667)
+++ trunk/data/defaultNexuiz.cfg 2008-06-01 16:57:58 UTC (rev 3668)
@@ -684,6 +684,7 @@
alias vmap "vcall gotomap $1"
alias vstop "cmd vote stop"
alias vmaster "cmd vote master"
+alias vlogin "cmd vote login $*"
alias vdo "cmd vote do $*"
alias vdomap "vdo gotomap $1"
alias vyes "cmd vote yes"
Modified: trunk/data/qcsrc/server/clientcommands.qc
===================================================================
--- trunk/data/qcsrc/server/clientcommands.qc 2008-06-01 13:23:44 UTC (rev 3667)
+++ trunk/data/qcsrc/server/clientcommands.qc 2008-06-01 16:57:58 UTC (rev 3668)
@@ -1,5 +1,4 @@
void ReadyCount();
-string ValidateMap(string vote);
void(entity e) DropFlag;
string MapVote_Suggest(string m);
@@ -138,290 +137,14 @@
strunzone(msgstr);
}
-float VoteCheckNasty(string cmd)
-{
- if(strstrofs(cmd, ";", 0) >= 0)
- return TRUE;
- if(strstrofs(cmd, "\n", 0) >= 0)
- return TRUE;
- if(strstrofs(cmd, "\r", 0) >= 0)
- return TRUE;
- if(strstrofs(cmd, "$", 0) >= 0)
- return TRUE;
- return FALSE;
-}
-
-string GetKickVoteVictim_newcommand;
-string GetKickVoteVictim_reason;
-entity GetKickVoteVictim(string vote, string cmd)
-{
- float tokens;
- float i, n, t;
- string ns;
- entity e;
-
- tokens = tokenize(vote);
- ns = "";
-
- if(tokens >= 2)
- if(substring(argv(1), 0, 1) == "#")
- {
- ns = substring(argv(1), 1, 999);
- t = 2;
- }
-
- if(tokens >= 3)
- if(argv(1) == "#")
- {
- ns = argv(2);
- t = 3;
- }
-
- if(ns != "")
- {
- GetKickVoteVictim_reason = "";
- for(i = t; i < tokens; ++i)
- GetKickVoteVictim_reason = strcat(GetKickVoteVictim_reason, argv(i), " ");
- GetKickVoteVictim_reason = substring(GetKickVoteVictim_reason, 0, strlen(GetKickVoteVictim_reason) - 1);
-
- n = stof(ns);
- if(ns == ftos(n)) if(n >= 1) if(n <= maxclients)
- {
- e = edict_num(n);
- if(clienttype(e) == CLIENTTYPE_REAL)
- {
- GetKickVoteVictim_newcommand = strcat(argv(0), " # ", ns);
- return e;
- }
- }
- }
-
- sprint(self, strcat("Usage: ", cmd, " ", argv(0), " #playernumber (as in \"status\")\n"));
- return world;
-}
-
void SV_ParseClientCommand(string s) {
local string cmd;
- local entity e;
local float i;
tokenize(s);
- if(argv(0) == "vote") {
- if(argv(1) == "help") {
- local string vmasterdis;
- if(!cvar("sv_vote_master")) {
- vmasterdis = " ^1(disabled)";
- }
- local string vcalldis;
- if(!cvar("sv_vote_call")) {
- vcalldis = " ^1(disabled)";
- }
- sprint(self, "^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 do ^3COMMAND ARGUMENTS^7\" \"^2cmd vote yes^7\" \"^2cmd vote no^7\" \"^2cmd vote dontcare^7\".\n");
- sprint(self, "^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\" \"^2vdo ^3COMMAND ARGUMENTS^7\" \"^2vyes^7\" \"^2vno^7\" \"^2vdontcare^7\".\n");
- sprint(self, "^7\"^2help^7\" shows this info.\n");
- sprint(self, "^7\"^2status^7\" shows if there is a vote called and who called it.\n");
- sprint(self, strcat("^7\"^2call^7\" is used to call a vote. See the list of allowed commands.", vcalldis, "^7\n"));
- sprint(self, "^7\"^2stop^7\" can be used by the vote caller or an admin to stop a vote and maybe correct it.\n");
- sprint(self, strcat("^7\"^2master^7\" is used to call a vote to become a master.", vmasterdis, "^7\n"));
- sprint(self, "^7\"^2do^7\" If you are a master you can execute a command without a vote. See the list of allowed commands.\n");
- sprint(self, "^7\"^2yes^7\", \"^2no^7\" and \"^2dontcare^7\" to make your vote.\n");
- sprint(self, "^7If enough of the players vote yes the vote is accepted.\n");
- sprint(self, "^7If enough of the players vote no the vote is rejected.\n");
- sprint(self, strcat("^7The vote will end after ", cvar_string("sv_vote_timeout"), "^7 seconds.\n"));
- sprint(self, "^7You can call a vote for or execute these commands:\n");
- sprint(self, strcat("^3", cvar_string("sv_vote_commands"), "^7 and maybe further ^3arguments^7\n"));
- } else if(argv(1) == "status") {
- if(votecalled) {
- sprint(self, strcat("^7Vote for ", votecalledvote_display, "^7 called by ^7", votecaller.netname, "^7.\n"));
- } else {
- sprint(self, "^1No vote called.\n");
- }
- } else if(argv(1) == "call") {
- if(cvar("sv_vote_call")) {
- if(tourneyInMatchStage && cvar("g_tourney_disable_spec_vote") && self.classname != "player") {
- sprint(self, "^1Error: Only players can call a vote during the match-stage.\n");
- }
- else if(timeoutStatus) { //don't allow a vote call during a timeout
- sprint(self, "^1Error: You can not call a vote while a timeout is active.\n");
- }
- else if(votecalled) {
- sprint(self, "^1There is already a vote called.\n");
- } else {
- local string vote;
- vote = VoteParse();
- if(vote == "") {
- sprint(self, "^1Your vote is empty. See help for more info.\n");
- } else if(time < self.vote_next) {
- sprint(self, strcat("^1You have to wait ^2", ftos(self.vote_next - time), "^1 seconds before you can again call a vote.\n"));
- } else if(VoteCheckNasty(vote)) {
- sprint(self, "Syntax error in command.\n");
- } else if(VoteAllowed(strcat1(argv(2)))) { // strcat seems to be necessary
- // remap chmap to gotomap (forces intermission)
- if(vote == "chmap" || vote == "gotomap") // won't work without arguments
- return;
- if(substring(vote, 0, 6) == "chmap ")
- vote = strcat("gotomap ", substring(vote, 6, strlen(vote) - 6));
- if(substring(vote, 0, 8) == "gotomap ")
- {
- if(!(vote = ValidateMap(substring(vote, 8, strlen(vote) - 8))))
- return;
- vote = strcat("gotomap ", vote);
- }
-
- // make kick and kickban votes a bit nicer (and reject them if formatted badly)
- if(substring(vote, 0, 5) == "kick " || substring(vote, 0, 8) == "kickban ")
- {
- if(!(e = GetKickVoteVictim(vote, "vcall")))
- return;
- vote = GetKickVoteVictim_newcommand;
- votecalledvote_display = strzone(strcat("^1", vote, " (^7", e.netname, "^1): ", GetKickVoteVictim_reason));
- }
- else
- {
- votecalledvote_display = strzone(strcat("^1", vote));
- }
- votecalledvote = strzone(vote);
- votecalled = TRUE;
- votecalledmaster = FALSE;
- votecaller = self; // remember who called the vote
- votefinished = time + cvar("sv_vote_timeout");
- votecaller.vote_vote = 1; // of course you vote yes
- votecaller.vote_next = time + cvar("sv_vote_wait");
- bprint("\{1}^2* ^3", votecaller.netname, "^2 calls a vote for ", votecalledvote_display, "\n");
- VoteCount(); // needed if you are the only one
- } else {
- sprint(self, "^1This vote is not ok. See help for more info.\n");
- }
- }
- } else {
- sprint(self, "^1Vote calling is NOT allowed.\n");
- }
- } else if(argv(1) == "stop") {
- if(!votecalled) {
- sprint(self, "^1No vote called.\n");
- } else if(self == votecaller) { // the votecaller can stop a vote
- VoteStop(self);
- } else if(self.vote_master) { // masters can, too
- VoteStop(self);
- } else {
- sprint(self, "^1You are not allowed to stop that Vote.\n");
- }
- } else if(argv(1) == "master") {
- if(cvar("sv_vote_master")) {
- if(votecalled) {
- sprint(self, "^1There is already a vote called.\n");
- } else {
- votecalled = TRUE;
- votecalledmaster = TRUE;
- votecalledvote = strzone("XXX");
- votecalledvote_display = strzone("^3master");
- votecaller = self; // remember who called the vote
- votefinished = time + cvar("sv_vote_timeout");
- votecaller.vote_vote = 1; // of course you vote yes
- votecaller.vote_next = time + cvar("sv_vote_wait");
- bprint("\{1}^2* ^3", votecaller.netname, "^2 calls a vote to become ^3master^2.\n");
- VoteCount(); // needed if you are the only one
- }
- } else {
- sprint(self, "^1Vote to become master is NOT allowed.\n");
- }
- } else if(argv(1) == "do") {
- if(argv(2) == "login") {
- local string masterpwd;
- masterpwd = cvar_string("sv_vote_master_password");
- if(masterpwd != "") {
- self.vote_master = (masterpwd == argv(3));
- if(self.vote_master) {
- ServerConsoleEcho(strcat("Accepted master login from ", self.netname), TRUE);
- bprint("\{1}^2* ^3", self.netname, "^2 logged in as ^3master^2\n");
- }
- else
- ServerConsoleEcho(strcat("REJECTED master login from ", self.netname), TRUE);
- }
- else
- sprint(self, "^1You are NOT a master.\n");
- } else if(self.vote_master) {
- local string dovote, dovote_display;
- dovote = VoteParse();
- if(dovote == "") {
- sprint(self, "^1Your command was empty. See help for more info.\n");
- } else if(VoteCheckNasty(dovote)) {
- sprint(self, "Syntax error in command.\n");
- } else if(VoteAllowed(strcat1(argv(2)))) { // strcat seems to be necessary
- if(dovote == "chmap" || dovote == "gotomap") // won't work without arguments
- return;
- if(substring(dovote, 0, 6) == "chmap ")
- dovote = strcat("gotomap ", substring(dovote, 6, strlen(dovote) - 6));
- if(substring(dovote, 0, 8) == "gotomap ")
- {
- if(!(dovote = ValidateMap(substring(dovote, 8, strlen(dovote) - 8))))
- return;
- dovote = strcat("gotomap ", dovote);
- }
-
- dovote_display = dovote;
- if(substring(dovote, 0, 5) == "kick " || substring(dovote, 0, 8) == "kickban ")
- {
- if(!(e = GetKickVoteVictim(dovote, "vdo")))
- return;
- dovote = GetKickVoteVictim_newcommand;
- dovote_display = strcat("^1", dovote, " (^7", e.netname, "^1): ", GetKickVoteVictim_reason);
- }
- bprint("\{1}^2* ^3", self.netname, "^2 used his ^3master^2 status to do \"^2", dovote_display, "^2\".\n");
- localcmd(strcat(dovote, "\n"));
- } else {
- sprint(self, "^1This command is not ok. See help for more info.\n");
- }
- } else {
- sprint(self, "^1You are NOT a master.\n");
- }
- } else if(argv(1) == "yes") {
- if(!votecalled) {
- sprint(self, "^1No vote called.\n");
- } else if(self.vote_vote == 0
- || cvar("sv_vote_change")) {
- sprint(self, "^1You accepted the vote.\n");
- self.vote_vote = 1;
- centerprint_expire(self, CENTERPRIO_VOTE);
- if(!cvar("sv_vote_singlecount")) {
- VoteCount();
- }
- } else {
- sprint(self, "^1You have already voted.\n");
- }
- } else if(argv(1) == "no") {
- if(!votecalled) {
- sprint(self, "^1No vote called.\n");
- } else if(self.vote_vote == 0
- || cvar("sv_vote_change")) {
- sprint(self, "^1You rejected the vote.\n");
- self.vote_vote = -1;
- centerprint_expire(self, CENTERPRIO_VOTE);
- if(!cvar("sv_vote_singlecount")) {
- VoteCount();
- }
- } else {
- sprint(self, "^1You have already voted.\n");
- }
- } else if(argv(1) == "abstain" || argv(1) == "dontcare") {
- if(!votecalled) {
- sprint(self, "^1No vote called.\n");
- } else if(self.vote_vote == 0
- || cvar("sv_vote_change")) {
- sprint(self, "^1You abstained from your vote.\n");
- self.vote_vote = -2;
- centerprint_expire(self, CENTERPRIO_VOTE);
- if(!cvar("sv_vote_singlecount")) {
- VoteCount();
- }
- } else {
- sprint(self, "^1You have already voted.\n");
- }
- } else {
- // ignore this?
- sprint(self, "^1Unknown vote command.\n");
- }
+ if(GameCommand_Vote(s, self)) {
+ return;
} else if(argv(0) == "autoswitch") {
// be backwards compatible with older clients (enabled)
self.autoswitch = ("0" != argv(1));
@@ -609,286 +332,6 @@
}
}
-string ValidateMap(string m)
-{
-#ifdef MAPINFO
- m = MapInfo_FixName(m);
- if(!m)
- {
- sprint(self, "This map is not available on this server.\n");
- return string_null;
- }
-#else
- if(!cvar("sv_vote_change_gametype"))
- if(!IsSameGametype(m))
- {
- sprint(self, "This server does not allow changing the game type by map votes.\n");
- return string_null;
- }
-#endif
- if(!cvar("sv_vote_override_mostrecent"))
- if(Map_IsRecent(m))
- {
- sprint(self, "This server does not allow for recent maps to be played again. Please be patient for some rounds.\n");
- return string_null;
- }
-#ifdef MAPINFO
- if(!MapInfo_CheckMap(m))
- {
- sprint(self, strcat("^1Invalid mapname, \"^3", m, "^1\" does not support the current game mode.\n"));
- return string_null;
- }
-#else
- if(!TryFile(strcat("maps/", m, ".mapcfg")))
- {
- sprint(self, strcat("^1Invalid mapname, \"^3", m, "^1\" does not exist on this server.\n"));
- return string_null;
- }
-#endif
-
- return m;
-}
-
-
-void VoteThink() {
- if(votefinished > 0 // a vote was called
- && time > votefinished) // time is up
- {
- VoteCount();
- }
-}
-
-string VoteParse() {
- local float index;
- index = 3;
- local string vote;
- vote = argv(2);
- while(argv(index) != "") {
- vote = strcat(vote, " ", argv(index));
- index++;
- }
-
- // necessary for some of the string operations
- vote = strzone(vote);
-
- // now we remove some things that could be misused
- index = 0;
- local float found;
- found = FALSE;
- local float votelength;
- votelength = strlen(vote);
- while(!found && index < votelength)
- {
- local string badchar;
- badchar = substring(vote, index, 1);
- if(badchar == ";"
- || badchar == "\r"
- || badchar == "\n")
- {
- found = TRUE;
- } else {
- index++;
- }
- }
- return substring(vote, 0, index);
-}
-
-float VoteAllowed(string votecommand) {
- tokenize(cvar_string("sv_vote_commands"));
- local float index;
- index = 0;
- while(argv(index) != "") {
- local string allowed;
- allowed = argv(index);
- if(votecommand == allowed) {
- return TRUE;
- }
- index++;
- }
- return FALSE;
-}
-
-void VoteReset() {
- local entity player;
-
- FOR_EACH_CLIENT(player)
- {
- player.vote_vote = 0;
- centerprint_expire(player, CENTERPRIO_VOTE);
- }
-
- if(votecalled)
- {
- strunzone(votecalledvote);
- strunzone(votecalledvote_display);
- }
-
- votecalled = FALSE;
- votecalledmaster = FALSE;
- votefinished = 0;
-}
-
-void VoteAccept() {
- bprint("\{1}^2* ^3", votecaller.netname, "^2's vote for ^1", votecalledvote_display, "^2 was accepted\n");
- if(votecalledmaster)
- {
- votecaller.vote_master = 1;
- } else {
- //in g_tourney mode and if the vote is a timelimit-change, don't change it immediately but after restart
- if(cvar("g_tourney") && substring(votecalledvote, 0, 10) == "timelimit ") {
- if( stof(substring(votecalledvote, 10, strlen(votecalledvote) - 10)) > 0 ) {
- timelimit_orig = stof(substring(votecalledvote, 10, strlen(votecalledvote) - 10));
- bprint(strcat("The timelimit will be set to ", ftos(timelimit_orig), " minutes after the next restart!\n"));
- }
- else //calls like "timelimit -1" can pass immediately
- localcmd(strcat(votecalledvote, "\n"));
- }
- else
- localcmd(strcat(votecalledvote, "\n"));
- }
- votecaller.vote_next = 0; // people like your votes, no wait for next vote
- VoteReset();
-}
-
-void VoteReject() {
- bprint("\{1}^2* ^3", votecaller.netname, "^2's vote for ", votecalledvote_display, "^2 was rejected\n");
- VoteReset();
-}
-
-void VoteTimeout() {
- bprint("\{1}^2* ^3", votecaller.netname, "^2's vote for ", votecalledvote_display, "^2 timed out\n");
- VoteReset();
-}
-
-void VoteStop(entity stopper) {
- bprint("\{1}^2* ^3", stopper.netname, "^2 stopped ^3", votecaller.netname, "^2's vote\n");
- if(stopper == votecaller) {
- // no wait for next vote so you can correct your vote
- votecaller.vote_next = 0;
- }
- VoteReset();
-}
-
-void VoteNag() {
- if(votecalled)
- if(self.vote_vote == 0)
- centerprint_atprio(self, CENTERPRIO_VOTE, strcat("^7^3", votecaller.netname, "^2 called a vote for ", votecalledvote_display, "\n\n^2You have not voted yet!\n^2HINT: By default, F1 is yes and F2 is no."));
-}
-
-void VoteSpam(float yescount, float nocount, float abstaincount, float notvoters, float mincount)
-{
- string s;
- if(mincount >= 0)
- {
- s = strcat("\{1}^2* vote results: ^1", ftos(yescount), "^2:^1");
- s = strcat(s, ftos(nocount), "^2 (^1");
- s = strcat(s, ftos(mincount), "^2 needed), ^1");
- s = strcat(s, ftos(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(yescount), "^2:^1");
- s = strcat(s, ftos(nocount), "^2 (^1");
- s = strcat(s, ftos(abstaincount), "^2 didn't care, ^1");
- s = strcat(s, ftos(notvoters), "^2 didn't have to vote\n");
- }
- bprint(s);
-}
-
-void VoteCount() {
- local float playercount;
- playercount = 0;
- local float yescount;
- yescount = 0;
- local float nocount;
- nocount = 0;
- local float abstaincount;
- abstaincount = 0;
- local entity player;
- //same for real players
- local float realplayercount;
- local float realplayeryescount;
- local float realplayernocount;
- local float realplayerabstaincount;
- realplayercount = realplayernocount = realplayerabstaincount = realplayeryescount = 0;
-
- FOR_EACH_REALCLIENT(player)
- {
- if(player.vote_vote == -1) {
- nocount++;
- } else if(player.vote_vote == 1) {
- yescount++;
- } else if(player.vote_vote == -2) {
- abstaincount++;
- }
- playercount++;
- //do the same for real players
- if(player.classname == "player") {
- if(player.vote_vote == -1) {
- realplayernocount++;
- } else if(player.vote_vote == 1) {
- realplayeryescount++;
- } else if(player.vote_vote == -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( cvar("g_tourney") && (realplayercount > 0) ) {
- yescount = realplayeryescount;
- nocount = realplayernocount;
- abstaincount = realplayerabstaincount;
- playercount = realplayercount;
- }
-
-
- if((playercount == 1) && votecalledmaster) {
- // 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?
- sprint(self, "^1You are the only player on this server so you can not become vote master.\n");
- votecaller.vote_next = 0;
- VoteReset();
- } else {
- float votefactor;
- votefactor = bound(0.5, cvar("sv_vote_majority_factor"), 0.999);
- if(yescount > (playercount - abstaincount) * votefactor)
- {
- VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, -1);
- VoteAccept();
- }
- else if(nocount >= (playercount - abstaincount) * (1 - votefactor)) // that means, yescount cannot reach minyes any more
- {
- VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, -1);
- VoteReject();
- }
- else if(time > votefinished)
- {
- if(cvar("sv_vote_simple_majority"))
- {
- VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, floor((yescount + nocount) * votefactor) + 1);
- if(yescount > (yescount + nocount) * votefactor)
- VoteAccept();
- else if(yescount + nocount > 0)
- VoteReject();
- else
- VoteTimeout();
- }
- else
- {
- VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, floor((playercount - abstaincount) * votefactor) + 1);
- VoteTimeout();
- }
- }
- }
-}
-
/**
* 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
Modified: trunk/data/qcsrc/server/gamecommand.qc
===================================================================
--- trunk/data/qcsrc/server/gamecommand.qc 2008-06-01 13:23:44 UTC (rev 3667)
+++ trunk/data/qcsrc/server/gamecommand.qc 2008-06-01 16:57:58 UTC (rev 3668)
@@ -31,12 +31,15 @@
print(" savedb filename\n");
print(" dumpdb filename\n");
print(" loaddb filename\n");
- print(" vstop\n");
+ GameCommand_Vote("help", world);
GameCommand_Ban("help");
GameCommand_Generic("help");
return;
}
+ if(GameCommand_Vote(command, world))
+ return;
+
if(GameCommand_Ban(command))
return;
@@ -184,18 +187,6 @@
bprint("That command can only be used in a team-based gamemode.\n");
return;
}
- if(argv(0) == "vstop")
- {
- local entity temp;
- temp = spawn();
- if(cvar_string("sv_adminnick") == "")
- temp.netname = cvar_string("hostname");
- else
- temp.netname = cvar_string("sv_adminnick");
- VoteStop(temp);
- remove(temp);
- return;
- }
print("Invalid command. For a list of supported commands, try sv_cmd help.\n");
}
Modified: trunk/data/qcsrc/server/progs.src
===================================================================
--- trunk/data/qcsrc/server/progs.src 2008-06-01 13:23:44 UTC (rev 3667)
+++ trunk/data/qcsrc/server/progs.src 2008-06-01 16:57:58 UTC (rev 3668)
@@ -21,6 +21,8 @@
antilag.qh
+vote.qh
+
miscfunctions.qc
waypointsprites.qc
@@ -87,6 +89,8 @@
clientcommands.qc
+vote.qc
+
campaign.qc
../common/campaign_file.qc
../common/campaign_setup.qc
Added: trunk/data/qcsrc/server/vote.qc
===================================================================
--- trunk/data/qcsrc/server/vote.qc (rev 0)
+++ trunk/data/qcsrc/server/vote.qc 2008-06-01 16:57:58 UTC (rev 3668)
@@ -0,0 +1,599 @@
+float VoteCheckNasty(string cmd)
+{
+ if(strstrofs(cmd, ";", 0) >= 0)
+ return TRUE;
+ if(strstrofs(cmd, "\n", 0) >= 0)
+ return TRUE;
+ if(strstrofs(cmd, "\r", 0) >= 0)
+ return TRUE;
+ if(strstrofs(cmd, "$", 0) >= 0)
+ return TRUE;
+ return FALSE;
+}
+
+entity GetKickVoteVictim(string vote, string cmd, entity caller)
+{
+ float tokens;
+ float i, n, t;
+ string ns;
+ entity e;
+
+ tokens = tokenize(vote);
+ ns = "";
+
+ if(tokens >= 2)
+ if(substring(argv(1), 0, 1) == "#")
+ {
+ ns = substring(argv(1), 1, 999);
+ t = 2;
+ }
+
+ if(tokens >= 3)
+ if(argv(1) == "#")
+ {
+ ns = argv(2);
+ t = 3;
+ }
+
+ if(ns != "")
+ {
+ GetKickVoteVictim_reason = "";
+ for(i = t; i < tokens; ++i)
+ GetKickVoteVictim_reason = strcat(GetKickVoteVictim_reason, argv(i), " ");
+ GetKickVoteVictim_reason = substring(GetKickVoteVictim_reason, 0, strlen(GetKickVoteVictim_reason) - 1);
+
+ n = stof(ns);
+ if(ns == ftos(n)) if(n >= 1) if(n <= maxclients)
+ {
+ e = edict_num(n);
+ if(clienttype(e) == CLIENTTYPE_REAL)
+ {
+ GetKickVoteVictim_newcommand = strcat(argv(0), " # ", ns);
+ return e;
+ }
+ }
+ }
+
+ print_to(caller, strcat("Usage: ", cmd, " ", argv(0), " #playernumber (as in \"status\")\n"));
+ return world;
+}
+
+float GameCommand_Vote(string s, entity e) {
+ if(argv(0) == "help") {
+ print_to(e, " vote COMMANDS ARGUMENTS. See 'vote help' for more info.");
+ return TRUE;
+ } else if(argv(0) == "vote") {
+ if(argv(1) == "") {
+ print_to(e, "^1You have to supply a vote command. See help for more info.");
+ } else if(argv(1) == "help") {
+ VoteHelp(e);
+ } else if(argv(1) == "status") {
+ if(votecalled) {
+ print_to(e, strcat("^7Vote for ", votecalledvote_display, "^7 called by ^7", VoteNetname(votecaller), "^7."));
+ } else {
+ print_to(e, "^1No vote called.");
+ }
+ } else if(argv(1) == "call") {
+ if(cvar("sv_vote_call")) {
+ if(tourneyInMatchStage
+ && cvar("g_tourney_disable_spec_vote")
+ && e.classname != "player") {
+ print_to(e, "^1Error: Only players can call a vote during the match-stage.");
+ }
+ else if(timeoutStatus) { //don't allow a vote call during a timeout
+ print_to(e, "^1Error: You can not call a vote while a timeout is active.");
+ }
+ else if(votecalled) {
+ print_to(e, "^1There is already a vote called.");
+ } else {
+ local string vote;
+ vote = VoteParse();
+ if(vote == "") {
+ print_to(e, "^1Your vote is empty. See help for more info.");
+ } else if(e
+ && time < e.vote_next) {
+ print_to(e, strcat("^1You have to wait ^2", ftos(e.vote_next - time), "^1 seconds before you can again call a vote."));
+ } else if(VoteCheckNasty(vote)) {
+ print_to(e, "Syntax error in command. See help for more info.");
+ } else if(VoteAllowed(strcat1(argv(2)))) { // strcat seems to be necessary
+ // remap chmap to gotomap (forces intermission)
+ if(vote == "chmap" || vote == "gotomap") // won't work without arguments
+ return TRUE;
+ if(substring(vote, 0, 6) == "chmap ")
+ vote = strcat("gotomap ", substring(vote, 6, strlen(vote) - 6));
+ if(substring(vote, 0, 8) == "gotomap ")
+ {
+ if(!(vote = ValidateMap(substring(vote, 8, strlen(vote) - 8), e)))
+ return TRUE;
+ vote = strcat("gotomap ", vote);
+ }
+
+ // make kick and kickban votes a bit nicer (and reject them if formatted badly)
+ if(substring(vote, 0, 5) == "kick " || substring(vote, 0, 8) == "kickban ")
+ {
+ if(!(e = GetKickVoteVictim(vote, "vcall", e)))
+ return TRUE;
+ vote = GetKickVoteVictim_newcommand;
+ votecalledvote_display = strzone(strcat("^1", vote, " (^7", VoteNetname(e), "^1): ", GetKickVoteVictim_reason));
+ }
+ else
+ {
+ votecalledvote_display = strzone(strcat("^1", vote));
+ }
+ votecalledvote = strzone(vote);
+ votecalled = TRUE;
+ votecalledmaster = FALSE;
+ votefinished = time + cvar("sv_vote_timeout");
+ votecaller = e; // remember who called the vote
+ if(e) {
+ e.vote_vote = 1; // of course you vote yes
+ e.vote_next = time + cvar("sv_vote_wait");
+ }
+ bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2 calls a vote for ", votecalledvote_display, "\n");
+ VoteCount(); // needed if you are the only one
+ } else {
+ print_to(e, "^1This vote is not ok. See help for more info.");
+ }
+ }
+ } else {
+ print_to(e, "^1Vote calling is NOT allowed.");
+ }
+ } else if(argv(1) == "stop") {
+ if(!votecalled) {
+ print_to(e, "^1No vote called.");
+ } else if(e == votecaller) { // the votecaller can stop a vote
+ VoteStop(e);
+ } else if(!e) { // server admin / console can too
+ VoteStop(e);
+ } else if(e.vote_master) { // masters can too
+ VoteStop(e);
+ } else {
+ print_to(e, "^1You are not allowed to stop that Vote.");
+ }
+ } else if(argv(1) == "master") {
+ if(cvar("sv_vote_master")) {
+ if(votecalled) {
+ print_to(e, "^1There is already a vote called.");
+ } else {
+ votecalled = TRUE;
+ votecalledmaster = TRUE;
+ votecalledvote = strzone("XXX");
+ votecalledvote_display = strzone("^3master");
+ votefinished = time + cvar("sv_vote_timeout");
+ votecaller = e; // remember who called the vote
+ if(e) {
+ e.vote_vote = 1; // of course you vote yes
+ e.vote_next = time + cvar("sv_vote_wait");
+ }
+ bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2 calls a vote to become ^3master^2.\n");
+ VoteCount(); // needed if you are the only one
+ }
+ } else {
+ print_to(e, "^1Vote to become master is NOT allowed.");
+ }
+ } else if(argv(1) == "do") {
+ if(!e || e.vote_master) {
+ local string dovote, dovote_display;
+ dovote = VoteParse();
+ if(dovote == "") {
+ print_to(e, "^1Your command was empty. See help for more info.");
+ } else if(VoteCheckNasty(dovote)) {
+ print_to(e, "Syntax error in command. See help for more info.");
+ } else if(VoteAllowed(strcat1(argv(2)))) { // strcat seems to be necessary
+ if(dovote == "chmap" || dovote == "gotomap") // won't work without arguments
+ return TRUE;
+ if(substring(dovote, 0, 6) == "chmap ")
+ dovote = strcat("gotomap ", substring(dovote, 6, strlen(dovote) - 6));
+ if(substring(dovote, 0, 8) == "gotomap ")
+ {
+ if(!(dovote = ValidateMap(substring(dovote, 8, strlen(dovote) - 8), e)))
+ return TRUE;
+ dovote = strcat("gotomap ", dovote);
+ }
+
+ dovote_display = dovote;
+ if(substring(dovote, 0, 5) == "kick " || substring(dovote, 0, 8) == "kickban ")
+ {
+ if(!(e = GetKickVoteVictim(dovote, "vdo", e)))
+ return TRUE;
+ dovote = GetKickVoteVictim_newcommand;
+ dovote_display = strcat("^1", dovote, " (^7", VoteNetname(e), "^1): ", GetKickVoteVictim_reason);
+ }
+ bprint("\{1}^2* ^3", VoteNetname(e), "^2 used his ^3master^2 status to do \"^2", dovote_display, "^2\".\n");
+ localcmd(strcat(dovote, "\n"));
+ } else {
+ print_to(e, "^1This command is not ok. See help for more info.");
+ }
+ } else {
+ print_to(e, "^1You are NOT a master. You might need to login or vote to become master first. See help for more info.");
+ }
+ } else if(argv(1) == "login") {
+ local string masterpwd;
+ masterpwd = cvar_string("sv_vote_master_password");
+ if(masterpwd != "") {
+ local float granted;
+ granted = (masterpwd == argv(2));
+ if (e)
+ e.vote_master = granted;
+ if(granted) {
+ ServerConsoleEcho(strcat("Accepted master login from ", VoteNetname(e)), TRUE);
+ bprint("\{1}^2* ^3", VoteNetname(e), "^2 logged in as ^3master^2\n");
+ }
+ else
+ ServerConsoleEcho(strcat("REJECTED master login from ", VoteNetname(e)), TRUE);
+ }
+ else
+ print_to(e, "^1Login to become master is NOT allowed.");
+ } else if(argv(1) == "yes") {
+ if(!votecalled) {
+ print_to(e, "^1No vote called.");
+ } else if (!e) {
+ print_to(e, "^1You can't vote from the server console.");
+ } else if(e.vote_vote == 0
+ || cvar("sv_vote_change")) {
+ print_to(e, "^1You accepted the vote.");
+ e.vote_vote = 1;
+ centerprint_expire(e, CENTERPRIO_VOTE);
+ if(!cvar("sv_vote_singlecount")) {
+ VoteCount();
+ }
+ } else {
+ print_to(e, "^1You have already voted.");
+ }
+ } else if(argv(1) == "no") {
+ if(!votecalled) {
+ print_to(e, "^1No vote called.");
+ } else if (!e) {
+ print_to(e, "^1You can't vote from the server console.");
+ } else if(e.vote_vote == 0
+ || cvar("sv_vote_change")) {
+ print_to(e, "^1You rejected the vote.");
+ e.vote_vote = -1;
+ centerprint_expire(e, CENTERPRIO_VOTE);
+ if(!cvar("sv_vote_singlecount")) {
+ VoteCount();
+ }
+ } else {
+ print_to(e, "^1You have already voted.");
+ }
+ } else if(argv(1) == "abstain" || argv(1) == "dontcare") {
+ if(!votecalled) {
+ print_to(e, "^1No vote called.");
+ } else if (!e) {
+ print_to(e, "^1You can't vote from the server console.");
+ } else if(e.vote_vote == 0
+ || cvar("sv_vote_change")) {
+ print_to(e, "^1You abstained from your vote.");
+ e.vote_vote = -2;
+ centerprint_expire(e, CENTERPRIO_VOTE);
+ if(!cvar("sv_vote_singlecount")) {
+ VoteCount();
+ }
+ } else {
+ print_to(e, "^1You have already voted.");
+ }
+ } else {
+ // ignore this?
+ print_to(e, "^1Unknown vote command.");
+ }
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void VoteHelp(entity e) {
+ local string vmasterdis;
+ if(!cvar("sv_vote_master")) {
+ vmasterdis = " ^1(disabled)";
+ }
+
+ local string vlogindis;
+ if("" == cvar_string("sv_vote_master_password")) {
+ vlogindis = " ^1(disabled)";
+ }
+
+ local string vcalldis;
+ if(!cvar("sv_vote_call")) {
+ vcalldis = " ^1(disabled)";
+ }
+
+ 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\".");
+ 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\".");
+ print_to(e, "^7\"^2help^7\" shows this info.");
+ print_to(e, "^7\"^2status^7\" shows if there is a vote called and who called it.");
+ print_to(e, strcat("^7\"^2call^7\" is used to call a vote. See the list of allowed commands.", vcalldis, "^7"));
+ print_to(e, "^7\"^2stop^7\" can be used by the vote caller or an admin to stop a vote and maybe correct it.");
+ print_to(e, strcat("^7\"^2master^7\" call a vote to become master who can execute commands without a vote", vmasterdis, "^7"));
+ print_to(e, strcat("^7\"^2login^7\" login to become master who can execute commands without a vote.", vlogindis, "^7"));
+ print_to(e, "^7\"^2do^7\" executes a command if you are a master. See the list of allowed commands.");
+ print_to(e, "^7\"^2yes^7\", \"^2no^7\", \"^2abstain^7\" and \"^2dontcare^7\" to make your vote.");
+ print_to(e, "^7If enough of the players vote yes the vote is accepted.");
+ print_to(e, "^7If enough of the players vote no the vote is rejected.");
+ print_to(e, strcat("^7If neither the vote will timeout after ", cvar_string("sv_vote_timeout"), "^7 seconds."));
+ print_to(e, "^7You can call a vote for or execute these commands:");
+ print_to(e, strcat("^3", cvar_string("sv_vote_commands"), "^7 and maybe further ^3arguments^7"));
+}
+
+string VoteNetname(entity e)
+{
+ if(e) {
+ return e.netname;
+ } else {
+ if(cvar_string("sv_adminnick") != "") {
+ return cvar_string("sv_adminnick");
+ } else {
+ return cvar_string("hostname");
+ }
+ }
+}
+
+string ValidateMap(string m, entity e)
+{
+#ifdef MAPINFO
+ m = MapInfo_FixName(m);
+ if(!m)
+ {
+ print_to(e, "This map is not available on this server.");
+ return string_null;
+ }
+#else
+ if(!cvar("sv_vote_change_gametype"))
+ if(!IsSameGametype(m))
+ {
+ print_to(e, "This server does not allow changing the game type by map votes.");
+ return string_null;
+ }
+#endif
+ if(!cvar("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;
+ }
+#ifdef MAPINFO
+ if(!MapInfo_CheckMap(m))
+ {
+ print_to(e, strcat("^1Invalid mapname, \"^3", m, "^1\" does not support the current game mode."));
+ return string_null;
+ }
+#else
+ if(!TryFile(strcat("maps/", m, ".mapcfg")))
+ {
+ print_to(e, strcat("^1Invalid mapname, \"^3", m, "^1\" does not exist on this server."));
+ return string_null;
+ }
+#endif
+
+ return m;
+}
+
+
+void VoteThink() {
+ if(votefinished > 0) // a vote was called
+ if(time > votefinished) // time is up
+ {
+ VoteCount();
+ }
+}
+
+string VoteParse() {
+ local float index;
+ index = 3;
+ local string vote;
+ vote = argv(2);
+ while(argv(index) != "") {
+ vote = strcat(vote, " ", argv(index));
+ index++;
+ }
+
+ // necessary for some of the string operations
+ vote = strzone(vote);
+
+ return vote;
+}
+
+float VoteAllowed(string votecommand) {
+ tokenize(cvar_string("sv_vote_commands"));
+ local float index;
+ index = 0;
+ while(argv(index) != "") {
+ if(votecommand == argv(index)) {
+ return TRUE;
+ }
+ index++;
+ }
+ return FALSE;
+}
+
+void VoteReset() {
+ local entity player;
+
+ FOR_EACH_CLIENT(player)
+ {
+ player.vote_vote = 0;
+ centerprint_expire(player, CENTERPRIO_VOTE);
+ }
+
+ if(votecalled)
+ {
+ strunzone(votecalledvote);
+ strunzone(votecalledvote_display);
+ }
+
+ votecalled = FALSE;
+ votecalledmaster = FALSE;
+ votefinished = 0;
+}
+
+void VoteAccept() {
+ bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ^1", votecalledvote_display, "^2 was accepted\n");
+ if(votecalledmaster)
+ {
+ if(votecaller) {
+ votecaller.vote_master = 1;
+ }
+ } else {
+ //in g_tourney mode and if the vote is a timelimit-change, don't change it immediately but after restart
+ if(cvar("g_tourney")
+ && substring(votecalledvote, 0, 10) == "timelimit ") {
+ if( stof(substring(votecalledvote, 10, strlen(votecalledvote) - 10)) > 0 ) {
+ timelimit_orig = stof(substring(votecalledvote, 10, strlen(votecalledvote) - 10));
+ bprint(strcat("The timelimit will be set to ", ftos(timelimit_orig), " minutes after the next restart!\n"));
+ }
+ else //calls like "timelimit -1" can pass immediately
+ localcmd(strcat(votecalledvote, "\n"));
+ }
+ else
+ localcmd(strcat(votecalledvote, "\n"));
+ }
+ if(votecaller) {
+ votecaller.vote_next = 0; // people like your votes,
+ // no wait for next vote
+ }
+ VoteReset();
+}
+
+void VoteReject() {
+ bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ", votecalledvote_display, "^2 was rejected\n");
+ VoteReset();
+}
+
+void VoteTimeout() {
+ bprint("\{1}^2* ^3", VoteNetname(votecaller), "^2's vote for ", votecalledvote_display, "^2 timed out\n");
+ VoteReset();
+}
+
+void VoteStop(entity stopper) {
+ bprint("\{1}^2* ^3", VoteNetname(stopper), "^2 stopped ^3", VoteNetname(votecaller), "^2's vote\n");
+ if(stopper == votecaller) {
+ // no wait for next vote so you can correct your vote
+ if(votecaller) {
+ votecaller.vote_next = 0;
+ }
+ }
+ VoteReset();
+}
+
+void VoteNag() {
+ if(votecalled)
+ if(self.vote_vote == 0)
+ centerprint_atprio(self, CENTERPRIO_VOTE, strcat("^7^3", VoteNetname(votecaller), "^2 called a vote for ", votecalledvote_display, "\n\n^2You have not voted yet!\n^2HINT: By default, F1 is yes and F2 is no."));
+}
+
+void VoteSpam(float yescount, float nocount, float abstaincount, float notvoters, float mincount)
+{
+ string s;
+ if(mincount >= 0)
+ {
+ s = strcat("\{1}^2* vote results: ^1", ftos(yescount), "^2:^1");
+ s = strcat(s, ftos(nocount), "^2 (^1");
+ s = strcat(s, ftos(mincount), "^2 needed), ^1");
+ s = strcat(s, ftos(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(yescount), "^2:^1");
+ s = strcat(s, ftos(nocount), "^2 (^1");
+ s = strcat(s, ftos(abstaincount), "^2 didn't care, ^1");
+ s = strcat(s, ftos(notvoters), "^2 didn't have to vote\n");
+ }
+ bprint(s);
+}
+
+void VoteCount() {
+ local float playercount;
+ playercount = 0;
+ local float yescount;
+ yescount = 0;
+ local float nocount;
+ nocount = 0;
+ local float abstaincount;
+ abstaincount = 0;
+ local entity player;
+ //same for real players
+ local float realplayercount;
+ local float realplayeryescount;
+ local float realplayernocount;
+ local float realplayerabstaincount;
+ realplayercount = realplayernocount = realplayerabstaincount = realplayeryescount = 0;
+
+ FOR_EACH_REALCLIENT(player)
+ {
+ if(player.vote_vote == -1) {
+ nocount++;
+ } else if(player.vote_vote == 1) {
+ yescount++;
+ } else if(player.vote_vote == -2) {
+ abstaincount++;
+ }
+ playercount++;
+ //do the same for real players
+ if(player.classname == "player") {
+ if(player.vote_vote == -1) {
+ realplayernocount++;
+ } else if(player.vote_vote == 1) {
+ realplayeryescount++;
+ } else if(player.vote_vote == -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(cvar("g_tourney"))
+ if(realplayercount > 0) {
+ yescount = realplayeryescount;
+ nocount = realplayernocount;
+ abstaincount = realplayerabstaincount;
+ playercount = realplayercount;
+ }
+
+
+ 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 {
+ float votefactor;
+ votefactor = bound(0.5, cvar("sv_vote_majority_factor"), 0.999);
+ if(yescount > (playercount - abstaincount) * votefactor)
+ {
+ VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, -1);
+ VoteAccept();
+ }
+ else if(nocount >= (playercount - abstaincount) * (1 - votefactor)) // that means, yescount cannot reach minyes any more
+ {
+ VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, -1);
+ VoteReject();
+ }
+ else if(time > votefinished)
+ {
+ if(cvar("sv_vote_simple_majority"))
+ {
+ VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, floor((yescount + nocount) * votefactor) + 1);
+ if(yescount > (yescount + nocount) * votefactor)
+ VoteAccept();
+ else if(yescount + nocount > 0)
+ VoteReject();
+ else
+ VoteTimeout();
+ }
+ else
+ {
+ VoteSpam(yescount, nocount, abstaincount, playercount - yescount - nocount - abstaincount, floor((playercount - abstaincount) * votefactor) + 1);
+ VoteTimeout();
+ }
+ }
+ }
+}
Added: trunk/data/qcsrc/server/vote.qh
===================================================================
--- trunk/data/qcsrc/server/vote.qh (rev 0)
+++ trunk/data/qcsrc/server/vote.qh 2008-06-01 16:57:58 UTC (rev 3668)
@@ -0,0 +1,20 @@
+string GetKickVoteVictim_newcommand;
+string GetKickVoteVictim_reason;
+
+float VoteCheckNasty(string cmd);
+entity GetKickVoteVictim(string vote, string cmd, entity caller);
+float GameCommand_Vote(string s, entity e);
+void VoteHelp(entity e);
+string VoteNetname(entity e);
+string ValidateMap(string m, entity e);
+void VoteThink();
+string VoteParse();
+float VoteAllowed(string votecommand);
+void VoteReset();
+void VoteAccept();
+void VoteReject();
+void VoteTimeout();
+void VoteStop(entity stopper);
+void VoteNag();
+void VoteSpam(float yescount, float nocount, float abstaincount, float notvoters, float mincount);
+void VoteCount();
More information about the nexuiz-commits
mailing list