diff options
Diffstat (limited to 'game/server/tf/tf_autobalance.cpp')
| -rw-r--r-- | game/server/tf/tf_autobalance.cpp | 520 |
1 files changed, 520 insertions, 0 deletions
diff --git a/game/server/tf/tf_autobalance.cpp b/game/server/tf/tf_autobalance.cpp new file mode 100644 index 0000000..e6b7b0f --- /dev/null +++ b/game/server/tf/tf_autobalance.cpp @@ -0,0 +1,520 @@ +//========= 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; } |