TF2 Voting

From AlliedModders Wiki
Revision as of 12:44, 5 October 2011 by Powerlord (talk | contribs) (Note about blank callvote.)
Jump to: navigation, search

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. 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.
  2. The VoteStart User Message is sent, the last argument determines the vote type.
  3. 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).
  4. When the vote is complete, the server sends either a VotePass or VoteFailed User Message.


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, or ScrambleTeams. Should return VoteSetup Usermessage if left blank.
string param1 Target for Kick, or map name for Changelevel/NextLevel. Otherwise, left blank


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 Valve represents no team as -1, no team is instead represented as its two's complement, 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

Name: VoteSetup
Structure:
byte issue_count Count of vote issues allowed on this server
string... issue An issue allowed to vote on


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

  • Kick
  • RestartGame
  • ChangeLevel
  • NextLevel
  • ScrambleTeams

These will be followed by " (Disabled on Server)" if they are disabled.

CallVoteFailed

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

Name: CallVoteFailed
Structure:
byte failure_code Failure reason (1-2, 5-10, 12-15)
short time For failure reason 2, time left until client can start another vote


Valid Failure Codes are:
1 - Cannot call vote while other players are still loading
2 - You called a vote recently and cannot call another one for X seconds (second argument to CallVoteFailed specifies the number of seconds)
5 - Server has disabled that issue.
6 - That map does not exist.
7 - You must specify a map name
8 - This vote failed recently
9 - Your team cannot call this vote
10 - Voting not allowed while Waiting for Players
12 - Can't Kick Server Admin
13 - Vote Scramble is pending
14 - Spectators can't vote
15 - Next level already set

VoteStart

Note: 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

Name: VoteStart
Structure:
byte team Team index or 255 for all
byte initiator Client index (NOT USERID) of person who started the vote, or 99 for the server.
string issue Vote issue translation string
string param1 Vote issue text
bool yesno 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_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 255 for all
string details Vote success translation string
string param1 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_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 255 for all
byte failure_code Failure reason code (0, 3-4)


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

Events

Used 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: Always -1]
long entityid entity id of the voter


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.

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


Unused Events

These are events that existed in L4D, but remain unused in the TF2 vote system.

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

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

new yesvotes;
new novotes;
#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 OnPluginStart()
{
	RegConsoleCmd("testvote",Callvote_Handler);
	RegConsoleCmd("vote", vote);
}

public Action:Callvote_Handler(client, args)
{
	new Handle:bf = StartMessageAll("VoteStart", USERMSG_RELIABLE);
	BfWriteByte(bf, TF2_TEAM_ALL);
	BfWriteByte(bf, TF2_SERVER_CLIENT_INDEX);
	BfWriteString(bf, "#TF_playerid_noteam");
	BfWriteString(bf, "Is gaben fat?");
	BfWriteBool(bf, true);
	EndMessage();
	
	yesvotes = 0;
	novotes = 0;
	
	return Plugin_Handled;
}

UpdateVotes()
{
	if (yesvotes+novotes >= MAX_VOTES)
	{
		PrintToServer("voting complete!");
		if (yesvotes > novotes)
		{
			new Handle:bf = StartMessageAll("VotePass");
			BfWriteByte(bf, TF2_TEAM_ALL);
			BfWriteString(bf, "#TF_playerid_noteam");
			BfWriteString(bf, "Gaben is fat");
			EndMessage();
		}
		else
		{
			new Handle:bf = StartMessageAll("VoteFailed");
			BfWriteByte(bf, TF2_TEAM_ALL);
			// Check list of failure reasons
			BfWriteByte(bf, 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(client, args)
{
	new String:arg[8];
	new option = 0;
	GetCmdArg(1,arg,8);
	PrintToServer("Got vote %s from %i",arg,client);
	if (strcmp(arg,"option1",true) == 0)
	{
		yesvotes++;
		option = 0;
	}
	else if (strcmp(arg,"option2",true) == 0)
	{
		novotes++;
		option = 1;
	}
	
	new Handle:msg = CreateEvent("vote_cast");
	SetEventInt(msg, "entityid", client);
	SetEventInt(msg, "team", -1);
	SetEventInt(msg, "vote_option", option);
	FireEvent(msg);
	
	UpdateVotes();
	return Plugin_Continue;
}

See the following images for examples what this looks like:

Vote started

Vote passed

Vote failed