2020-04-22 16:56:21 +00:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Base VoteController. Handles holding and voting on issues.
//
// $NoKeywords: $
//=============================================================================//
# include "cbase.h"
# include "vote_controller.h"
# include "shareddefs.h"
# include "eiface.h"
# include "team.h"
# include "gameinterface.h"
# ifdef TF_DLL
# include "tf/tf_gamerules.h"
# endif
// memdbgon must be the last include file in a .cpp file!!!
# include "tier0/memdbgon.h"
# define MAX_VOTER_HISTORY 64
// Datatable
IMPLEMENT_SERVERCLASS_ST ( CVoteController , DT_VoteController )
SendPropInt ( SENDINFO ( m_iActiveIssueIndex ) ) ,
SendPropInt ( SENDINFO ( m_iOnlyTeamToVote ) ) ,
SendPropArray3 ( SENDINFO_ARRAY3 ( m_nVoteOptionCount ) , SendPropInt ( SENDINFO_ARRAY ( m_nVoteOptionCount ) , 8 , SPROP_UNSIGNED ) ) ,
SendPropInt ( SENDINFO ( m_nPotentialVotes ) ) ,
SendPropBool ( SENDINFO ( m_bIsYesNoVote ) )
END_SEND_TABLE ( )
BEGIN_DATADESC ( CVoteController )
DEFINE_THINKFUNC ( VoteControllerThink ) ,
END_DATADESC ( )
LINK_ENTITY_TO_CLASS ( vote_controller , CVoteController ) ;
CVoteController * g_voteController = NULL ;
2022-03-01 20:00:42 +00:00
ConVar sv_vote_timer_duration ( " sv_vote_timer_duration " , " 15 " , FCVAR_DEVELOPMENTONLY , " How long to allow voting on an issue " ) ;
ConVar sv_vote_command_delay ( " sv_vote_command_delay " , " 2 " , FCVAR_DEVELOPMENTONLY , " How long after a vote passes until the action happens " , false , 0 , true , 4.5 ) ;
ConVar sv_allow_votes ( " sv_allow_votes " , " 1 " , 0 , " Allow voting? " ) ;
ConVar sv_vote_failure_timer ( " sv_vote_failure_timer " , " 300 " , 0 , " A vote that fails cannot be re-submitted for this long " ) ;
2020-04-22 16:56:21 +00:00
# ifdef TF_DLL
2022-03-01 20:00:42 +00:00
ConVar sv_vote_failure_timer_mvm ( " sv_vote_failure_timer_mvm " , " 120 " , 0 , " A vote that fails in MvM cannot be re-submitted for this long " ) ;
2020-04-22 16:56:21 +00:00
# endif // TF_DLL
2022-03-01 20:00:42 +00:00
ConVar sv_vote_creation_timer ( " sv_vote_creation_timer " , " 120 " , FCVAR_DEVELOPMENTONLY , " How often someone can individually call a vote. " ) ;
ConVar sv_vote_quorum_ratio ( " sv_vote_quorum_ratio " , " 0.6 " , 1 , " The minimum ratio of players needed to vote on an issue to resolve it. " , true , 0.1 , true , 1.0 ) ;
ConVar sv_vote_allow_spectators ( " sv_vote_allow_spectators " , " 0 " , 0 , " Allow spectators to vote? " ) ;
ConVar sv_vote_ui_hide_disabled_issues ( " sv_vote_ui_hide_disabled_issues " , " 1 " , 0 , " Suppress listing of disabled issues in the vote setup screen. " ) ;
2020-04-22 16:56:21 +00:00
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CommandListIssues ( void )
{
CBasePlayer * commandIssuer = UTIL_GetCommandClient ( ) ;
if ( g_voteController & & commandIssuer )
{
g_voteController - > ListIssues ( commandIssuer ) ;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
ConCommand ListIssues ( " listissues " , CommandListIssues , " List all the issues that can be voted on. " , 0 ) ;
//-----------------------------------------------------------------------------
// Purpose: This should eventually ask the player what team they are voting on
// to take into account different idle / spectator rules.
//-----------------------------------------------------------------------------
int GetVoterTeam ( CBaseEntity * pEntity )
{
if ( ! pEntity )
return TEAM_UNASSIGNED ;
int iTeam = pEntity - > GetTeamNumber ( ) ;
return iTeam ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CON_COMMAND ( callvote , " Start a vote on an issue. " )
{
if ( ! g_voteController )
{
DevMsg ( " Vote Controller Not Found! \n " ) ;
return ;
}
CBasePlayer * pVoteCaller = UTIL_GetCommandClient ( ) ;
2022-03-01 20:00:42 +00:00
if ( ! pVoteCaller )
2020-04-22 16:56:21 +00:00
return ;
if ( ! sv_vote_allow_spectators . GetBool ( ) )
{
if ( pVoteCaller - > GetTeamNumber ( ) = = TEAM_SPECTATOR )
{
2022-03-01 20:00:42 +00:00
g_voteController - > SendVoteFailedMessage ( VOTE_FAILED_SPECTATOR , pVoteCaller ) ;
2020-04-22 16:56:21 +00:00
return ;
}
}
2022-03-01 20:00:42 +00:00
// Prevent spamming commands
# ifndef _DEBUG
2020-04-22 16:56:21 +00:00
int nCooldown = 0 ;
2022-03-01 20:00:42 +00:00
if ( ! g_voteController - > CanEntityCallVote ( pVoteCaller , nCooldown ) )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
g_voteController - > SendVoteFailedMessage ( VOTE_FAILED_RATE_EXCEEDED , pVoteCaller , nCooldown ) ;
2020-04-22 16:56:21 +00:00
return ;
}
2022-03-01 20:00:42 +00:00
# endif
2020-04-22 16:56:21 +00:00
// Parameters
char szEmptyDetails [ MAX_VOTE_DETAILS_LENGTH ] ;
szEmptyDetails [ 0 ] = ' \0 ' ;
const char * arg2 = args [ 1 ] ;
const char * arg3 = args . ArgC ( ) > = 3 ? args [ 2 ] : szEmptyDetails ;
// If we don't have any arguments, invoke VoteSetup UI
2022-03-01 20:00:42 +00:00
if ( args . ArgC ( ) < 2 )
2020-04-22 16:56:21 +00:00
{
g_voteController - > SetupVote ( pVoteCaller - > entindex ( ) ) ;
return ;
}
g_voteController - > CreateVote ( pVoteCaller - > entindex ( ) , arg2 , arg3 ) ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CVoteController : : ~ CVoteController ( )
{
g_voteController = NULL ;
for ( int issueIndex = 0 ; issueIndex < m_potentialIssues . Count ( ) ; + + issueIndex )
{
delete m_potentialIssues [ issueIndex ] ;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CVoteController : : ResetData ( void )
{
m_iActiveIssueIndex = INVALID_ISSUE ;
for ( int index = 0 ; index < m_nVoteOptionCount . Count ( ) ; index + + )
{
m_nVoteOptionCount . Set ( index , 0 ) ;
}
m_nPotentialVotes = 0 ;
m_acceptingVotesTimer . Invalidate ( ) ;
m_executeCommandTimer . Invalidate ( ) ;
m_iEntityHoldingVote = - 1 ;
2022-03-01 20:00:42 +00:00
m_iOnlyTeamToVote = TEAM_INVALID ;
2020-04-22 16:56:21 +00:00
m_bIsYesNoVote = true ;
for ( int voteIndex = 0 ; voteIndex < ARRAYSIZE ( m_nVotesCast ) ; + + voteIndex )
{
m_nVotesCast [ voteIndex ] = VOTE_UNCAST ;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CVoteController : : Spawn ( void )
{
ResetData ( ) ;
BaseClass : : Spawn ( ) ;
SetThink ( & CVoteController : : VoteControllerThink ) ;
SetNextThink ( gpGlobals - > curtime ) ;
SetDefLessFunc ( m_VoteCallers ) ;
g_voteController = this ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
int CVoteController : : UpdateTransmitState ( void )
{
// ALWAYS transmit to all clients.
return SetTransmitState ( FL_EDICT_ALWAYS ) ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CVoteController : : CanTeamCastVote ( int iTeam ) const
{
2022-03-01 20:00:42 +00:00
if ( m_iOnlyTeamToVote = = TEAM_INVALID )
2020-04-22 16:56:21 +00:00
return true ;
return iTeam = = m_iOnlyTeamToVote ;
}
//-----------------------------------------------------------------------------
// Purpose: Handles menu-driven setup of Voting
//-----------------------------------------------------------------------------
bool CVoteController : : SetupVote ( int iEntIndex )
{
CBasePlayer * pVoteCaller = UTIL_PlayerByIndex ( iEntIndex ) ;
if ( ! pVoteCaller )
return false ;
int nIssueCount = 0 ;
// Passing an nIssueCount of 0 triggers a "Voting disabled on server" message in the setup UI
2022-03-01 20:00:42 +00:00
if ( sv_allow_votes . GetBool ( ) )
2020-04-22 16:56:21 +00:00
{
for ( int iIndex = 0 ; iIndex < m_potentialIssues . Count ( ) ; + + iIndex )
{
// Hide disabled issues?
CBaseIssue * pCurrentIssue = m_potentialIssues [ iIndex ] ;
if ( pCurrentIssue )
{
if ( ! pCurrentIssue - > IsEnabled ( ) & & sv_vote_ui_hide_disabled_issues . GetBool ( ) )
continue ;
nIssueCount + + ;
}
}
}
CSingleUserRecipientFilter filter ( pVoteCaller ) ;
filter . MakeReliable ( ) ;
UserMessageBegin ( filter , " VoteSetup " ) ;
WRITE_BYTE ( nIssueCount ) ;
for ( int iIndex = 0 ; iIndex < m_potentialIssues . Count ( ) ; + + iIndex )
{
CBaseIssue * pCurrentIssue = m_potentialIssues [ iIndex ] ;
if ( pCurrentIssue )
{
2022-03-01 20:00:42 +00:00
if ( pCurrentIssue - > IsEnabled ( ) )
{
WRITE_STRING ( pCurrentIssue - > GetTypeString ( ) ) ;
}
else
{
// Don't send/display disabled issues when set
if ( sv_vote_ui_hide_disabled_issues . GetBool ( ) )
continue ;
char szDisabledIssueStr [ MAX_COMMAND_LENGTH + 12 ] ;
V_strcpy ( szDisabledIssueStr , pCurrentIssue - > GetTypeString ( ) ) ;
V_strcat ( szDisabledIssueStr , " (Disabled on Server) " , sizeof ( szDisabledIssueStr ) ) ;
WRITE_STRING ( szDisabledIssueStr ) ;
}
2020-04-22 16:56:21 +00:00
}
}
MessageEnd ( ) ;
return true ;
}
//-----------------------------------------------------------------------------
// Purpose: Handles console-driven setup of Voting
//-----------------------------------------------------------------------------
bool CVoteController : : CreateVote ( int iEntIndex , const char * pszTypeString , const char * pszDetailString )
{
// Terrible Hack: Dedicated servers pass 99 as the EntIndex
bool bDedicatedServer = ( iEntIndex = = DEDICATED_SERVER ) ? true : false ;
2022-03-01 20:00:42 +00:00
if ( ! sv_allow_votes . GetBool ( ) )
2020-04-22 16:56:21 +00:00
return false ;
// Already running a vote?
2022-03-01 20:00:42 +00:00
if ( IsVoteActive ( ) )
2020-04-22 16:56:21 +00:00
return false ;
CBasePlayer * pVoteCaller = UTIL_PlayerByIndex ( iEntIndex ) ;
2022-03-01 20:00:42 +00:00
if ( ! pVoteCaller & & ! bDedicatedServer )
2020-04-22 16:56:21 +00:00
return false ;
// Find the issue the user is asking for
2022-03-01 20:00:42 +00:00
for ( int issueIndex = 0 ; issueIndex < m_potentialIssues . Count ( ) ; + + issueIndex )
2020-04-22 16:56:21 +00:00
{
CBaseIssue * pCurrentIssue = m_potentialIssues [ issueIndex ] ;
if ( ! pCurrentIssue )
return false ;
2022-03-01 20:00:42 +00:00
if ( FStrEq ( pszTypeString , pCurrentIssue - > GetTypeString ( ) ) )
2020-04-22 16:56:21 +00:00
{
vote_create_failed_t nErrorCode = VOTE_FAILED_GENERIC ;
int nTime = 0 ;
2022-03-01 20:00:42 +00:00
if ( pCurrentIssue - > CanCallVote ( iEntIndex , pszDetailString , nErrorCode , nTime ) )
2020-04-22 16:56:21 +00:00
{
// Establish a bunch of data on this particular issue
pCurrentIssue - > SetIssueDetails ( pszDetailString ) ;
m_bIsYesNoVote = pCurrentIssue - > IsYesNoVote ( ) ;
m_iActiveIssueIndex = issueIndex ;
m_iEntityHoldingVote = iEntIndex ;
if ( ! bDedicatedServer )
{
2022-03-01 20:00:42 +00:00
if ( pCurrentIssue - > IsAllyRestrictedVote ( ) )
{
m_iOnlyTeamToVote = GetVoterTeam ( pVoteCaller ) ;
}
else
{
m_iOnlyTeamToVote = TEAM_INVALID ;
}
2020-04-22 16:56:21 +00:00
}
// Now get our choices
m_VoteOptions . RemoveAll ( ) ;
pCurrentIssue - > GetVoteOptions ( m_VoteOptions ) ;
int nNumVoteOptions = m_VoteOptions . Count ( ) ;
if ( nNumVoteOptions > = 2 )
{
IGameEvent * event = gameeventmanager - > CreateEvent ( " vote_options " ) ;
if ( event )
{
event - > SetInt ( " count " , nNumVoteOptions ) ;
for ( int iIndex = 0 ; iIndex < nNumVoteOptions ; iIndex + + )
{
char szNumber [ 2 ] ;
Q_snprintf ( szNumber , sizeof ( szNumber ) , " %i " , iIndex + 1 ) ;
char szOptionName [ 8 ] = " option " ;
Q_strncat ( szOptionName , szNumber , sizeof ( szOptionName ) , COPY_ALL_CHARACTERS ) ;
event - > SetString ( szOptionName , m_VoteOptions [ iIndex ] ) ;
}
gameeventmanager - > FireEvent ( event ) ;
}
}
else
{
Assert ( nNumVoteOptions > = 2 ) ;
}
// Have the issue start working on it
pCurrentIssue - > OnVoteStarted ( ) ;
// Now the vote handling and UI
m_nPotentialVotes = pCurrentIssue - > CountPotentialVoters ( ) ;
2022-03-01 20:00:42 +00:00
m_acceptingVotesTimer . Start ( sv_vote_timer_duration . GetFloat ( ) ) ;
2020-04-22 16:56:21 +00:00
// Force the vote holder to agree with a Yes/No vote
2022-03-01 20:00:42 +00:00
if ( m_bIsYesNoVote & & ! bDedicatedServer )
2020-04-22 16:56:21 +00:00
{
TryCastVote ( iEntIndex , " Option1 " ) ;
}
// Get the data out to the client
CBroadcastRecipientFilter filter ;
filter . MakeReliable ( ) ;
UserMessageBegin ( filter , " VoteStart " ) ;
WRITE_BYTE ( m_iOnlyTeamToVote ) ; // move into the filter
WRITE_BYTE ( m_iEntityHoldingVote ) ;
WRITE_STRING ( pCurrentIssue - > GetDisplayString ( ) ) ;
WRITE_STRING ( pCurrentIssue - > GetDetailsString ( ) ) ;
2022-03-01 20:00:42 +00:00
WRITE_BOOL ( m_bIsYesNoVote ) ;
2020-04-22 16:56:21 +00:00
MessageEnd ( ) ;
if ( ! bDedicatedServer )
{
TrackVoteCaller ( pVoteCaller ) ;
}
return true ;
}
else
{
if ( ! bDedicatedServer )
{
2022-03-01 20:00:42 +00:00
SendVoteFailedMessage ( nErrorCode , pVoteCaller , nTime ) ;
2020-04-22 16:56:21 +00:00
}
}
}
}
return false ;
}
//-----------------------------------------------------------------------------
2022-03-01 20:00:42 +00:00
// Purpose: Sent to everyone, unless we pass a player pointer
2020-04-22 16:56:21 +00:00
//-----------------------------------------------------------------------------
2022-03-01 20:00:42 +00:00
void CVoteController : : SendVoteFailedMessage ( vote_create_failed_t nReason , CBasePlayer * pVoteCaller , int nTime )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
// driller: need to merge all failure case stuff into a single path
if ( pVoteCaller )
{
CSingleUserRecipientFilter user ( pVoteCaller ) ;
user . MakeReliable ( ) ;
2020-04-22 16:56:21 +00:00
2022-03-01 20:00:42 +00:00
UserMessageBegin ( user , " CallVoteFailed " ) ;
WRITE_BYTE ( nReason ) ;
WRITE_SHORT ( nTime ) ;
MessageEnd ( ) ;
}
else
{
UTIL_LogPrintf ( " Vote failed \" %s %s \" \n " ,
m_potentialIssues [ m_iActiveIssueIndex ] - > GetTypeString ( ) ,
m_potentialIssues [ m_iActiveIssueIndex ] - > GetDetailsString ( ) ) ;
2020-04-22 16:56:21 +00:00
2022-03-01 20:00:42 +00:00
CBroadcastRecipientFilter filter ;
filter . MakeReliable ( ) ;
2020-04-22 16:56:21 +00:00
2022-03-01 20:00:42 +00:00
UserMessageBegin ( filter , " VoteFailed " ) ;
WRITE_BYTE ( m_iOnlyTeamToVote ) ;
WRITE_BYTE ( nReason ) ;
MessageEnd ( ) ;
}
2020-04-22 16:56:21 +00:00
}
//-----------------------------------------------------------------------------
// Purpose: Player generated a vote command. i.e. /vote option1
//-----------------------------------------------------------------------------
CVoteController : : TryCastVoteResult CVoteController : : TryCastVote ( int iEntIndex , const char * pszVoteString )
{
2022-03-01 20:00:42 +00:00
if ( ! sv_allow_votes . GetBool ( ) )
2020-04-22 16:56:21 +00:00
return CAST_FAIL_SERVER_DISABLE ;
2022-03-01 20:00:42 +00:00
if ( iEntIndex > = ARRAYSIZE ( m_nVotesCast ) )
2020-04-22 16:56:21 +00:00
return CAST_FAIL_SYSTEM_ERROR ;
2022-03-01 20:00:42 +00:00
if ( ! IsVoteActive ( ) )
2020-04-22 16:56:21 +00:00
return CAST_FAIL_NO_ACTIVE_ISSUE ;
2022-03-01 20:00:42 +00:00
if ( m_executeCommandTimer . HasStarted ( ) )
2020-04-22 16:56:21 +00:00
return CAST_FAIL_VOTE_CLOSED ;
2022-03-01 20:00:42 +00:00
if ( m_potentialIssues [ m_iActiveIssueIndex ] & & m_potentialIssues [ m_iActiveIssueIndex ] - > IsAllyRestrictedVote ( ) )
2020-04-22 16:56:21 +00:00
{
CBaseEntity * pVoteHolder = UTIL_EntityByIndex ( m_iEntityHoldingVote ) ;
CBaseEntity * pVoter = UTIL_EntityByIndex ( iEntIndex ) ;
2022-03-01 20:00:42 +00:00
if ( ( pVoteHolder = = NULL ) | | ( pVoter = = NULL ) | | ( GetVoterTeam ( pVoteHolder ) ! = GetVoterTeam ( pVoter ) ) )
2020-04-22 16:56:21 +00:00
{
return CAST_FAIL_TEAM_RESTRICTED ;
}
}
// Look for a previous vote
int nOldVote = m_nVotesCast [ iEntIndex ] ;
# ifndef DEBUG
2022-03-01 20:00:42 +00:00
if ( nOldVote ! = VOTE_UNCAST )
2020-04-22 16:56:21 +00:00
{
return CAST_FAIL_NO_CHANGES ;
}
# endif // !DEBUG
// Which option are they voting for?
int nCurrentVote = VOTE_UNCAST ;
if ( Q_strnicmp ( pszVoteString , " Option " , 6 ) ! = 0 )
return CAST_FAIL_SYSTEM_ERROR ;
nCurrentVote = ( CastVote ) ( atoi ( pszVoteString + 6 ) - 1 ) ;
if ( nCurrentVote < VOTE_OPTION1 | | nCurrentVote > VOTE_OPTION5 )
return CAST_FAIL_SYSTEM_ERROR ;
// They're changing their vote
# ifdef DEBUG
if ( nOldVote ! = VOTE_UNCAST )
{
if ( nOldVote = = nCurrentVote )
{
return CAST_FAIL_DUPLICATE ;
}
VoteChoice_Decrement ( nOldVote ) ;
}
# endif // DEBUG
// With a Yes/No vote, slam anything past "No" to No
if ( m_potentialIssues [ m_iActiveIssueIndex ] - > IsYesNoVote ( ) )
{
if ( nCurrentVote > VOTE_OPTION2 )
nCurrentVote = VOTE_OPTION2 ;
}
// Register and track this vote
VoteChoice_Increment ( nCurrentVote ) ;
m_nVotesCast [ iEntIndex ] = nCurrentVote ;
// Tell the client-side UI
IGameEvent * event = gameeventmanager - > CreateEvent ( " vote_cast " ) ;
if ( event )
{
event - > SetInt ( " vote_option " , nCurrentVote ) ;
event - > SetInt ( " team " , m_iOnlyTeamToVote ) ;
event - > SetInt ( " entityid " , iEntIndex ) ;
gameeventmanager - > FireEvent ( event ) ;
}
CheckForEarlyVoteClose ( ) ;
return CAST_OK ;
}
//-----------------------------------------------------------------------------
// Purpose: Increments the vote count for a particular vote option
// i.e. nVoteChoice = 0 might mean a Yes vote
//-----------------------------------------------------------------------------
void CVoteController : : VoteChoice_Increment ( int nVoteChoice )
{
if ( nVoteChoice < VOTE_OPTION1 | | nVoteChoice > VOTE_OPTION5 )
return ;
int nValue = m_nVoteOptionCount . Get ( nVoteChoice ) ;
m_nVoteOptionCount . Set ( nVoteChoice , + + nValue ) ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CVoteController : : VoteChoice_Decrement ( int nVoteChoice )
{
if ( nVoteChoice < VOTE_OPTION1 | | nVoteChoice > VOTE_OPTION5 )
return ;
int nValue = m_nVoteOptionCount . Get ( nVoteChoice ) ;
m_nVoteOptionCount . Set ( nVoteChoice , - - nValue ) ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CVoteController : : VoteControllerThink ( void )
{
if ( ! m_potentialIssues . IsValidIndex ( m_iActiveIssueIndex ) )
{
SetNextThink ( gpGlobals - > curtime + 0.5f ) ;
return ;
}
// Vote time is up - process the result
2022-03-01 20:00:42 +00:00
if ( m_acceptingVotesTimer . HasStarted ( ) & & m_acceptingVotesTimer . IsElapsed ( ) )
2020-04-22 16:56:21 +00:00
{
m_acceptingVotesTimer . Invalidate ( ) ;
2022-03-01 20:00:42 +00:00
int nVoteTally = 0 ;
for ( int index = 0 ; index < MAX_VOTE_OPTIONS ; index + + )
{
nVoteTally + = m_nVoteOptionCount . Get ( index ) ;
}
bool bVotePassed = true ;
// for record-keeping
2020-04-22 16:56:21 +00:00
if ( m_potentialIssues [ m_iActiveIssueIndex ] - > IsYesNoVote ( ) )
{
m_potentialIssues [ m_iActiveIssueIndex ] - > SetYesNoVoteCount ( m_nVoteOptionCount [ VOTE_OPTION1 ] , m_nVoteOptionCount [ VOTE_OPTION2 ] , m_nPotentialVotes ) ;
}
2022-03-01 20:00:42 +00:00
// Have we exceeded the required ratio of Voted-vs-Abstained?
if ( nVoteTally > = m_nPotentialVotes * sv_vote_quorum_ratio . GetFloat ( ) )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
int nWinningVoteOption = GetWinningVoteOption ( ) ;
Assert ( nWinningVoteOption > = 0 & & nWinningVoteOption < m_VoteOptions . Count ( ) ) ;
if ( nWinningVoteOption > = 0 & & nWinningVoteOption < MAX_VOTE_OPTIONS )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
// YES/NO VOTES
2020-04-22 16:56:21 +00:00
if ( m_potentialIssues [ m_iActiveIssueIndex ] - > IsYesNoVote ( ) )
{
2022-03-01 20:00:42 +00:00
// Option1 is Yes
if ( nWinningVoteOption ! = VOTE_OPTION1 )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
SendVoteFailedMessage ( VOTE_FAILED_YES_MUST_EXCEED_NO ) ;
bVotePassed = false ;
}
2020-04-22 16:56:21 +00:00
}
2022-03-01 20:00:42 +00:00
// GENERAL VOTES:
// We set the details string after the vote, since that's when
// we finally have a parameter to pass along and execute
else if ( nWinningVoteOption < m_VoteOptions . Count ( ) )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
m_potentialIssues [ m_iActiveIssueIndex ] - > SetIssueDetails ( m_VoteOptions [ nWinningVoteOption ] ) ;
2020-04-22 16:56:21 +00:00
}
}
}
2022-03-01 20:00:42 +00:00
else
{
SendVoteFailedMessage ( VOTE_FAILED_QUORUM_FAILURE ) ;
bVotePassed = false ;
}
2020-04-22 16:56:21 +00:00
2022-03-01 20:00:42 +00:00
if ( bVotePassed )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
m_executeCommandTimer . Start ( sv_vote_command_delay . GetFloat ( ) ) ;
m_resetVoteTimer . Start ( 5.0 ) ;
2020-04-22 16:56:21 +00:00
2022-03-01 20:00:42 +00:00
UTIL_LogPrintf ( " Vote succeeded \" %s %s \" \n " ,
m_potentialIssues [ m_iActiveIssueIndex ] - > GetTypeString ( ) ,
m_potentialIssues [ m_iActiveIssueIndex ] - > GetDetailsString ( ) ) ;
2020-04-22 16:56:21 +00:00
CBroadcastRecipientFilter filter ;
filter . MakeReliable ( ) ;
UserMessageBegin ( filter , " VotePass " ) ;
WRITE_BYTE ( m_iOnlyTeamToVote ) ;
WRITE_STRING ( m_potentialIssues [ m_iActiveIssueIndex ] - > GetVotePassedString ( ) ) ;
WRITE_STRING ( m_potentialIssues [ m_iActiveIssueIndex ] - > GetDetailsString ( ) ) ;
MessageEnd ( ) ;
}
else
{
m_potentialIssues [ m_iActiveIssueIndex ] - > OnVoteFailed ( m_iEntityHoldingVote ) ;
2022-03-01 20:00:42 +00:00
m_resetVoteTimer . Start ( 5.0 ) ;
2020-04-22 16:56:21 +00:00
}
}
// Vote passed check moved down to FrameUpdatePostEntityThink at bottom of this file...
if ( m_resetVoteTimer . HasStarted ( ) & & m_resetVoteTimer . IsElapsed ( ) )
{
ResetData ( ) ;
m_resetVoteTimer . Invalidate ( ) ;
}
// Size maintenance on m_VoteCallers
if ( m_VoteCallers . Count ( ) > = MAX_VOTER_HISTORY )
{
// Remove older entries
for ( int iIdx = m_VoteCallers . FirstInorder ( ) ; iIdx ! = m_VoteCallers . InvalidIndex ( ) ; iIdx = m_VoteCallers . NextInorder ( iIdx ) )
{
if ( m_VoteCallers [ iIdx ] - gpGlobals - > curtime < = 0 )
{
m_VoteCallers . Remove ( iIdx ) ;
}
}
}
SetNextThink ( gpGlobals - > curtime + 0.5f ) ;
}
//-----------------------------------------------------------------------------
// Purpose: End the vote early if everyone's voted
//-----------------------------------------------------------------------------
void CVoteController : : CheckForEarlyVoteClose ( void )
{
int nVoteTally = 0 ;
for ( int index = 0 ; index < MAX_VOTE_OPTIONS ; index + + )
{
nVoteTally + = m_nVoteOptionCount . Get ( index ) ;
}
if ( nVoteTally > = m_nPotentialVotes )
{
m_acceptingVotesTimer . Start ( 0 ) ; // Run the timer out right now
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CVoteController : : IsValidVoter ( CBasePlayer * pWhom )
{
2022-03-01 20:00:42 +00:00
if ( pWhom = = NULL )
2020-04-22 16:56:21 +00:00
return false ;
if ( ! pWhom - > IsConnected ( ) )
return false ;
if ( ! sv_vote_allow_spectators . GetBool ( ) )
{
if ( pWhom - > GetTeamNumber ( ) = = TEAM_SPECTATOR )
return false ;
}
# ifndef DEBUG // Don't want to do this check for debug builds (so we can test with bots)
if ( pWhom - > IsBot ( ) )
return false ;
if ( pWhom - > IsFakeClient ( ) )
return false ;
# endif // DEBUG
if ( pWhom - > IsHLTV ( ) )
return false ;
if ( pWhom - > IsReplay ( ) )
return false ;
# ifdef TF_DLL
if ( TFGameRules ( ) & & TFGameRules ( ) - > IsMannVsMachineMode ( ) )
{
if ( pWhom - > GetTeamNumber ( ) ! = TF_TEAM_PVE_DEFENDERS )
return false ;
}
# endif // TF_DLL
return true ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CVoteController : : RegisterIssue ( CBaseIssue * pszNewIssue )
{
m_potentialIssues . AddToTail ( pszNewIssue ) ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CVoteController : : ListIssues ( CBasePlayer * pForWhom )
{
2022-03-01 20:00:42 +00:00
if ( ! sv_allow_votes . GetBool ( ) )
2020-04-22 16:56:21 +00:00
return ;
ClientPrint ( pForWhom , HUD_PRINTCONSOLE , " ---Vote commands--- \n " ) ;
for ( int issueIndex = 0 ; issueIndex < m_potentialIssues . Count ( ) ; + + issueIndex )
{
CBaseIssue * pCurrentIssue = m_potentialIssues [ issueIndex ] ;
pCurrentIssue - > ListIssueDetails ( pForWhom ) ;
}
ClientPrint ( pForWhom , HUD_PRINTCONSOLE , " --- End Vote commands--- \n " ) ;
}
//-----------------------------------------------------------------------------
2022-03-01 20:00:42 +00:00
// Purpose:
2020-04-22 16:56:21 +00:00
//-----------------------------------------------------------------------------
2022-03-01 20:00:42 +00:00
int CVoteController : : GetWinningVoteOption ( void )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
if ( m_potentialIssues [ m_iActiveIssueIndex ] - > IsYesNoVote ( ) )
2020-04-22 16:56:21 +00:00
{
return ( m_nVoteOptionCount [ VOTE_OPTION1 ] > m_nVoteOptionCount [ VOTE_OPTION2 ] ) ? VOTE_OPTION1 : VOTE_OPTION2 ;
}
else
{
2022-03-01 20:00:42 +00:00
CUtlVector < int > pVoteCounts ;
2020-04-22 16:56:21 +00:00
2022-03-01 20:00:42 +00:00
// Which option had the most votes?
// driller: Need to handle ties
int nHighest = m_nVoteOptionCount [ 0 ] ;
2020-04-22 16:56:21 +00:00
for ( int iIndex = 0 ; iIndex < m_nVoteOptionCount . Count ( ) ; iIndex + + )
{
2022-03-01 20:00:42 +00:00
nHighest = ( ( nHighest < m_nVoteOptionCount [ iIndex ] ) ? m_nVoteOptionCount [ iIndex ] : nHighest ) ;
pVoteCounts . AddToTail ( m_nVoteOptionCount [ iIndex ] ) ;
}
m_nHighestCountIndex = - 1 ;
for ( int iIndex = 0 ; iIndex < m_nVoteOptionCount . Count ( ) ; iIndex + + )
{
if ( m_nVoteOptionCount [ iIndex ] = = nHighest )
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
m_nHighestCountIndex = iIndex ;
// henryg: break on first match, not last. this avoids a crash
// if we are all tied at zero and we pick something beyond the
// last vote option. this code really ought to ignore attempts
// to tally votes for options beyond the last valid one!
break ;
2020-04-22 16:56:21 +00:00
}
}
2022-03-01 20:00:42 +00:00
return m_nHighestCountIndex ;
2020-04-22 16:56:21 +00:00
}
2022-03-01 20:00:42 +00:00
return - 1 ;
2020-04-22 16:56:21 +00:00
}
//-----------------------------------------------------------------------------
// Purpose: Store steamIDs for every player that calls a vote
//-----------------------------------------------------------------------------
void CVoteController : : TrackVoteCaller ( CBasePlayer * pPlayer )
{
if ( ! pPlayer )
return ;
CSteamID steamID ;
pPlayer - > GetSteamID ( & steamID ) ;
int iIdx = m_VoteCallers . Find ( steamID . ConvertToUint64 ( ) ) ;
if ( iIdx ! = m_VoteCallers . InvalidIndex ( ) )
{
// Already being tracked - update timer
m_VoteCallers [ iIdx ] = gpGlobals - > curtime + sv_vote_creation_timer . GetInt ( ) ;
return ;
}
m_VoteCallers . Insert ( steamID . ConvertToUint64 ( ) , gpGlobals - > curtime + sv_vote_creation_timer . GetInt ( ) ) ;
} ;
//-----------------------------------------------------------------------------
// Purpose: Check the history of steamIDs that called votes and test against a timer
//-----------------------------------------------------------------------------
2022-03-01 20:00:42 +00:00
bool CVoteController : : CanEntityCallVote ( CBasePlayer * pPlayer , int & nCooldown )
2020-04-22 16:56:21 +00:00
{
if ( ! pPlayer )
return false ;
2022-03-01 20:00:42 +00:00
2020-04-22 16:56:21 +00:00
CSteamID steamID ;
pPlayer - > GetSteamID ( & steamID ) ;
// Has this SteamID tried to call a vote recently?
int iIdx = m_VoteCallers . Find ( steamID . ConvertToUint64 ( ) ) ;
if ( iIdx ! = m_VoteCallers . InvalidIndex ( ) )
{
// Timer elapsed?
nCooldown = ( int ) ( m_VoteCallers [ iIdx ] - gpGlobals - > curtime ) ;
if ( nCooldown > 0 )
return false ;
// Expired
m_VoteCallers . Remove ( iIdx ) ;
}
return true ;
} ;
//-----------------------------------------------------------------------------
// Purpose: BaseIssue
//-----------------------------------------------------------------------------
CBaseIssue : : CBaseIssue ( const char * pszTypeString )
{
2022-03-01 20:00:42 +00:00
Q_strcpy ( m_szTypeString , pszTypeString ) ;
2020-04-22 16:56:21 +00:00
m_iNumYesVotes = 0 ;
m_iNumNoVotes = 0 ;
m_iNumPotentialVotes = 0 ;
ASSERT ( g_voteController ) ;
g_voteController - > RegisterIssue ( this ) ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CBaseIssue : : ~ CBaseIssue ( )
{
for ( int index = 0 ; index < m_FailedVotes . Count ( ) ; index + + )
{
FailedVote * pFailedVote = m_FailedVotes [ index ] ;
delete pFailedVote ;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
const char * CBaseIssue : : GetTypeString ( void )
{
return m_szTypeString ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
const char * CBaseIssue : : GetDetailsString ( void )
{
return m_szDetailsString ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CBaseIssue : : SetIssueDetails ( const char * pszDetails )
{
2022-03-01 20:00:42 +00:00
Q_strcpy ( m_szDetailsString , pszDetails ) ;
2020-04-22 16:56:21 +00:00
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
2022-03-01 20:00:42 +00:00
bool CBaseIssue : : IsAllyRestrictedVote ( void )
2020-04-22 16:56:21 +00:00
{
return false ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
const char * CBaseIssue : : GetVotePassedString ( void )
{
return " Unknown vote passed. " ;
}
//-----------------------------------------------------------------------------
// Purpose: Store failures to prevent vote spam
//-----------------------------------------------------------------------------
void CBaseIssue : : OnVoteFailed ( int iEntityHoldingVote )
{
// Don't track failed dedicated server votes
if ( BRecordVoteFailureEventForEntity ( iEntityHoldingVote ) )
{
// Check for an existing match
for ( int index = 0 ; index < m_FailedVotes . Count ( ) ; index + + )
{
FailedVote * pFailedVote = m_FailedVotes [ index ] ;
if ( Q_strcmp ( pFailedVote - > szFailedVoteParameter , GetDetailsString ( ) ) = = 0 )
{
int nTime = sv_vote_failure_timer . GetInt ( ) ;
# ifdef TF_DLL
if ( TFGameRules ( ) & & TFGameRules ( ) - > IsMannVsMachineMode ( ) )
{
nTime = sv_vote_failure_timer_mvm . GetInt ( ) ;
}
# endif // TF_DLL
pFailedVote - > flLockoutTime = gpGlobals - > curtime + nTime ;
return ;
}
}
// Need to create a new one
FailedVote * pNewFailedVote = new FailedVote ;
int iIndex = m_FailedVotes . AddToTail ( pNewFailedVote ) ;
2022-03-01 20:00:42 +00:00
Q_strcpy ( m_FailedVotes [ iIndex ] - > szFailedVoteParameter , GetDetailsString ( ) ) ;
2020-04-22 16:56:21 +00:00
m_FailedVotes [ iIndex ] - > flLockoutTime = gpGlobals - > curtime + sv_vote_failure_timer . GetFloat ( ) ;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CBaseIssue : : CanTeamCallVote ( int iTeam ) const
{
return true ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CBaseIssue : : CanCallVote ( int iEntIndex , const char * pszDetails , vote_create_failed_t & nFailCode , int & nTime )
{
// Automated server vote - don't bother testing against it
if ( ! BRecordVoteFailureEventForEntity ( iEntIndex ) )
return true ;
// Bogus player
2022-03-01 20:00:42 +00:00
if ( iEntIndex = = - 1 )
2020-04-22 16:56:21 +00:00
return false ;
# ifdef TF_DLL
if ( TFGameRules ( ) & & TFGameRules ( ) - > IsInWaitingForPlayers ( ) & & ! TFGameRules ( ) - > IsInTournamentMode ( ) )
{
nFailCode = VOTE_FAILED_WAITINGFORPLAYERS ;
return false ;
}
# endif // TF_DLL
CBaseEntity * pVoteCaller = UTIL_EntityByIndex ( iEntIndex ) ;
2022-03-01 20:00:42 +00:00
if ( pVoteCaller & & ! CanTeamCallVote ( GetVoterTeam ( pVoteCaller ) ) )
2020-04-22 16:56:21 +00:00
{
nFailCode = VOTE_FAILED_TEAM_CANT_CALL ;
return false ;
}
// Did this fail recently?
2022-03-01 20:00:42 +00:00
for ( int iIndex = 0 ; iIndex < m_FailedVotes . Count ( ) ; iIndex + + )
2020-04-22 16:56:21 +00:00
{
FailedVote * pCurrentFailure = m_FailedVotes [ iIndex ] ;
int nTimeRemaining = pCurrentFailure - > flLockoutTime - gpGlobals - > curtime ;
bool bFailed = false ;
// If this issue requires a parameter, see if we're voting for the same one again (i.e. changelevel ctf_2fort)
if ( Q_strlen ( pCurrentFailure - > szFailedVoteParameter ) > 0 )
{
if ( nTimeRemaining > 1 & & FStrEq ( pCurrentFailure - > szFailedVoteParameter , pszDetails ) )
{
bFailed = true ;
}
}
// Otherwise we have a parameter-less vote, so just check the lockout timer (i.e. restartgame)
else
{
if ( nTimeRemaining > 1 )
{
bFailed = true ;
}
}
if ( bFailed )
{
2022-03-01 20:00:42 +00:00
nFailCode = VOTE_FAILED_FAILED_RECENTLY ;
2020-04-22 16:56:21 +00:00
nTime = nTimeRemaining ;
return false ;
}
}
return true ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
int CBaseIssue : : CountPotentialVoters ( void )
{
int nTotalPlayers = 0 ;
for ( int playerIndex = 1 ; playerIndex < = MAX_PLAYERS ; + + playerIndex )
{
CBasePlayer * pPlayer = UTIL_PlayerByIndex ( playerIndex ) ;
if ( g_voteController - > IsValidVoter ( pPlayer ) )
{
if ( g_voteController - > CanTeamCastVote ( GetVoterTeam ( pPlayer ) ) )
{
nTotalPlayers + + ;
}
}
}
return nTotalPlayers ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
int CBaseIssue : : GetNumberVoteOptions ( void )
{
return 2 ; // The default issue is Yes/No (so 2), but it can be anywhere between 1 and MAX_VOTE_COUNT
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CBaseIssue : : IsYesNoVote ( void )
{
return true ; // Default
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CBaseIssue : : SetYesNoVoteCount ( int iNumYesVotes , int iNumNoVotes , int iNumPotentialVotes )
{
m_iNumYesVotes = iNumYesVotes ;
m_iNumNoVotes = iNumNoVotes ;
m_iNumPotentialVotes = iNumPotentialVotes ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CBaseIssue : : ListStandardNoArgCommand ( CBasePlayer * forWhom , const char * issueString )
{
ClientPrint ( forWhom , HUD_PRINTCONSOLE , " callvote %s1 \n " , issueString ) ;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CBaseIssue : : GetVoteOptions ( CUtlVector < const char * > & vecNames )
{
// The default vote issue is a Yes/No vote
vecNames . AddToHead ( " Yes " ) ;
vecNames . AddToTail ( " No " ) ;
return true ;
}
//-----------------------------------------------------------------------------
2022-03-01 20:00:42 +00:00
// Purpose: Game system to detect maps without cameras in them, and move on
2020-04-22 16:56:21 +00:00
//-----------------------------------------------------------------------------
2022-03-01 20:00:42 +00:00
class CVoteControllerSystem : public CAutoGameSystemPerFrame
2020-04-22 16:56:21 +00:00
{
2022-03-01 20:00:42 +00:00
public :
CVoteControllerSystem ( char const * name ) : CAutoGameSystemPerFrame ( name )
{
}
2020-04-22 16:56:21 +00:00
2022-03-01 20:00:42 +00:00
virtual void LevelInitPreEntity ( )
{
}
virtual void FrameUpdatePostEntityThink ( void )
{
// Executing the vote controller command needs to happen in the PostEntityThink as it can restart levels and
// blast entities, etc. If you're doing this during a regular think, this can cause entities thinking after
// you in Physics_RunThinkFunctions() to get grumpy and crash.
if ( g_voteController )
{
// Vote passed - execute the command
if ( g_voteController - > m_executeCommandTimer . HasStarted ( ) & & g_voteController - > m_executeCommandTimer . IsElapsed ( ) )
{
g_voteController - > m_executeCommandTimer . Invalidate ( ) ;
g_voteController - > m_potentialIssues [ g_voteController - > m_iActiveIssueIndex ] - > ExecuteCommand ( ) ;
}
}
}
} ;
CVoteControllerSystem VoteControllerSystem ( " CVoteControllerSystem " ) ;
2020-04-22 16:56:21 +00:00