Difference between revisions of "TF2 Voting"

From AlliedModders Wiki
Jump to: navigation, search
(Note about intercepting these messages.)
m (Example voting plugin: Update naming and formatting)
 
(40 intermediate revisions by 2 users not shown)
Line 4: Line 4:
  
 
== How voting works ==
 
== How voting works ==
 +
# A client issues a callvote with the vote type and argument, or the server calls a vote (server uses entity index 99).
 
# The server sends a vote_options event.
 
# The server sends a vote_options event.
#* The values it sends are garbage data (or the previous multiple choice vote's choices) for Yes/No votes or the actual vote options for multiple choice.
+
#* For Yes/No votes, the values it sends are Yes and No (in that order).  For Multiple Choice votes, the actual vote options are sent.
 
# The VoteStart User Message is sent, the last argument determines the vote type.
 
# The VoteStart User Message is sent, the last argument determines the vote type.
# Clients use the "vote" command to register their votes (option1 through option5), after which the server sends a vote_cast event with a 0-based option number (so option1 = 0, option5 = 4).
+
# Clients use the "vote" command to register their votes (option1 through option5), after which the server sends a vote_cast event with a 0-based option number (so option1 = 0, option5 = 4).  It also updates the vote_controller entity's vote counts.
 
# When the vote is complete, the server sends either a VotePass or VoteFailed User Message.
 
# When the vote is complete, the server sends either a VotePass or VoteFailed User Message.
  
 +
== Server Entity ==
 +
 +
The server should update this as appropriate.  Unfortunately, the valid values for m_iActiveIssueIndex is unknown.
 +
 +
{{begin-hl2msg|vote_controller (CVoteController)|string}}
 +
{{hl2msg|int|m_iActiveIssueIndex|Number of the active issue}}
 +
{{hl2msg|int|m_iOnlyTeamToVote|Corresponds to VoteStart's team argument.}}
 +
{{hl2msg|int[5]|m_nVoteOptionCount|Table of 5 vote counts numbered 0-4}}
 +
{{hl2msg|int|m_nPotentialVotes|Number of players eligible to vote}}
 +
{{hl2msg|int|m_bIsYesNoVote|Corresponds to VoteStart's yesno argument}}
 +
{{end-hl2msg}}
  
 
== Console Commands ==
 
== Console Commands ==
Line 16: Line 28:
 
{{qnotice|This command can take 0 arguments.  If it does, a VoteSetup usermessage is returned to that user.}}<br />
 
{{qnotice|This command can take 0 arguments.  If it does, a VoteSetup usermessage is returned to that user.}}<br />
 
{{begin-hl2msg|callvote|string}}
 
{{begin-hl2msg|callvote|string}}
{{hl2msg|string|type|Kick, RestartGame, ChangeLevel, NextLevel, or ScrambleTeams.  Should return VoteSetup Usermessage if left blank.}}
+
{{hl2msg|string|type|"Kick", "RestartGame", "ChangeLevel", "NextLevel", "ScrambleTeams", or "ChangeMission".  Should return VoteSetup Usermessage if left blank.}}
{{hl2msg|string|param1|Target for Kick, or map name for Changelevel/NextLevel. Otherwise, left blank}}
+
{{hl2msg|string|param1|"userid_short kicktype_string" for Kick, map name for Changelevel/NextLevel, or popfile for ChangeMission. Otherwise, left blank}}
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
This data is primarily there if you want to intercept these messages.
+
kicktype_string is one of these (at least in the English version):
 +
* other
 +
* cheating
 +
* idle
 +
* scamming
 +
 
 +
This data is primarily there if you want to intercept these messages in a plugin.
 +
 
 +
CallVoteFailed UserMessage is sent back if a vote cannot be called unless a vote is already being displayed.
  
 
=== vote ===
 
=== vote ===
 
{{qnotice|This command is only valid when a vote is ongoing.}}<br />
 
{{qnotice|This command is only valid when a vote is ongoing.}}<br />
 
{{begin-hl2msg|vote|string}}
 
{{begin-hl2msg|vote|string}}
{{hl2msg|string|option|F1 is option1, F2 is option2, F3 is option3, F4 is option4, F5 is option5}}
+
{{hl2msg|string|option|F1 is "option1", F2 is "option2", F3 is "option3", F4 is "option4", F5 is "option5"}}
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
Line 31: Line 51:
  
 
== User Messages ==
 
== User Messages ==
{{qnotice|These user messages use unsigned bytes for the team number.  Since Valve represents no team as -1, no team is instead represented as its two's complement, 255.}}<br>
+
{{qnotice|These user messages use unsigned bytes for the team number.  Since the October 15, 2014 update, all teams is now represented by 0 (it used to be -1 / 255)}}<br>
  
  
Line 38: Line 58:
 
===VoteSetup===
 
===VoteSetup===
 
{{qnotice|Sent to a player when they load the Call Vote screen (which sends the callvote command to the server), lists what votes are allowed on the server}}<br>
 
{{qnotice|Sent to a player when they load the Call Vote screen (which sends the callvote command to the server), lists what votes are allowed on the server}}<br>
 +
{{qnotice|This changed in the October 15, 2014 update}}<br>
 +
 
{{begin-hl2msg|VoteSetup|string}}
 
{{begin-hl2msg|VoteSetup|string}}
{{hl2msg|byte|issue_count|Count of vote issues allowed on this server}}
+
{{hl2msg|byte|issue_count|Count of vote issues allowed on this server. Not present in protobuf version}}
{{hl2msg|string...|issue|An issue allowed to vote on}}
+
{{hl2msg|repeated message|issue|issue fields as shown below}}
 +
{{end-hl2msg}}
 +
 
 +
{{begin-hl2msg|issue|string}}
 +
{{hl2msg|string|potential_issue|Vote issue name that will be sent for the callvote command}}
 +
{{hl2msg|string|translation name|Translation name, such as #TF_RestartGame}}
 +
{{hl2msg|byte|enabled|1 if this issue is enabled, 0 if not}}
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
There is one string sent for each supported issue.
+
There is one "issue" sent for each supported issue.
Valid strings are:
+
Valid potential_issue strings are:
 
* Kick
 
* Kick
 
* RestartGame
 
* RestartGame
Line 50: Line 78:
 
* NextLevel
 
* NextLevel
 
* ScrambleTeams
 
* ScrambleTeams
 +
* ChangeMission
 +
* TeamAutoBalance
 +
* ClassLimits
 +
 +
Valid translation name strings are:
 +
* #TF_Kick
 +
* #TF_RestartGame
 +
* #TF_ChangeLevel
 +
* #TF_NextLevel
 +
* #TF_ScrambleTeams
 +
* #TF_ChangeMission
 +
* #TF_TeamAutoBalance_Enable
 +
* #TF_TeamAutoBalance_Disable
 +
* #TF_ClassLimit_Enable
 +
* #TF_ClassLimit_Disable
  
These will be followed by " (Disabled on Server)" if they are disabled.
+
By default, disabled issues are omitted (sv_vote_ui_hide_disabled_issues 1).
  
 
===CallVoteFailed===
 
===CallVoteFailed===
 
{{qnotice|Sent to a player when they attempt to call a vote and fail.}}<br>
 
{{qnotice|Sent to a player when they attempt to call a vote and fail.}}<br>
 
{{begin-hl2msg|CallVoteFailed|string}}
 
{{begin-hl2msg|CallVoteFailed|string}}
{{hl2msg|byte|failure_code|Failure reason (1-2, 5-10, 12-15)}}
+
{{hl2msg|byte|reason|Failure reason (1-2, 5-10, 12-19)}}
{{hl2msg|short|time|For failure reason 2, time left until client can start another vote}}
+
{{hl2msg|short|time|For failure reasons 2 and 8, time in seconds until client can start another vote. 2 is per user, 8 is per vote type.}}
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
 
Valid Failure Codes are:<br />
 
Valid Failure Codes are:<br />
1 - Cannot call vote while other players are still loading<br />
+
1 - VOTE_FAILED_TRANSITIONING_PLAYERS - Cannot call vote while other players are still loading.  This appears to be a holdover from L4D2; use code 10 instead.<br />
2 - You called a vote recently and cannot call another one for X seconds (second argument to CallVoteFailed specifies the number of seconds)<br />
+
2 - VOTE_FAILED_RATE_EXCEEDED - You called a vote recently and cannot call another one for X seconds (second argument to CallVoteFailed specifies the number of seconds)<br />
5 - Server has disabled that issue.<br />
+
5 - VOTE_FAILED_ISSUE_DISABLED - Server has disabled that issue.<br />
6 - That map does not exist.<br />
+
6 - VOTE_FAILED_MAP_NOT_FOUND - That map does not exist.<br />
7 - You must specify a map name<br />
+
7 - VOTE_FAILED_MAP_NAME_REQUIRED - You must specify a map name<br />
8 - This vote failed recently<br />
+
8 - VOTE_FAILED_FAILED_RECENTLY - This vote failed recently<br />
9 - Your team cannot call this vote<br />
+
9 - VOTE_FAILED_TEAM_CANT_CALL - Your team cannot call this vote<br />
10 - Voting not allowed while Waiting for Players<br />
+
10 - VOTE_FAILED_WAITINGFORPLAYERS - Voting not allowed while Waiting for Players<br />
12 - Can't Kick Server Admin<br />
+
11 - VOTE_FAILED_PLAYERNOTFOUND - Doesn't appear to work<br />
13 - Vote Scramble is pending<br />
+
12 - VOTE_FAILED_CANNOT_KICK_ADMIN - Can't Kick Server Admin<br />
14 - Spectators can't vote<br />
+
13 - VOTE_FAILED_SCRAMBLE_IN_PROGRESS - Vote Scramble is pending<br />
15 - Next level already set
+
14 - VOTE_FAILED_SPECTATOR - Spectators can't vote<br />
 +
15 - VOTE_FAILED_NEXTLEVEL_SET - Next level already set<br />
 +
16 - VOTE_FAILED_MAP_NOT_VALID - Map is not in the map list<br />
 +
17 - VOTE_FAILED_CANNOT_KICK_FOR_TIME - Cannot kick yet.  Used for MvM<br />
 +
18 - VOTE_FAILED_CANNOT_KICK_DURING_ROUND - Cannot kick during round.  Used for MvM<br />
 +
19 - VOTE_FAILED_MODIFICATION_ALREADY_ACTIVE - Modification is already active, used by Eternaween<br />
  
 
===VoteStart===
 
===VoteStart===
{{qnotice|Sent to all players currently online.  The default implementation also sends it to bots. The default implementation sends team specific votes to all players, but with the team byte set }}<br />
+
{{qnotice|Sent to all players currently online.  The default implementation also sends it to bots.}}<br />
  
 
{{begin-hl2msg|VoteStart|string}}
 
{{begin-hl2msg|VoteStart|string}}
{{hl2msg|byte|team|Team index or 255 for all}}
+
{{hl2msg|byte|team|Team index or 0 for all}}
{{hl2msg|byte|initiator|Client index (NOT USERID) of person who started the vote, or 99 for the server.}}
+
{{hl2msg|byte|ent_idx|Client index of person who started the vote, or 99 for the server.}}
{{hl2msg|string|issue|Vote issue translation string}}
+
{{hl2msg|string|disp_str|Vote issue translation string}}
{{hl2msg|string|param1|Vote issue text}}
+
{{hl2msg|string|details_str|Vote issue text}}
{{hl2msg|bool|yesno|true for Yes/No, false for Multiple choice}}
+
{{hl2msg|bool|is_yes_no_vote|true for Yes/No, false for Multiple choice}}
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
Line 97: Line 145:
 
* #TF_vote_should_scramble_round - Scramble teams at round end vote.  param1 ignored.  This vote is not in the user vote menu.
 
* #TF_vote_should_scramble_round - Scramble teams at round end vote.  param1 ignored.  This vote is not in the user vote menu.
 
* #TF_vote_td_start_round - Start the round?  param1 ignored.  This vote is not in the user vote menu.
 
* #TF_vote_td_start_round - Start the round?  param1 ignored.  This vote is not in the user vote menu.
 +
* #TF_vote_changechallenge - Change MvM mission? param1 is mission. This vote is in the menu on MvM maps.
 +
* #TF_vote_eternaween - Activate Halloween mode?  param1 ignored. This vote is activated by someone using the Eternaween item.
 +
* #TF_vote_autobalance_enable - Active Autobalance? param1 ignored.
 +
* #TF_vote_autobalance_disable - Disable Autobalance? param1 ignored.
 +
* #TF_vote_classlimits_enable - Enable class limits? param1 ignored.
 +
* #TF_vote_classlimits_disable - Disable class limits? param1 ignored.
 
* #TF_playerid_noteam - Unofficially used for a custom vote.  param1 is custom vote issue.
 
* #TF_playerid_noteam - Unofficially used for a custom vote.  param1 is custom vote issue.
  
Line 103: Line 157:
  
 
{{begin-hl2msg|VotePass|string}}
 
{{begin-hl2msg|VotePass|string}}
{{hl2msg|byte|team|Team index or 255 for all}}
+
{{hl2msg|byte|team|Team index or 0 for all}}
{{hl2msg|string|details|Vote success translation string}}
+
{{hl2msg|string|disp_str|Vote success translation string}}
{{hl2msg|string|param1|Vote winner}}
+
{{hl2msg|string|details_str|Vote winner}}
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
Line 116: Line 170:
 
* #TF_vote_passed_scramble_teams - Team Scramble vote passed.  param1 is ignored.
 
* #TF_vote_passed_scramble_teams - Team Scramble vote passed.  param1 is ignored.
 
* #TF_vote_passed_td_start_round - Round start vote passed.  param1 is ignored.
 
* #TF_vote_passed_td_start_round - Round start vote passed.  param1 is ignored.
 +
* #TF_vote_passed_changechallenge - MvM mission change passed.  param1 is new mission.
 +
* #TF_vote_passed_eternaween - Eternaween vote passed. param1 ignored.
 +
* #TF_vote_passed_autobalance_enable - Autobalance enable vote passed. param1 ignored.
 +
* #TF_vote_passed_autobalance_disable - Autobalance disable vote passed. param1 ignored.
 +
* #TF_vote_passed_classlimits_enable - Class Limit enable vote passed. param1 ignored.
 +
* #TF_vote_passed_classlimits_disable - Class Limit disable vote passed. param1 ignored.
 
* #TF_playerid_noteam - Unofficially used for a custom success string.  param1 is custom success string.
 
* #TF_playerid_noteam - Unofficially used for a custom success string.  param1 is custom success string.
 
  
 
===VoteFailed===
 
===VoteFailed===
Line 123: Line 182:
  
 
{{begin-hl2msg|VoteFailed|string}}
 
{{begin-hl2msg|VoteFailed|string}}
{{hl2msg|byte|team|Team index or 255 for all}}
+
{{hl2msg|byte|team|Team index or 0 for all}}
{{hl2msg|byte|failure_code|Failure reason code (0, 3-4)}}
+
{{hl2msg|byte|reason|Failure reason code (0, 3-4)}}
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
 
Valid Failure codes are:<br />
 
Valid Failure codes are:<br />
0 - Generic "Vote Failed" message<br />   
+
0 - VOTE_FAILED_GENERIC - Generic "Vote Failed" message<br />   
3 - Yes votes must outnumber No votes<br />
+
3 - VOTE_FAILED_YES_MUST_EXCEED_NO - Yes votes must outnumber No votes<br />
4 - Not Enough Votes
+
4 - VOTE_FAILED_QUORUM_FAILURE - Not Enough Votes
  
 
== Events ==
 
== Events ==
  
=== Used Events ===
+
=== Server to Client Events ===
  
 
==== vote_cast ====
 
==== vote_cast ====
Line 140: Line 199:
 
{{begin-hl2msg|vote_cast|string}}
 
{{begin-hl2msg|vote_cast|string}}
 
{{hl2msg|byte|vote_option|which option the player voted on}}
 
{{hl2msg|byte|vote_option|which option the player voted on}}
{{hl2msg|short|team|[Ed: Always -1]}}
+
{{hl2msg|short|team|[Ed: Usually -1, but team-specific votes can be 2 for RED or 3 for BLU]}}
 
{{hl2msg|long|entityid|entity id of the voter}}
 
{{hl2msg|long|entityid|entity id of the voter}}
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
When it says entity ID, it actually means client index.  It is very rare to see Valve use a client index instead of a user ID, but I tested this, and it's definitely the client index... I primarily tested this with two users.  We had user ids 3 and 4.  My votes came up with entityid 1, his came up with 2.  We both disconnected, and when I later reconnected I got user id 7.  My votes still came up with entityid 1.
+
This event is unusual as it uses the client's entity ID instead of its userid.
  
 
==== vote_options ====
 
==== vote_options ====
Line 157: Line 216:
 
{{end-hl2msg}}
 
{{end-hl2msg}}
  
=== Unused Events ===
+
If sv_vote_issue_nextlevel_allowextend is set to 1, option5 is "Extend current Map"
  
These are events that existed in L4D, but remain unused in the TF2 vote system.
+
=== Client-only Events ===
 +
 
 +
These events are passed between the TF2 client and the client's VGUI voting panels.  They have no server interaction and the server should not be touching them in any way.
  
 
==== vote_ended ====
 
==== vote_ended ====
Line 176: Line 237:
  
 
==== vote_changed ====
 
==== vote_changed ====
 +
{{qnotice|These values are read from the vote_controller entity on the server}}<br>
  
 
{{begin-hl2msg|vote_changed|string}}
 
{{begin-hl2msg|vote_changed|string}}
Line 203: Line 265:
 
This is a basic plugin that starts a vote, "Is gaben fat?".  It does not ensure the same client does not vote multiple times, nor does it actually kick the user.
 
This is a basic plugin that starts a vote, "Is gaben fat?".  It does not ensure the same client does not vote multiple times, nor does it actually kick the user.
  
<pre>#include <sourcemod>
+
<sourcepawn>#include <sourcemod>
 
// TF2's internal map vote uses client index 99 for the server
 
// TF2's internal map vote uses client index 99 for the server
 
#define TF2_SERVER_CLIENT_INDEX 99
 
#define TF2_SERVER_CLIENT_INDEX 99
 
#define TF2_TEAM_ALL -1
 
#define TF2_TEAM_ALL -1
  
new yesvotes;
+
int g_iYesVotes;
new novotes;
+
int g_iNoVotes;
 +
 
 
#define MAX_VOTES 4
 
#define MAX_VOTES 4
  
public Plugin:myinfo =  
+
public Plugin myinfo =  
 
{
 
{
name = "Test Yes/No Vote",
+
    name = "Test Yes/No Vote",
author = "Powerlord",
+
    author = "Powerlord",
description = "A test vote plugin for the AlliedMods wiki",
+
    description = "A test vote plugin for the AlliedMods wiki",
version = "1.0",
+
    version = "1.0",
url = "http://wiki.alliedmods.net/TF2_Voting"
+
    url = "http://wiki.alliedmods.net/TF2_Voting"
 
}
 
}
  
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
RegConsoleCmd("testvote",Callvote_Handler);
+
    RegConsoleCmd("testvote", Callvote_Handler);
RegConsoleCmd("vote", vote);
+
    RegConsoleCmd("vote", vote);
 
}
 
}
  
public Action:Callvote_Handler(client, args)
+
public Action Callvote_Handler(int client, int args)
 
{
 
{
new Handle:bf = StartMessageAll("VoteStart", USERMSG_RELIABLE);
+
    BfWrite bf = UserMessageToBfWrite(StartMessageAll("VoteStart", USERMSG_RELIABLE));
BfWriteByte(bf, TF2_TEAM_ALL);
+
    bf.WriteByte(TF2_TEAM_ALL);
BfWriteByte(bf, TF2_SERVER_CLIENT_INDEX);
+
    bf.WriteByte(TF2_SERVER_CLIENT_INDEX);
BfWriteString(bf, "#TF_playerid_noteam");
+
    bf.WriteString("#TF_playerid_noteam");
BfWriteString(bf, "Is gaben fat?");
+
    bf.WriteString("Is gaben fat?");
BfWriteBool(bf, true);
+
    bf.WriteBool(true);
EndMessage();
+
    EndMessage();
+
   
yesvotes = 0;
+
    g_iYesVotes = 0;
novotes = 0;
+
    g_iNoVotes = 0;
+
   
return Plugin_Handled;
+
    return Plugin_Handled;
 
}
 
}
  
UpdateVotes()
+
void UpdateVotes()
 
{
 
{
if (yesvotes+novotes >= MAX_VOTES)
+
    if (g_iYesVotes + g_iNoVotes >= MAX_VOTES)
{
+
    {
PrintToServer("voting complete!");
+
        PrintToServer("voting complete!");
if (yesvotes > novotes)
+
        if (g_iYesVotes > g_iNoVotes)
{
+
        {
new Handle:bf = StartMessageAll("VotePass");
+
            BfWrite bf = UserMessageToBfWrite(StartMessageAll("VotePass"));
BfWriteByte(bf, TF2_TEAM_ALL);
+
            bf.WriteByte(TF2_TEAM_ALL);
BfWriteString(bf, "#TF_playerid_noteam");
+
            bf.WriteString("#TF_playerid_noteam");
BfWriteString(bf, "Gaben is fat");
+
            bf.WriteString("Gaben is fat");
EndMessage();
+
            EndMessage();
}
+
        }
else
+
        else
{
+
        {
new Handle:bf = StartMessageAll("VoteFailed");
+
            BfWrite bf = UserMessageToBfWrite(StartMessageAll("VoteFailed"));
BfWriteByte(bf, TF2_TEAM_ALL);
+
            bf.WriteByte(TF2_TEAM_ALL);
// Check list of failure reasons
+
            // Check list of failure reasons
BfWriteByte(bf, 3);
+
            bf.WriteByte(3);
EndMessage();
+
            EndMessage();
}
+
        }
}
+
    }
 
}
 
}
  
// If the TF2 vote system is running (sv_allow_votes 1), this needs to be a command listener because TF2 registers the vote command only when a vote is ongoing, and thus hooking it using RegConsoleCmd doesn't work.
+
// If the TF2 vote system is running (sv_allow_votes 1), this needs to be a command listener
public Action:vote(client, args)
+
// because TF2 registers the vote command only when a vote is ongoing, and thus hooking it using RegConsoleCmd doesn't work.
 +
public Action vote(int client, int args)
 
{
 
{
new String:arg[8];
+
    char arg[8];
new option = 0;
+
    GetCmdArg(1, arg, 8);
GetCmdArg(1,arg,8);
+
 
PrintToServer("Got vote %s from %i",arg,client);
+
    PrintToServer("Got vote %s from %i", arg, client);
if (strcmp(arg,"option1",true) == 0)
+
 
{
+
    int option = 0;
yesvotes++;
+
    if (strcmp(arg, "option1", true) == 0)
option = 0;
+
    {
}
+
        g_iYesVotes++;
else if (strcmp(arg,"option2",true) == 0)
+
    }
{
+
    else if (strcmp(arg, "option2", true) == 0)
novotes++;
+
    {
option = 1;
+
        g_iNoVotes++;
}
+
        option = 1;
+
    }
new Handle:msg = CreateEvent("vote_cast");
+
 
SetEventInt(msg, "entityid", client);
+
    Event event = CreateEvent("vote_cast");
SetEventInt(msg, "team", -1);
+
    event.SetInt("entityid", client);
SetEventInt(msg, "vote_option", option);
+
    event.SetInt("team", -1);
FireEvent(msg);
+
    event.SetInt("vote_option", option);
+
    event.Fire();
UpdateVotes();
+
 
return Plugin_Continue;
+
    UpdateVotes();
}</pre>
+
 
 +
    return Plugin_Continue;
 +
}</sourcepawn>
  
 
See the following images for examples what this looks like:
 
See the following images for examples what this looks like:
Line 302: Line 368:
  
 
[http://cloud.steampowered.com/ugc/542899697038049485/C469BD2D358F9524A8B17164AD3FE06A3574E837/ Vote failed]
 
[http://cloud.steampowered.com/ugc/542899697038049485/C469BD2D358F9524A8B17164AD3FE06A3574E837/ Vote failed]
 +
 +
==See Also==
 +
* [[Left 4 Voting]]
 +
* [[Left 4 Voting 2]]
 +
* [https://forums.alliedmods.net/showthread.php?t=162164 BuiltinVotes], a SourceMod extension that exposes a voting API that uses this voting system.

Latest revision as of 22:30, 29 March 2020

This is for SourceMod plugin development involving the TF2 Vote System. If you are looking into the details on configuring TF2 Voting on your TF2 server, see Voting on the Official Team Fortress Wiki.

Team Fortress 2 has a new VGUI voting system based on the Left 4 Dead Voting system and is controlled by Game Events and User Messages. You can use either a string from the resource file, or TF_playerid_noteam which will let you create any vote you want.

How voting works

  1. A client issues a callvote with the vote type and argument, or the server calls a vote (server uses entity index 99).
  2. The server sends a vote_options event.
    • For Yes/No votes, the values it sends are Yes and No (in that order). For Multiple Choice votes, the actual vote options are sent.
  3. The VoteStart User Message is sent, the last argument determines the vote type.
  4. Clients use the "vote" command to register their votes (option1 through option5), after which the server sends a vote_cast event with a 0-based option number (so option1 = 0, option5 = 4). It also updates the vote_controller entity's vote counts.
  5. When the vote is complete, the server sends either a VotePass or VoteFailed User Message.

Server Entity

The server should update this as appropriate. Unfortunately, the valid values for m_iActiveIssueIndex is unknown.

Name: vote_controller (CVoteController)
Structure:
int m_iActiveIssueIndex Number of the active issue
int m_iOnlyTeamToVote Corresponds to VoteStart's team argument.
int[5] m_nVoteOptionCount Table of 5 vote counts numbered 0-4
int m_nPotentialVotes Number of players eligible to vote
int m_bIsYesNoVote Corresponds to VoteStart's yesno argument


Console Commands

callvote

Note: This command can take 0 arguments. If it does, a VoteSetup usermessage is returned to that user.

Name: callvote
Structure:
string type "Kick", "RestartGame", "ChangeLevel", "NextLevel", "ScrambleTeams", or "ChangeMission". Should return VoteSetup Usermessage if left blank.
string param1 "userid_short kicktype_string" for Kick, map name for Changelevel/NextLevel, or popfile for ChangeMission. Otherwise, left blank


kicktype_string is one of these (at least in the English version):

  • other
  • cheating
  • idle
  • scamming

This data is primarily there if you want to intercept these messages in a plugin.

CallVoteFailed UserMessage is sent back if a vote cannot be called unless a vote is already being displayed.

vote

Note: This command is only valid when a vote is ongoing.

Name: vote
Structure:
string option F1 is "option1", F2 is "option2", F3 is "option3", F4 is "option4", F5 is "option5"


Note that these are 1-based, but the vote_cast event is 0-based.

User Messages

Note: These user messages use unsigned bytes for the team number. Since the October 15, 2014 update, all teams is now represented by 0 (it used to be -1 / 255)


The User Messages that exist in the TF2 Voting system are:

VoteSetup

Note: Sent to a player when they load the Call Vote screen (which sends the callvote command to the server), lists what votes are allowed on the server
Note: This changed in the October 15, 2014 update

Name: VoteSetup
Structure:
byte issue_count Count of vote issues allowed on this server. Not present in protobuf version
repeated message issue issue fields as shown below


Name: issue
Structure:
string potential_issue Vote issue name that will be sent for the callvote command
string translation name Translation name, such as #TF_RestartGame
byte enabled 1 if this issue is enabled, 0 if not


There is one "issue" sent for each supported issue. Valid potential_issue strings are:

  • Kick
  • RestartGame
  • ChangeLevel
  • NextLevel
  • ScrambleTeams
  • ChangeMission
  • TeamAutoBalance
  • ClassLimits

Valid translation name strings are:

  • #TF_Kick
  • #TF_RestartGame
  • #TF_ChangeLevel
  • #TF_NextLevel
  • #TF_ScrambleTeams
  • #TF_ChangeMission
  • #TF_TeamAutoBalance_Enable
  • #TF_TeamAutoBalance_Disable
  • #TF_ClassLimit_Enable
  • #TF_ClassLimit_Disable

By default, disabled issues are omitted (sv_vote_ui_hide_disabled_issues 1).

CallVoteFailed

Note: Sent to a player when they attempt to call a vote and fail.

Name: CallVoteFailed
Structure:
byte reason Failure reason (1-2, 5-10, 12-19)
short time For failure reasons 2 and 8, time in seconds until client can start another vote. 2 is per user, 8 is per vote type.


Valid Failure Codes are:
1 - VOTE_FAILED_TRANSITIONING_PLAYERS - Cannot call vote while other players are still loading. This appears to be a holdover from L4D2; use code 10 instead.
2 - VOTE_FAILED_RATE_EXCEEDED - You called a vote recently and cannot call another one for X seconds (second argument to CallVoteFailed specifies the number of seconds)
5 - VOTE_FAILED_ISSUE_DISABLED - Server has disabled that issue.
6 - VOTE_FAILED_MAP_NOT_FOUND - That map does not exist.
7 - VOTE_FAILED_MAP_NAME_REQUIRED - You must specify a map name
8 - VOTE_FAILED_FAILED_RECENTLY - This vote failed recently
9 - VOTE_FAILED_TEAM_CANT_CALL - Your team cannot call this vote
10 - VOTE_FAILED_WAITINGFORPLAYERS - Voting not allowed while Waiting for Players
11 - VOTE_FAILED_PLAYERNOTFOUND - Doesn't appear to work
12 - VOTE_FAILED_CANNOT_KICK_ADMIN - Can't Kick Server Admin
13 - VOTE_FAILED_SCRAMBLE_IN_PROGRESS - Vote Scramble is pending
14 - VOTE_FAILED_SPECTATOR - Spectators can't vote
15 - VOTE_FAILED_NEXTLEVEL_SET - Next level already set
16 - VOTE_FAILED_MAP_NOT_VALID - Map is not in the map list
17 - VOTE_FAILED_CANNOT_KICK_FOR_TIME - Cannot kick yet. Used for MvM
18 - VOTE_FAILED_CANNOT_KICK_DURING_ROUND - Cannot kick during round. Used for MvM
19 - VOTE_FAILED_MODIFICATION_ALREADY_ACTIVE - Modification is already active, used by Eternaween

VoteStart

Note: Sent to all players currently online. The default implementation also sends it to bots.

Name: VoteStart
Structure:
byte team Team index or 0 for all
byte ent_idx Client index of person who started the vote, or 99 for the server.
string disp_str Vote issue translation string
string details_str Vote issue text
bool is_yes_no_vote true for Yes/No, false for Multiple choice


Valid issue strings:

  • #TF_vote_kick_player_other - Generic Kick vote. param1 is player name.
  • #TF_vote_kick_player_idle - Idler Kick vote. param1 is player name.
  • #TF_vote_kick_player_cheating - Cheater Kick vote. param1 is player name.
  • #TF_vote_kick_player_scamming - Scammer Kick vote. param1 is player name.
  • #TF_vote_restart_game - Restart map vote. param1 ignored.
  • #TF_vote_changelevel - Change map vote. param1 is map name.
  • #TF_vote_nextlevel - Set next level vote. param1 is map name.
  • #TF_vote_nextlevel_choices - End of map map vote. param1 ignored. This vote is not in the user vote menu.
  • #TF_vote_scramble_teams - Scramble teams vote. param1 ignored.
  • #TF_vote_should_scramble_round - Scramble teams at round end vote. param1 ignored. This vote is not in the user vote menu.
  • #TF_vote_td_start_round - Start the round? param1 ignored. This vote is not in the user vote menu.
  • #TF_vote_changechallenge - Change MvM mission? param1 is mission. This vote is in the menu on MvM maps.
  • #TF_vote_eternaween - Activate Halloween mode? param1 ignored. This vote is activated by someone using the Eternaween item.
  • #TF_vote_autobalance_enable - Active Autobalance? param1 ignored.
  • #TF_vote_autobalance_disable - Disable Autobalance? param1 ignored.
  • #TF_vote_classlimits_enable - Enable class limits? param1 ignored.
  • #TF_vote_classlimits_disable - Disable class limits? param1 ignored.
  • #TF_playerid_noteam - Unofficially used for a custom vote. param1 is custom vote issue.

VotePass

Note: Sent to all players after a vote passes.

Name: VotePass
Structure:
byte team Team index or 0 for all
string disp_str Vote success translation string
string details_str Vote winner


Valid success strings:

  • #TF_vote_passed_kick_player - Kick vote passed. param1 is player name.
  • #TF_vote_passed_restart_game - Restart vote passed. param1 ignored.
  • #TF_vote_passed_changelevel - Change level vote passed. param1 is map name.
  • #TF_vote_passed_nextlevel - Set next level vote passed. param1 is map name.
  • #TF_vote_passed_nextlevel_extend - Current map has been extended. param1 ignored.
  • #TF_vote_passed_scramble_teams - Team Scramble vote passed. param1 is ignored.
  • #TF_vote_passed_td_start_round - Round start vote passed. param1 is ignored.
  • #TF_vote_passed_changechallenge - MvM mission change passed. param1 is new mission.
  • #TF_vote_passed_eternaween - Eternaween vote passed. param1 ignored.
  • #TF_vote_passed_autobalance_enable - Autobalance enable vote passed. param1 ignored.
  • #TF_vote_passed_autobalance_disable - Autobalance disable vote passed. param1 ignored.
  • #TF_vote_passed_classlimits_enable - Class Limit enable vote passed. param1 ignored.
  • #TF_vote_passed_classlimits_disable - Class Limit disable vote passed. param1 ignored.
  • #TF_playerid_noteam - Unofficially used for a custom success string. param1 is custom success string.

VoteFailed

Note: Sent to all players after a vote fails.

Name: VoteFailed
Structure:
byte team Team index or 0 for all
byte reason Failure reason code (0, 3-4)


Valid Failure codes are:
0 - VOTE_FAILED_GENERIC - Generic "Vote Failed" message
3 - VOTE_FAILED_YES_MUST_EXCEED_NO - Yes votes must outnumber No votes
4 - VOTE_FAILED_QUORUM_FAILURE - Not Enough Votes

Events

Server to Client Events

vote_cast

Note: Sent to all players when a player chooses a vote option (or more specifically, the server receives a vote command)

Name: vote_cast
Structure:
byte vote_option which option the player voted on
short team [Ed: Usually -1, but team-specific votes can be 2 for RED or 3 for BLU]
long entityid entity id of the voter


This event is unusual as it uses the client's entity ID instead of its userid.

vote_options

Note: Sent to players before VoteStart UserMessage to populate choices for a multiple choice vote

Name: vote_options
Structure:
byte count Number of options - up to MAX_VOTE_OPTIONS [ed: 5]
string option1
string option2
string option3
string option4
string option5


If sv_vote_issue_nextlevel_allowextend is set to 1, option5 is "Extend current Map"

Client-only Events

These events are passed between the TF2 client and the client's VGUI voting panels. They have no server interaction and the server should not be touching them in any way.

vote_ended

Name: vote_ended
Structure:


vote_started

Name: vote_started
Structure:
string issue
string param1
byte team
long initiator entity id of the player who initiated the vote


vote_changed

Note: These values are read from the vote_controller entity on the server

Name: vote_changed
Structure:
byte option1
byte option2
byte option3
byte option4
byte option5
byte potentialVotes


vote_passed

Name: vote_passed
Structure:
string details
string param1
byte team


vote_failed

Name: vote_failed
Structure:
byte team


Example voting plugin

This is a basic plugin that starts a vote, "Is gaben fat?". It does not ensure the same client does not vote multiple times, nor does it actually kick the user.

#include <sourcemod>
// TF2's internal map vote uses client index 99 for the server
#define TF2_SERVER_CLIENT_INDEX 99
#define TF2_TEAM_ALL -1
 
int g_iYesVotes;
int g_iNoVotes;
 
#define MAX_VOTES 4
 
public Plugin myinfo = 
{
    name = "Test Yes/No Vote",
    author = "Powerlord",
    description = "A test vote plugin for the AlliedMods wiki",
    version = "1.0",
    url = "http://wiki.alliedmods.net/TF2_Voting"
}
 
public void OnPluginStart()
{
    RegConsoleCmd("testvote", Callvote_Handler);
    RegConsoleCmd("vote", vote);
}
 
public Action Callvote_Handler(int client, int args)
{
    BfWrite bf = UserMessageToBfWrite(StartMessageAll("VoteStart", USERMSG_RELIABLE));
    bf.WriteByte(TF2_TEAM_ALL);
    bf.WriteByte(TF2_SERVER_CLIENT_INDEX);
    bf.WriteString("#TF_playerid_noteam");
    bf.WriteString("Is gaben fat?");
    bf.WriteBool(true);
    EndMessage();
 
    g_iYesVotes = 0;
    g_iNoVotes = 0;
 
    return Plugin_Handled;
}
 
void UpdateVotes()
{
    if (g_iYesVotes + g_iNoVotes >= MAX_VOTES)
    {
        PrintToServer("voting complete!");
        if (g_iYesVotes > g_iNoVotes)
        {
            BfWrite bf = UserMessageToBfWrite(StartMessageAll("VotePass"));
            bf.WriteByte(TF2_TEAM_ALL);
            bf.WriteString("#TF_playerid_noteam");
            bf.WriteString("Gaben is fat");
            EndMessage();
        }
        else
        {
            BfWrite bf = UserMessageToBfWrite(StartMessageAll("VoteFailed"));
            bf.WriteByte(TF2_TEAM_ALL);
            // Check list of failure reasons
            bf.WriteByte(3);
            EndMessage();
        }
    }
}
 
// If the TF2 vote system is running (sv_allow_votes 1), this needs to be a command listener
// because TF2 registers the vote command only when a vote is ongoing, and thus hooking it using RegConsoleCmd doesn't work.
public Action vote(int client, int args)
{
    char arg[8];
    GetCmdArg(1, arg, 8);
 
    PrintToServer("Got vote %s from %i", arg, client);
 
    int option = 0;
    if (strcmp(arg, "option1", true) == 0)
    {
        g_iYesVotes++;
    }
    else if (strcmp(arg, "option2", true) == 0)
    {
        g_iNoVotes++;
        option = 1;
    }
 
    Event event = CreateEvent("vote_cast");
    event.SetInt("entityid", client);
    event.SetInt("team", -1);
    event.SetInt("vote_option", option);
    event.Fire();
 
    UpdateVotes();
 
    return Plugin_Continue;
}

See the following images for examples what this looks like:

Vote started

Vote passed

Vote failed

See Also