//========= 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 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( 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( 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; }