Team Fortress 2 Source Code as on 22/4/2020
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

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. // $NoKeywords: $
  6. //=============================================================================
  7. #include "cbase.h"
  8. #include "tf_autobalance.h"
  9. #include "tf_gamerules.h"
  10. #include "tf_matchmaking_shared.h"
  11. #include "team.h"
  12. #include "minigames/tf_duel.h"
  13. #include "player_resource.h"
  14. #include "tf_player_resource.h"
  15. // memdbgon must be the last include file in a .cpp file!!!
  16. #include <tier0/memdbgon.h>
  17. extern ConVar mp_developer;
  18. extern ConVar mp_teams_unbalance_limit;
  19. extern ConVar tf_arena_use_queue;
  20. extern ConVar mp_autoteambalance;
  21. extern ConVar tf_autobalance_query_lifetime;
  22. extern ConVar tf_autobalance_xp_bonus;
  23. ConVar tf_autobalance_detected_delay( "tf_autobalance_detected_delay", "30", FCVAR_NONE );
  24. //-----------------------------------------------------------------------------
  25. // Purpose:
  26. //-----------------------------------------------------------------------------
  27. CTFAutobalance::CTFAutobalance()
  28. {
  29. Reset();
  30. }
  31. //-----------------------------------------------------------------------------
  32. // Purpose:
  33. //-----------------------------------------------------------------------------
  34. CTFAutobalance::~CTFAutobalance()
  35. {
  36. }
  37. //-----------------------------------------------------------------------------
  38. // Purpose:
  39. //-----------------------------------------------------------------------------
  40. void CTFAutobalance::Reset()
  41. {
  42. m_iCurrentState = AB_STATE_INACTIVE;
  43. m_iLightestTeam = m_iHeaviestTeam = TEAM_INVALID;
  44. m_nNeeded = 0;
  45. m_flBalanceTeamsTime = -1.f;
  46. if ( m_vecPlayersAsked.Count() > 0 )
  47. {
  48. // if we're resetting and we have people we haven't heard from yet, tell them to close their notification
  49. FOR_EACH_VEC( m_vecPlayersAsked, i )
  50. {
  51. if ( m_vecPlayersAsked[i].hPlayer.Get() && ( m_vecPlayersAsked[i].eState == AB_VOLUNTEER_STATE_ASKED ) )
  52. {
  53. CSingleUserRecipientFilter filter( m_vecPlayersAsked[i].hPlayer.Get() );
  54. filter.MakeReliable();
  55. UserMessageBegin( filter, "AutoBalanceVolunteer_Cancel" );
  56. MessageEnd();
  57. }
  58. }
  59. m_vecPlayersAsked.Purge();
  60. }
  61. }
  62. //-----------------------------------------------------------------------------
  63. // Purpose:
  64. //-----------------------------------------------------------------------------
  65. void CTFAutobalance::Shutdown()
  66. {
  67. Reset();
  68. }
  69. //-----------------------------------------------------------------------------
  70. // Purpose:
  71. //-----------------------------------------------------------------------------
  72. void CTFAutobalance::LevelShutdownPostEntity()
  73. {
  74. Reset();
  75. }
  76. //-----------------------------------------------------------------------------
  77. // Purpose:
  78. //-----------------------------------------------------------------------------
  79. bool CTFAutobalance::ShouldBeActive() const
  80. {
  81. if ( !TFGameRules() )
  82. return false;
  83. if ( TFGameRules()->IsInTraining() || TFGameRules()->IsInItemTestingMode() )
  84. return false;
  85. if ( TFGameRules()->IsInArenaMode() && tf_arena_use_queue.GetBool() )
  86. return false;
  87. #if defined( _DEBUG ) || defined( STAGING_ONLY )
  88. if ( mp_developer.GetBool() )
  89. return false;
  90. #endif // _DEBUG || STAGING_ONLY
  91. if ( mp_teams_unbalance_limit.GetInt() <= 0 )
  92. return false;
  93. const IMatchGroupDescription *pMatchDesc = GetMatchGroupDescription( TFGameRules()->GetCurrentMatchGroup() );
  94. if ( pMatchDesc )
  95. {
  96. return pMatchDesc->m_params.m_bUseAutoBalance;
  97. }
  98. // outside of managed matches, we don't normally do any balancing for tournament mode
  99. if ( TFGameRules()->IsInTournamentMode() )
  100. return false;
  101. return ( mp_autoteambalance.GetInt() == 2 );
  102. }
  103. //-----------------------------------------------------------------------------
  104. // Purpose:
  105. //-----------------------------------------------------------------------------
  106. bool CTFAutobalance::AreTeamsUnbalanced()
  107. {
  108. if ( !TFGameRules() )
  109. return false;
  110. // don't bother switching teams if the round isn't running
  111. if ( TFGameRules()->State_Get() != GR_STATE_RND_RUNNING )
  112. return false;
  113. if ( mp_teams_unbalance_limit.GetInt() <= 0 )
  114. return false;
  115. if ( TFGameRules()->ArePlayersInHell() )
  116. return false;
  117. int nDiffBetweenTeams = 0;
  118. m_iLightestTeam = m_iHeaviestTeam = TEAM_INVALID;
  119. m_nNeeded = 0;
  120. CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
  121. if ( pMatch )
  122. {
  123. int nNumTeamRed = pMatch->GetNumActiveMatchPlayersForTeam( TFGameRules()->GetGCTeamForGameTeam( TF_TEAM_RED ) );
  124. int nNumTeamBlue = pMatch->GetNumActiveMatchPlayersForTeam( TFGameRules()->GetGCTeamForGameTeam( TF_TEAM_BLUE ) );
  125. m_iLightestTeam = ( nNumTeamRed > nNumTeamBlue ) ? TF_TEAM_BLUE : TF_TEAM_RED;
  126. m_iHeaviestTeam = ( nNumTeamRed > nNumTeamBlue ) ? TF_TEAM_RED : TF_TEAM_BLUE;
  127. nDiffBetweenTeams = abs( nNumTeamRed - nNumTeamBlue );
  128. }
  129. else
  130. {
  131. int iMostPlayers = 0;
  132. int iLeastPlayers = MAX_PLAYERS + 1;
  133. int i = FIRST_GAME_TEAM;
  134. for ( CTeam *pTeam = GetGlobalTeam( i ); pTeam != NULL; pTeam = GetGlobalTeam( ++i ) )
  135. {
  136. int iNumPlayers = pTeam->GetNumPlayers();
  137. if ( iNumPlayers < iLeastPlayers )
  138. {
  139. iLeastPlayers = iNumPlayers;
  140. m_iLightestTeam = i;
  141. }
  142. if ( iNumPlayers > iMostPlayers )
  143. {
  144. iMostPlayers = iNumPlayers;
  145. m_iHeaviestTeam = i;
  146. }
  147. }
  148. nDiffBetweenTeams = ( iMostPlayers - iLeastPlayers );
  149. }
  150. if ( nDiffBetweenTeams > mp_teams_unbalance_limit.GetInt() )
  151. {
  152. m_nNeeded = ( nDiffBetweenTeams / 2 );
  153. return true;
  154. }
  155. return false;
  156. }
  157. //-----------------------------------------------------------------------------
  158. // Purpose:
  159. //-----------------------------------------------------------------------------
  160. void CTFAutobalance::MonitorTeams()
  161. {
  162. if ( AreTeamsUnbalanced() )
  163. {
  164. if ( m_flBalanceTeamsTime < 0.f )
  165. {
  166. // trigger a small waiting period to see if the GC sends us someone before we need to balance the teams
  167. m_flBalanceTeamsTime = gpGlobals->curtime + tf_autobalance_detected_delay.GetInt();
  168. }
  169. else if ( m_flBalanceTeamsTime < gpGlobals->curtime )
  170. {
  171. if ( IsOkayToBalancePlayers() )
  172. {
  173. UTIL_ClientPrintAll( HUD_PRINTTALK, "#TF_Autobalance_Start", ( m_iHeaviestTeam == TF_TEAM_RED ) ? "#TF_RedTeam_Name" : "#TF_BlueTeam_Name" );
  174. m_iCurrentState = AB_STATE_FIND_VOLUNTEERS;
  175. }
  176. }
  177. }
  178. else
  179. {
  180. m_flBalanceTeamsTime = -1.f;
  181. }
  182. }
  183. //-----------------------------------------------------------------------------
  184. // Purpose:
  185. //-----------------------------------------------------------------------------
  186. bool CTFAutobalance::HaveAlreadyAskedPlayer( CTFPlayer *pTFPlayer ) const
  187. {
  188. FOR_EACH_VEC( m_vecPlayersAsked, i )
  189. {
  190. if ( m_vecPlayersAsked[i].hPlayer == pTFPlayer )
  191. return true;
  192. }
  193. return false;
  194. }
  195. //-----------------------------------------------------------------------------
  196. // Purpose:
  197. //-----------------------------------------------------------------------------
  198. int CTFAutobalance::GetTeamAutoBalanceScore( int nTeam ) const
  199. {
  200. CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
  201. if ( pMatch && TFGameRules() )
  202. {
  203. return pMatch->GetTotalSkillRatingForTeam( TFGameRules()->GetGCTeamForGameTeam( nTeam ) );
  204. }
  205. int nTotalScore = 0;
  206. CTFPlayerResource *pTFPlayerResource = dynamic_cast<CTFPlayerResource *>( g_pPlayerResource );
  207. if ( pTFPlayerResource )
  208. {
  209. CTeam *pTeam = GetGlobalTeam( nTeam );
  210. if ( pTeam )
  211. {
  212. for ( int i = 0; i < pTeam->GetNumPlayers(); i++ )
  213. {
  214. CTFPlayer *pTFPlayer = ToTFPlayer( pTeam->GetPlayer( i ) );
  215. if ( pTFPlayer )
  216. {
  217. nTotalScore += pTFPlayerResource->GetTotalScore( pTFPlayer->entindex() );
  218. }
  219. }
  220. }
  221. }
  222. return nTotalScore;
  223. }
  224. //-----------------------------------------------------------------------------
  225. // Purpose:
  226. //-----------------------------------------------------------------------------
  227. int CTFAutobalance::GetPlayerAutoBalanceScore( CTFPlayer *pTFPlayer ) const
  228. {
  229. if ( !pTFPlayer )
  230. return 0;
  231. CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
  232. if ( pMatch )
  233. {
  234. CSteamID steamID;
  235. pTFPlayer->GetSteamID( &steamID );
  236. if ( steamID.IsValid() )
  237. {
  238. const CMatchInfo::PlayerMatchData_t* pPlayerMatchData = pMatch->GetMatchDataForPlayer( steamID );
  239. if ( pPlayerMatchData )
  240. {
  241. FixmeMMRatingBackendSwapping(); // Make sure this makes sense with arbitrary skill rating values --
  242. // e.g. maybe we want a smarter glicko-weighting thing.
  243. return (int)pPlayerMatchData->unMMSkillRating;
  244. }
  245. }
  246. }
  247. int nTotalScore = 0;
  248. CTFPlayerResource *pTFPlayerResource = dynamic_cast<CTFPlayerResource *>( g_pPlayerResource );
  249. if ( pTFPlayerResource )
  250. {
  251. nTotalScore = pTFPlayerResource->GetTotalScore( pTFPlayer->entindex() );
  252. }
  253. return nTotalScore;
  254. }
  255. //-----------------------------------------------------------------------------
  256. // Purpose:
  257. //-----------------------------------------------------------------------------
  258. CTFPlayer *CTFAutobalance::FindPlayerToAsk()
  259. {
  260. CTFPlayer *pRetVal = NULL;
  261. CUtlVector< CTFPlayer* > vecCandiates;
  262. CTeam *pTeam = GetGlobalTeam( m_iHeaviestTeam );
  263. if ( pTeam )
  264. {
  265. // loop through and get a list of possible candidates
  266. for ( int i = 0; i < pTeam->GetNumPlayers(); i++ )
  267. {
  268. CTFPlayer *pTFPlayer = ToTFPlayer( pTeam->GetPlayer( i ) );
  269. if ( pTFPlayer && !HaveAlreadyAskedPlayer( pTFPlayer ) && pTFPlayer->CanBeAutobalanced() )
  270. {
  271. vecCandiates.AddToTail( pTFPlayer );
  272. }
  273. }
  274. }
  275. // no need to go any further if there's only one candidate
  276. if ( vecCandiates.Count() == 1 )
  277. {
  278. pRetVal = vecCandiates[0];
  279. }
  280. else if ( vecCandiates.Count() > 1 )
  281. {
  282. int nTotalDiff = abs( GetTeamAutoBalanceScore( m_iHeaviestTeam ) - GetTeamAutoBalanceScore( m_iLightestTeam ) );
  283. int nAverageNeeded = ( nTotalDiff / 2 ) / m_nNeeded;
  284. // now look a player on the heaviest team with skillrating closest to that average
  285. int nClosest = INT_MAX;
  286. FOR_EACH_VEC( vecCandiates, iIndex )
  287. {
  288. int nDiff = abs( nAverageNeeded - GetPlayerAutoBalanceScore( vecCandiates[iIndex] ) );
  289. if ( nDiff < nClosest )
  290. {
  291. nClosest = nDiff;
  292. pRetVal = vecCandiates[iIndex];
  293. }
  294. }
  295. }
  296. return pRetVal;
  297. }
  298. //-----------------------------------------------------------------------------
  299. // Purpose:
  300. //-----------------------------------------------------------------------------
  301. void CTFAutobalance::FindVolunteers()
  302. {
  303. // keep track of the state of things, this will also update our counts if more players drop from the server
  304. if ( !AreTeamsUnbalanced() || !IsOkayToBalancePlayers() )
  305. {
  306. Reset();
  307. return;
  308. }
  309. int nPendingReplies = 0;
  310. int nRepliedNo = 0;
  311. FOR_EACH_VEC( m_vecPlayersAsked, i )
  312. {
  313. // if the player is valid
  314. if ( m_vecPlayersAsked[i].hPlayer.Get() )
  315. {
  316. switch ( m_vecPlayersAsked[i].eState )
  317. {
  318. case AB_VOLUNTEER_STATE_ASKED:
  319. if ( m_vecPlayersAsked[i].flQueryExpireTime < gpGlobals->curtime )
  320. {
  321. // they've timed out the request period without replying
  322. m_vecPlayersAsked[i].eState = AB_VOLUNTEER_STATE_NO;
  323. nRepliedNo++;
  324. }
  325. else
  326. {
  327. nPendingReplies++;
  328. }
  329. break;
  330. case AB_VOLUNTEER_STATE_NO:
  331. nRepliedNo++;
  332. break;
  333. default:
  334. break;
  335. }
  336. }
  337. }
  338. int nNumToAsk = ( m_nNeeded * 2 );
  339. // do we need to ask for more volunteers?
  340. if ( nPendingReplies < nNumToAsk )
  341. {
  342. int nNumNeeded = nNumToAsk - nPendingReplies;
  343. int nNumAsked = 0;
  344. while ( nNumAsked < nNumNeeded )
  345. {
  346. CTFPlayer *pTFPlayer = FindPlayerToAsk();
  347. if ( pTFPlayer )
  348. {
  349. int iIndex = m_vecPlayersAsked.AddToTail();
  350. m_vecPlayersAsked[iIndex].hPlayer = pTFPlayer;
  351. m_vecPlayersAsked[iIndex].eState = AB_VOLUNTEER_STATE_ASKED;
  352. m_vecPlayersAsked[iIndex].flQueryExpireTime = gpGlobals->curtime + tf_autobalance_query_lifetime.GetInt() + 3; // add 3 seconds to allow for travel time to/from the client
  353. CSingleUserRecipientFilter filter( pTFPlayer );
  354. filter.MakeReliable();
  355. UserMessageBegin( filter, "AutoBalanceVolunteer" );
  356. MessageEnd();
  357. nNumAsked++;
  358. nPendingReplies++;
  359. }
  360. else
  361. {
  362. // we couldn't find anyone else to ask
  363. if ( nPendingReplies <= 0 )
  364. {
  365. // we're not waiting on anyone else to reply....so we should just reset
  366. Reset();
  367. }
  368. return;
  369. }
  370. }
  371. }
  372. }
  373. //-----------------------------------------------------------------------------
  374. // Purpose:
  375. //-----------------------------------------------------------------------------
  376. void CTFAutobalance::FrameUpdatePostEntityThink()
  377. {
  378. bool bActive = ShouldBeActive();
  379. if ( !bActive )
  380. {
  381. Reset();
  382. return;
  383. }
  384. switch ( m_iCurrentState )
  385. {
  386. case AB_STATE_INACTIVE:
  387. // we should be active if we've made it this far
  388. m_iCurrentState = AB_STATE_MONITOR;
  389. break;
  390. case AB_STATE_MONITOR:
  391. MonitorTeams();
  392. break;
  393. case AB_STATE_FIND_VOLUNTEERS:
  394. FindVolunteers();
  395. break;
  396. default:
  397. break;
  398. }
  399. }
  400. //-----------------------------------------------------------------------------
  401. // Purpose:
  402. //-----------------------------------------------------------------------------
  403. bool CTFAutobalance::IsOkayToBalancePlayers()
  404. {
  405. if ( GTFGCClientSystem()->GetLiveMatch() && !GTFGCClientSystem()->CanChangeMatchPlayerTeams() )
  406. return false;
  407. return true;
  408. }
  409. //-----------------------------------------------------------------------------
  410. // Purpose:
  411. //-----------------------------------------------------------------------------
  412. void CTFAutobalance::ReplyReceived( CTFPlayer *pTFPlayer, bool bResponse )
  413. {
  414. if ( m_iCurrentState != AB_STATE_FIND_VOLUNTEERS )
  415. return;
  416. if ( !AreTeamsUnbalanced() || !IsOkayToBalancePlayers() )
  417. {
  418. Reset();
  419. return;
  420. }
  421. FOR_EACH_VEC( m_vecPlayersAsked, i )
  422. {
  423. // is this a player we asked?
  424. if ( m_vecPlayersAsked[i].hPlayer == pTFPlayer )
  425. {
  426. m_vecPlayersAsked[i].eState = bResponse ? AB_VOLUNTEER_STATE_YES : AB_VOLUNTEER_STATE_NO;
  427. if ( bResponse && pTFPlayer->CanBeAutobalanced() )
  428. {
  429. pTFPlayer->ChangeTeam( m_iLightestTeam, false, false, true );
  430. pTFPlayer->ForceRespawn();
  431. pTFPlayer->SetLastAutobalanceTime( gpGlobals->curtime );
  432. CMatchInfo *pMatch = GTFGCClientSystem()->GetLiveMatch();
  433. if ( pMatch )
  434. {
  435. CSteamID steamID;
  436. pTFPlayer->GetSteamID( &steamID );
  437. // We're going to give the switching player a bonus pool of XP. This should encourage
  438. // them to keep playing to earn what's in the pool, rather than just quit after getting
  439. // a big payout
  440. if ( !pMatch->BSentResult() )
  441. {
  442. pMatch->GiveXPBonus( steamID, CMsgTFXPSource_XPSourceType_SOURCE_AUTOBALANCE_BONUS, 1, tf_autobalance_xp_bonus.GetInt() );
  443. }
  444. GTFGCClientSystem()->ChangeMatchPlayerTeam( steamID, TFGameRules()->GetGCTeamForGameTeam( m_iLightestTeam ) );
  445. }
  446. }
  447. }
  448. }
  449. }
  450. CTFAutobalance gTFAutobalance;
  451. CTFAutobalance *TFAutoBalance(){ return &gTFAutobalance; }