Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

520 lines
14 KiB

//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//=============================================================================
#include "cbase.h"
#include "tf_autobalance.h"
#include "tf_gamerules.h"
#include "tf_matchmaking_shared.h"
#include "team.h"
#include "minigames/tf_duel.h"
#include "player_resource.h"
#include "tf_player_resource.h"
// memdbgon must be the last include file in a .cpp file!!!
#include <tier0/memdbgon.h>
extern ConVar mp_developer;
extern ConVar mp_teams_unbalance_limit;
extern ConVar tf_arena_use_queue;
extern ConVar mp_autoteambalance;
extern ConVar tf_autobalance_query_lifetime;
extern ConVar tf_autobalance_xp_bonus;
ConVar tf_autobalance_detected_delay( "tf_autobalance_detected_delay", "30", FCVAR_NONE );
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CTFAutobalance::CTFAutobalance()
{
Reset();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CTFAutobalance::~CTFAutobalance()
{
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFAutobalance::Reset()
{
m_iCurrentState = AB_STATE_INACTIVE;
m_iLightestTeam = m_iHeaviestTeam = TEAM_INVALID;
m_nNeeded = 0;
m_flBalanceTeamsTime = -1.f;
if ( m_vecPlayersAsked.Count() > 0 )
{
// if we're resetting and we have people we haven't heard from yet, tell them to close their notification
FOR_EACH_VEC( m_vecPlayersAsked, i )
{
if ( m_vecPlayersAsked[i].hPlayer.Get() && ( m_vecPlayersAsked[i].eState == AB_VOLUNTEER_STATE_ASKED ) )
{
CSingleUserRecipientFilter filter( m_vecPlayersAsked[i].hPlayer.Get() );
filter.MakeReliable();
UserMessageBegin( filter, "AutoBalanceVolunteer_Cancel" );
MessageEnd();
}
}
m_vecPlayersAsked.Purge();
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFAutobalance::Shutdown()
{
Reset();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFAutobalance::LevelShutdownPostEntity()
{
Reset();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CTFAutobalance::ShouldBeActive() const
{
if ( !TFGameRules() )
return false;
if ( TFGameRules()->IsInTraining() || TFGameRules()->IsInItemTestingMode() )
return false;
if ( TFGameRules()->IsInArenaMode() && tf_arena_use_queue.GetBool() )
return false;
#if defined( _DEBUG ) || defined( STAGING_ONLY )
if ( mp_developer.GetBool() )
return false;
#endif // _DEBUG || STAGING_ONLY
if ( mp_teams_unbalance_limit.GetInt() <= 0 )
return false;
const IMatchGroupDescription *pMatchDesc = GetMatchGroupDescription( TFGameRules()->GetCurrentMatchGroup() );
if ( pMatchDesc )
{
return pMatchDesc->m_params.m_bUseAutoBalance;
}
// outside of managed matches, we don't normally do any balancing for tournament mode
if ( TFGameRules()->IsInTournamentMode() )
return false;
return ( mp_autoteambalance.GetInt() == 2 );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CTFAutobalance::AreTeamsUnbalanced()
{
if ( !TFGameRules() )
return false;
// don't bother switching teams if the round isn't running
if ( TFGameRules()->State_Get() != GR_STATE_RND_RUNNING )
return false;
if ( mp_teams_unbalance_limit.GetInt() <= 0 )
return false;
if ( TFGameRules()->ArePlayersInHell() )
return false;
int nDiffBetweenTeams = 0;
m_iLightestTeam = m_iHeaviestTeam = TEAM_INVALID;
m_nNeeded = 0;
CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
if ( pMatch )
{
int nNumTeamRed = pMatch->GetNumActiveMatchPlayersForTeam( TFGameRules()->GetGCTeamForGameTeam( TF_TEAM_RED ) );
int nNumTeamBlue = pMatch->GetNumActiveMatchPlayersForTeam( TFGameRules()->GetGCTeamForGameTeam( TF_TEAM_BLUE ) );
m_iLightestTeam = ( nNumTeamRed > nNumTeamBlue ) ? TF_TEAM_BLUE : TF_TEAM_RED;
m_iHeaviestTeam = ( nNumTeamRed > nNumTeamBlue ) ? TF_TEAM_RED : TF_TEAM_BLUE;
nDiffBetweenTeams = abs( nNumTeamRed - nNumTeamBlue );
}
else
{
int iMostPlayers = 0;
int iLeastPlayers = MAX_PLAYERS + 1;
int i = FIRST_GAME_TEAM;
for ( CTeam *pTeam = GetGlobalTeam( i ); pTeam != NULL; pTeam = GetGlobalTeam( ++i ) )
{
int iNumPlayers = pTeam->GetNumPlayers();
if ( iNumPlayers < iLeastPlayers )
{
iLeastPlayers = iNumPlayers;
m_iLightestTeam = i;
}
if ( iNumPlayers > iMostPlayers )
{
iMostPlayers = iNumPlayers;
m_iHeaviestTeam = i;
}
}
nDiffBetweenTeams = ( iMostPlayers - iLeastPlayers );
}
if ( nDiffBetweenTeams > mp_teams_unbalance_limit.GetInt() )
{
m_nNeeded = ( nDiffBetweenTeams / 2 );
return true;
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFAutobalance::MonitorTeams()
{
if ( AreTeamsUnbalanced() )
{
if ( m_flBalanceTeamsTime < 0.f )
{
// trigger a small waiting period to see if the GC sends us someone before we need to balance the teams
m_flBalanceTeamsTime = gpGlobals->curtime + tf_autobalance_detected_delay.GetInt();
}
else if ( m_flBalanceTeamsTime < gpGlobals->curtime )
{
if ( IsOkayToBalancePlayers() )
{
UTIL_ClientPrintAll( HUD_PRINTTALK, "#TF_Autobalance_Start", ( m_iHeaviestTeam == TF_TEAM_RED ) ? "#TF_RedTeam_Name" : "#TF_BlueTeam_Name" );
m_iCurrentState = AB_STATE_FIND_VOLUNTEERS;
}
}
}
else
{
m_flBalanceTeamsTime = -1.f;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CTFAutobalance::HaveAlreadyAskedPlayer( CTFPlayer *pTFPlayer ) const
{
FOR_EACH_VEC( m_vecPlayersAsked, i )
{
if ( m_vecPlayersAsked[i].hPlayer == pTFPlayer )
return true;
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
int CTFAutobalance::GetTeamAutoBalanceScore( int nTeam ) const
{
CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
if ( pMatch && TFGameRules() )
{
return pMatch->GetTotalSkillRatingForTeam( TFGameRules()->GetGCTeamForGameTeam( nTeam ) );
}
int nTotalScore = 0;
CTFPlayerResource *pTFPlayerResource = dynamic_cast<CTFPlayerResource *>( g_pPlayerResource );
if ( pTFPlayerResource )
{
CTeam *pTeam = GetGlobalTeam( nTeam );
if ( pTeam )
{
for ( int i = 0; i < pTeam->GetNumPlayers(); i++ )
{
CTFPlayer *pTFPlayer = ToTFPlayer( pTeam->GetPlayer( i ) );
if ( pTFPlayer )
{
nTotalScore += pTFPlayerResource->GetTotalScore( pTFPlayer->entindex() );
}
}
}
}
return nTotalScore;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
int CTFAutobalance::GetPlayerAutoBalanceScore( CTFPlayer *pTFPlayer ) const
{
if ( !pTFPlayer )
return 0;
CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
if ( pMatch )
{
CSteamID steamID;
pTFPlayer->GetSteamID( &steamID );
if ( steamID.IsValid() )
{
const CMatchInfo::PlayerMatchData_t* pPlayerMatchData = pMatch->GetMatchDataForPlayer( steamID );
if ( pPlayerMatchData )
{
FixmeMMRatingBackendSwapping(); // Make sure this makes sense with arbitrary skill rating values --
// e.g. maybe we want a smarter glicko-weighting thing.
return (int)pPlayerMatchData->unMMSkillRating;
}
}
}
int nTotalScore = 0;
CTFPlayerResource *pTFPlayerResource = dynamic_cast<CTFPlayerResource *>( g_pPlayerResource );
if ( pTFPlayerResource )
{
nTotalScore = pTFPlayerResource->GetTotalScore( pTFPlayer->entindex() );
}
return nTotalScore;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CTFPlayer *CTFAutobalance::FindPlayerToAsk()
{
CTFPlayer *pRetVal = NULL;
CUtlVector< CTFPlayer* > vecCandiates;
CTeam *pTeam = GetGlobalTeam( m_iHeaviestTeam );
if ( pTeam )
{
// loop through and get a list of possible candidates
for ( int i = 0; i < pTeam->GetNumPlayers(); i++ )
{
CTFPlayer *pTFPlayer = ToTFPlayer( pTeam->GetPlayer( i ) );
if ( pTFPlayer && !HaveAlreadyAskedPlayer( pTFPlayer ) && pTFPlayer->CanBeAutobalanced() )
{
vecCandiates.AddToTail( pTFPlayer );
}
}
}
// no need to go any further if there's only one candidate
if ( vecCandiates.Count() == 1 )
{
pRetVal = vecCandiates[0];
}
else if ( vecCandiates.Count() > 1 )
{
int nTotalDiff = abs( GetTeamAutoBalanceScore( m_iHeaviestTeam ) - GetTeamAutoBalanceScore( m_iLightestTeam ) );
int nAverageNeeded = ( nTotalDiff / 2 ) / m_nNeeded;
// now look a player on the heaviest team with skillrating closest to that average
int nClosest = INT_MAX;
FOR_EACH_VEC( vecCandiates, iIndex )
{
int nDiff = abs( nAverageNeeded - GetPlayerAutoBalanceScore( vecCandiates[iIndex] ) );
if ( nDiff < nClosest )
{
nClosest = nDiff;
pRetVal = vecCandiates[iIndex];
}
}
}
return pRetVal;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFAutobalance::FindVolunteers()
{
// keep track of the state of things, this will also update our counts if more players drop from the server
if ( !AreTeamsUnbalanced() || !IsOkayToBalancePlayers() )
{
Reset();
return;
}
int nPendingReplies = 0;
int nRepliedNo = 0;
FOR_EACH_VEC( m_vecPlayersAsked, i )
{
// if the player is valid
if ( m_vecPlayersAsked[i].hPlayer.Get() )
{
switch ( m_vecPlayersAsked[i].eState )
{
case AB_VOLUNTEER_STATE_ASKED:
if ( m_vecPlayersAsked[i].flQueryExpireTime < gpGlobals->curtime )
{
// they've timed out the request period without replying
m_vecPlayersAsked[i].eState = AB_VOLUNTEER_STATE_NO;
nRepliedNo++;
}
else
{
nPendingReplies++;
}
break;
case AB_VOLUNTEER_STATE_NO:
nRepliedNo++;
break;
default:
break;
}
}
}
int nNumToAsk = ( m_nNeeded * 2 );
// do we need to ask for more volunteers?
if ( nPendingReplies < nNumToAsk )
{
int nNumNeeded = nNumToAsk - nPendingReplies;
int nNumAsked = 0;
while ( nNumAsked < nNumNeeded )
{
CTFPlayer *pTFPlayer = FindPlayerToAsk();
if ( pTFPlayer )
{
int iIndex = m_vecPlayersAsked.AddToTail();
m_vecPlayersAsked[iIndex].hPlayer = pTFPlayer;
m_vecPlayersAsked[iIndex].eState = AB_VOLUNTEER_STATE_ASKED;
m_vecPlayersAsked[iIndex].flQueryExpireTime = gpGlobals->curtime + tf_autobalance_query_lifetime.GetInt() + 3; // add 3 seconds to allow for travel time to/from the client
CSingleUserRecipientFilter filter( pTFPlayer );
filter.MakeReliable();
UserMessageBegin( filter, "AutoBalanceVolunteer" );
MessageEnd();
nNumAsked++;
nPendingReplies++;
}
else
{
// we couldn't find anyone else to ask
if ( nPendingReplies <= 0 )
{
// we're not waiting on anyone else to reply....so we should just reset
Reset();
}
return;
}
}
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFAutobalance::FrameUpdatePostEntityThink()
{
bool bActive = ShouldBeActive();
if ( !bActive )
{
Reset();
return;
}
switch ( m_iCurrentState )
{
case AB_STATE_INACTIVE:
// we should be active if we've made it this far
m_iCurrentState = AB_STATE_MONITOR;
break;
case AB_STATE_MONITOR:
MonitorTeams();
break;
case AB_STATE_FIND_VOLUNTEERS:
FindVolunteers();
break;
default:
break;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CTFAutobalance::IsOkayToBalancePlayers()
{
if ( GTFGCClientSystem()->GetLiveMatch() && !GTFGCClientSystem()->CanChangeMatchPlayerTeams() )
return false;
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFAutobalance::ReplyReceived( CTFPlayer *pTFPlayer, bool bResponse )
{
if ( m_iCurrentState != AB_STATE_FIND_VOLUNTEERS )
return;
if ( !AreTeamsUnbalanced() || !IsOkayToBalancePlayers() )
{
Reset();
return;
}
FOR_EACH_VEC( m_vecPlayersAsked, i )
{
// is this a player we asked?
if ( m_vecPlayersAsked[i].hPlayer == pTFPlayer )
{
m_vecPlayersAsked[i].eState = bResponse ? AB_VOLUNTEER_STATE_YES : AB_VOLUNTEER_STATE_NO;
if ( bResponse && pTFPlayer->CanBeAutobalanced() )
{
pTFPlayer->ChangeTeam( m_iLightestTeam, false, false, true );
pTFPlayer->ForceRespawn();
pTFPlayer->SetLastAutobalanceTime( gpGlobals->curtime );
CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
if ( pMatch )
{
CSteamID steamID;
pTFPlayer->GetSteamID( &steamID );
// We're going to give the switching player a bonus pool of XP. This should encourage
// them to keep playing to earn what's in the pool, rather than just quit after getting
// a big payout
if ( !pMatch->BSentResult() )
{
pMatch->GiveXPBonus( steamID, CMsgTFXPSource_XPSourceType_SOURCE_AUTOBALANCE_BONUS, 1, tf_autobalance_xp_bonus.GetInt() );
}
GTFGCClientSystem()->ChangeMatchPlayerTeam( steamID, TFGameRules()->GetGCTeamForGameTeam( m_iLightestTeam ) );
}
}
}
}
}
CTFAutobalance gTFAutobalance;
CTFAutobalance *TFAutoBalance(){ return &gTFAutobalance; }