From d499fc138da158917ce0c4772184e40f990ce26d Mon Sep 17 00:00:00 2001 From: Marco Cawthorne Date: Sat, 5 Aug 2023 16:30:10 -0700 Subject: [PATCH] Add our first iteration of the callvote system. --- progs.src | 4 +- voting.qc | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ world.qc | 3 + 3 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 voting.qc diff --git a/progs.src b/progs.src index 8f12942..7b1b1b6 100644 --- a/progs.src +++ b/progs.src @@ -1,4 +1,4 @@ -./qwprogs.dat +../qwprogs.dat defs.qc subs.qc @@ -15,5 +15,5 @@ buttons.qc triggers.qc plats.qc misc.qc - server.qc +voting.qc diff --git a/voting.qc b/voting.qc new file mode 100644 index 0000000..ae79524 --- /dev/null +++ b/voting.qc @@ -0,0 +1,327 @@ +/* In order to incorporate this file into your mod, you have to do two things: + +1. Include this file in your progs.src +2. Call Vote_Init(); inside worldspawn. + +You need fteqcc and fteqw-sv for this to work. + +*/ + +/* builtins required */ +float(string) tokenize = #441; +string(float) argv = #442; +void(entity e, string s) clientcommand = #440; +void(entity player, string key, string value) forceinfokey = #213; +string(string key) serverkey = #354; +float(string key, optional float assumevalue) serverkeyfloat = #0:serverkeyfloat; + +void ChatMessage_Parse( entity, string ); +string Util_TimeToString(float fTime); +void Vote_Cmd_VoteYes(void); +void Vote_Cmd_VoteNo(void); +void Vote_Cmd_CallVote(string); +void Vote_Help(entity); + +var string g_strVoteCmd; +var float g_flVoteTime; +var float g_iVoteState; +var float autocvar_mp_allowvote = 1; + +.float voted; +entity voteHandler; + +enum +{ + VOTE_INACTIVE, + VOTE_INPROGRESS, + VOTE_PASSED +}; + +/* callback from fteqw-sv */ +void +SV_ParseClientCommand(string cmd) +{ + float argc; + + argc = tokenize(cmd); + + switch (argv(0)) { + case "say": + clientcommand(self, cmd); + ChatMessage_Parse(self, argv(1)); + break; + default: + clientcommand(self, cmd); + } +} + + +void +ChatMessage_Parse(entity sayingEnt, string commandString) +{ + float isMap = 0; + if (whichpack(strcat("maps/", commandString, ".bsp"))) { + isMap = 1; + } + + float args = tokenize(commandString); + + switch (argv(0)) { + case "yes": + self = sayingEnt; + Vote_Cmd_VoteYes(); + break; + case "no": + self = sayingEnt; + Vote_Cmd_VoteNo(); + break; + case "rtv": + bprint(PRINT_CHAT, sprintf("rock the vote requested by %s\n", sayingEnt.netname)); + break; + case "timeleft": + string msg; + string timestring; + float timeleft; + timeleft = cvar("timelimit") - (time / 60); + timestring = Util_TimeToString(timeleft); + msg = sprintf("we have %s minutes remaining\n", timestring); + sprint(sayingEnt, PRINT_CHAT, msg); + break; + case "callvote": + Vote_Cmd_CallVote(commandString); + break; + case "votehelp": + Vote_Help(sayingEnt); + break; + default: + if (isMap) { + bprint(PRINT_CHAT, sprintf("%s was nominated by %s\n", commandString, sayingEnt.netname)); + } + break; + } +} + +string +Util_TimeToString(float fTime) +{ + fTime = rint(fTime); + + switch (fTime) { + case 0: return "less than one"; + case 1: return "one"; + case 2: return "two"; + case 3: return "three"; + case 4: return "four"; + case 5: return "five"; + case 6: return "six"; + case 7: return "seven"; + case 8: return "eight"; + case 9: return "nine"; + case 10: return "ten"; + default: return "over ten"; + } +} + +void +Vote_End(void) +{ + + localcmd(sprintf("%s\n", g_strVoteCmd)); + g_flVoteTime = 0.0f; + g_iVoteState = VOTE_INACTIVE; + remove(voteHandler); +} + +void +Vote_Reset(void) +{ + forceinfokey(world, "votes_y", "0"); + forceinfokey(world, "votes_n", "0"); + forceinfokey(world, "vote_cmd", ""); + + for (entity e = world; (e = find(e, ::classname, "player"));) { + e.voted = 0; + } +} + +void +Vote_Passed(void) +{ + g_flVoteTime = time + 5.0f; + g_iVoteState = VOTE_PASSED; + bprint(PRINT_CHAT, "Vote passed.\n"); + g_strVoteCmd = serverkey("vote_cmd"); + Vote_Reset(); +} + +void +Vote_Failed(void) +{ + g_flVoteTime = 0.0; + g_iVoteState = VOTE_INACTIVE; + bprint(PRINT_CHAT, "Vote failed.\n"); + Vote_Reset(); + remove(voteHandler); +} + +void +Vote_Frame(void) +{ + if (time >= g_flVoteTime) { + if (g_iVoteState == VOTE_INPROGRESS) { + if (serverkeyfloat("votes_y") > serverkeyfloat("votes_n")) { + Vote_Passed(); + } else { + Vote_Failed(); + } + } else if (g_iVoteState == VOTE_PASSED) { + Vote_End(); + } + } + + self.nextthink = time; +} + +void +Vote_Cmd_VoteYes(void) +{ + /* No vote is in progress */ + if (g_iVoteState != VOTE_INPROGRESS) { + return; + } + + if (self.classname != "player") { + return; + } + + if (self.voted) { + return; + } + + forceinfokey(world, "votes_y", ftos(serverkeyfloat("votes_y")+1)); + self.voted = 1; + + /* HACK: Is there a better way to do this? */ + float playernums = 0; + for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) { + playernums++; + } + + /* We need at least half the players agreeing. */ + if (serverkeyfloat("votes_y") > rint(playernums / 2)) { + Vote_Passed(); + return; + } + + if (serverkeyfloat("votes_n") + serverkeyfloat("votes_y") == playernums) { + g_flVoteTime = time + 0.0f; + } +} + +void +Vote_Cmd_VoteNo(void) +{ + /* No vote is in progress */ + if (g_iVoteState != VOTE_INPROGRESS) { + return; + } + + if (self.classname != "player") { + return; + } + + if (self.voted) { + return; + } + + forceinfokey(world, "votes_n", ftos(serverkeyfloat("votes_n")+1)); + self.voted = 1; + + /* HACK: Is there a better way to do this? */ + float playernums = 0; + for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) { + playernums++; + } + + /* We need at least half the players disagreeing. */ + if (serverkeyfloat("votes_n") > rint(playernums / 2)) { + Vote_Failed(); + return; + } + + if (serverkeyfloat("votes_n") + serverkeyfloat("votes_y") == playernums) { + g_flVoteTime = time + 0.0f; + } +} + +void +Vote_InitiateVote(string votemsg) +{ + /* A vote is in progress */ + if (g_iVoteState != VOTE_INACTIVE) { + return; + } + + if (self.classname != "player") { + return; + } + + Vote_Reset(); + + forceinfokey(world, "vote_cmd", votemsg); + g_flVoteTime = time + 30.0f; + g_iVoteState = VOTE_INPROGRESS; + + bprint(PRINT_CHAT, sprintf("vote: %S, type yes/no in chat!\n", votemsg)); +} + +void +Vote_Cmd_CallVote(string text) +{ + if (autocvar_mp_allowvote == 0) { + return; + } + + /* No vote is in progress */ + if (g_iVoteState != VOTE_INACTIVE) { + sprint(self, PRINT_CHAT, "A vote is already in progress.\n"); + return; + } + + voteHandler = spawn(); + voteHandler.think = Vote_Frame; + voteHandler.nextthink = time; + + tokenize(text); + switch (argv(1)) { + case "map": + if not (whichpack(sprintf("maps/%s.bsp", argv(2)))) { + sprint(self, PRINT_CHAT, sprintf("Map '%s' not available on server.\n", argv(2))); + break; + } + case "kick": + case "slowmo": + case "timelimit": + case "fraglimit": + case "map_restart": + case "nextmap": + Vote_InitiateVote(sprintf("%s %s", argv(1), argv(2))); + Vote_Cmd_VoteYes(); + break; + default: + sprint(self, PRINT_CHAT, sprintf("Cannot callvote for '%s'.\n", argv(1))); + } + +} + +void +Vote_Init(void) +{ + Vote_Reset(); +} + +void +Vote_Help(entity targetPlayer) +{ + centerprint(targetPlayer, "Voting is easy:\nsay: callvote map dm3\n\nplayers say 'yes' or 'no'\nto decide the outcome!\n\ncommands include:\nmap, kick, fraglimit, timelimit"); +} diff --git a/world.qc b/world.qc index 142b1ff..790e0f4 100644 --- a/world.qc +++ b/world.qc @@ -26,6 +26,7 @@ */ void() InitBodyQue; +void() Vote_Init; void() main = @@ -370,6 +371,8 @@ void() worldspawn = // 63 testing lightstyle(63, "a"); + + Vote_Init(); }; void() StartFrame =