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.

3848 lines
141 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. #include "cbase.h"
  3. #include "tf_gc_server.h"
  4. #include "gcsdk/gcsdk_auto.h"
  5. #include "tf_gcmessages.h"
  6. #include "tf_player.h"
  7. #include "rtime.h"
  8. // XXX(JohnS): Eventually, we want to send a smaller lobby object to clients. For now, they use the CTFGSLobby, which is
  9. // in shared code for that reason.
  10. #include "tf_lobby_server.h"
  11. #include "tf_gamerules.h"
  12. #include "eiface.h"
  13. #include "cdll_int.h"
  14. #include "econ_item_inventory.h"
  15. #include "gameinterface.h"
  16. #include "client.h"
  17. #include "tier1/convar.h"
  18. #include "tf_matchmaking_shared.h"
  19. #include "tf_quickplay_shared.h"
  20. #include "tf_mann_vs_machine_stats.h"
  21. #include "tf_objective_resource.h"
  22. #include "tf_player.h"
  23. #include "tf_voteissues.h"
  24. #include "player_vs_environment/tf_population_manager.h"
  25. #include "quest_objective_manager.h"
  26. #include "player_resource.h"
  27. #include "tf_player_resource.h"
  28. #include "tf_gamestats.h"
  29. #include "tf_player.h"
  30. #include "tf_match_description.h"
  31. #include "util.h"
  32. #include "tier1/utlqueue.h"
  33. #include "tf_player_resource.h"
  34. #include "tf_gc_shared.h"
  35. #include "tf_party.h"
  36. // memdbgon must be the last include file in a .cpp file!!!
  37. #include "tier0/memdbgon.h"
  38. using namespace GCSDK;
  39. // How many minutes before we assume something is FUBAR and reboot if we're empty and waiting for the GC to acknowledge us.
  40. // With valid match data: wait a while. GC could be having trouble, or connectivity issues, and we want to hold on to
  41. // the results for it to come back up. After three hours, assume its us.
  42. const int k_InvalidState_Timeout_With_Match = 60 * 2;
  43. const int k_InvalidState_Timeout_Without_Match = 5;
  44. #ifdef ENABLE_GC_MATCHMAKING
  45. /***********************************************************************************************************************
  46. ////////////////////////////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
  47. XXX(JohnS) NOTE The current state of the matchmaking flow through this class is a bit of a mess. Have been
  48. incrementally cleaning things up, but be careful.
  49. UpdateConnectedPlayersAndServerInfo()
  50. This is the heug god function that sync's our state with the GC's state via the Lobby shared object:
  51. - Our actual connected players
  52. - m_pMatchInfo (via GetMatch()) - this represents our match in progress, and should generally mirror the GC, but
  53. *MIGHT NOT*. For instance, when the GC is unavailable this object is locked, and when the GC returns we may be
  54. desync'd. This function is in charge of managing that. Outside code should simply look at the MatchInfo object
  55. and trust that it is the state of the match.
  56. - m_vecReservationExpiryTime - this should be merged into MatchInfo eventually, but is an array of active
  57. reservations and when they expire. This isn't in MatchInfo because in some modes we operate with reservations
  58. but without running a proper Match. When we're running a match, anyone in this vector should be in the MatchInfo
  59. - CTFGSLobby - This is the shared object from the server that represents the match we are hosting. However, it is
  60. *NOT* the article of record on the match. This is due to matches being designed to be resilient to GC connection
  61. loss. Essentially, only this function should be looking at CTFGSLobby and negotiating the state of the actual
  62. match it believes itself to have in MatchInfo.
  63. == Gameserver / GC Authority
  64. - GC forms matches, adds players to matches, passes them to servers
  65. - Servers run matches to completion, have authority on abandons/etc. regardless of GC state
  66. - Servers pass result, including any abandons, to GC. Message is queued if GC is unavailable.
  67. - GC takes match results and does ELO calculation and any stats/etc.
  68. - GC can request players be kicked from matches or matches be canceled
  69. - If more players are needed
  70. - Gameserver requests GC attention with appropriate flag (6v6: Stalled, waiting on complete match, 12v12:
  71. Non-full match)
  72. - GC adds players to lobby, making them part of the match
  73. - If server state is poor (hypothetically: lag, too many abandons, abnormal something or other)
  74. - Game server sends KickLobby to terminate match, sends failed match result
  75. - If GC is unavailable
  76. - Game server still carries out duties, may decide to make changes like end match instead of request late joins
  77. if it decides GC wont be able to provide them.
  78. == Match Start
  79. - GC creates a lobby and hands it to us. UpdateConnectedPlayers tick initializes a MatchInfo struct as
  80. appropriate, accepts players.
  81. == Adding Players
  82. - The GC adds players to the lobby (so, when GC down, matches cannot gain players)
  83. - UpdateConnectedPlayersAndServerInfo ensures that makes sense (it should, though, we no longer have legacy match
  84. types where the GC adds players we shouldn't accept)
  85. - UpdateConnectedPlayers calls AcceptGCReservation, player is added to match and put in reservation list
  86. == Dropping Players
  87. - Case 1: Player is not present, but is in the lobby (GC *might* be down, doesn't matter)
  88. - Player marked missing in MatchInfo by UpdateConnectedPlayers tick
  89. - After a grace period, player marked dropped, as an abandoner in MatchInfo
  90. - PlayerLeftMatch message is sent to tell the GC about their leaving.
  91. - Case 2: Player is dropped from GC lobby
  92. - UpdateConnectedPlayers assumes GC kicked them, marks them dropped from match and kicks them.
  93. - TODO: Ideally there'd be a KickThisGuy GC message, and we'd respond with PlayerLeftMatch, rather than the GC
  94. unilaterally dropping people like this.
  95. - Case 3: Votekicked
  96. - PlayerLeftMatch is sent, from server
  97. - All cases:
  98. - A reliable GC message player-abandoned (or was kicked or never joined) message queued to reconcile this with
  99. the lobby state, but if GC is unavailable it will be informed when it returns.
  100. - Player is marked dropped in MatchInfo
  101. == Team Assignments
  102. - The GC delivers an initial team assignment for each player added to the match. This team assignment does not
  103. change when game teams change sides, see TFGameRules::GameTeamToLobbyTeam and its inverse to map these to game
  104. logic teams (vs TF_GC_TEAM objects)
  105. - All other team changes have to be initiated by a game server message, in modes that allow it, to prevent
  106. race-conditions.
  107. - The NewMatchForLobby message expects the GC to shuffle our teams. We prevent races by not issuing other team
  108. change messages while this message is pending. If we time out waiting for the GC, some modes may start a
  109. speculative server-created match (expecting the GC to come back and respond to that message positively). In
  110. this case, we queue a ChangeMatchPlayerTeams message to stomp any assignments back to our known state,
  111. allowing us to ignore the temporary de-sync (queued messages always get processed in sequence)
  112. - The ChangeMatchPlayerTeams message allows the gameserver to change match player teams mid-game in match modes
  113. that allow it. The game server is in charge of not queuing this message in parallel with NewMatchForLobby
  114. above, or handling the potential race.
  115. - When processing either of these messages, the GC cancels any players that are awaiting acceptance by the
  116. game-server, and re-tries if necessary. This prevents team changes from racing with player-joins which may
  117. have been predicated on differing team layouts.
  118. - The game server does not accept pending players or send any heartbeats until any queued messages have been
  119. responded to. See Queued Messages below.
  120. == Match End
  121. - Match result message provides canonical record of match, is queued to send to GC when available.
  122. - GameServerKickingLobby message dissolves live match if GC is available/tracking it. Queued similarly.
  123. - ** This can happen before or after the match result.
  124. - In MvM, we send potentially multiple victory messages per match -- they can cycle missions and keep winning.
  125. - As of right now, in competitive, we end the match coincident with sending a match result.
  126. - Match ended doesn't necessarily kick players, so a dead/finished match will stick around on our end until
  127. everyone Disconnects, (or the game logic kicks them, e.g. MatchInfo->BEnded + a timeout)
  128. - Ended matches have queued a message to dissolve their lobby, though, so further GC interaction with the match is
  129. not possible, and players are allowed to leave (since they're now allowed to be put in a new match by the GC)
  130. == Queued Messages And Match State And Race Conditions
  131. - Since queued messages are sent in order until confirmed, the GC will always see (eventually) a coherent
  132. story. For instance:
  133. - PlayerLeftMatch - GC marks this player as leaving match
  134. - KickingLobby - GC marks match as finished, result pending
  135. - MatchResult (minus the two players who left) - GC finishes match accounting, marks match complete, missing
  136. players are already noted as leavers so their absence from the result is expected.
  137. - While messages are queued, we do not run the UpdateConnectedPlayers() think. This prevents having to worry about
  138. a fractal of potential edge cases -- we don't look at updated lobby data or send heartbeats while anything we're
  139. trying to tell the GC hasn't been confirmed. This also means we won't send a heartbeat until all such actions
  140. have been confirmed.
  141. - GC message handlers for queued messages do have to handle possible races -- if the GC sends us players while
  142. we're sending a "Reassign Player Team" message, this behavior means we'll stubbornly wait for a response to
  143. the team message before acknowledging any players, allowing the GC to easily resolve the race (in this case,
  144. by canceling or retrying any attempted add-player-match actions)
  145. == Gameserver Crashes
  146. - If GC is available, it handles it, otherwise, match is lost. Gameservers don't currently try to persist this
  147. state.
  148. == Match empties out
  149. - If the match is still going, it should reach ended as everyone in it gets timed out as an abandon.
  150. - If the GC is around, it will revoke the lobby once we inform it everyone has dropped.
  151. - Once the match is marked ended, and the GC concurs and deletes the lobby, we delete MatchInfo
  152. - If the GC is not around, we hang out on the completed match state until it is. We can't exactly take new
  153. matches in the mean time. (but, see k_InvalidState_Timeout_With_Match)
  154. \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\////////////////////////////////////////////////////////////
  155. ***********************************************************************************************************************/
  156. static const char g_pszIdleKickString[] = "#TF_Idle_kicked";
  157. //ConVar dota_force_upload_match_stats( "dota_force_upload_match_stats", "0", FCVAR_CHEAT, "If enabled, server will upload match stats even when there aren't human players on each side" );
  158. //extern ConVar dota_force_bot_cycle;
  159. extern CServerGameDLL g_ServerGameDLL;
  160. // How long a player can be missing from a MM match before they are dropped and given an abandon. Set to -1 to disable.
  161. ConVar tf_mm_player_disconnect_time_before_abandon( "tf_mm_player_disconnect_time_before_abandon", "180", FCVAR_DEVELOPMENTONLY );
  162. // How quickly we should forgive a match player's disconnected time after they return. At a ratio of 10, 30 minutes of
  163. // connected time would cancel out 3 minutes of disconnected type. Set to 0 to disable.
  164. ConVar tf_mm_player_disconnect_time_forgive_ratio( "tf_mm_player_disconnect_time_forgive_ratio", "10", FCVAR_DEVELOPMENTONLY );
  165. // Any disconnect, no matter for how long, should count as this many seconds of disconnected time. This is because the
  166. // act of reconnecting can be more disruptive than just the absense -- ready-up timers reset, the game may
  167. // pause/unpause, etc..
  168. //
  169. // Currently at 90 -- two rapid rejoins in a row, even with instant loading, will eat up your DC allowance. Note that
  170. // if you take at least 90s to rejoin/load anyway, this would have no effect.
  171. ConVar tf_mm_player_disconnect_time_minimum_penalty( "tf_mm_player_disconnect_time_minimum_penalty", "90", FCVAR_DEVELOPMENTONLY );
  172. ConVar tf_mm_next_map_result_hold_time( "tf_mm_next_map_result_hold_time", "7" );
  173. ConVar tf_mvm_allow_abandon_after_seconds( "tf_mvm_allow_abandon_after_seconds", "600", FCVAR_DEVELOPMENTONLY );
  174. ConVar tf_mvm_allow_abandon_below_players( "tf_mvm_allow_abandon_below_players", "5", FCVAR_DEVELOPMENTONLY );
  175. ConVar tf_allow_server_hibernation( "tf_allow_server_hibernation", "1", FCVAR_NONE, "Allow the server to hibernate when empty." );
  176. #ifdef STAGING_ONLY
  177. ConVar tf_debug_xp_changes( "tf_debug_xp_changes", "0" );
  178. #endif
  179. //DEFINE_LOGGING_CHANNEL_NO_TAGS( LOG_CONSOLE, "Console" );
  180. static CTFGCServerSystem s_TFGCServerSystem;
  181. CTFGCServerSystem *GTFGCClientSystem() { return &s_TFGCServerSystem; }
  182. //bool g_bServerReceivedGCWelcome = false;
  183. int g_gcServerVersion = 0; // Version from the GC
  184. static bool g_bWarnedAboutMaxplayersInMVM = false;
  185. extern ConVar tf_mm_servermode;
  186. extern ConVar tf_mm_trusted;
  187. extern ConVar tf_mm_strict;
  188. // Some reliable messages don't know the matchID yet when they are queued, but we should have it by time they send. This
  189. // helper takes their current match ID and returns the one they should use, for use in OnPrepare().
  190. //
  191. // Returns the current match's ID if:
  192. // - The msg's match ID is 0, and we now have a match ID
  193. //
  194. // Calls AbortInvalidMatchState if:
  195. // - The msg's match ID is not zero, and different from the current match
  196. // - Or they're both zero and we're in a match group that requires match IDs.
  197. //
  198. // NOTE We always wait for all pending messages before accepting new matches, so the above should hold unless something
  199. // got badly confused. Only matches with bServerCreated start without knowing their match ID, and should know it
  200. // by time any message that needs it gets sent. (a previous message in queue should be requesting it)
  201. static uint64 ReliableMsgCheckUpdateMatchID( uint64 nMsgMatchID )
  202. {
  203. uint64 nCurrentMatchID = GTFGCClientSystem()->GetMatch()->m_nMatchID;
  204. Assert( !nMsgMatchID || nMsgMatchID == nCurrentMatchID );
  205. // If we were queued for a match we didn't know the ID of yet, we can now glom it
  206. if ( nCurrentMatchID && nMsgMatchID == 0 )
  207. {
  208. return nCurrentMatchID;
  209. }
  210. else if ( nCurrentMatchID != nMsgMatchID )
  211. {
  212. // Something is bad
  213. GTFGCClientSystem()->AbortInvalidMatchState();
  214. }
  215. else if ( !nCurrentMatchID && !nMsgMatchID )
  216. {
  217. auto *pMatchDesc = GetMatchGroupDescription( GTFGCClientSystem()->GetMatch()->m_eMatchGroup );
  218. if ( !pMatchDesc || pMatchDesc->BRequiresMatchID() )
  219. {
  220. GTFGCClientSystem()->AbortInvalidMatchState();
  221. }
  222. }
  223. return nMsgMatchID;
  224. }
  225. //-----------------------------------------------------------------------------
  226. // Reliable messages
  227. //-----------------------------------------------------------------------------
  228. class ReliableMsgNewMatchForLobby
  229. : public CJobReliableMessageBase < ReliableMsgNewMatchForLobby,
  230. CMsgGCNewMatchForLobbyRequest, k_EMsgGC_NewMatchForLobbyRequest,
  231. CMsgGCNewMatchForLobbyResponse, k_EMsgGC_NewMatchForLobbyResponse >
  232. {
  233. public:
  234. void OnReply( Reply_t &msgReply )
  235. { GTFGCClientSystem()->NewMatchForLobbyResponse( msgReply.Body().success() ); }
  236. void OnPrepare()
  237. { Assert( Msg().Body().current_match_id() == GTFGCClientSystem()->GetMatch()->m_nMatchID ); }
  238. const char *MsgName() { return "NewMatchForLobby"; }
  239. void InitDebugString( CUtlString &dbgStr )
  240. {
  241. dbgStr.Format( "Match %llx, Lobby %llx, Next Map %d",
  242. Msg().Body().current_match_id(),
  243. Msg().Body().lobby_id(),
  244. Msg().Body().next_map_id() );
  245. }
  246. };
  247. //-----------------------------------------------------------------------------
  248. class ReliableMsgChangeMatchPlayerTeams
  249. : public CJobReliableMessageBase < ReliableMsgChangeMatchPlayerTeams,
  250. CMsgGCChangeMatchPlayerTeamsRequest, k_EMsgGC_ChangeMatchPlayerTeamsRequest,
  251. CMsgGCChangeMatchPlayerTeamsResponse, k_EMsgGC_ChangeMatchPlayerTeamsResponse >
  252. {
  253. public:
  254. void OnReply( Reply_t &msgReply )
  255. { GTFGCClientSystem()->ChangeMatchPlayerTeamsResponse( msgReply.Body().success() ); }
  256. // May have been queued for a pending match
  257. void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
  258. const char *MsgName() { return "ChangeMatchPlayerTeams"; }
  259. void InitDebugString( CUtlString &dbgStr )
  260. {
  261. dbgStr.Format( "Match %llx, Lobby %llx, %d members",
  262. Msg().Body().match_id(), Msg().Body().lobby_id(), Msg().Body().member_size() );
  263. }
  264. };
  265. //-----------------------------------------------------------------------------
  266. class ReliableMsgMvMVictory
  267. : public CJobReliableMessageBase < ReliableMsgMvMVictory,
  268. CMsgMvMVictory, k_EMsgGCMvMVictory,
  269. CMsgMvMMannUpVictoryReply, k_EMsgGCMvMVictoryReply >
  270. {
  271. public:
  272. const char *MsgName() { return "MvMVictory"; }
  273. void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Lobby %016llx", Msg().Body().lobby_id() ); }
  274. };
  275. //-----------------------------------------------------------------------------
  276. class ReliableMsgGameServerKickingLobby
  277. : public CJobReliableMessageBase < ReliableMsgGameServerKickingLobby,
  278. CMsgGameServerKickingLobby, k_EMsgGCGameServerKickingLobby,
  279. CMsgGameServerKickingLobbyResponse, k_EMsgGCGameServerKickingLobbyResponse >
  280. {
  281. public:
  282. // May have been queued for a pending match
  283. void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
  284. const char *MsgName() { return "GameServerKickingLobby"; }
  285. void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Match %llx, Lobby %llx",
  286. Msg().Body().match_id(), Msg().Body().lobby_id() ); }
  287. };
  288. //-----------------------------------------------------------------------------
  289. class ReliableMsgPlayerLeftMatch
  290. : public CJobReliableMessageBase < ReliableMsgPlayerLeftMatch,
  291. CMsgPlayerLeftMatch, k_EMsgGCPlayerLeftMatch,
  292. CMsgPlayerLeftMatchResponse, k_EMsgGCPlayerLeftMatchResponse >
  293. {
  294. public:
  295. // May have been queued for a pending match
  296. void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
  297. const char *MsgName() { return "PlayerLeftMatch"; }
  298. void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Player %s, Match %llx, Lobby %llx",
  299. CSteamID( Msg().Body().steam_id() ).Render(),
  300. Msg().Body().match_id(), Msg().Body().lobby_id() ); }
  301. };
  302. //-----------------------------------------------------------------------------
  303. // Sent for players who where votekicked after leaving the match
  304. // - That is, were being votekicked when they left, it later passed, to resolve the race-condition by posthumously
  305. // upgrading their penalty GC-side)
  306. class ReliableMsgPlayerVoteKickedAfterLeavingMatch
  307. : public CJobReliableMessageBase < ReliableMsgPlayerVoteKickedAfterLeavingMatch,
  308. CMsgPlayerVoteKickedAfterLeavingMatch, k_EMsgGCPlayerVoteKickedAfterLeavingMatch,
  309. CMsgPlayerVoteKickedAfterLeavingMatchResponse, k_EMsgGCPlayerVoteKickedAfterLeavingMatchResponse >
  310. {
  311. public:
  312. // May have been queued for a pending match
  313. void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
  314. const char *MsgName() { return "PlayerVoteKickedAfterLeavingMatch"; }
  315. void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Player %s, Match %llx, Lobby %llx",
  316. CSteamID( Msg().Body().steam_id() ).Render(),
  317. Msg().Body().match_id(), Msg().Body().lobby_id() ); }
  318. };
  319. //-----------------------------------------------------------------------------
  320. class ReliableMsgMatchResult
  321. : public CJobReliableMessageBase < ReliableMsgMatchResult,
  322. CMsgGC_Match_Result, k_EMsgGC_Match_Result,
  323. CMsgGC_Match_ResultResponse, k_EMsgGC_Match_ResultResponse >
  324. {
  325. public:
  326. // May have been queued for a pending match
  327. void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
  328. const char *MsgName() { return "MatchResult"; }
  329. void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Match %016llx", Msg().Body().match_id() ); }
  330. };
  331. //-----------------------------------------------------------------------------
  332. // CMvMVictoryInfo
  333. //-----------------------------------------------------------------------------
  334. void CMvMVictoryInfo::Init ( CTFGSLobby *pLobby )
  335. {
  336. if ( !pLobby )
  337. {
  338. MMLog( "CTFGCServerSystem::MvMVictory() -- no lobby, so not sending results to GC\n" );
  339. return;
  340. }
  341. m_nLobbyId = pLobby->GetGroupID();
  342. m_sChallengeName = pLobby->GetMissionName();
  343. #ifdef USE_MVM_TOUR
  344. if ( IsMannUpGroup( pLobby->GetMatchGroup() ) )
  345. {
  346. const char *pszTourName = pLobby->GetMannUpTourName();
  347. Assert( pszTourName );
  348. m_sMannUpTourOfDuty = pszTourName;
  349. }
  350. #endif // USE_MVM_TOUR
  351. m_tEventTime = CRTime::RTime32TimeCur();
  352. m_vPlayerIds.RemoveAll();
  353. m_vSquadSurplus.RemoveAll();
  354. for ( int iMember = 0; iMember < pLobby->GetNumMembers(); iMember++ )
  355. {
  356. m_vPlayerIds.AddToTail( pLobby->GetMember( iMember ).ConvertToUint64() );
  357. m_vSquadSurplus.AddToTail( pLobby->GetMemberDetails( iMember )->squad_surplus() );
  358. }
  359. }
  360. //-----------------------------------------------------------------------------
  361. // CCompetitiveMatchInfo
  362. //-----------------------------------------------------------------------------
  363. CMatchInfo::CMatchInfo( const CTFGSLobby *pLobby )
  364. : m_nMatchID( pLobby->GetMatchID() )
  365. , m_nLobbyID( pLobby->GetGroupID() )
  366. , m_eMatchGroup( pLobby->GetMatchGroup() )
  367. , m_uLobbyFlags( pLobby->GetFlags() )
  368. , m_uAverageRank( pLobby->Obj().average_rank() )
  369. , m_rtMatchCreated( CRTime::RTime32TimeCur() )
  370. , m_unEventTeamStatus( pLobby->Obj().is_war_match() )
  371. , m_bFirstPersonActive( false )
  372. , m_nBotsAdded( 0 )
  373. , m_bServerCreated( false )
  374. , m_strMapName( pLobby->GetMapName() )
  375. , m_bMatchEnded( false )
  376. , m_bSentResult( false )
  377. , m_nGCMatchSize( pLobby->Obj().has_fixed_match_size() ? pLobby->Obj().fixed_match_size() : 0 )
  378. #ifdef STAGING_ONLY
  379. , m_flBronzePercentile( 0.5f )
  380. , m_flSilverPercentile( 0.65f )
  381. , m_flGoldPercentile( 0.8f )
  382. #else
  383. , m_flBronzePercentile( 0.6f )
  384. , m_flSilverPercentile( 0.75f )
  385. , m_flGoldPercentile( 0.9f )
  386. #endif
  387. {
  388. uint32 nNumCompLevels = GetMatchGroupDescription( k_nMatchGroup_Casual_6v6 )->m_pProgressionDesc->GetNumLevels();
  389. m_vDailyStatsRankData.EnsureCapacity( nNumCompLevels );
  390. RequestGCRankData();
  391. }
  392. CMatchInfo::~CMatchInfo()
  393. {
  394. m_vMatchRankData.PurgeAndDeleteElements();
  395. }
  396. //-----------------------------------------------------------------------------
  397. //
  398. //-----------------------------------------------------------------------------
  399. CMatchInfo::CMatchInfo()
  400. {
  401. // Don't do this
  402. Assert( 0 );
  403. }
  404. //-----------------------------------------------------------------------------
  405. //
  406. //-----------------------------------------------------------------------------
  407. CMatchInfo::CMatchInfo( const CMatchInfo &otherinfo )
  408. {
  409. // Don't do this
  410. Assert( 0 );
  411. }
  412. //-----------------------------------------------------------------------------
  413. //
  414. //-----------------------------------------------------------------------------
  415. CMatchInfo::PlayerMatchData_t::PlayerMatchData_t( const PlayerMatchData_t& rhs )
  416. : m_mapXPAccumulation( DefLessFunc( CMsgTFXPSource::XPSourceType ) )
  417. {
  418. steamID = rhs.steamID;
  419. uPartyID = rhs.uPartyID;
  420. eGCTeam = rhs.eGCTeam;
  421. bDropped = rhs.bDropped;
  422. bConnected = rhs.bConnected;
  423. rtJoinedMatch = CRTime::RTime32TimeCur();
  424. nVoteKickAttempts = rhs.nVoteKickAttempts;
  425. nDisconnectedSeconds = 0;
  426. nScoreMedal = rhs.nScoreMedal;
  427. nKillsMedal = rhs.nKillsMedal;
  428. nDamageMedal = rhs.nDamageMedal;
  429. nHealingMedal = rhs.nHealingMedal;
  430. nSupportMedal = rhs.nSupportMedal;
  431. bLateJoin = rhs.bLateJoin;
  432. nScore = rhs.nScore;
  433. rtLastActiveEvent = CRTime::RTime32TimeCur();
  434. bAlwaysSafeToLeave = rhs.bAlwaysSafeToLeave;
  435. bEverConnected = rhs.bEverConnected;
  436. bDropWasAbandon = rhs.bDropWasAbandon;
  437. eDropReason = rhs.eDropReason;
  438. nConnectingButNotActiveIndex = rhs.nConnectingButNotActiveIndex;
  439. bPlayed = false;
  440. unMMSkillRating = rhs.unMMSkillRating;
  441. nDrilloRatingDelta = 0;
  442. unClassesPlayed = 0u;
  443. }
  444. //-----------------------------------------------------------------------------
  445. //
  446. //-----------------------------------------------------------------------------
  447. MM_PlayerConnectionState_t CMatchInfo::PlayerMatchData_t::GetConnectionState() const
  448. {
  449. if ( bConnected )
  450. {
  451. return nConnectingButNotActiveIndex == 0 ? MM_CONNECTED : MM_LOADING;
  452. }
  453. else
  454. {
  455. return bEverConnected ? MM_DISCONNECTED : MM_CONNECTING;
  456. }
  457. }
  458. //-----------------------------------------------------------------------------
  459. //
  460. //-----------------------------------------------------------------------------
  461. void CMatchInfo::PlayerMatchData_t::UpdateClassesPlayed( int nClass )
  462. {
  463. Assert( nClass >= TF_FIRST_NORMAL_CLASS && nClass <= TF_LAST_NORMAL_CLASS );
  464. unClassesPlayed = unClassesPlayed | ( 1 << nClass );
  465. }
  466. //-----------------------------------------------------------------------------
  467. //
  468. //-----------------------------------------------------------------------------
  469. void CMatchInfo::PlayerMatchData_t::OnConnected( int nEntindex )
  470. {
  471. if ( bConnected )
  472. {
  473. // This is before steamID validation, so make sure we don't add a path that would reward spoof connections.
  474. Assert( !"Player connecting is marked connected" );
  475. return;
  476. }
  477. nConnectingButNotActiveIndex = nEntindex;
  478. // Mark connected.
  479. bConnected = true;
  480. bEverConnected = true;
  481. RTime32 now = CRTime::RTime32TimeCur();
  482. MMLog( "Match player %s reconnected into slot %d, last active %u seconds ago.\n",
  483. steamID.Render(), nConnectingButNotActiveIndex, now - rtLastActiveEvent );
  484. }
  485. //-----------------------------------------------------------------------------
  486. //
  487. //-----------------------------------------------------------------------------
  488. void CMatchInfo::PlayerMatchData_t::OnActive()
  489. {
  490. nConnectingButNotActiveIndex = 0;
  491. CMatchInfo* pMatch = GTFGCClientSystem()->GetMatch();
  492. Assert( pMatch);
  493. if ( pMatch && !pMatch->m_bFirstPersonActive )
  494. {
  495. MMLog( "Match going active\n" );
  496. pMatch->m_bFirstPersonActive = true;
  497. }
  498. // Disconnected seconds for the time since they were last active, including DC'd time and time spent loading. This
  499. // prevents people who crash but rejoin quickly being able to be not-in-game for far longer than intended. Since we
  500. // already marked them connected, the abandon think won't touch them if this accumulation goes over the limit, but
  501. // it will count against them if they drop again.
  502. RTime32 now = CRTime::RTime32TimeCur();
  503. RTime32 missing = now - rtLastActiveEvent;
  504. // See this convar's comment for why we do this.
  505. RTime32 minimum = (RTime32)Clamp( tf_mm_player_disconnect_time_minimum_penalty.GetInt(), 0, INT_MAX );
  506. nDisconnectedSeconds += Max( missing, minimum );
  507. rtLastActiveEvent = now;
  508. }
  509. //-----------------------------------------------------------------------------
  510. // Add a rank bucket stats vector
  511. //-----------------------------------------------------------------------------
  512. void CMatchInfo::SetDailyRankData( DailyStatsRankBucket_t vecRankData )
  513. {
  514. m_vDailyStatsRankData.AddToTail( vecRankData );
  515. }
  516. //-----------------------------------------------------------------------------
  517. // Request the competitive daily stats rollup from the GC
  518. //-----------------------------------------------------------------------------
  519. bool CMatchInfo::RequestGCRankData( void )
  520. {
  521. if ( !GetMatchGroupDescription( m_eMatchGroup ) ||
  522. !GetMatchGroupDescription( m_eMatchGroup )->m_params.m_bDistributePerformanceMedals )
  523. {
  524. return false;
  525. }
  526. GCSDK::CProtoBufMsg< CMsgGC_DailyCompetitiveStatsRollup > msg( k_EMsgGC_DailyCompetitiveStatsRollup );
  527. return GCClientSystem()->BSendMessage( msg );
  528. }
  529. //-----------------------------------------------------------------------------
  530. void CMatchInfo::AddPlayer( const PlayerMatchData_t &player, int nEntIndex, bool bActive )
  531. {
  532. PlayerMatchData_t* pOldPlayerMatchData = GetMatchDataForPlayer( player.steamID );
  533. if ( pOldPlayerMatchData )
  534. {
  535. // Already have data?
  536. if ( pOldPlayerMatchData->bDropped )
  537. {
  538. // Returning a player that had dropped from the match. Re-create their entry as a fresh player, so the
  539. // constructor re-does everything.
  540. MMLog( "Player %s re-added to match they previously dropped from, replacing existing entry\n",
  541. player.steamID.Render() );
  542. m_vMatchRankData.FindAndRemove( pOldPlayerMatchData );
  543. delete pOldPlayerMatchData;
  544. pOldPlayerMatchData = nullptr;
  545. }
  546. else
  547. {
  548. // This player is already in the match
  549. Assert( false );
  550. MMLog( "!! Player %s being added to the match, but they are already present\n",
  551. player.steamID.Render() );
  552. return;
  553. }
  554. }
  555. PlayerMatchData_t* pPlayerMatchData = new PlayerMatchData_t( player );
  556. m_vMatchRankData.AddToTail( pPlayerMatchData );
  557. if ( nEntIndex != 0 )
  558. {
  559. pPlayerMatchData->OnConnected( nEntIndex );
  560. }
  561. if ( bActive )
  562. {
  563. pPlayerMatchData->OnActive();
  564. }
  565. }
  566. //-----------------------------------------------------------------------------
  567. void CMatchInfo::AddPlayer( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntIndex, bool bActive )
  568. {
  569. PlayerMatchData_t playerMatchData( steamID, pMemberData );
  570. playerMatchData.unMMSkillRating = pMemberData->skillrating();
  571. playerMatchData.bLateJoin = bIsLateJoin;
  572. AddPlayer( playerMatchData, nEntIndex, bActive );
  573. }
  574. //-----------------------------------------------------------------------------
  575. void CMatchInfo::DropPlayer( CSteamID steamID, TFMatchLeaveReason eReason, bool bWasAbandon )
  576. {
  577. CMatchInfo::PlayerMatchData_t *pPlayerMatchData = GetMatchDataForPlayer( steamID );
  578. AssertMsg( pPlayerMatchData, "If we have competitive match info, this player should be known" );
  579. if ( pPlayerMatchData )
  580. {
  581. if ( pPlayerMatchData->bDropped )
  582. {
  583. MMLog( "!! Double-dropping player %s\n", steamID.Render() );
  584. Assert( false );
  585. }
  586. pPlayerMatchData->bDropped = true;
  587. pPlayerMatchData->eDropReason = eReason;
  588. pPlayerMatchData->bDropWasAbandon = bWasAbandon;
  589. }
  590. }
  591. //-----------------------------------------------------------------------------
  592. const CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( CSteamID steamID ) const
  593. {
  594. return const_cast<CMatchInfo*>(this)->GetMatchDataForPlayer( steamID );
  595. }
  596. //-----------------------------------------------------------------------------
  597. CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( CSteamID steamID )
  598. {
  599. FOR_EACH_VEC( m_vMatchRankData, i )
  600. {
  601. if ( m_vMatchRankData[i]->steamID == steamID )
  602. return ( m_vMatchRankData[i] );
  603. }
  604. return NULL;
  605. }
  606. //-----------------------------------------------------------------------------
  607. CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( int idx )
  608. {
  609. return m_vMatchRankData[idx];
  610. }
  611. //-----------------------------------------------------------------------------
  612. int CMatchInfo::GetNumTotalMatchPlayers() const
  613. {
  614. return m_vMatchRankData.Count();
  615. }
  616. //-----------------------------------------------------------------------------
  617. int CMatchInfo::GetNumActiveMatchPlayers() const
  618. {
  619. int nActivePlayers = 0;
  620. FOR_EACH_VEC( m_vMatchRankData, idx )
  621. {
  622. nActivePlayers += !m_vMatchRankData[idx]->bDropped;
  623. }
  624. return nActivePlayers;
  625. }
  626. //-----------------------------------------------------------------------------
  627. int CMatchInfo::GetNumActiveMatchPlayersForTeam( int nTeam ) const
  628. {
  629. int nActivePlayers = 0;
  630. FOR_EACH_VEC( m_vMatchRankData, idx )
  631. {
  632. if ( !m_vMatchRankData[idx]->bDropped )
  633. {
  634. if ( m_vMatchRankData[idx]->eGCTeam == nTeam )
  635. {
  636. nActivePlayers++;
  637. }
  638. }
  639. }
  640. return nActivePlayers;
  641. }
  642. //-----------------------------------------------------------------------------
  643. int CMatchInfo::GetTotalSkillRatingForTeam( int nTeam ) const
  644. {
  645. // Re-evaluate this when skillrating might be for other backends
  646. FixmeMMRatingBackendSwapping();
  647. int nSkillRating = 0;
  648. FOR_EACH_VEC( m_vMatchRankData, idx )
  649. {
  650. if ( !m_vMatchRankData[idx]->bDropped )
  651. {
  652. if ( m_vMatchRankData[idx]->eGCTeam == nTeam )
  653. {
  654. nSkillRating += m_vMatchRankData[idx]->unMMSkillRating;
  655. }
  656. }
  657. }
  658. return nSkillRating;
  659. }
  660. //-----------------------------------------------------------------------------
  661. int CMatchInfo::GetNumConnectedMatchPlayers() const
  662. {
  663. int nConnectedPlayers = 0;
  664. FOR_EACH_VEC( m_vMatchRankData, idx )
  665. {
  666. nConnectedPlayers += ( m_vMatchRankData[idx]->bConnected && !m_vMatchRankData[idx]->bDropped );
  667. }
  668. return nConnectedPlayers;
  669. }
  670. //-----------------------------------------------------------------------------
  671. uint32 CMatchInfo::GetCanonicalMatchSize() const
  672. {
  673. return m_nGCMatchSize ? m_nGCMatchSize : GetMatchGroupDescription( m_eMatchGroup )->GetMatchSize();
  674. }
  675. //-----------------------------------------------------------------------------
  676. void CMatchInfo::GiveXPRewardToPlayerForAction( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nCount )
  677. {
  678. // Needs to be a positive number!
  679. if ( nCount <= 0 )
  680. return;
  681. GiveXPDirectly( steamID, eType, ceil( (float)nCount * g_XPSourceDefs[ eType ].m_flValueMultiplier ), true );
  682. }
  683. //-----------------------------------------------------------------------------
  684. void CMatchInfo::GiveXPDirectly( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nAmount, bool bCanAwardBonusXP )
  685. {
  686. const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_eMatchGroup );
  687. if ( !pMatchDesc || !pMatchDesc->BUsesXP() || nAmount <= 0 )
  688. {
  689. return;
  690. }
  691. PlayerMatchData_t *pMatchPlayer = GetMatchDataForPlayer( steamID );
  692. if ( pMatchPlayer && !pMatchPlayer->bDropped )
  693. {
  694. CMsgTFXPSource* pSource = NULL;
  695. auto idx = pMatchPlayer->m_mapXPAccumulation.Find( eType );
  696. if ( idx == pMatchPlayer->m_mapXPAccumulation.InvalidIndex() )
  697. {
  698. idx = pMatchPlayer->m_mapXPAccumulation.Insert( eType, 0.f );
  699. }
  700. // You can only draw from the bonus pool if you GAINED xp
  701. if ( nAmount > 0 && bCanAwardBonusXP )
  702. {
  703. FOR_EACH_VEC_BACK( pMatchPlayer->m_vecXPBonusPools, i )
  704. {
  705. PlayerMatchData_t::XPBonusPool_t& xpMultiplier = pMatchPlayer->m_vecXPBonusPools[ i ];
  706. // We do this so when specifying the multiplier, you can say you want the multiplier to be
  707. int nBonusAmount = ceil( nAmount * xpMultiplier.m_flMultiplier );
  708. // If there's a maximum amount to give for this bonus, subtract from the total
  709. // and remove this bonus if the pool is emptied
  710. Assert( xpMultiplier.m_nBonusPoolRemaining > 0 );
  711. nBonusAmount = Min( nBonusAmount, xpMultiplier.m_nBonusPoolRemaining );
  712. xpMultiplier.m_nBonusPoolRemaining -= nBonusAmount;
  713. // Save the type so we can recursively pass it below
  714. CMsgTFXPSource::XPSourceType eBonusType = xpMultiplier.m_eType;
  715. // If there's no more in the pool, then we can remove this from the list
  716. if ( xpMultiplier.m_nBonusPoolRemaining <= 0 )
  717. {
  718. // We're going backwards, so this is ok
  719. pMatchPlayer->m_vecXPBonusPools.Remove( i );
  720. }
  721. // Give the bonus
  722. GiveXPDirectly( steamID, eBonusType, nBonusAmount, false );
  723. }
  724. }
  725. // Accumulate in the map
  726. pMatchPlayer->m_mapXPAccumulation[ idx ] += nAmount;
  727. int nAccum = pMatchPlayer->m_mapXPAccumulation[ idx ];
  728. // Don't make a XPSource proto object if there's nothing to even report
  729. if ( nAccum == 0 )
  730. return;
  731. // Find the type if it exists.
  732. for( int i=0; i < pMatchPlayer->m_XPBreakdown.sources_size(); ++i )
  733. {
  734. if ( pMatchPlayer->m_XPBreakdown.sources( i ).type() == eType )
  735. {
  736. pSource = pMatchPlayer->m_XPBreakdown.mutable_sources( i );
  737. break;
  738. }
  739. }
  740. // Create a new one if we need to
  741. if ( pSource == NULL )
  742. {
  743. pSource = pMatchPlayer->m_XPBreakdown.add_sources();
  744. pSource->set_account_id( steamID.GetAccountID() );
  745. pSource->set_match_group( m_eMatchGroup );
  746. pSource->set_type( eType );
  747. pSource->set_match_id( m_nMatchID );
  748. pSource->set_amount( 0 );
  749. }
  750. #ifdef STAGING_ONLY
  751. if ( tf_debug_xp_changes.GetBool() && nAccum != pSource->amount() )
  752. {
  753. CBasePlayer* pPlayer = UTIL_PlayerBySteamID( steamID );
  754. if ( pPlayer )
  755. {
  756. Msg( "%s received %d %s xp\n", pPlayer->GetPlayerName(),
  757. nAccum - pSource->amount(),
  758. CMsgTFXPSource_XPSourceType_descriptor()->value( eType )->name().c_str() );
  759. }
  760. }
  761. #endif
  762. // Update the amount
  763. pSource->set_amount( nAccum );
  764. }
  765. }
  766. //-----------------------------------------------------------------------------
  767. void CMatchInfo::GiveXPBonus( CSteamID steamID,
  768. CMsgTFXPSource_XPSourceType eType,
  769. float flMultipler,
  770. int nBonusPool )
  771. {
  772. const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_eMatchGroup );
  773. if ( !pMatchDesc || !pMatchDesc->BUsesXP() )
  774. {
  775. return;
  776. }
  777. PlayerMatchData_t *pMatchPlayer = GetMatchDataForPlayer( steamID );
  778. if ( pMatchPlayer && !pMatchPlayer->bDropped )
  779. {
  780. // Find existing entry if there is one
  781. auto idx = pMatchPlayer->m_vecXPBonusPools.InvalidIndex();
  782. FOR_EACH_VEC( pMatchPlayer->m_vecXPBonusPools, i )
  783. {
  784. // Found it
  785. if( pMatchPlayer->m_vecXPBonusPools[ i ].m_eType == eType )
  786. {
  787. idx = i;
  788. break;
  789. }
  790. }
  791. // Create new entry if we didnt have an existing one
  792. if ( idx == pMatchPlayer->m_vecXPBonusPools.InvalidIndex() )
  793. {
  794. idx = pMatchPlayer->m_vecXPBonusPools.AddToTail();
  795. }
  796. // Add bonus
  797. PlayerMatchData_t::XPBonusPool_t& currentXPMultiplier = pMatchPlayer->m_vecXPBonusPools[ idx ];
  798. currentXPMultiplier.m_nBonusPoolRemaining += nBonusPool;
  799. currentXPMultiplier.m_eType = eType;
  800. currentXPMultiplier.m_flMultiplier = Max( currentXPMultiplier.m_flMultiplier, flMultipler );
  801. }
  802. }
  803. #ifdef STAGING_ONLY
  804. CON_COMMAND( give_xp_bonus, "Gives the player with the specified name an xp boost. Usage: give_xp_bonus <name> <type> <multiplier> <bonus_pool>" )
  805. {
  806. if ( args.ArgC() != 5 )
  807. {
  808. Msg( "Incorrect arguments. Usage: give_xp_bonus <name> <type> <multiplier> <bonus_pool>\n" );
  809. return;
  810. }
  811. CBasePlayer* pPlayer = UTIL_PlayerByName( args.Arg( 1 ) );
  812. if ( !pPlayer )
  813. {
  814. Msg( "No player named %s\n", args.Arg( 1 ) );
  815. return;
  816. }
  817. if ( !GTFGCClientSystem()->GetMatch() )
  818. {
  819. Msg( "Not running a match\n" );
  820. return;
  821. }
  822. CMsgTFXPSource_XPSourceType nType = (CMsgTFXPSource_XPSourceType)atoi( args.Arg( 2 ) );
  823. if ( nType < CMsgTFXPSource_XPSourceType_XPSourceType_MIN
  824. || nType >= CMsgTFXPSource_XPSourceType_NUM_SOURCE_TYPES )
  825. {
  826. Msg( "Type is not a valid type!\n" );
  827. return;
  828. }
  829. CSteamID steamID;
  830. pPlayer->GetSteamID( &steamID );
  831. GTFGCClientSystem()->GetMatch()->GiveXPBonus( steamID,
  832. nType,
  833. atof( args.Arg( 3 ) ),
  834. atoi( args.Arg( 4 ) ) );
  835. }
  836. #endif
  837. //-----------------------------------------------------------------------------
  838. bool CMatchInfo::BPlayerSafeToLeaveMatch( CSteamID steamID )
  839. {
  840. PlayerMatchData_t *pMatchPlayer = this->GetMatchDataForPlayer( steamID );
  841. // Right now, you cannot leave while the match is running
  842. bool bSafe = m_bMatchEnded || !pMatchPlayer || pMatchPlayer->bDropped || pMatchPlayer->bAlwaysSafeToLeave;
  843. // The match description might have special exceptions
  844. if ( !bSafe && pMatchPlayer )
  845. {
  846. bSafe = bSafe || GetMatchGroupDescription( m_eMatchGroup )->BMatchIsSafeToLeaveForPlayer( this, pMatchPlayer );
  847. }
  848. return bSafe;
  849. }
  850. //-----------------------------------------------------------------------------
  851. // Determine the performance ranking of each player after a competitive match
  852. //-----------------------------------------------------------------------------
  853. bool CMatchInfo::CalculatePlayerMatchRankData( void )
  854. {
  855. Assert( TFGameRules() );
  856. if ( !TFGameRules() )
  857. return false;
  858. CTFPlayerResource *pTFResource = dynamic_cast< CTFPlayerResource* >( g_pPlayerResource );
  859. if ( !pTFResource )
  860. return false;
  861. CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
  862. if ( !pMatch )
  863. return false;
  864. if ( !m_vDailyStatsRankData.Count() )
  865. {
  866. Warning( "CalculatePlayerMatchRankData(): DailyStatsRankData is empty\n" );
  867. return false;
  868. }
  869. const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( pMatch->m_eMatchGroup );
  870. if ( !pMatchDesc ||
  871. !pMatchDesc->m_pProgressionDesc ||
  872. !pMatchDesc->m_params.m_bDistributePerformanceMedals )
  873. {
  874. return false;
  875. }
  876. CUtlVector < CTFPlayer* > vecPlayers;
  877. CollectHumanPlayers( &vecPlayers );
  878. FOR_EACH_VEC( vecPlayers, i )
  879. {
  880. if ( !vecPlayers[i] )
  881. continue;
  882. CSteamID steamID;
  883. if ( !vecPlayers[i]->GetSteamID( &steamID ) )
  884. continue;
  885. PlayerStats_t *pStats = CTF_GameStats.FindPlayerStats( vecPlayers[i] );
  886. CMatchInfo::PlayerMatchData_t *matchData = GetMatchDataForPlayer( steamID );
  887. if ( !matchData || !pStats )
  888. {
  889. Warning( "Missing player data in CalculatePlayerMatchRankData\n" );
  890. Assert( false );
  891. continue;
  892. }
  893. // Get player's competitive rank
  894. FixmeMMRatingBackendSwapping(); // This is assuming we're using primary skill rating for rank
  895. uint32 unRank = pMatchDesc->m_pProgressionDesc->GetLevelForExperience( matchData->unMMSkillRating ).m_nLevelNum;
  896. int nRankIndex = -1;
  897. // Let's find the typical stats for your rank
  898. FOR_EACH_VEC( m_vDailyStatsRankData, j )
  899. {
  900. if ( unRank == m_vDailyStatsRankData[j].nRank )
  901. {
  902. #ifndef STAGING_ONLY
  903. if ( m_vDailyStatsRankData[j].nRecords < 10 )
  904. {
  905. Warning( "CalculatePlayerMatchRankData(): Too few stat entries (%d) for rank %d\n", m_vDailyStatsRankData[j].nRecords, unRank );
  906. return false;
  907. }
  908. #endif // !STAGING_ONLY
  909. nRankIndex = j;
  910. break;
  911. }
  912. }
  913. uint32 unScoreMedal = GetRankForStat( RankStat_Score, nRankIndex, pTFResource->GetTotalScore( vecPlayers[i]->entindex() ) );
  914. uint32 unKillsMedal = GetRankForStat( RankStat_Kills, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_KILLS] );
  915. uint32 unDamageMedal = GetRankForStat( RankStat_Damage, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_DAMAGE] );
  916. uint32 unHealingMedal = GetRankForStat( RankStat_Healing, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_HEALING] );
  917. uint32 unSupportMedal = GetRankForStat( RankStat_Support, nRankIndex, TFGameRules()->CalcPlayerSupportScore( &pStats->statsAccumulated, vecPlayers[i]->entindex() ) );
  918. matchData->nScoreMedal = unScoreMedal;
  919. matchData->nKillsMedal = unKillsMedal;
  920. matchData->nDamageMedal = unDamageMedal;
  921. matchData->nHealingMedal = unHealingMedal;
  922. matchData->nSupportMedal = unSupportMedal;
  923. }
  924. return true;
  925. }
  926. //-----------------------------------------------------------------------------
  927. //
  928. //-----------------------------------------------------------------------------
  929. bool CMatchInfo::CalculateMatchSkillRatingAdjustments( int iWinningTeam )
  930. {
  931. // This is assuming skill rating is drillo,and doing a client-side prediction on it
  932. FixmeMMRatingBackendSwapping();
  933. if ( !iWinningTeam )
  934. {
  935. Log( "CalculateMatchSkillRatingAdjustments(): Invalid team!\n" );
  936. return false;
  937. }
  938. EMatchGroup matchGroup = m_eMatchGroup;
  939. if ( !IsLadderGroup( matchGroup ) )
  940. {
  941. Assert( false );
  942. Log( "CalculateMatchSkillRatingAdjustments(): Match %llu has an invalid MatchGroup (%i)\n", m_nMatchID, (int)matchGroup );
  943. return false;
  944. }
  945. const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( matchGroup );
  946. if ( !pMatchDesc || !pMatchDesc->m_pProgressionDesc )
  947. {
  948. Log( "CalculateMatchSkillRatingAdjustments(): Match has bogus MatchGroupDescription\n" );
  949. return false;
  950. }
  951. CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
  952. if ( !pMatch )
  953. {
  954. Log( "CalculateMatchSkillRatingAdjustments(): Match has bogus CMatchInfo\n" );
  955. return false;
  956. }
  957. int nWinnerTotal = 0;
  958. int nLoserTotal = 0;
  959. uint32 unWinningPlayers = 0u;
  960. uint32 unLosingPlayers = 0u;
  961. // Gather data so we can figure out rating adjustments
  962. for ( int i = 0; i < GetNumTotalMatchPlayers(); i++ )
  963. {
  964. CMatchInfo::PlayerMatchData_t *pPlayerInfo = GetMatchDataForPlayer( i );
  965. Assert( pPlayerInfo );
  966. if ( !pPlayerInfo || pPlayerInfo->bDropped )
  967. continue;
  968. if ( TFGameRules()->GetGameTeamForGCTeam( pPlayerInfo->eGCTeam ) == iWinningTeam )
  969. {
  970. nWinnerTotal += pPlayerInfo->unMMSkillRating;
  971. ++unWinningPlayers;
  972. }
  973. else
  974. {
  975. nLoserTotal += pPlayerInfo->unMMSkillRating;
  976. ++unLosingPlayers;
  977. }
  978. }
  979. if ( pMatchDesc->m_params.m_bRequireCompleteMatch && ( unWinningPlayers + unLosingPlayers != GetCanonicalMatchSize() ) )
  980. {
  981. Assert( false );
  982. Log( "CalculateMatchSkillRatingAdjustments(): Match %llu has invalid team size(s): %d vs %d\n",
  983. m_nMatchID, unWinningPlayers, unLosingPlayers );
  984. }
  985. int nTeamSize = ( pMatch->GetCanonicalMatchSize() % 2 ) ? ( pMatch->GetCanonicalMatchSize() / 2 + 1 ) : ( pMatch->GetCanonicalMatchSize() / 2 );
  986. int nWinningTeamAverage = (float)nWinnerTotal / Max( nTeamSize, 1 );
  987. int nLosingTeamAverage = (float)nLoserTotal / Max( nTeamSize, 1 );
  988. int nRatingDiff = nLosingTeamAverage - nWinningTeamAverage;
  989. // Determine adjustment based on difference between teams
  990. const int nChange = RemapValClamped( nRatingDiff, /* from */ -(float)k_unDrilloRating_MaxDifference, (float)k_unDrilloRating_MaxDifference,
  991. /* to */ (float)k_nDrilloRating_MinRatingAdjust, (float)k_nDrilloRating_Ladder_MaxRatingAdjust );
  992. // Cap loss for low-rated teams, but not low-rated winners. This breaks the loose "sort-of-zero-sum" system we have, but that's ok in the lower range.
  993. const int nLoserChange = ( nLosingTeamAverage <= k_unDrilloRating_Ladder_LowSkill ) ? Min( nChange, k_nDrilloRating_Ladder_MaxLossAdjust_LowRank ) : nChange;
  994. // Rating delta update
  995. for ( int i = 0; i < GetNumTotalMatchPlayers(); i++ )
  996. {
  997. CMatchInfo::PlayerMatchData_t *pPlayerInfo = GetMatchDataForPlayer( i );
  998. Assert( pPlayerInfo );
  999. if ( !pPlayerInfo )
  1000. continue;
  1001. int nAmount = nChange;
  1002. if ( pPlayerInfo->BDropWasAbandon() )
  1003. {
  1004. // Abandon
  1005. nAmount = -k_nDrilloRating_Ladder_MaxRatingAdjust;
  1006. if ( m_eMatchGroup == k_nMatchGroup_Ladder_6v6 )
  1007. {
  1008. GiveXPDirectly( pPlayerInfo->steamID, CMsgTFXPSource_XPSourceType::CMsgTFXPSource_XPSourceType_SOURCE_COMPETITIVE_ABANDON, nAmount );
  1009. }
  1010. }
  1011. else if ( TFGameRules()->GetGameTeamForGCTeam( pPlayerInfo->eGCTeam ) != iWinningTeam )
  1012. {
  1013. // Loss
  1014. nAmount = -nLoserChange;
  1015. }
  1016. pPlayerInfo->nDrilloRatingDelta = nAmount;
  1017. // Scoreboard
  1018. IGameEvent *pEvent = gameeventmanager->CreateEvent( "competitive_stats_update" );
  1019. if ( pEvent )
  1020. {
  1021. CBasePlayer *pPlayer = UTIL_PlayerBySteamID( pPlayerInfo->steamID );
  1022. if ( !pPlayer )
  1023. continue;
  1024. pEvent->SetInt( "index", pPlayer->entindex() );
  1025. pEvent->SetInt( "rating", pPlayerInfo->unMMSkillRating );
  1026. // This is the only place this guy is used. We should eventually have the GC send down results and use that
  1027. // instead of running this prediction step here.
  1028. pEvent->SetInt( "delta", pPlayerInfo->nDrilloRatingDelta );
  1029. CMatchInfo::PlayerMatchData_t *pMatchRankData = GetMatchDataForPlayer( pPlayerInfo->steamID );
  1030. pEvent->SetInt( "score_rank", pMatchRankData ? pMatchRankData->nScoreMedal : 0 ); // medal won (if any)
  1031. pEvent->SetInt( "kills_rank", pMatchRankData ? pMatchRankData->nKillsMedal : 0 ); //
  1032. pEvent->SetInt( "damage_rank", pMatchRankData ? pMatchRankData->nDamageMedal : 0 ); //
  1033. pEvent->SetInt( "healing_rank", pMatchRankData ? pMatchRankData->nHealingMedal : 0 ); //
  1034. pEvent->SetInt( "support_rank", pMatchRankData ? pMatchRankData->nSupportMedal : 0 ); //
  1035. gameeventmanager->FireEvent( pEvent );
  1036. }
  1037. }
  1038. return true;
  1039. }
  1040. //-----------------------------------------------------------------------------
  1041. // Returns the medal rank (if any) for this stat
  1042. //-----------------------------------------------------------------------------
  1043. int CMatchInfo::GetRankForStat( RankStatType_t statType, int nRankIndex, uint32 nValue )
  1044. {
  1045. if ( !m_vDailyStatsRankData.IsValidIndex( nRankIndex ) )
  1046. return StatMedal_None;
  1047. // Get match duration, so we can scale values accordingly (total time won't have last round time included yet)
  1048. uint16 nMatchDuration = CTF_GameStats.m_currentMap.m_Header.m_iTotalTime + ( gpGlobals->curtime - TFGameRules()->GetRoundStart() );
  1049. // Assume 9 minute average match duration; TO DO: Use actual values generated from matchresults table
  1050. uint16 nAverageMatchDuration = 9 * 60;
  1051. // Adjusted Value
  1052. float flStatAdjustment = ( float ) nAverageMatchDuration / ( float ) nMatchDuration;
  1053. flStatAdjustment = clamp( flStatAdjustment, 0.33f, 3.0f );
  1054. nValue = nValue * flStatAdjustment;
  1055. uint32 unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgScore;
  1056. uint32 unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevScore;
  1057. switch ( statType )
  1058. {
  1059. case RankStat_Score:
  1060. break;
  1061. case RankStat_Kills:
  1062. unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgKills;
  1063. unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevKills;
  1064. break;
  1065. case RankStat_Damage:
  1066. unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgDamage;
  1067. unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevDamage;
  1068. break;
  1069. case RankStat_Healing:
  1070. unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgHealing;
  1071. unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevHealing;
  1072. break;
  1073. case RankStat_Support:
  1074. unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgSupport;
  1075. unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevSupport;
  1076. break;
  1077. default:
  1078. Assert( 0 );
  1079. return 0;
  1080. }
  1081. if ( !unStatAvg || !unStatStdDev )
  1082. return 0;
  1083. int nMedalRank = StatMedal_None;
  1084. // Non-zero value?
  1085. if ( unStatAvg && unStatStdDev )
  1086. {
  1087. int nDelta = nValue - unStatAvg;
  1088. if ( nDelta > 0 )
  1089. {
  1090. float flPercentile = NormalDistributionCDF( (float) nValue, (float) unStatAvg, (float) unStatStdDev );
  1091. if ( flPercentile >= m_flGoldPercentile )
  1092. {
  1093. nMedalRank = StatMedal_Gold;
  1094. }
  1095. else if ( flPercentile >= m_flSilverPercentile )
  1096. {
  1097. nMedalRank = StatMedal_Silver;
  1098. }
  1099. else if ( flPercentile >= m_flBronzePercentile )
  1100. {
  1101. nMedalRank = StatMedal_Bronze;
  1102. }
  1103. // TODO:
  1104. // - Stat must be "n" std deviations above the match average, too (anti-farming)
  1105. // - Match must qualify:
  1106. // - Less than "n" minutes
  1107. // - At least "x" of "y" players at match end (no leavers?)
  1108. }
  1109. }
  1110. return clamp( nMedalRank, StatMedal_None, StatMedal_Gold );
  1111. }
  1112. float CMatchInfo::NormalDistributionCDF( float flValue, float flMu, float flSigma )
  1113. {
  1114. if ( flSigma <= 0.f )
  1115. return 0.5f;
  1116. return 0.5f * ( 1.f + erf( ( flValue - flMu ) / ( flSigma * sqrt( 2.f ) ) ) );
  1117. }
  1118. //-----------------------------------------------------------------------------
  1119. // CGCCompetitiveDailyStatsRollupJob
  1120. //-----------------------------------------------------------------------------
  1121. class CGCCompetitiveDailyStatsRollupJob : public GCSDK::CGCClientJob
  1122. {
  1123. public:
  1124. CGCCompetitiveDailyStatsRollupJob( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) {}
  1125. virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
  1126. {
  1127. GCSDK::CProtoBufMsg< CMsgGC_DailyCompetitiveStatsRollup_Response > msg( pNetPacket );
  1128. CMatchInfo *pInfo = GTFGCClientSystem()->GetMatch();
  1129. if ( !pInfo )
  1130. return false;
  1131. // Empty rankdata is valid (GC runs checks that might cause this as people reach new ranks)
  1132. for ( int i = 0; i < msg.Body().rankdata_size(); i++ )
  1133. {
  1134. CMatchInfo::DailyStatsRankBucket_t rankBucket = {
  1135. msg.Body().rankdata( i ).rank(),
  1136. msg.Body().rankdata( i ).records(),
  1137. msg.Body().rankdata( i ).avg_score(),
  1138. msg.Body().rankdata( i ).stdev_score(),
  1139. msg.Body().rankdata( i ).avg_kills(),
  1140. msg.Body().rankdata( i ).stdev_kills(),
  1141. msg.Body().rankdata( i ).avg_damage(),
  1142. msg.Body().rankdata( i ).stdev_damage(),
  1143. msg.Body().rankdata( i ).avg_healing(),
  1144. msg.Body().rankdata( i ).stdev_healing(),
  1145. msg.Body().rankdata( i ).avg_support(),
  1146. msg.Body().rankdata( i ).stdev_support()
  1147. };
  1148. pInfo->SetDailyRankData( rankBucket );
  1149. }
  1150. return true;
  1151. }
  1152. };
  1153. GC_REG_JOB( GCSDK::CGCClient, CGCCompetitiveDailyStatsRollupJob, "CGCCompetitiveDailyStatsRollupJob", k_EMsgGC_DailyCompetitiveStatsRollup_Response, k_EServerTypeGCClient );
  1154. //-----------------------------------------------------------------------------
  1155. // CGCVoteSystemVoteKickResponse
  1156. //-----------------------------------------------------------------------------
  1157. class CGCVoteSystemVoteKickResponse : public GCSDK::CGCClientJob
  1158. {
  1159. public:
  1160. CGCVoteSystemVoteKickResponse( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) {}
  1161. virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
  1162. {
  1163. GCSDK::CProtoBufMsg< CMsgGC_VoteKickPlayerRequestResponse > msg( pNetPacket );
  1164. if ( g_voteController )
  1165. {
  1166. g_voteController->GCResponseReceived( msg.Body().allowed() );
  1167. }
  1168. return true;
  1169. }
  1170. };
  1171. GC_REG_JOB( GCSDK::CGCClient, CGCVoteSystemVoteKickResponse, "CGCVoteSystemVoteKickResponse", k_EMsgGCVoteKickPlayerRequestResponse, k_EServerTypeGCClient );
  1172. //-----------------------------------------------------------------------------
  1173. // Purpose:
  1174. //-----------------------------------------------------------------------------
  1175. class CGCKickPlayerFromLobbyJob : public GCSDK::CGCClientJob
  1176. {
  1177. public:
  1178. CGCKickPlayerFromLobbyJob( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
  1179. virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
  1180. {
  1181. GCSDK::CProtoBufMsg<CMsgGC_KickPlayerFromLobby> msg( pNetPacket );
  1182. CSteamID steamID( msg.Body().targetid() );
  1183. if ( steamID.IsValid() )
  1184. {
  1185. GTFGCClientSystem()->EjectMatchPlayer( steamID, TFMatchLeaveReason_ADMIN_KICK );
  1186. }
  1187. return true;
  1188. }
  1189. };
  1190. GC_REG_JOB( GCSDK::CGCClient, CGCKickPlayerFromLobbyJob, "CGCKickPlayerFromLobbyJob", k_EMsgGC_KickPlayerFromLobby, GCSDK::k_EServerTypeGCClient );
  1191. //-----------------------------------------------------------------------------
  1192. // Purpose:
  1193. //-----------------------------------------------------------------------------
  1194. CTFGCServerSystem::CTFGCServerSystem()
  1195. : m_flTimeRequestedLateJoin( -1.f )
  1196. , m_bLateJoinEligible( false )
  1197. , m_iSavedVisibleMaxPlayers( -1 )
  1198. , m_bOverridingVisibleMaxPlayers( false )
  1199. , m_bWaitingForNewMatchID( false )
  1200. , m_flWaitingForNewMatchTime( 0.f )
  1201. {
  1202. // replace base GCClientSystem
  1203. SetGCClientSystem( this );
  1204. m_unGameStartTime = 0;
  1205. m_bSetupSchema = false;
  1206. m_timeLastSendGameServerInfoAndConnectedPlayers = 0;
  1207. //m_flUpdateGCGameTime = 0;
  1208. //m_nUploadingMatchStats = EDOTA_MATCH_STATS_IDLE;
  1209. //m_nParentRelayCount = 0;
  1210. //m_nLastUpdateGCServerType = -1;
  1211. m_eLastGameServerUpdateState = ServerMatchmakingState_NOT_PARTICIPATING;
  1212. m_eLastGameServerUpdateMatchmakingMode = TF_Matchmaking_MVM;
  1213. m_nLastGameServerUpdateBotCount = -1;
  1214. m_nLastGameServerUpdateMaxHumans = -1;
  1215. m_nLastGameServerUpdateSlotsFree = -1;
  1216. m_nLastGameServerUpdateLobbyMMVersion = 0;
  1217. m_flTimeBecameEmptyWithLobby = 0.0f;
  1218. m_timeLastConnectedToGC = 0.f;
  1219. m_pMatchInfo = NULL;
  1220. g_bWarnedAboutMaxplayersInMVM = false;
  1221. }
  1222. CTFGCServerSystem::~CTFGCServerSystem( void )
  1223. {
  1224. // Prevent other system from using this pointer after it's destroyed
  1225. SetGCClientSystem( NULL );
  1226. if ( m_pMatchInfo )
  1227. {
  1228. delete m_pMatchInfo;
  1229. }
  1230. }
  1231. bool CTFGCServerSystem::Init()
  1232. {
  1233. ListenForGameEvent( "player_disconnect" );
  1234. ListenForGameEvent( "player_score_changed" );
  1235. g_bWarnedAboutMaxplayersInMVM = false;
  1236. return true;
  1237. }
  1238. //-----------------------------------------------------------------------------
  1239. // Purpose:
  1240. //-----------------------------------------------------------------------------
  1241. void CTFGCServerSystem::PreInitGC()
  1242. {
  1243. BaseClass::PreInitGC();
  1244. if ( !m_bSetupSchema )
  1245. {
  1246. // REG_SHARED_OBJECT_SUBCLASS( CDOTAHeroStandings );
  1247. // REG_SHARED_OBJECT_SUBCLASS( CDOTAGameAccountClient );
  1248. REG_SHARED_OBJECT_SUBCLASS( CTFGSLobby );
  1249. REG_SHARED_OBJECT_SUBCLASS( CTFParty );
  1250. m_bSetupSchema = true;
  1251. }
  1252. }
  1253. //-----------------------------------------------------------------------------
  1254. // Purpose:
  1255. //-----------------------------------------------------------------------------
  1256. void CTFGCServerSystem::PostInitGC()
  1257. {
  1258. BaseClass::PostInitGC();
  1259. }
  1260. //-----------------------------------------------------------------------------
  1261. void CTFGCServerSystem::LevelShutdownPostEntity()
  1262. {
  1263. BaseClass::LevelShutdownPostEntity();
  1264. }
  1265. //-----------------------------------------------------------------------------
  1266. void CTFGCServerSystem::Shutdown()
  1267. {
  1268. BaseClass::Shutdown();
  1269. // Remove listener, if we have one
  1270. if ( m_ourSteamID.IsValid() )
  1271. {
  1272. GCClientSystem()->GetGCClient()->RemoveSOCacheListener( m_ourSteamID, this );
  1273. }
  1274. }
  1275. void CTFGCServerSystem::LevelInitPreEntity()
  1276. {
  1277. BaseClass::LevelInitPreEntity();
  1278. // Assert( m_nUploadingMatchStats != EDOTA_MATCH_STATS_UPLOADING );
  1279. // if ( m_nUploadingMatchStats == EDOTA_MATCH_STATS_UPLOADING )
  1280. // {
  1281. // Warning( "Error, changed level while waiting for match stats to upload!\n" );
  1282. // return;
  1283. // }
  1284. // m_nUploadingMatchStats = EDOTA_MATCH_STATS_IDLE;
  1285. }
  1286. //-----------------------------------------------------------------------------
  1287. void CTFGCServerSystem::ClientActive( CSteamID steamIDClient )
  1288. {
  1289. if ( !steamIDClient.IsValid() || !steamIDClient.BIndividualAccount() )
  1290. {
  1291. if ( !HushAsserts() )
  1292. {
  1293. Assert( steamIDClient.IsValid() );
  1294. Assert( steamIDClient.BIndividualAccount() );
  1295. }
  1296. return;
  1297. }
  1298. CMatchInfo *pMatch = GetMatch();
  1299. CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL;
  1300. if ( !pMatchPlayer )
  1301. return;
  1302. pMatchPlayer->OnActive();
  1303. // Only subscribe to match players' SOCaches. They're the only ones who will have
  1304. // parties that we care about.
  1305. GetGCClient()->AddSOCacheListener( steamIDClient, this );
  1306. }
  1307. //-----------------------------------------------------------------------------
  1308. void CTFGCServerSystem::ClientConnected( CSteamID steamIDClient, edict_t *pEntity )
  1309. {
  1310. // Note that we won't be notified of players connecting with unknown steamIDs, SteamIDAllowedToConnect() should be
  1311. // used to reject those in a strict MM scenario where that is not acceptable.
  1312. CMatchInfo *pMatch = GetMatch();
  1313. CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL;
  1314. if ( !pMatchPlayer )
  1315. return;
  1316. pMatchPlayer->OnConnected( pEntity->m_EdictIndex );
  1317. }
  1318. //-----------------------------------------------------------------------------
  1319. void CTFGCServerSystem::ClientDisconnected( CSteamID steamIDClient )
  1320. {
  1321. if ( !steamIDClient.IsValid() || !steamIDClient.BIndividualAccount() )
  1322. {
  1323. Assert( steamIDClient.IsValid() );
  1324. Assert( steamIDClient.BIndividualAccount() );
  1325. return;
  1326. }
  1327. GetGCClient()->RemoveSOCacheListener( steamIDClient, this );
  1328. // This is here because ClientDisconnected code is not called on gamerules or player
  1329. // when the game is in state g_fGameOver. See CServerGameClients::ClientDisconnect.
  1330. CBasePlayer* pPlayer = UTIL_PlayerBySteamID( steamIDClient );
  1331. if ( TFGameRules() && pPlayer )
  1332. {
  1333. TFGameRules()->SetPlayerNextMapVote( pPlayer->entindex(), CTFGameRules::USER_NEXT_MAP_VOTE_UNDECIDED );
  1334. }
  1335. CMatchInfo *pMatch = GetMatch();
  1336. CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL;
  1337. if ( !pMatchPlayer )
  1338. {
  1339. return;
  1340. }
  1341. if ( !pMatchPlayer->bConnected )
  1342. {
  1343. Assert( !"Player disconnecting is not marked connected" );
  1344. return;
  1345. }
  1346. // Did they disconnect while still loading in?
  1347. bool bWasActive = pMatchPlayer->nConnectingButNotActiveIndex == 0;
  1348. RTime32 now = CRTime::RTime32TimeCur();
  1349. // Time spent in the active state.
  1350. RTime32 timeSpentActive = bWasActive ? ( now - pMatchPlayer->rtLastActiveEvent ) : 0;
  1351. // Mark disconnected
  1352. pMatchPlayer->bConnected = false;
  1353. pMatchPlayer->nConnectingButNotActiveIndex = 0;
  1354. // If they were active, they now transitioned to inactive. If they were loading, this value is still the last time
  1355. // they went inactive, and shouldn't change.
  1356. if ( bWasActive )
  1357. { pMatchPlayer->rtLastActiveEvent = now; }
  1358. // Optionally forgive some amount of their disconnected seconds accumulation based on how long they were present.
  1359. int nForgiveRatio = tf_mm_player_disconnect_time_forgive_ratio.GetInt();
  1360. if ( timeSpentActive > 0 && nForgiveRatio > 0 && pMatchPlayer->nDisconnectedSeconds > 0 )
  1361. {
  1362. double dForgiven = (double)pMatchPlayer->nDisconnectedSeconds - ( (double)timeSpentActive / nForgiveRatio );
  1363. int nOldVal = pMatchPlayer->nDisconnectedSeconds;
  1364. pMatchPlayer->nDisconnectedSeconds = Max( 0, (int)dForgiven );
  1365. MMLog("Client %s was connected for %u seconds, disconnect timer lowered from %i to %i\n",
  1366. steamIDClient.Render(), timeSpentActive, nOldVal, pMatchPlayer->nDisconnectedSeconds );
  1367. }
  1368. }
  1369. //-----------------------------------------------------------------------------
  1370. void CTFGCServerSystem::PreClientUpdate( )
  1371. {
  1372. BaseClass::PreClientUpdate();
  1373. CRTime::UpdateRealTime();
  1374. if ( GCClientSystem()->BConnectedtoGC() )
  1375. {
  1376. m_timeLastConnectedToGC = Plat_FloatTime();
  1377. }
  1378. // We want a pause so players can read what the next map is. Once we've waited
  1379. // long enough, we're doing a map change regardless of if the GC got back to us
  1380. // with a new match ID.
  1381. if ( Plat_FloatTime() > m_flWaitingForNewMatchTime
  1382. && m_flWaitingForNewMatchTime != 0.f )
  1383. {
  1384. LaunchNewMatchForLobby();
  1385. }
  1386. //
  1387. // Check for updating the caches that we're listening to
  1388. //
  1389. CSteamID const *pSteamID = engine->GetGameServerSteamID();
  1390. if ( pSteamID && m_ourSteamID != *pSteamID )
  1391. {
  1392. Assert( pSteamID->BGameServerAccount() );
  1393. // If we were previously listening to somebody else, stop listening. This
  1394. // means we were connected, then reconnected and got a different Steam ID,
  1395. // and is weird, but possible
  1396. if ( m_ourSteamID.IsValid() )
  1397. {
  1398. MMLog( "CTFGCServerSystem - removing listener to old Steam ID %s\n", m_ourSteamID.Render() );
  1399. GCClientSystem()->GetGCClient()->RemoveSOCacheListener( m_ourSteamID, this );
  1400. }
  1401. // Remember our new Steam ID
  1402. m_ourSteamID = *pSteamID;
  1403. // And start listening
  1404. GCClientSystem()->GetGCClient()->AddSOCacheListener( m_ourSteamID, this );
  1405. }
  1406. MatchPlayerAbandonThink();
  1407. UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, false );
  1408. // Check if the game is empty, and we need to shut down our lobby
  1409. CTFGSLobby *pLobby = GetLobby();
  1410. if ( pLobby )
  1411. {
  1412. switch ( pLobby->GetState() )
  1413. {
  1414. case CSOTFGameServerLobby_State_SERVERSETUP:
  1415. // We could most definitely be empty here, waiting for players to join!
  1416. // Don't kill the server just yet
  1417. break;
  1418. case CSOTFGameServerLobby_State_RUN:
  1419. break;
  1420. default:
  1421. case CSOTFGameServerLobby_State_UNKNOWN:
  1422. MMLog( "Lobby in invalid state %d\n", (int)pLobby->GetState() );
  1423. break;
  1424. }
  1425. }
  1426. // Check for slamming visiblemaxplayers
  1427. static ConVarRef sv_visiblemaxplayers( "sv_visiblemaxplayers" );
  1428. if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
  1429. {
  1430. // Abort the server if they don't have enough maxplayers
  1431. if ( gpGlobals->maxClients < 32 )
  1432. {
  1433. if( !g_bWarnedAboutMaxplayersInMVM )
  1434. {
  1435. // Prevent this warning from endlessly spamming the console...
  1436. g_bWarnedAboutMaxplayersInMVM = true;
  1437. Warning( "You must set maxplayers to 32 to host Mann vs. Machine\n" );
  1438. }
  1439. if ( engine->IsDedicatedServer() )
  1440. {
  1441. engine->ServerCommand( "exit\n" );
  1442. }
  1443. return;
  1444. }
  1445. // This changes what the server browser displays
  1446. // update sv_visiblemaxplayers for MvM, count only non-bot spectators
  1447. CUtlVector<CTFPlayer *> spectatorVector;
  1448. CollectPlayers( &spectatorVector, TEAM_SPECTATOR );
  1449. int spectatorCount = 0;
  1450. FOR_EACH_VEC ( spectatorVector, iIndex )
  1451. {
  1452. if ( !spectatorVector[iIndex]->IsBot() && !spectatorVector[iIndex]->IsReplay() && !spectatorVector[iIndex]->IsHLTV() )
  1453. {
  1454. spectatorCount++;
  1455. }
  1456. }
  1457. int playerCount = kMVM_DefendersTeamSize + spectatorCount;
  1458. if ( sv_visiblemaxplayers.GetInt() <= 0 || sv_visiblemaxplayers.GetInt() != playerCount )
  1459. {
  1460. MMLog( "Setting sv_visiblemaxplayers to %d for MvM\n", playerCount );
  1461. // save off visible players
  1462. if ( !m_bOverridingVisibleMaxPlayers )
  1463. {
  1464. m_bOverridingVisibleMaxPlayers = true;
  1465. m_iSavedVisibleMaxPlayers = sv_visiblemaxplayers.GetInt();
  1466. }
  1467. sv_visiblemaxplayers.SetValue( playerCount );
  1468. }
  1469. }
  1470. else
  1471. {
  1472. // Not in MvM. Check for restoring sv_visiblemaxplayers
  1473. if ( m_bOverridingVisibleMaxPlayers )
  1474. {
  1475. MMLog( "Restoring sv_visiblemaxplayers to %d\n", m_iSavedVisibleMaxPlayers );
  1476. sv_visiblemaxplayers.SetValue( m_iSavedVisibleMaxPlayers );
  1477. m_bOverridingVisibleMaxPlayers = false;
  1478. m_iSavedVisibleMaxPlayers = -1;
  1479. }
  1480. }
  1481. // You may not be in matchmaking if you have a password!
  1482. static ConVarRef sv_password( "sv_password" );
  1483. if ( tf_mm_servermode.GetInt() != 0 && *sv_password.GetString() != '\0' )
  1484. {
  1485. Warning( "Setting tf_mm_servermode=0 due to sv_password\n" );
  1486. tf_mm_servermode.SetValue( 0 );
  1487. }
  1488. // TFGameRules()->SetStableMode( IsStableMode() );
  1489. //
  1490. // if ( HLTVDirector() && HLTVDirector()->GetHLTVServer() )
  1491. // {
  1492. // gcGameTime = Max( 0.0f, TFGameRules()->GetDOTATime() - HLTVDirector()->GetDelay() );
  1493. // }
  1494. // else
  1495. // {
  1496. // gcGameTime = TFGameRules()->GetDOTATime();
  1497. // }
  1498. // // Slam server region to 255 while in PVE mode
  1499. // static ConVarRef sv_region( "sv_region" );
  1500. // if ( sv_region.GetInt() != 255 )
  1501. // {
  1502. // MMLog( "Setting 'sv_region 255 ' due to tf_mm_servermode\n" );
  1503. // sv_region.SetValue( 255 );
  1504. // }
  1505. }
  1506. void CTFGCServerSystem::MatchPlayerAbandonThink()
  1507. {
  1508. CMatchInfo *pMatchInfo = GetMatch();
  1509. if ( !pMatchInfo || pMatchInfo->m_bMatchEnded )
  1510. { return; }
  1511. int nAbandonSeconds = tf_mm_player_disconnect_time_before_abandon.GetInt();
  1512. // Disabled
  1513. if ( nAbandonSeconds < 0 )
  1514. { return; }
  1515. int nPlayers = pMatchInfo->GetNumTotalMatchPlayers();
  1516. bool bDroppedPlayers = false;
  1517. for ( int idx = 0; idx < nPlayers; idx++ )
  1518. {
  1519. CMatchInfo::PlayerMatchData_t *pPlayer = pMatchInfo->GetMatchDataForPlayer( idx );
  1520. // The engine doesn't really tell the game of connected-but-not-active players dropping. Keep an eye on their
  1521. // entity being quietly cleaned up and note the disconnect.
  1522. if ( pPlayer->nConnectingButNotActiveIndex )
  1523. {
  1524. const CSteamID *pIndexSteamID = engine->GetClientSteamIDByPlayerIndex( pPlayer->nConnectingButNotActiveIndex );
  1525. if ( !pIndexSteamID || *pIndexSteamID != pPlayer->steamID )
  1526. {
  1527. MMLog( "Match player %s dropped before going active\n", pPlayer->steamID.Render() );
  1528. ClientDisconnected( pPlayer->steamID );
  1529. }
  1530. }
  1531. if ( !pPlayer->bConnected && !pPlayer->bDropped )
  1532. {
  1533. // nDisconnectedSeconds is accumulated from previous absences, but doesn't include the current disconnect.
  1534. int nTimeGone = CRTime::RTime32TimeCur() - pPlayer->rtLastActiveEvent + pPlayer->nDisconnectedSeconds;
  1535. if ( nTimeGone > nAbandonSeconds )
  1536. {
  1537. MMLog( "Match player %s has been absent for a combined total of %u seconds, dropping from match\n",
  1538. pPlayer->steamID.Render(), nTimeGone );
  1539. SetMatchPlayerDropped( pPlayer->steamID, pPlayer->bEverConnected ? TFMatchLeaveReason_AWOL : TFMatchLeaveReason_NO_SHOW );
  1540. bDroppedPlayers = true;
  1541. }
  1542. }
  1543. }
  1544. if ( bDroppedPlayers )
  1545. { UpdateServerDetails(); }
  1546. }
  1547. //-----------------------------------------------------------------------------
  1548. bool CTFGCServerSystem::EjectMatchPlayer( CSteamID steamID, TFMatchLeaveReason eReason )
  1549. {
  1550. CMatchInfo *pMatch = GetLiveMatch();
  1551. CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamID ) : NULL;
  1552. if ( !pMatchPlayer || pMatchPlayer->bDropped )
  1553. { return false; }
  1554. SetMatchPlayerDropped( steamID, eReason );
  1555. KickRemovedMatchPlayer( steamID );
  1556. return true;
  1557. }
  1558. //-----------------------------------------------------------------------------
  1559. void CTFGCServerSystem::MatchPlayerVoteKicked( CSteamID steamID )
  1560. {
  1561. bool bEjected = EjectMatchPlayer( steamID, TFMatchLeaveReason_VOTE_KICK );
  1562. if ( bEjected )
  1563. {
  1564. // Was part of our match, handled.
  1565. MMLog( "Player %s vote-kicked from live match\n", steamID.Render() );
  1566. return;
  1567. }
  1568. // Not part of our match, check if they used to be
  1569. CMatchInfo *pMatch = GetLiveMatch();
  1570. if ( !pMatch )
  1571. return;
  1572. CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( steamID );
  1573. if ( !pPlayer || ( pPlayer && !pPlayer->bDropped ) )
  1574. {
  1575. AssertMsg( !pPlayer || pPlayer->bDropped,
  1576. "Player is still part of our match, so EjectMatchPlayer should have succeeded" );
  1577. return;
  1578. }
  1579. // Previously in this match, but left before kick arrived. Send this message made just for that occasion, update our
  1580. // record to reflect the reason.
  1581. MMLog( "Player %s vote-kicked after departing match\n", steamID.Render() );
  1582. pPlayer->eDropReason = TFMatchLeaveReason_VOTE_KICK;
  1583. ReliableMsgPlayerVoteKickedAfterLeavingMatch *pReliable = new ReliableMsgPlayerVoteKickedAfterLeavingMatch();
  1584. auto &msg = pReliable->Msg().Body();
  1585. msg.set_steam_id( steamID.ConvertToUint64() );
  1586. msg.set_lobby_id( pMatch->m_nLobbyID );
  1587. msg.set_match_id( pMatch->m_nMatchID );
  1588. pReliable->Enqueue();
  1589. }
  1590. //-----------------------------------------------------------------------------
  1591. bool CTFGCServerSystem::KickRemovedMatchPlayer( CSteamID steamIDClient )
  1592. {
  1593. CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerBySteamID( steamIDClient ) );
  1594. if ( !pPlayer )
  1595. { return false; }
  1596. MMLog( "Kicking ejected player %s\n", steamIDClient.Render() );
  1597. engine->ServerCommand( UTIL_VarArgs( "kickid %d %s\n", pPlayer->GetUserID(), "#TF_MM_Generic_Kicked" ) );
  1598. return true;
  1599. }
  1600. //-----------------------------------------------------------------------------
  1601. bool CTFGCServerSystem::CanChangeMatchPlayerTeams()
  1602. {
  1603. // Warning: LaunchNewMatchForLobby is counting on being able to do this, so avoid the temptation to forbid this
  1604. // during match-result phase or similar (this is only for is-our-state-consistent-to-allow-this, not
  1605. // should-gamerules-be-doing-this, that's on them)
  1606. CMatchInfo *pMatch = GetMatch();
  1607. const IMatchGroupDescription* pMatchDesc = pMatch ? GetMatchGroupDescription( pMatch->m_eMatchGroup ) : NULL;
  1608. if ( !pMatch || !pMatchDesc || pMatch->BMatchTerminated() || !pMatchDesc->BCanServerChangeMatchPlayerTeams() )
  1609. { return false; }
  1610. // If we're waiting to launch a new match, the team change would be for the new match that the GC is about to send
  1611. // down, which has new teams. We probably are not intending that since we have no idea what this player's current
  1612. // team is.
  1613. //
  1614. // (See the Team Assignments comment at the start of this file for ordering regarding new matches and team changes.)
  1615. if ( BPendingNewMatch() )
  1616. { return false; }
  1617. return true;
  1618. }
  1619. //-----------------------------------------------------------------------------
  1620. // ChangeMatchPlayerTeams handling
  1621. //-----------------------------------------------------------------------------
  1622. void CTFGCServerSystem::ChangeMatchPlayerTeam( CSteamID steamID, TF_GC_TEAM eTeam )
  1623. {
  1624. // Helper for single member.
  1625. CUtlVectorFixed< PlayerTeamPair_t, 1 > vec;
  1626. vec.AddToTail( { steamID, eTeam } );
  1627. ChangeMatchPlayerTeams( vec );
  1628. }
  1629. template< typename ANY_ALLOCATOR >
  1630. void CTFGCServerSystem::ChangeMatchPlayerTeams( const CUtlVector< PlayerTeamPair_t, ANY_ALLOCATOR > &vecNewTeams )
  1631. {
  1632. if ( !CanChangeMatchPlayerTeams() )
  1633. {
  1634. // Some match logic is badly out of sync if it thinks it can do this.
  1635. MMLog( "!! Game server is attempting to change player teams in an invalidate state\n" );
  1636. AbortInvalidMatchState();
  1637. return;
  1638. }
  1639. // Job takes ownership of message
  1640. MMLog( "Sending team assignment request to GC:\n" );
  1641. ReliableMsgChangeMatchPlayerTeams *pReliable = new ReliableMsgChangeMatchPlayerTeams();
  1642. auto &msg = pReliable->Msg().Body();
  1643. msg.set_match_id( GetMatch()->m_nMatchID );
  1644. msg.set_lobby_id( GetMatch()->m_nLobbyID );
  1645. FOR_EACH_VEC( vecNewTeams, idx )
  1646. {
  1647. const CSteamID &steamID = vecNewTeams[idx].steamID;
  1648. const TF_GC_TEAM &eTeam = vecNewTeams[idx].eTeam;
  1649. // Do we know about this guy?
  1650. CMatchInfo::PlayerMatchData_t *pPlayer = m_pMatchInfo->GetMatchDataForPlayer( steamID );
  1651. if ( !pPlayer || pPlayer->bDropped )
  1652. {
  1653. MMLog("!! Got team change request for player not in match %s\n", steamID.Render() );
  1654. continue;
  1655. }
  1656. MMLog(" %37s -> %d\n", steamID.Render(), eTeam );
  1657. auto *member = msg.add_member();
  1658. member->set_member_id( steamID.ConvertToUint64() );
  1659. member->set_new_team( eTeam );
  1660. // Reflect change locally immediately, this message should not fail
  1661. pPlayer->eGCTeam = eTeam;
  1662. }
  1663. pReliable->Enqueue();
  1664. }
  1665. void CTFGCServerSystem::ChangeMatchPlayerTeamsResponse( bool bSuccess )
  1666. {
  1667. if ( !bSuccess && GetLobby() )
  1668. {
  1669. // If the lobby went away prior to the GC responding, it is out of sync and can't do anything meaningful with
  1670. // these updates right now, but we still have authority to finish the match and send a result, so just keep
  1671. // plugging along. But if we still HAVE the lobby, and the GC said no, something is badly out of sync with this
  1672. // match.
  1673. MMLog( "!! ChangeMatchPlayerTeams rejected, something is confused\n" );
  1674. AbortInvalidMatchState();
  1675. return;
  1676. }
  1677. MMLog( "ChangeMatchPlayerTeams acknowledged\n" );
  1678. }
  1679. //-----------------------------------------------------------------------------
  1680. const MapDef_t* CTFGCServerSystem::GetNextMapVoteByIndex( int nIndex ) const
  1681. {
  1682. const CTFGSLobby *pLobby = GetLobby();
  1683. if ( pLobby && nIndex < pLobby->Obj().next_maps_for_vote_size() )
  1684. {
  1685. return GetItemSchema()->GetMasterMapDefByIndex( pLobby->Obj().next_maps_for_vote( nIndex ) );
  1686. }
  1687. Assert( false );
  1688. return GetItemSchema()->GetMasterMapDefByName( "ctf_2fort" );
  1689. }
  1690. //-----------------------------------------------------------------------------
  1691. // Purpose: GC Msg to request starting a new match for an existing lobby
  1692. //-----------------------------------------------------------------------------
  1693. void CTFGCServerSystem::NewMatchForLobbyResponse( bool bSuccess )
  1694. {
  1695. // We should be expecting this
  1696. if ( !m_bWaitingForNewMatchID )
  1697. {
  1698. MMLog( "!! Got a NewMatchForLobbyResponse when not expecting it\n" );
  1699. AbortInvalidMatchState();
  1700. }
  1701. Assert( TFGameRules() );
  1702. MMLog( "NewMatchID response recieved -- %s.\n", bSuccess ? "Success!" : "Failed!" );
  1703. m_bWaitingForNewMatchID = false;
  1704. CMatchInfo *pMatch = GetMatch();
  1705. if ( pMatch && pMatch->m_bServerCreated )
  1706. {
  1707. // We went ahead without a match ID, the new ID should've already arrived in SOUpdated
  1708. if ( bSuccess )
  1709. {
  1710. if ( !pMatch || pMatch->m_bServerCreated || !pMatch->m_nMatchID )
  1711. {
  1712. MMLog( "!! Got a NewMatchForLobby response but have not received a new match ID" );
  1713. AbortInvalidMatchState();
  1714. }
  1715. }
  1716. else
  1717. {
  1718. // Failed, but we already have a running speculative match. It is essentially an unofficial match now.
  1719. MMLog( "!! NewMatchForLobby responded negatively, this match will likely not be acknowledged by the system.\n" );
  1720. // TODO ROLLING MATCHES: Check that the jobs that will now send MatchID 0 do something salient
  1721. }
  1722. }
  1723. else
  1724. {
  1725. // Still waiting to actually kick off the new match. If the response was a failure, we can just abort.
  1726. if ( !bSuccess )
  1727. {
  1728. MMLog( "!! NewMatchForLobby responded negatively. We haven't launched the match yet, so just shutting down.\n" );
  1729. if ( TFGameRules() )
  1730. {
  1731. TFGameRules()->KickPlayersNewMatchIDRequestFailed();
  1732. }
  1733. else
  1734. {
  1735. AbortInvalidMatchState();
  1736. }
  1737. }
  1738. }
  1739. }
  1740. bool CTFGCServerSystem::CanRequestNewMatchForLobby()
  1741. {
  1742. // If this is a match that is not in sync with the GC, or it's not even a match, then no
  1743. if ( !m_pMatchInfo || !GetLobby() || m_pMatchInfo->BMatchTerminated() )
  1744. { return false; }
  1745. // If we're waiting on other pending match magic, then no you can't stack them god help your soul.
  1746. if ( m_pMatchInfo->m_bServerCreated || m_bWaitingForNewMatchID || m_flWaitingForNewMatchTime != 0.f )
  1747. { return false; }
  1748. // Match description allow it?
  1749. const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_pMatchInfo->m_eMatchGroup );
  1750. if ( !pMatchDesc->BCanServerRequestNewMatchForLobby() )
  1751. { return false; }
  1752. return true;
  1753. }
  1754. void CTFGCServerSystem::RequestNewMatchForLobby( const MapDef_t* pNewMap )
  1755. {
  1756. // Wat r u doin
  1757. if ( !CanRequestNewMatchForLobby() )
  1758. {
  1759. AbortInvalidMatchState();
  1760. }
  1761. m_flWaitingForNewMatchTime = Plat_FloatTime() + tf_mm_next_map_result_hold_time.GetFloat();
  1762. m_bWaitingForNewMatchID = true;
  1763. m_pMatchInfo->m_strMapName = pNewMap->pszMapName;
  1764. ReliableMsgNewMatchForLobby *pReliable = new ReliableMsgNewMatchForLobby();
  1765. auto &msg = pReliable->Msg().Body();
  1766. msg.set_next_map_id( pNewMap->m_nDefIndex );
  1767. msg.set_lobby_id( GetLobby()->GetGroupID() );
  1768. msg.set_current_match_id( GetMatch()->m_nMatchID );
  1769. MMLog( "Sending request to GC for a new match ID.\n" );
  1770. pReliable->Enqueue();
  1771. }
  1772. //-----------------------------------------------------------------------------
  1773. void CTFGCServerSystem::SetMatchPlayerDropped( CSteamID steamID, TFMatchLeaveReason eReason )
  1774. {
  1775. CMatchInfo *pMatch = GetMatch();
  1776. CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamID ) : NULL;
  1777. Assert( pMatchPlayer );
  1778. if ( !pMatchPlayer )
  1779. { return; }
  1780. Assert( !pMatchPlayer->bDropped );
  1781. // Determine if this was an abandon
  1782. bool bAbandon = true;
  1783. switch ( eReason )
  1784. {
  1785. case TFMatchLeaveReason_VOTE_KICK:
  1786. // Vote kicks don't penalize you currently. We need to revisit how these tie in with e.g. abuse reports/etc..
  1787. bAbandon = false;
  1788. break;
  1789. case TFMatchLeaveReason_NO_SHOW:
  1790. case TFMatchLeaveReason_GC_REMOVED:
  1791. // For right now, until we have more confidence in our network connectivity and possibly have SDR hooked up,
  1792. // we'll give no shows the benefit of the doubt if they never made it to connect. ( If they can't connect an
  1793. // give up and click abandon on their end, it will show up as GC_REMOVED )
  1794. bAbandon = pMatchPlayer->bEverConnected;
  1795. break;
  1796. case TFMatchLeaveReason_ADMIN_KICK:
  1797. case TFMatchLeaveReason_AWOL:
  1798. case TFMatchLeaveReason_IDLE:
  1799. break;
  1800. default: AssertMsg( false, "Unhandled TFMatchLeaveReason" );
  1801. }
  1802. bAbandon = bAbandon && !pMatch->BPlayerSafeToLeaveMatch( steamID );
  1803. /// TODO ROLLING MATCHES: Technically if this happens with a rolling match in queue, we'll drop them from the old
  1804. /// match without record of them in the new?
  1805. pMatch->DropPlayer( steamID, eReason, bAbandon );
  1806. SendPlayerLeftMatch( steamID, eReason, bAbandon );
  1807. }
  1808. void CTFGCServerSystem::UpdateServerDetails(void)
  1809. {
  1810. UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, false );
  1811. }
  1812. bool CTFGCServerSystem::ShouldHibernate()
  1813. {
  1814. // We only hibernate if we're just sitting there with a freshly loaded map
  1815. return engine->IsDedicatedServer() && tf_allow_server_hibernation.GetBool() && !GetLobby() && !BPendingReliableMessages() && !m_pMatchInfo;
  1816. }
  1817. void CTFGCServerSystem::FireGameEvent( IGameEvent *event )
  1818. {
  1819. // Disconnected from gameserver
  1820. if ( !Q_stricmp( event->GetName(), "player_disconnect" ) )
  1821. {
  1822. const char * pszReason = event->GetString( "reason", "" );
  1823. if ( Q_strstr( pszReason, "kick" ) || Q_strstr( pszReason, "Kick" ) || Q_strstr( pszReason, g_pszVoteKickString ) )
  1824. {
  1825. CBasePlayer *pPlayer = UTIL_PlayerByUserId( event->GetInt( "userid", 0 ) );
  1826. if ( !pPlayer )
  1827. return;
  1828. CSteamID steamId;
  1829. if ( !pPlayer->GetSteamID( &steamId ) )
  1830. return;
  1831. // Only care if this is a member of a live match
  1832. CMatchInfo *pMatch = GetMatch();
  1833. CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamId ) : NULL;
  1834. if ( !pMatch || !pMatchPlayer || pMatch->m_bMatchEnded || pMatchPlayer->bDropped )
  1835. { return; }
  1836. TFMatchLeaveReason eReason = TFMatchLeaveReason_ADMIN_KICK;
  1837. if ( Q_strstr( pszReason, g_pszIdleKickString ) )
  1838. {
  1839. eReason = TFMatchLeaveReason_IDLE;
  1840. }
  1841. // kickid %d You have been voted off;
  1842. // Vote kicks should not trigger abandon
  1843. else if ( Q_strstr( pszReason, g_pszVoteKickString ) )
  1844. {
  1845. eReason = TFMatchLeaveReason_VOTE_KICK;
  1846. }
  1847. SetMatchPlayerDropped( steamId, eReason );
  1848. UpdateServerDetails();
  1849. }
  1850. }
  1851. else if ( FStrEq( event->GetName(), "player_score_changed" ) )
  1852. {
  1853. CMatchInfo *pMatch = GetMatch();
  1854. if ( !pMatch )
  1855. return;
  1856. CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( event->GetInt( "player" ) ) );
  1857. if ( !pPlayer )
  1858. return;
  1859. CSteamID steamId;
  1860. if ( !pPlayer->GetSteamID( &steamId ) )
  1861. return;
  1862. const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( pMatch->m_eMatchGroup );
  1863. if ( !pMatchDesc || !pMatchDesc->m_pProgressionDesc )
  1864. return;
  1865. // Add to this player's score XP
  1866. pMatch->GiveXPRewardToPlayerForAction( steamId, CMsgTFXPSource_XPSourceType_SOURCE_SCORE, event->GetInt( "delta", 0 ) );
  1867. }
  1868. }
  1869. CTFParty* CTFGCServerSystem::GetPartyForPlayer( CSteamID steamID ) const
  1870. {
  1871. // Dig up this guy's party
  1872. CGCClientSharedObjectCache* pSOCache = const_cast< CTFGCServerSystem* >( this )->GetSOCache( steamID );
  1873. if ( !pSOCache )
  1874. {
  1875. return NULL;
  1876. }
  1877. CSharedObjectTypeCache* pPartyTypeCache = pSOCache->FindTypeCache( CTFParty::k_nTypeID );
  1878. if ( !pPartyTypeCache || pPartyTypeCache->GetCount() == 0 )
  1879. {
  1880. return NULL;
  1881. }
  1882. return assert_cast< CTFParty* >( pPartyTypeCache->GetObject( 0 ) );
  1883. }
  1884. const CMatchInfo::PlayerMatchData_t *CTFGCServerSystem::GetLiveMatchPlayer( CSteamID steamID ) const
  1885. {
  1886. return const_cast<CTFGCServerSystem*>(this)->GetLiveMatchPlayer( steamID );
  1887. }
  1888. CMatchInfo::PlayerMatchData_t *CTFGCServerSystem::GetLiveMatchPlayer( CSteamID steamID )
  1889. {
  1890. CMatchInfo *pMatch = GetMatch();
  1891. if ( !pMatch || pMatch->m_bMatchEnded )
  1892. { return NULL; }
  1893. CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( steamID );
  1894. if ( !pMatchPlayer || pMatchPlayer->bDropped )
  1895. { return NULL; }
  1896. return pMatchPlayer;
  1897. }
  1898. void CTFGCServerSystem::SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent )
  1899. {
  1900. // Msg( "CTFGCServerSystem::SOCreated type = %d owner = %s\n", pObject->GetTypeID(), steamIDOwner.Render() );
  1901. // Lobby handling
  1902. if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID )
  1903. {
  1904. const CTFGSLobby *pConstLobby = static_cast<const CTFGSLobby*>( pObject );
  1905. CTFGSLobby *pLobby = const_cast<CTFGSLobby *>( pConstLobby ); // GROSS
  1906. Assert( pLobby == GetLobby() ); // There can be only be one...
  1907. MMLog( "Lobby %016llx instanced on this server in state %s\n",
  1908. pLobby->GetGroupID(), CSOTFGameServerLobby_State_Name( pLobby->GetState() ).c_str() );
  1909. // Check if we need to switch the map or load a pop file.
  1910. CMsgGameServerMatchmakingStatus_Event statusEvent = CMsgGameServerMatchmakingStatus_Event_None;
  1911. bool bNewLobby = ( pLobby->GetState() == CSOTFGameServerLobby_State_SERVERSETUP );
  1912. if ( m_bMMServerMode && bNewLobby )
  1913. {
  1914. MMLog( " Map: '%s'\n", pLobby->GetMapName() );
  1915. MMLog( " Mission: '%s'\n", pLobby->GetMissionName() );
  1916. EMatchGroup eMatchGroup = (EMatchGroup)pLobby->Obj().match_group();
  1917. // Acknowledge the players that just connected. (This will create
  1918. // reservations for the players and let the GC we are expecting the
  1919. // players.)
  1920. statusEvent = CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers;
  1921. // Create a record of the match on first connect.
  1922. if ( m_pMatchInfo )
  1923. {
  1924. MMLog( "!! Received new anticipated lobby while running existing match. "
  1925. "Old match ID [ %llu ] ended [ %u ] "
  1926. "New matchID [ %llu ]\n",
  1927. m_pMatchInfo->m_nMatchID, m_pMatchInfo->m_bMatchEnded,
  1928. pLobby->GetMatchID() );
  1929. Assert( false );
  1930. delete m_pMatchInfo;
  1931. // In theory the overwritten match will now be forgotten by us, all errant players kicked by the
  1932. // UpdateConnectedPlayers tick...
  1933. }
  1934. m_pMatchInfo = new CMatchInfo( pLobby );
  1935. GTFGCClientSystem()->DumpLobby();
  1936. if ( eMatchGroup == EMatchGroup::k_nMatchGroup_Invalid ||
  1937. !GetMatchGroupDescription( eMatchGroup )->InitServerSettingsForMatch( pConstLobby ) )
  1938. {
  1939. AbortInvalidMatchState();
  1940. }
  1941. // FIXME We should have some version checking like this.
  1942. // int engineServerVersion = engine->GetServerVersion();
  1943. //
  1944. // // Version checking is enforced if both sides do not report zero as their version
  1945. // if ( engineServerVersion && g_gcServerVersion && engineServerVersion != g_gcServerVersion )
  1946. // {
  1947. // // If we're out of date exit
  1948. // Msg("Version out of date (GC wants %d, we are %d), terminating!\n", g_gcServerVersion, engine->GetServerVersion() );
  1949. // engine->ServerCommand( "quit\n" );
  1950. // }
  1951. }
  1952. else
  1953. {
  1954. // We could've just gotten re-sent this lobby, is it the match we think we're running? If we are running a
  1955. // match for a different lobby, something is super wrong
  1956. uint64 nExistingMatchID = m_pMatchInfo ? m_pMatchInfo->m_nMatchID : 0;
  1957. uint64 nLobbyMatchID = pLobby->Obj().has_match_id() ? pLobby->GetMatchID() : 0;
  1958. if ( m_pMatchInfo && nExistingMatchID == nLobbyMatchID )
  1959. {
  1960. MMLog( "GC refreshed lobby for match ID [ %llu ]\n", m_pMatchInfo->m_nMatchID );
  1961. }
  1962. else
  1963. {
  1964. MMLog( "!! Got assigned a lobby not in server-setup state, or when not accepting lobbies. Rejecting.\n"
  1965. "Lobby matchID [ %llu ], existing match [ %llu ]\n",
  1966. pLobby->GetMatchID(), m_pMatchInfo ? m_pMatchInfo->m_nMatchID : 0ull );
  1967. if ( !m_pMatchInfo )
  1968. {
  1969. // Not running a match, don't want this one, just reject the lobby.
  1970. //
  1971. // This can happen when we crash and are handed a stale lobby upon reboot, rejecting it will
  1972. // terminate that match.
  1973. SendRejectLobby();
  1974. }
  1975. else
  1976. {
  1977. // Otherwise, we thought we had a lobby, but the GC sent us a different match? No idea what is going
  1978. // on, probably some bad de-sync happened.
  1979. //
  1980. // No faith we can continue and send authoritative match results about anything.
  1981. AbortInvalidMatchState();
  1982. }
  1983. }
  1984. }
  1985. UpdateConnectedPlayersAndServerInfo( statusEvent, false );
  1986. }
  1987. }
  1988. void CTFGCServerSystem::SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent )
  1989. {
  1990. // Don't care if we're not running a match
  1991. CMatchInfo *pMatch = GetMatch();
  1992. if ( !pMatch )
  1993. return;
  1994. // Lobby handling
  1995. if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID )
  1996. {
  1997. const CTFGSLobby *pConstLobby = static_cast<const CTFGSLobby*>( pObject );
  1998. CTFGSLobby *pLobby = const_cast<CTFGSLobby *>( pConstLobby ); // GROSS
  1999. Assert( pLobby == GetLobby() ); // There can be only be one...
  2000. bool bNeedsToUpdatePlayerAndServer = false;
  2001. // Check if we have new reservations not part of the match
  2002. for ( int i = 0; i < pLobby->GetNumMembers(); i++ )
  2003. {
  2004. const CTFLobbyMember *pMemberDetails = pLobby->GetMemberDetails( i );
  2005. Assert( pMemberDetails );
  2006. if ( !pMemberDetails )
  2007. continue;
  2008. CSteamID steamID( pMemberDetails->id() );
  2009. CTFLobbyMember_ConnectState eLobbyState = pLobby->GetMemberConnectState( i );
  2010. if ( eLobbyState == CTFLobbyMember_ConnectState_RESERVATION_PENDING )
  2011. {
  2012. CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( pLobby->GetMember( i ) );
  2013. if ( !pPlayer || pPlayer->bDropped )
  2014. {
  2015. // Lobby has a new player we don't think is in our match, force an update to acknowledge them ASAP
  2016. bNeedsToUpdatePlayerAndServer = true;
  2017. }
  2018. }
  2019. }
  2020. if ( bNeedsToUpdatePlayerAndServer )
  2021. {
  2022. UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers, true );
  2023. }
  2024. // If we terminated while the new match ID was pending we're still unwinding the incoming messages
  2025. bool bNewMatchID = m_pMatchInfo && !m_pMatchInfo->BMatchTerminated() && ( m_pMatchInfo->m_nMatchID != pLobby->GetMatchID() );
  2026. if ( bNewMatchID )
  2027. {
  2028. if ( m_bWaitingForNewMatchID && m_pMatchInfo->m_bServerCreated )
  2029. {
  2030. // We sent a request for a new matchID to put in for the match
  2031. // we're running, and it just came back.
  2032. MMLog( "Received new matchID for server-created match. "
  2033. "New matchID [ %llu ]\n",
  2034. pLobby->GetMatchID() );
  2035. m_pMatchInfo->m_nMatchID = pLobby->GetMatchID();
  2036. m_pMatchInfo->m_bServerCreated = false;
  2037. }
  2038. else if ( m_bWaitingForNewMatchID && m_flWaitingForNewMatchTime != 0.f )
  2039. {
  2040. // We're counting down to launching a new match, and the new match ID arrived. We'll pick it up from the
  2041. // lobby in LaunchNewMatchForLobby
  2042. MMLog( "Received new matchID while waiting for new matchID. "
  2043. "Old match ID [ %llu ] ended [ %u ] "
  2044. "New matchID [ %llu ]\n",
  2045. m_pMatchInfo->m_nMatchID, m_pMatchInfo->m_bMatchEnded,
  2046. pLobby->GetMatchID() );
  2047. }
  2048. else if ( !m_bWaitingForNewMatchID && m_flWaitingForNewMatchTime == 0.f )
  2049. {
  2050. // A lobby came in with a match ID that's not what our current
  2051. // one is, and we were not expecting this.
  2052. //
  2053. // Note that we hold on to the stale lobby between NewMatchForLobby and LaunchNewMatchForLobby, so we
  2054. // don't panic if the stale lobby updates. The only other way out of that state is terminating the
  2055. // match.
  2056. MMLog( "Received new matchID when we weren't expecting one! "
  2057. "Current matchID [ %llu ] "
  2058. "New matchID [ %llu ]\n",
  2059. m_pMatchInfo->m_nMatchID,
  2060. pLobby->GetMatchID() );
  2061. AbortInvalidMatchState();
  2062. }
  2063. }
  2064. }
  2065. }
  2066. void CTFGCServerSystem::SODestroyed( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent )
  2067. {
  2068. // Lobby handling
  2069. if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID )
  2070. {
  2071. // Lobby is gone! Reset
  2072. UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, true );
  2073. }
  2074. }
  2075. const CTFGSLobby *CTFGCServerSystem::GetLobby() const
  2076. {
  2077. if ( !m_ourSteamID.IsValid() )
  2078. return NULL;
  2079. GCSDK::CGCClientSharedObjectCache *pSOCache = GCClientSystem()->GetSOCache( m_ourSteamID );
  2080. if ( !pSOCache )
  2081. return NULL;
  2082. CSharedObjectTypeCache *pTypeCache = pSOCache->FindBaseTypeCache( CTFGSLobby::k_nTypeID );
  2083. if ( pTypeCache && pTypeCache->GetCount() > 0 )
  2084. {
  2085. AssertMsg1( pTypeCache->GetCount() == 1, "Server has %d lobby objects in his cache! He should only have 1.", pTypeCache->GetCount() );
  2086. const CTFGSLobby *pLobby = static_cast<CTFGSLobby*>( pTypeCache->GetObject( pTypeCache->GetCount() - 1 ) );
  2087. return pLobby;
  2088. }
  2089. return NULL;
  2090. }
  2091. CTFGSLobby *CTFGCServerSystem::GetLobby()
  2092. {
  2093. // It's safe to un-constify the returned lobby if we're being called through a non-const reference ourselves.
  2094. return const_cast< CTFGSLobby * >( ((const CTFGCServerSystem *)this)->GetLobby() );
  2095. }
  2096. void CTFGCServerSystem::DumpLobby()
  2097. {
  2098. CTFGSLobby *pLobby = GetLobby();
  2099. if ( !pLobby )
  2100. {
  2101. Msg( "Failed to find lobby shared object\n" );
  2102. return;
  2103. }
  2104. pLobby->SpewDebug();
  2105. }
  2106. bool CTFGCServerSystem::HasLobby() const
  2107. {
  2108. return GetLobby() != NULL;
  2109. }
  2110. void CTFGCServerSystem::SetHibernation( bool bHibernating )
  2111. {
  2112. // !FIXME! Need to get rid of all the hibernation crap. We don't really need it
  2113. }
  2114. bool CTFGCServerSystem::ShouldHideServer()
  2115. {
  2116. // !NO! Don't set this right now. We'll just pass the "hidden" tag and so the server
  2117. // browser wil not list us.
  2118. // if ( m_bMMServerMode && tf_mm_strict.GetBool() )
  2119. // return true;
  2120. return false;
  2121. }
  2122. bool CTFGCServerSystem::SteamIDAllowedToConnect(const CSteamID &steamID) const
  2123. {
  2124. // If we're not in strict mode, anybody can join!
  2125. if ( !m_bMMServerMode || tf_mm_strict.GetInt() != 1 )
  2126. return true;
  2127. // If we don't have a match, nobody can join
  2128. const CMatchInfo *pMatchInfo = GetMatch();
  2129. if ( !pMatchInfo )
  2130. {
  2131. return false;
  2132. }
  2133. const CMatchInfo::PlayerMatchData_t *pMatchData = pMatchInfo->GetMatchDataForPlayer( steamID );
  2134. if ( !pMatchData || pMatchData->bDropped )
  2135. {
  2136. // Not in the match or was dropped, reject
  2137. return false;
  2138. }
  2139. return true;
  2140. }
  2141. ////-----------------------------------------------------------------------------
  2142. //int CTFGCServerSystem::GetTeamForLobbyMember( const CSteamID &steamId ) const
  2143. //{
  2144. // const CTFGSLobby *pLobby = GetLobby();
  2145. // if ( !pLobby )
  2146. // {
  2147. // return DOTA_TEAM_NOTEAM;
  2148. // }
  2149. //
  2150. // int team = pLobby->GetMemberTeam( steamId );
  2151. //
  2152. // switch ( team )
  2153. // {
  2154. // case DOTA_GC_TEAM_GOOD_GUYS:
  2155. // return DOTA_TEAM_GOODGUYS;
  2156. //
  2157. // case DOTA_GC_TEAM_BAD_GUYS:
  2158. // return DOTA_TEAM_BADGUYS;
  2159. //
  2160. // case DOTA_GC_TEAM_BROADCASTER:
  2161. // case DOTA_GC_TEAM_PLAYER_POOL:
  2162. // case DOTA_GC_TEAM_SPECTATOR:
  2163. // return TEAM_SPECTATOR;
  2164. // }
  2165. //
  2166. // return DOTA_TEAM_NOTEAM;
  2167. //}
  2168. //
  2169. ////-----------------------------------------------------------------------------
  2170. //bool CTFGCServerSystem::IsLobbyMemberBroadcaster( const CSteamID &steamId ) const
  2171. //{
  2172. // const CTFGSLobby *pLobby = GetLobby();
  2173. // if ( !pLobby )
  2174. // {
  2175. // return false;
  2176. // }
  2177. //
  2178. // return pLobby->GetMemberTeam( steamId ) == DOTA_GC_TEAM_BROADCASTER;
  2179. //}
  2180. //
  2181. ////-----------------------------------------------------------------------------
  2182. //ELanguage CTFGCServerSystem::GetBroadcasterLanguage( const CSteamID &steamId ) const
  2183. //{
  2184. // const CTFGSLobby *pLobby = GetLobby();
  2185. // if ( !pLobby )
  2186. // {
  2187. // return k_Lang_English;
  2188. // }
  2189. //
  2190. // if ( pLobby->GetMemberTeam( steamId ) != DOTA_GC_TEAM_BROADCASTER )
  2191. // return k_Lang_English;
  2192. //
  2193. // int index = pLobby->GetMemberIndexBySteamID( steamId );
  2194. // if ( index < 0 )
  2195. // return k_Lang_English;
  2196. //
  2197. // const CTFLobbyMember* pMember = pLobby->GetMemberDetails( index );
  2198. // switch( pMember->slot() )
  2199. // {
  2200. // default:
  2201. // case 1:
  2202. // return k_Lang_English;
  2203. // case 2:
  2204. // return k_Lang_German;
  2205. // case 3:
  2206. // return k_Lang_Simplified_Chinese;
  2207. // case 4:
  2208. // return k_Lang_Russian;
  2209. // }
  2210. //
  2211. // return k_Lang_English;
  2212. //}
  2213. //-----------------------------------------------------------------------------
  2214. CON_COMMAND( tf_server_lobby_debug, "Prints server lobby object" )
  2215. {
  2216. GTFGCClientSystem()->DumpLobby();
  2217. }
  2218. ConVar dbg_spew_connected_players_level( "dbg_spew_connected_players_level", "0", FCVAR_NONE, "If enabled, server will spew connected player GC updates\n" );
  2219. // Inform the GC of any change in the connected players
  2220. void CTFGCServerSystem::UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event event, bool bForceSendMessages )
  2221. {
  2222. VPROF_BUDGET( "CTFGCServerSystem::UpdateConnectedPlayersAndServerInfo", VPROF_BUDGETGROUP_OTHER_NETWORKING );
  2223. // Don't bother sending if we aren't initialized yet
  2224. if ( gpGlobals->maxClients == 0 || TFGameRules() == NULL )
  2225. return;
  2226. /// TODO ROLLING MATCH: Remove event field from this message. We might just ignore some events, and they're not
  2227. /// useful.
  2228. // Don't send heartbeats while we're waiting for reliable messages to process, our state is not in sync with what we
  2229. // tried to send to the GC, and sending a new heartbeat before pending messages have been responded to isn't
  2230. // helpful.
  2231. if ( BPendingReliableMessages() )
  2232. { return; }
  2233. // Or if we're in the waiting period to kick off a new match -- if all pending messages came back, our lobby now
  2234. // reflects the requested match, but we haven't actually launched it yet, so heartbeats would not be valid.
  2235. if ( m_flWaitingForNewMatchTime != 0.f )
  2236. { return; }
  2237. const CTFGSLobby *pLobby = GetLobby();
  2238. if ( !pLobby || !m_bMMServerMode )
  2239. {
  2240. Assert( event == CMsgGameServerMatchmakingStatus_Event_None );
  2241. }
  2242. double now = Plat_FloatTime();
  2243. if ( dbg_spew_connected_players_level.GetInt() >= 4 ) { Msg( "UpdateConnectedPlayers ======================================\n" ); }
  2244. static ConVarRef sv_visiblemaxplayers( "sv_visiblemaxplayers" );
  2245. CProtoBufMsg<CMsgGameServerMatchmakingStatus> msg( k_EMsgGCGameServerMatchmakingStatus );
  2246. ServerMatchmakingState eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
  2247. TF_MatchmakingMode eGameServerInfoMatchmakingMode = TF_Matchmaking_INVALID;
  2248. CUtlString sGameServerInfoMap;
  2249. CUtlString sGameServerInfoTags;
  2250. int nBotCountToSend = -1;
  2251. float flSendInterval = 60.0f;
  2252. int nUnconnectedPlayerReservationRequests = 0;
  2253. bool bLobbyIncorrect = false;
  2254. CUtlVector<CSteamID> vecFailedLoaders;
  2255. TF_GC_GameState gcState = TF_GC_GAMESTATE_DISCONNECT;
  2256. CMatchInfo *pMatch = GetMatch();
  2257. const IMatchGroupDescription* pMatchDesc = pMatch ? GetMatchGroupDescription( pMatch->m_eMatchGroup ) : NULL;
  2258. // Build list of currently connected clients, and classify them according to their role
  2259. struct Reservation_t
  2260. {
  2261. CSteamID m_steamID;
  2262. int m_nEntindex;
  2263. bool m_bActive;
  2264. };
  2265. CUtlVector< Reservation_t > vecReservationRequests;
  2266. CUtlVector<CSteamID> vecConnectedPlayers;
  2267. int nAdminSlots = 0;
  2268. int nAdHocPlayers = 0;
  2269. int nMatchPlayers = 0;
  2270. int nBots = 0;
  2271. for ( int i = 1; i <= gpGlobals->maxClients; i++ )
  2272. {
  2273. const CSteamID *pPlayerSteamID = engine->GetClientSteamIDByPlayerIndex( i );
  2274. // Filter out non-players
  2275. player_info_t sPlayerInfo;
  2276. bool bActive = false;
  2277. if ( engine->GetPlayerInfo( i, &sPlayerInfo ) )
  2278. {
  2279. if ( sPlayerInfo.ishltv || sPlayerInfo.isreplay )
  2280. {
  2281. ++nAdminSlots;
  2282. continue;
  2283. }
  2284. if ( sPlayerInfo.fakeplayer )
  2285. {
  2286. ++nBots;
  2287. continue;
  2288. }
  2289. if ( pPlayerSteamID == NULL || !pPlayerSteamID->IsValid() )
  2290. {
  2291. // This can occur in lan-mode
  2292. Warning( "Player with no steam ID, counting as ad-hoc\n" );
  2293. }
  2294. bActive = true;
  2295. }
  2296. else
  2297. {
  2298. // Client not "active", but might be connected.
  2299. // this happens during changelevel
  2300. if ( pPlayerSteamID == NULL || !pPlayerSteamID->IsValid() )
  2301. {
  2302. continue;
  2303. }
  2304. // Connected, but not active.
  2305. bActive = false;
  2306. // Shove in a dummy name or debug spew
  2307. V_strcpy_safe( sPlayerInfo.name, pPlayerSteamID->Render() );
  2308. }
  2309. // Some kind of player, add them to match players or ad-hoc
  2310. CSteamID playerSteamID;
  2311. if ( pPlayerSteamID && pPlayerSteamID->IsValid() )
  2312. playerSteamID = *pPlayerSteamID;
  2313. CMatchInfo::PlayerMatchData_t *pMatchPlayer = ( pMatch && playerSteamID.IsValid() ) \
  2314. ? pMatch->GetMatchDataForPlayer( playerSteamID ) \
  2315. : NULL;
  2316. bool bMatchPlayer = pMatchPlayer && !pMatchPlayer->bDropped;
  2317. if ( bMatchPlayer )
  2318. { ++nMatchPlayers; }
  2319. else
  2320. { ++nAdHocPlayers; }
  2321. if ( dbg_spew_connected_players_level.GetInt() >= 4 ) { Msg( " Client[%d]: %s '%s':\n", i, playerSteamID.Render(), sPlayerInfo.name ); }
  2322. //
  2323. // !! In lan mode, this player may not have a steamID. They can't be a lobby member or similar, so the below
  2324. // !! code should just assume they're ad-hoc if !playerSteamID.IsValid()
  2325. //
  2326. if ( playerSteamID.IsValid() )
  2327. vecConnectedPlayers.AddToTail( playerSteamID );
  2328. // If we don't have a lobby, then we may still be running a match after a GC crash/reboot, in which case the
  2329. // lobby might've been lost -- but we're still expected to complete the match on our own authority and report
  2330. // the result.
  2331. /// XXX(JohnS): Ideally, in the state where the GC rebooted and the lobby disintegrated, we'd have some way
  2332. /// to tell the GC to recreate the lobby on its end when we re-establish, rather than finishing
  2333. /// out a phantom match -- it doesn't know the user is still in a match until the match result
  2334. /// arrives. However, as we locally track and report the match result and any abandons, the user
  2335. /// can't really exploit this state other than potentially alt-F4ing and requeuing faster than
  2336. /// their abandon timeout. The GC, however, loses the ability to kick the player from this
  2337. /// lobby. (that it no longer knows about)
  2338. if ( pLobby )
  2339. {
  2340. // If he's in the lobby, them count him as a connected player.
  2341. // Otherwise, he's an ad-hoc join.
  2342. CMsgGameServerMatchmakingStatus_PlayerConnectState sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_INVALID;
  2343. const CTFLobbyMember *pMember = pLobby->GetMemberDetails( playerSteamID );
  2344. if ( pMember )
  2345. {
  2346. CTFLobbyMember_ConnectState eLobbyState = pMember->connect_state();
  2347. if ( dbg_spew_connected_players_level.GetInt() >= 4 )
  2348. {
  2349. Msg( " '%s' In lobby with state %s\n", sPlayerInfo.name,
  2350. CTFLobbyMember_ConnectState_Name( eLobbyState ).c_str() );
  2351. }
  2352. switch ( eLobbyState )
  2353. {
  2354. case CTFLobbyMember_ConnectState_RESERVATION_PENDING:
  2355. // Check if we have match data for this guy
  2356. if ( !bMatchPlayer )
  2357. {
  2358. bLobbyIncorrect = true;
  2359. vecReservationRequests.AddToTail( { *pPlayerSteamID, i, bActive } );
  2360. }
  2361. break;
  2362. case CTFLobbyMember_ConnectState_RESERVED:
  2363. // Only count them as actually "connected" if they are active.
  2364. // We do not count them as "connected", to make sure we treat a
  2365. // disconnection before they become "active" as a failure to load,
  2366. // but a disconnection after they become active as a "leaver"
  2367. if ( bActive )
  2368. {
  2369. sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED;
  2370. bLobbyIncorrect = true;
  2371. }
  2372. else
  2373. {
  2374. sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED;
  2375. if ( eLobbyState != CTFLobbyMember_ConnectState_RESERVED )
  2376. bLobbyIncorrect = true;
  2377. }
  2378. break;
  2379. case CTFLobbyMember_ConnectState_CONNECTED:
  2380. sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED;
  2381. break;
  2382. case CTFLobbyMember_ConnectState_DISCONNECTED:
  2383. sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED;
  2384. bLobbyIncorrect = true;
  2385. break;
  2386. default:
  2387. AssertMsg1( false, "Unknown lobby member state %d", eLobbyState );
  2388. break;
  2389. }
  2390. }
  2391. else if ( m_pMatchInfo && !m_pMatchInfo->m_bMatchEnded )
  2392. {
  2393. // Competitive match, player missing from lobby
  2394. if ( bMatchPlayer )
  2395. {
  2396. // Player was part of the match, but GC removed them.
  2397. MMLog( "Removing match player %s -- dropped from lobby, but still in match and game\n",
  2398. playerSteamID.Render() );
  2399. EjectMatchPlayer( playerSteamID, TFMatchLeaveReason_GC_REMOVED );
  2400. nMatchPlayers--;
  2401. }
  2402. else if ( tf_mm_strict.GetInt() == 1 )
  2403. {
  2404. // A player is present that shouldn't be
  2405. MMLog( "!! Unknown player in managed match %s\n", playerSteamID.Render() );
  2406. KickRemovedMatchPlayer( playerSteamID );
  2407. nAdHocPlayers--;
  2408. }
  2409. }
  2410. else
  2411. {
  2412. // Not a managed match
  2413. if ( dbg_spew_connected_players_level.GetInt() >= 4 )
  2414. {
  2415. Msg( " '%s' Not in lobby, client is ad-hoc join\n", sPlayerInfo.name );
  2416. }
  2417. }
  2418. if ( sendPlayerConnectState != CMsgGameServerMatchmakingStatus_PlayerConnectState_INVALID )
  2419. {
  2420. CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players();
  2421. pMsgPlayer->set_steam_id( playerSteamID.ConvertToUint64() );
  2422. pMsgPlayer->set_connect_state( sendPlayerConnectState );
  2423. }
  2424. }
  2425. } // end For each client
  2426. //
  2427. // Now, check match for players that we are tracking but are not connected, and count them in the total and the
  2428. // status message
  2429. //
  2430. if ( pMatch && !pMatch->BMatchTerminated() )
  2431. {
  2432. int nTotalMatch = pMatch->GetNumTotalMatchPlayers();
  2433. for ( int idx = 0; idx < nTotalMatch; idx++ )
  2434. {
  2435. CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( idx );
  2436. // Don't care if they are now dropped or were handled in the connected players loop above
  2437. if ( pPlayer->bDropped || vecConnectedPlayers.Find( pPlayer->steamID ) != vecConnectedPlayers.InvalidIndex() )
  2438. { continue; }
  2439. if ( pPlayer->bConnected )
  2440. {
  2441. MMLog( "!! Match player %s not present but marked connected\n", pPlayer->steamID.Render() );
  2442. }
  2443. // Note that if the GC lost our lobby (which should only occur due to system failure on the other end), we
  2444. // just keep dutifully sending status updates for the players we have as long as we have a match
  2445. if ( pLobby && !pLobby->GetMemberDetails( pPlayer->steamID ) )
  2446. {
  2447. // Player was part of the match, but GC removed them.
  2448. MMLog( "Removing player %s, not present in match and dropped from lobby\n",
  2449. pPlayer->steamID.Render() );
  2450. SetMatchPlayerDropped( pPlayer->steamID, TFMatchLeaveReason_GC_REMOVED );
  2451. }
  2452. else
  2453. {
  2454. // We are holding a valid reservation. Add this fact to the message, to confirm
  2455. // that we are aware of the player.
  2456. nMatchPlayers++;
  2457. CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players();
  2458. pMsgPlayer->set_steam_id( pPlayer->steamID.ConvertToUint64() );
  2459. if ( dbg_spew_connected_players_level.GetInt() >= 4 )
  2460. { Msg( " Player[%d]: %s reserved\n", msg.Body().players_size(), pPlayer->steamID.Render() ); }
  2461. pMsgPlayer->set_connect_state( CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED );
  2462. bLobbyIncorrect = true;
  2463. }
  2464. }
  2465. }
  2466. //
  2467. // Scan lobby, and check for lobby player entries that don't match our local state.
  2468. //
  2469. if ( pLobby )
  2470. {
  2471. if ( dbg_spew_connected_players_level.GetInt() >= 4 )
  2472. { Msg( "Checking all connected players are marked connected in lobby:\n" ); }
  2473. for ( int i = 0; i < pLobby->GetNumMembers(); i++ )
  2474. {
  2475. const CTFLobbyMember *pMemberDetails = pLobby->GetMemberDetails( i );
  2476. Assert( pMemberDetails );
  2477. if ( !pMemberDetails )
  2478. continue;
  2479. CSteamID steamID( pMemberDetails->id() );
  2480. CTFLobbyMember_ConnectState eLobbyState = pLobby->GetMemberConnectState( i );
  2481. if ( dbg_spew_connected_players_level.GetInt() >= 4 )
  2482. { Msg( " Lobby member %s is in state %s\n", steamID.Render(), CTFLobbyMember_ConnectState_Name( eLobbyState ).c_str() ); }
  2483. int iConnectedPlayer = vecConnectedPlayers.Find( steamID );
  2484. if ( iConnectedPlayer >= 0 )
  2485. { continue; } // we handled them earlier
  2486. // Player is not currently connected. Check against what the lobby thinks
  2487. switch ( eLobbyState )
  2488. {
  2489. case CTFLobbyMember_ConnectState_RESERVATION_PENDING:
  2490. {
  2491. // Check if we already have a reservation for this guy
  2492. CMatchInfo::PlayerMatchData_t *pMatchPlayer = GetMatch() ? GetMatch()->GetMatchDataForPlayer( steamID ) : NULL;
  2493. if ( GetMatch() && ( !pMatchPlayer || pMatchPlayer->bDropped ) )
  2494. {
  2495. bLobbyIncorrect = true;
  2496. vecReservationRequests.AddToTail( { steamID, 0, false } );
  2497. ++nUnconnectedPlayerReservationRequests;
  2498. }
  2499. else
  2500. {
  2501. if ( dbg_spew_connected_players_level.GetInt() >= 4 )
  2502. { Msg( " Player[%d]: %s requested reservation. We already had one.\n", msg.Body().players_size(), steamID.Render() ); }
  2503. }
  2504. } break;
  2505. case CTFLobbyMember_ConnectState_RESERVED:
  2506. // We'll handle it below when we process our reservations
  2507. break;
  2508. case CTFLobbyMember_ConnectState_CONNECTED:
  2509. if ( dbg_spew_connected_players_level.GetInt() >= 4 )
  2510. { Msg( " Lobby member %s no longer connected, lobby is incorrect\n", steamID.Render() ); }
  2511. bLobbyIncorrect = true;
  2512. break;
  2513. case CTFLobbyMember_ConnectState_DISCONNECTED:
  2514. break;
  2515. default:
  2516. AssertMsg1( false, "Unknown lobby member state %d", eLobbyState );
  2517. break;
  2518. }
  2519. }
  2520. }
  2521. // Now we've scanned connected players, our match, and the lobby object. Count up the total taken slots, and how
  2522. // many slots the match could have (0 if no match) These are slots that are spoken for, not necessarily currently
  2523. // connected
  2524. // NOTE: These might be updated by accepting reservations or dropping players in the next section
  2525. bool bLiveMatch = pMatch && pMatchDesc && !pMatch->m_bMatchEnded;
  2526. // TODO ROLLING MATCHES: Need a check for no-latejoins state for after we've sent a match result?
  2527. int nMaxMatchPlayers = bLiveMatch ? pMatch->GetCanonicalMatchSize() : 0;
  2528. int nMaxHumans = gpGlobals->maxClients - nAdminSlots;
  2529. {
  2530. // Maybe cap visible max humans. Honor the override MvM mode might apply, but if we are accepting arbitrary new
  2531. // matches expose the real value we would allow a new potentially non-mvm match to use.
  2532. int nLimitVisibleSlots = sv_visiblemaxplayers.GetInt();
  2533. if ( m_bOverridingVisibleMaxPlayers && !bLiveMatch && m_bMMServerMode )
  2534. { nLimitVisibleSlots = m_iSavedVisibleMaxPlayers; }
  2535. // Don't limit visible slots to below the current match
  2536. if ( nMaxMatchPlayers > 0 )
  2537. { nLimitVisibleSlots = Max( nMaxMatchPlayers, nLimitVisibleSlots ); }
  2538. if ( nLimitVisibleSlots > 0 )
  2539. { nMaxHumans = Min( nMaxHumans, nLimitVisibleSlots ); }
  2540. }
  2541. int nHumans = nAdHocPlayers + nMatchPlayers;
  2542. int nClients = nHumans + nBots + nAdminSlots;
  2543. // Maximum nHumans should be allowed to be. Max clients - AdminSlots, capped to visiblemaxplayers
  2544. // If we've never added a player to our match this is the first think
  2545. bool bNewMatch = bLiveMatch && pMatch->GetNumTotalMatchPlayers() == 0;
  2546. // If our current state allows us to accept new match players
  2547. bool bRequestMatchLateJoin = bLiveMatch && \
  2548. nHumans < nMaxMatchPlayers && \
  2549. nClients < gpGlobals->maxClients && \
  2550. pMatchDesc->ShouldRequestLateJoin();
  2551. //
  2552. // Check if the GC is requesting us to make some more reservations, and accepting them would not exceed
  2553. // desired match size or engine capabilities.
  2554. //
  2555. if ( pLobby && vecReservationRequests.Count() &&
  2556. ( bNewMatch || bRequestMatchLateJoin ) &&
  2557. nUnconnectedPlayerReservationRequests + nHumans <= nMaxMatchPlayers &&
  2558. nUnconnectedPlayerReservationRequests + nClients <= gpGlobals->maxClients )
  2559. {
  2560. MMLog( "GC is requesting us to reserve %d slots.\n", vecReservationRequests.Count() );
  2561. // Accept one at a time and check if we can handle more
  2562. FOR_EACH_VEC( vecReservationRequests, idx )
  2563. {
  2564. const CTFLobbyMember *pMember = pLobby->GetMemberDetails( vecReservationRequests[ idx ].m_steamID );
  2565. AcceptGCReservation( vecReservationRequests[ idx ].m_steamID, pMember, !bNewMatch,
  2566. vecReservationRequests[ idx ].m_nEntindex, vecReservationRequests[ idx ].m_bActive );
  2567. // Add them to our message for this pass
  2568. CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players();
  2569. pMsgPlayer->set_steam_id( vecReservationRequests[ idx ].m_steamID.ConvertToUint64() );
  2570. pMsgPlayer->set_connect_state( CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED );
  2571. }
  2572. // We promised more people slots, recompute this
  2573. nMatchPlayers += nUnconnectedPlayerReservationRequests;
  2574. nHumans += nUnconnectedPlayerReservationRequests;
  2575. nClients += nUnconnectedPlayerReservationRequests;
  2576. bRequestMatchLateJoin = bRequestMatchLateJoin && \
  2577. nHumans < nMaxMatchPlayers && \
  2578. nClients < gpGlobals->maxClients && \
  2579. pMatchDesc && pMatchDesc->ShouldRequestLateJoin();
  2580. }
  2581. else if ( nUnconnectedPlayerReservationRequests )
  2582. {
  2583. MMLog( "Refused %d reservations -- not accepting match players or exceeds capacity\n",
  2584. vecReservationRequests.Count() );
  2585. }
  2586. // Check if they think that they are acknowledging some players, make sure
  2587. // we would have decided to send a message anyway, even without their event
  2588. if ( event == CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers )
  2589. {
  2590. Assert( bLobbyIncorrect == true );
  2591. }
  2592. //
  2593. // Clean up complete match if all players have left and the GC has dissolved the lobby.
  2594. //
  2595. // Deleting this should clear us up to accept new matches below,
  2596. // where our ready-for-match state depends on !pLobby && !pMatch.
  2597. //
  2598. // Don't clean up if GC hasn't acknowledged dissolution of lobby yet, or we'll have a lobby with no associated
  2599. // match to indicate what state it was in. If the GC is MIA to clean-up lobbies that's okay, we can't start a
  2600. // new match until it's ready anyway, and the empty-with-lobby below check will kill us if we get stuck in this
  2601. // state.
  2602. if ( vecConnectedPlayers.Count() == 0 &&
  2603. m_pMatchInfo && !pLobby && m_pMatchInfo->m_bMatchEnded )
  2604. {
  2605. MMLog( "Cleaning out finished match %llu\n", m_pMatchInfo->m_nMatchID );
  2606. delete m_pMatchInfo;
  2607. m_pMatchInfo = NULL;
  2608. bLiveMatch = false;
  2609. pMatch = NULL;
  2610. pMatchDesc = NULL;
  2611. }
  2612. // Check if we're empty with a lobby. Ordinarily, we shouldn't linger too long in this state. Either we're in
  2613. // the process of timing out everyone as abandoners (which should take a lot less than this timeout) or the GC
  2614. // is down. But if that state persists for two hours, assume we're in a bad stuck state and reboot.
  2615. if ( pLobby && vecConnectedPlayers.Count() == 0 )
  2616. {
  2617. if ( m_flTimeBecameEmptyWithLobby == 0.0 )
  2618. {
  2619. m_flTimeBecameEmptyWithLobby = now;
  2620. }
  2621. else
  2622. {
  2623. int nSecondsEmptyWithLobby = int( now - m_flTimeBecameEmptyWithLobby );
  2624. int nTimeoutMinutes = ( BPendingReliableMessages() || m_pMatchInfo ) ? k_InvalidState_Timeout_With_Match \
  2625. : k_InvalidState_Timeout_Without_Match;
  2626. if ( nSecondsEmptyWithLobby > nTimeoutMinutes*60 )
  2627. {
  2628. MMLog( "**** Server has been empty with a lobby for %d seconds. Quitting\n", nSecondsEmptyWithLobby );
  2629. AbortInvalidMatchState();
  2630. }
  2631. }
  2632. }
  2633. else
  2634. {
  2635. m_flTimeBecameEmptyWithLobby = 0.0;
  2636. }
  2637. // Determine game state
  2638. gcState = TF_GC_GAMESTATE_GAME_IN_PROGRESS;
  2639. switch ( TFGameRules()->State_Get() )
  2640. {
  2641. case GR_STATE_INIT:
  2642. gcState = TF_GC_GAMESTATE_STATE_INIT;
  2643. break;
  2644. case GR_STATE_PREGAME:
  2645. case GR_STATE_STARTGAME:
  2646. case GR_STATE_PREROUND:
  2647. case GR_STATE_RESTART:
  2648. gcState = TF_GC_GAMESTATE_STRATEGY_TIME;
  2649. break;
  2650. default:
  2651. Assert( false );
  2652. case GR_STATE_RND_RUNNING:
  2653. case GR_STATE_BETWEEN_RNDS:
  2654. case GR_STATE_BONUS:
  2655. break;
  2656. case GR_STATE_TEAM_WIN:
  2657. case GR_STATE_STALEMATE:
  2658. if ( TFGameRules()->IsMannVsMachineMode() )
  2659. {
  2660. // *Currently* can only end in victory (or dissolves because everyone leaves)
  2661. if (
  2662. TFGameRules()->State_Get() == GR_STATE_TEAM_WIN
  2663. && TFGameRules()->GetWinningTeam() == TF_TEAM_PVE_DEFENDERS )
  2664. {
  2665. gcState = TF_GC_GAMESTATE_POST_GAME;
  2666. }
  2667. }
  2668. else if ( TFGameRules()->IsCompetitiveMode() )
  2669. {
  2670. if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER )
  2671. {
  2672. gcState = TF_GC_GAMESTATE_POST_GAME;
  2673. }
  2674. }
  2675. break;
  2676. case GR_STATE_GAME_OVER:
  2677. gcState = TF_GC_GAMESTATE_GAME_IN_PROGRESS;
  2678. if ( TFGameRules()->IsMannVsMachineMode() ||
  2679. TFGameRules()->IsCompetitiveMode() ) // right?
  2680. {
  2681. gcState = TF_GC_GAMESTATE_DISCONNECT;
  2682. }
  2683. break;
  2684. }
  2685. // What state are we?
  2686. if ( m_bMMServerMode )
  2687. {
  2688. static ConVarRef sv_tags( "sv_tags" );
  2689. eGameServerInfoMatchmakingMode = TF_Matchmaking_LADDER;
  2690. nBotCountToSend = -1;
  2691. sGameServerInfoMap = STRING( gpGlobals->mapname );
  2692. sGameServerInfoTags = sv_tags.GetString();
  2693. sGameServerInfoTags.Clear();
  2694. // Set the "map" to the current challenge, if in MvM
  2695. if ( TFGameRules()->IsMannVsMachineMode() )
  2696. {
  2697. const char *pszFilenameShort = g_pPopulationManager ? g_pPopulationManager->GetPopulationFilenameShort() : NULL;
  2698. if ( pszFilenameShort && pszFilenameShort[0] )
  2699. {
  2700. sGameServerInfoMap = pszFilenameShort;
  2701. }
  2702. }
  2703. // Determine state
  2704. if ( !m_pMatchInfo && !pLobby )
  2705. {
  2706. // No match, lobby, or players, ready for match
  2707. if ( BPendingReliableMessages() )
  2708. {
  2709. eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
  2710. if ( m_eLastGameServerUpdateState != eGameServerInfoState )
  2711. { MMLog( "No match, but have not finished sending reliable messages, not re-enrolling in MM yet\n" ); }
  2712. }
  2713. else
  2714. {
  2715. eGameServerInfoState = ServerMatchmakingState_EMPTY;
  2716. if ( m_eLastGameServerUpdateState != eGameServerInfoState )
  2717. { MMLog( "No match, but configured for MM, enrolling in matchmaking\n" ); }
  2718. }
  2719. // Unless we're not setup with no actual usable slots or have random unknown humans in strict mode
  2720. if ( nClients >= gpGlobals->maxClients || nMaxHumans < 1 ||
  2721. ( nHumans && tf_mm_strict.GetInt() == 1 ) )
  2722. {
  2723. eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
  2724. if ( m_eLastGameServerUpdateState != eGameServerInfoState )
  2725. {
  2726. MMLog( "!! No match, but no usable slots or unexpected clients, cannot enroll in matchmaking. "
  2727. "[ nClients %d, maxClients %d, nHumans %d, nMaxHumans %d ]\n",
  2728. nClients, gpGlobals->maxClients, nHumans, nMaxHumans );
  2729. }
  2730. }
  2731. }
  2732. else if ( bLiveMatch )
  2733. {
  2734. // Have a running match.
  2735. eGameServerInfoState = bRequestMatchLateJoin ? ServerMatchmakingState_ACTIVE_MATCH_REQUESTING_LATE_JOIN \
  2736. : ServerMatchmakingState_ACTIVE_MATCH;
  2737. }
  2738. else
  2739. {
  2740. // We have a match but it isn't live, or we have no match but the GC hasn't torn down the lobby yet ( we
  2741. // should have either rejected the lobby in SOCreated or sent a cleanup message when ending the match, but
  2742. // our GC connection may be lagged, just stay out of the pool until we reconcile )
  2743. if ( m_eLastGameServerUpdateState != eGameServerInfoState )
  2744. { MMLog( "Match state is not in sync with GC, remaining out of MM until lobby is cleaned up\n" ); }
  2745. eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
  2746. }
  2747. }
  2748. // This is probably not worth the risk / reward right now. We've given instructions
  2749. // telling server operators how to avoid this from happening, and it might break something
  2750. // // Check if we have a lobby, and they have switched to/from MvM mode, then don't
  2751. // // put us in matchmaking for now
  2752. // bool bMapIsMvmMap = ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() );
  2753. // if ( ( pLobby != NULL ) && ( bMapIsMvmMap != bIsMvmMode ) )
  2754. // {
  2755. // eGameServerInfoMatchmakingMode = TF_Matchmaking_INVALID;
  2756. // eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
  2757. // MMLog( "Sending NOT_PARTICIPATING. Is MvM Map: %d, tf_mm_servermode=%d\n", bMapIsMvmMap ? 1 : 0, tf_mm_servermode.GetInt() );
  2758. // }
  2759. int nSlotsFree = nMaxHumans - nHumans;
  2760. // Check if number of slots available is changing. Our urgency to notify the GC about this
  2761. // change depends on which direction it is changing!
  2762. if ( nSlotsFree < m_nLastGameServerUpdateSlotsFree )
  2763. {
  2764. // We currently have fewer slots available than the GC thinks we do.
  2765. // This is an important state change and we need to let the GC know about
  2766. // this immediately, otherwise it might ask us to fill reservations we cannot
  2767. // satisfy. We want the window for this race condition to be as small as
  2768. // possible.
  2769. bForceSendMessages = true;
  2770. }
  2771. else if ( nSlotsFree > m_nLastGameServerUpdateSlotsFree )
  2772. {
  2773. // We have more slots open than the GC thinks we do. We should let the GC
  2774. // know relatively soon, but it's really not urgent that we flush this out
  2775. // *immediately*. Also, because players come and go frequently (especially
  2776. // in PvP), having this timer avoids massive spam if tons of players all decide
  2777. // to leave at once.
  2778. flSendInterval = Min( flSendInterval, 10.0f );
  2779. }
  2780. // Check if we MUST send a message, no matter how recently we sent the last update.
  2781. if ( event == CMsgGameServerMatchmakingStatus_Event_None &&
  2782. !bForceSendMessages &&
  2783. ( eGameServerInfoState == m_eLastGameServerUpdateState ) &&
  2784. ( eGameServerInfoMatchmakingMode == m_eLastGameServerUpdateMatchmakingMode ) &&
  2785. // map changes are infrequent, and matter quite a bit, so always send them
  2786. Q_stricmp( m_sLastGameServerUpdateMap, sGameServerInfoMap ) == 0 )
  2787. {
  2788. // No need to send periodic updates if we're not participating and don't think we have a lobby or match at all.
  2789. if ( eGameServerInfoState == ServerMatchmakingState_NOT_PARTICIPATING && !pLobby && !m_pMatchInfo )
  2790. return;
  2791. // Check for certain rules changes. When they change, we care about them being
  2792. // eventually correct, but it's not urgent
  2793. if ( ( Q_stricmp( m_sLastGameServerUpdateTags, sGameServerInfoTags ) != 0 ) ||
  2794. ( nMaxHumans != m_nLastGameServerUpdateMaxHumans ) ||
  2795. ( nBotCountToSend != m_nLastGameServerUpdateBotCount ) )
  2796. {
  2797. flSendInterval = Min( flSendInterval, 20.0f );
  2798. }
  2799. // If lobby is incorrect in an ordinary way (player left, etc),
  2800. // flush the change decently quickly
  2801. if ( pLobby && bLobbyIncorrect )
  2802. {
  2803. // Send updates more quickly if the GC hasn't acknowledged, but don't DDoS. Ideally the event that made the
  2804. // lobby incorrect triggered a Update( bForce = true );
  2805. flSendInterval = Min( flSendInterval, 10.0f );
  2806. }
  2807. if ( now < m_timeLastSendGameServerInfoAndConnectedPlayers + flSendInterval )
  2808. { return; }
  2809. }
  2810. // Fill in info about our connection state
  2811. msg.Body().set_server_version( engine->GetServerVersion() );
  2812. msg.Body().set_matchmaking_state( eGameServerInfoState );
  2813. if ( eGameServerInfoState == ServerMatchmakingState_NOT_PARTICIPATING )
  2814. {
  2815. msg.Body().set_match_group( k_nMatchGroup_Invalid );
  2816. if ( dbg_spew_connected_players_level.GetInt() >= 2 )
  2817. {
  2818. MMLog("Sending CMsgGameServerMatchmakingStatus (state=%s)\n",
  2819. ServerMatchmakingState_Name( msg.Body().matchmaking_state() ).c_str() );
  2820. }
  2821. }
  2822. else
  2823. {
  2824. static ConVarRef sv_region( "sv_region" );
  2825. msg.Body().set_server_region( sv_region.GetInt() );
  2826. msg.Body().set_server_loadavg( GetCPUUsage() );
  2827. msg.Body().set_server_dedicated( engine->IsDedicatedServer() );
  2828. msg.Body().set_server_trusted( tf_mm_trusted.GetBool() );
  2829. msg.Body().set_matchmaking_mode( eGameServerInfoMatchmakingMode );
  2830. msg.Body().set_map( sGameServerInfoMap );
  2831. msg.Body().set_game_state( gcState );
  2832. if ( pLobby )
  2833. msg.Body().set_lobby_mm_version( pLobby->GetLobbyMMVersion() );
  2834. if ( nBotCountToSend >= 0 )
  2835. msg.Body().set_bot_count( (uint32)nBotCountToSend );
  2836. Assert( nMaxHumans > 0 );
  2837. msg.Body().set_max_players( nMaxHumans );
  2838. Assert( nSlotsFree >= 0 );
  2839. msg.Body().set_slots_free( nSlotsFree );
  2840. msg.Body().set_tags( sGameServerInfoTags );
  2841. msg.Body().set_strict( tf_mm_strict.GetInt() );
  2842. if ( event != CMsgGameServerMatchmakingStatus_Event_None )
  2843. { msg.Body().set_event( event ); }
  2844. if ( ( dbg_spew_connected_players_level.GetInt() >= 2 ) ||
  2845. ( event != CMsgGameServerMatchmakingStatus_Event_None && dbg_spew_connected_players_level.GetInt() >= 1 ) )
  2846. {
  2847. MMLog("Sending CMsgGameServerMatchmakingStatus (state=%s, slots_free=%d, event=%s, %s)\n",
  2848. ServerMatchmakingState_Name( msg.Body().matchmaking_state() ).c_str(),
  2849. msg.Body().slots_free(),
  2850. CMsgGameServerMatchmakingStatus_Event_Name( msg.Body().event() ).c_str(),
  2851. ( tf_mm_trusted.GetBool() ? ", trusted=true" : "" )
  2852. );
  2853. }
  2854. if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
  2855. {
  2856. msg.Body().set_mvm_credits_acquired( MannVsMachineStats_GetAcquiredCredits( -1 ) );
  2857. msg.Body().set_mvm_credits_dropped( MannVsMachineStats_GetAcquiredCredits( -1 ) );
  2858. msg.Body().set_mvm_wave( MannVsMachineStats_GetCurrentWave() );
  2859. }
  2860. EMatchGroup eCurrentGroup = k_nMatchGroup_Invalid;
  2861. if ( m_pMatchInfo )
  2862. {
  2863. eCurrentGroup = m_pMatchInfo->m_eMatchGroup;
  2864. }
  2865. msg.Body().set_match_group( eCurrentGroup );
  2866. }
  2867. // Check if we MUST send a message, no matter how recently we sent the last update.
  2868. if ( event == CMsgGameServerMatchmakingStatus_Event_None &&
  2869. !bForceSendMessages &&
  2870. ( msg.Body().lobby_mm_version() == m_nLastGameServerUpdateLobbyMMVersion ) &&
  2871. ( msg.Body().matchmaking_state() == m_eLastGameServerUpdateState ) &&
  2872. ( msg.Body().matchmaking_mode() == m_eLastGameServerUpdateMatchmakingMode ) &&
  2873. // map changes are infrequent, and matter quite a bit, so always send them
  2874. Q_stricmp( m_sLastGameServerUpdateMap, msg.Body().map().c_str() ) == 0 )
  2875. {
  2876. // No need to send periodic updates if we're not participating and don't think we have a lobby or match at all.
  2877. if ( msg.Body().matchmaking_state() == ServerMatchmakingState_NOT_PARTICIPATING && !pLobby && !m_pMatchInfo )
  2878. return;
  2879. // Check for certain rules changes. When they change, we care about them being
  2880. // eventually correct, but it's not urgent
  2881. if ( ( Q_stricmp( m_sLastGameServerUpdateTags, msg.Body().tags().c_str() ) != 0 ) ||
  2882. ( msg.Body().max_players() != (uint32)m_nLastGameServerUpdateMaxHumans ) ||
  2883. ( msg.Body().bot_count() != (uint32)m_nLastGameServerUpdateBotCount ) )
  2884. {
  2885. flSendInterval = Min( flSendInterval, 20.0f );
  2886. }
  2887. // If lobby is incorrect in an ordinary way (player left, etc),
  2888. // flush the change decently quickly
  2889. if ( pLobby && bLobbyIncorrect )
  2890. {
  2891. // Send updates more quickly if the GC hasn't acknowledged, but don't DDoS. Ideally the event that made the
  2892. // lobby incorrect triggered a Update( bForce = true );
  2893. flSendInterval = Min( flSendInterval, 10.0f );
  2894. }
  2895. if ( now < m_timeLastSendGameServerInfoAndConnectedPlayers + flSendInterval )
  2896. { return; }
  2897. }
  2898. GCClientSystem()->BSendMessage( msg );
  2899. // Remember what/when we sent, so we can tell next time if we need to send
  2900. m_timeLastSendGameServerInfoAndConnectedPlayers = now;
  2901. m_eLastGameServerUpdateMatchmakingMode = msg.Body().matchmaking_mode();
  2902. m_eLastGameServerUpdateState = msg.Body().matchmaking_state();
  2903. m_sLastGameServerUpdateMap = msg.Body().map().c_str();
  2904. m_sLastGameServerUpdateTags = msg.Body().tags().c_str();
  2905. m_nLastGameServerUpdateBotCount = nBotCountToSend;
  2906. m_nLastGameServerUpdateMaxHumans = nMaxHumans;
  2907. m_nLastGameServerUpdateSlotsFree = nSlotsFree;
  2908. m_nLastGameServerUpdateLobbyMMVersion = msg.Body().lobby_mm_version();
  2909. // Remember when we started requesting late join, so we can compare it to our lobby's late-join state to reason
  2910. // about how long we've been waiting.
  2911. if ( eGameServerInfoState == ServerMatchmakingState_ACTIVE_MATCH_REQUESTING_LATE_JOIN )
  2912. {
  2913. if ( m_flTimeRequestedLateJoin == -1.f )
  2914. {
  2915. m_flTimeRequestedLateJoin = CRTime::RTime32TimeCur();
  2916. MMLog( "Requested late join for active match\n" );
  2917. }
  2918. }
  2919. else if ( m_flTimeRequestedLateJoin != -1.f )
  2920. {
  2921. MMLog( "Stopped requesting late join for active match after %.02fs\n",
  2922. CRTime::RTime32TimeCur() - m_flTimeRequestedLateJoin );
  2923. m_flTimeRequestedLateJoin = -1.f;
  2924. }
  2925. // Only late join eligible when are requesting late join, we have a lobby from the GC, and it has marked itself as
  2926. // late join eligible. If we've lost our lobby or it hasn't updated to become eligible, there may be GC connection
  2927. // difficulties.
  2928. // We only update this at the end of updates, rather than on the fly, to ensure we don't expose this value prior to
  2929. // processing other updates in the lobby object. For instance, the lobby might remove us from late join and give us
  2930. // reserved members at the same time, we don't want callers to see one, but not the other.
  2931. m_bLateJoinEligible = m_flTimeRequestedLateJoin != -1.f && GetLobby() && GetLobby()->GetLateJoinEligible();
  2932. }
  2933. // ***************************************************************************************************************
  2934. void CTFGCServerSystem::SendMvMVictoryResult()
  2935. {
  2936. // Note that we don't have to have an *ended* match -- MvM code technically allows players to continue in the same
  2937. // match and achieve multiple victories.
  2938. Assert( m_pMatchInfo );
  2939. CTFGSLobby *pLobby = GetLobby();
  2940. if ( !pLobby )
  2941. {
  2942. // FIXME - We should be able to submit this even if the GC reboots and loses our lobby state (though it wont
  2943. // happen that often, as the GC tries to revive lobby state from memcached)
  2944. MMLog( "CTFGCServerSystem::MvMVictory() -- no lobby, so not sending results to GC\n" );
  2945. return;
  2946. }
  2947. if ( IsMannUpGroup( pLobby->GetMatchGroup() ) )
  2948. {
  2949. m_mvmVictoryInfo.Init( pLobby );
  2950. ReliableMsgMvMVictory *pReliable = new ReliableMsgMvMVictory;
  2951. auto &msg = pReliable->Msg().Body();
  2952. msg.set_mission_name( m_mvmVictoryInfo.m_sChallengeName );
  2953. #ifdef USE_MVM_TOUR
  2954. if ( !m_mvmVictoryInfo.m_sMannUpTourOfDuty.IsEmpty() )
  2955. {
  2956. msg.set_tour_name_mannup( m_mvmVictoryInfo.m_sMannUpTourOfDuty );
  2957. }
  2958. #endif // USE_MVM_TOUR
  2959. msg.set_lobby_id( m_mvmVictoryInfo.m_nLobbyId );
  2960. msg.set_event_time( m_mvmVictoryInfo.m_tEventTime );
  2961. FOR_EACH_VEC( m_mvmVictoryInfo.m_vPlayerIds, iMember )
  2962. {
  2963. CMsgMvMVictory_Player *pMsgPlayer = msg.add_players();
  2964. pMsgPlayer->set_steam_id( m_mvmVictoryInfo.m_vPlayerIds[ iMember ]);
  2965. pMsgPlayer->set_squad_surplus( m_mvmVictoryInfo.m_vSquadSurplus[ iMember ] );
  2966. }
  2967. pReliable->Enqueue();
  2968. }
  2969. }
  2970. ////-----------------------------------------------------------------------------
  2971. //// Purpose: Job for being told when the server GC connection is established
  2972. ////-----------------------------------------------------------------------------
  2973. //class CGCClientJobServerWelcome : public GCSDK::CGCClientJob
  2974. //{
  2975. //public:
  2976. // CGCClientJobServerWelcome( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
  2977. //
  2978. // virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
  2979. // {
  2980. // CProtoBufMsg<CMsgServerWelcome> msg( pNetPacket );
  2981. //
  2982. // g_bServerReceivedGCWelcome = true;
  2983. //
  2984. // GTFGCClientSystem()->UpdateGCServerInfo();
  2985. //
  2986. // // Validate version
  2987. // int engineServerVersion = engine->GetServerVersion();
  2988. // g_gcServerVersion = (int)msg.Body().version();
  2989. //
  2990. // // Version checking is enforced if both sides do not report zero as their version
  2991. // if ( engineServerVersion && g_gcServerVersion && engineServerVersion != g_gcServerVersion )
  2992. // {
  2993. // // If we're out of date exit
  2994. // Msg("Version out of date (GC wants %d, we are %d)!\n", g_gcServerVersion, engine->GetServerVersion() );
  2995. //
  2996. // // If we hibernating, quit now, otherwise we will quit on hibernation
  2997. // if ( g_ServerGameDLL.m_bIsHibernating )
  2998. // {
  2999. // engine->ServerCommand( "quit\n" );
  3000. // }
  3001. // }
  3002. // else
  3003. // {
  3004. // Msg("GC Connection established for server version %d\n", engine->GetServerVersion() );
  3005. // }
  3006. //
  3007. // return true;
  3008. // }
  3009. //};
  3010. //GC_REG_JOB( GCSDK::CGCClient, CGCClientJobServerWelcome, "CGCClientJobServerWelcome", k_EMsgGCServerWelcome, k_EServerTypeGCClient );
  3011. //// temp for tracking down machines submitted stats
  3012. //#if defined ( _WIN32 )
  3013. //#define WIN32_LEAN_AND_MEAN
  3014. //#undef INVALID_HANDLE_VALUE
  3015. //#undef DECLARE_HANDLE
  3016. //#include <windows.h>
  3017. //bool DOTA_GetComputerName( char *pszComputerName, DWORD *length )
  3018. //{
  3019. // return !!GetComputerName( pszComputerName, length );
  3020. //}
  3021. //#endif
  3022. // **************************************************************************************************
  3023. void CTFGCServerSystem::SendRejectLobby()
  3024. {
  3025. MMLog( "Sending CMsgGameServerKickingLobby to reject stale lobby\n" );
  3026. ReliableMsgGameServerKickingLobby *pReliable = new ReliableMsgGameServerKickingLobby();
  3027. auto &msg = pReliable->Msg().Body();
  3028. msg.set_create_party( false );
  3029. if ( GetLobby() )
  3030. {
  3031. msg.set_lobby_id( GetLobby()->GetGroupID() );
  3032. msg.set_lobby_id( GetLobby()->GetMatchID() );
  3033. }
  3034. pReliable->Enqueue();
  3035. }
  3036. // **************************************************************************************************
  3037. void CTFGCServerSystem::EndManagedMatch( bool bKickPlayersToParties )
  3038. {
  3039. CMatchInfo *pMatch = GetMatch();
  3040. // Sanity
  3041. AssertMsg( !pMatch || !pMatch->m_bMatchEnded, "Ending an already ended match" );
  3042. if ( !pMatch )
  3043. { return; }
  3044. pMatch->SetEnded();
  3045. // Cancel launching the new match. Leave the rest of the state alone, we'll send a NewMatch -> EndMatch and things
  3046. // will just work out as responses come in.
  3047. m_flWaitingForNewMatchTime = 0.f;
  3048. if ( !m_pMatchInfo->m_bSentResult )
  3049. {
  3050. Warning( "Ending a managed match without sending a result" );
  3051. Assert( false );
  3052. }
  3053. ReliableMsgGameServerKickingLobby *pReliable = new ReliableMsgGameServerKickingLobby();
  3054. auto &msg = pReliable->Msg().Body();
  3055. if ( bKickPlayersToParties )
  3056. {
  3057. CUtlVector<CSteamID> vecConnectedPlayers;
  3058. int total = pMatch->GetNumTotalMatchPlayers();
  3059. for ( int idx = 0; idx < total; idx++ )
  3060. {
  3061. CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( idx );
  3062. if ( !pMatchPlayer->bDropped && pMatchPlayer->bConnected )
  3063. {
  3064. msg.add_connected_players( pMatchPlayer->steamID.ConvertToUint64() );
  3065. }
  3066. }
  3067. if ( msg.connected_players_size() <= 0 )
  3068. {
  3069. bKickPlayersToParties = false;
  3070. }
  3071. }
  3072. if ( bKickPlayersToParties )
  3073. {
  3074. MMLog( "Sending CMsgGameServerKickingLobby, requesting party with %d connected players\n", msg.connected_players_size() );
  3075. }
  3076. else
  3077. {
  3078. MMLog( "Sending CMsgGameServerKickingLobby, not requesting party\n" );
  3079. }
  3080. msg.set_create_party( bKickPlayersToParties );
  3081. msg.set_lobby_id( pMatch->m_nLobbyID );
  3082. msg.set_match_id( pMatch->m_nMatchID );
  3083. pReliable->Enqueue();
  3084. }
  3085. // **************************************************************************************************
  3086. void CTFGCServerSystem::SendPlayerLeftMatch( CSteamID targetPlayer, TFMatchLeaveReason eReason, bool bIsAbandon )
  3087. {
  3088. CMatchInfo *pMatch = GetMatch();
  3089. // Sanity
  3090. AssertMsg( pMatch && !pMatch->m_bMatchEnded, "Don't expect to be sending this without a live match" );
  3091. if ( !pMatch )
  3092. { return; }
  3093. ReliableMsgPlayerLeftMatch *pReliable = new ReliableMsgPlayerLeftMatch();
  3094. auto &msg = pReliable->Msg().Body();
  3095. msg.set_steam_id( targetPlayer.ConvertToUint64() );
  3096. msg.set_leave_reason( eReason );
  3097. MMLog( "Sending CMsgPlayerLeftMatch with target of %s [ abandon = %d ]\n", targetPlayer.Render(), bIsAbandon );
  3098. msg.set_lobby_id( pMatch->m_nLobbyID );
  3099. msg.set_match_id( pMatch->m_nMatchID );
  3100. msg.set_was_abandon( bIsAbandon );
  3101. pReliable->Enqueue();
  3102. }
  3103. // **************************************************************************************************
  3104. void CTFGCServerSystem::SendCompetitiveMatchResult( GCSDK::CProtoBufMsg< CMsgGC_Match_Result > *pMatchResultMsg )
  3105. {
  3106. // We should have matchinfo when completing a ladder match
  3107. if ( !m_pMatchInfo )
  3108. {
  3109. Warning( "Sending competitive match results without match info!\n" );
  3110. Assert( false );
  3111. }
  3112. if ( m_pMatchInfo->m_bSentResult )
  3113. {
  3114. Warning( "Sending competitive match results without an ended match\n" );
  3115. Assert( false );
  3116. }
  3117. ReliableMsgMatchResult *pReliable = new ReliableMsgMatchResult;
  3118. auto &msg = pReliable->Msg().Body();
  3119. /// XXX(JohnS): With refactor this is now kinda silly. Callers should really just be giving us a CMsgGC_Match_Result
  3120. /// instead of the wrapper.
  3121. msg.CopyFrom( pMatchResultMsg->Body() );
  3122. pReliable->Enqueue();
  3123. m_pMatchInfo->m_bSentResult = true;
  3124. }
  3125. // **************************************************************************************************
  3126. bool CTFGCServerSystem::BLateJoinEligible()
  3127. {
  3128. return m_bLateJoinEligible;
  3129. }
  3130. // **************************************************************************************************
  3131. void CTFGCServerSystem::AcceptGCReservation( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntindex, bool bActive )
  3132. {
  3133. if ( m_pMatchInfo )
  3134. {
  3135. // Accepting new player to competitive match, add to match data
  3136. MMLog( "New match player %s\n", steamID.Render() );
  3137. m_pMatchInfo->AddPlayer( steamID, pMemberData, bIsLateJoin, nEntindex, bActive );
  3138. }
  3139. }
  3140. // **************************************************************************************************
  3141. void CTFGCServerSystem::AbortInvalidMatchState()
  3142. {
  3143. // TODO ROLLING MATCHES: SteamAPI_SetMiniDumpComment / SteamAPI_WriteMiniDump
  3144. MMLog( "**** MM Server in invalid match state, terminating\n" );
  3145. engine->ServerCommand( "quit\n" );
  3146. }
  3147. // **************************************************************************************************
  3148. void CTFGCServerSystem::MMServerModeChanged()
  3149. {
  3150. // Save old boolean state
  3151. bool bSaveMMServerMode = m_bMMServerMode;
  3152. // Set new state
  3153. m_bMMServerMode = ( tf_mm_servermode.GetInt() != 0 );
  3154. // Check if logical state is changing; output some text no matter what
  3155. if ( m_bMMServerMode )
  3156. {
  3157. if ( bSaveMMServerMode )
  3158. {
  3159. MMLog( "Lobby-based matchmaking is active\n" );
  3160. }
  3161. else
  3162. {
  3163. MMLog( "Entering lobby-based matchmaking mode\n" );
  3164. }
  3165. if ( tf_mm_strict.GetInt() == 0 )
  3166. {
  3167. MMLog( " Open mode active. Gameserver will show in server browser and accept ad-hoc joins.\n" );
  3168. }
  3169. else if ( tf_mm_strict.GetInt() == 1 )
  3170. {
  3171. MMLog( " Strict mode is active. Gameserver will not show in server browser or accept ad-hoc joins.\n" );
  3172. }
  3173. else
  3174. {
  3175. MMLog( " Server is hidden from server browser list, but will accept ad-hoc joins.\n" );
  3176. }
  3177. if ( tf_mm_trusted.GetInt() != 0 )
  3178. {
  3179. MMLog( " Requested trusted server status.\n" );
  3180. }
  3181. }
  3182. else
  3183. {
  3184. if ( bSaveMMServerMode )
  3185. {
  3186. MMLog( "Leaving lobby-based matchmaking mode\n" );
  3187. }
  3188. else
  3189. {
  3190. MMLog( "Lobby-based matchmaking mode not active\n" );
  3191. }
  3192. }
  3193. // Force this major change out immediately
  3194. UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, true );
  3195. }
  3196. //-----------------------------------------------------------------------------
  3197. // Purpose:
  3198. //-----------------------------------------------------------------------------
  3199. void CTFGCServerSystem::LaunchNewMatchForLobby()
  3200. {
  3201. /// XXX(JohnS): Technically the lobby might legitimately be gone here -- if we have gotten the NewMatchForLobby
  3202. /// response and the GC then croaks, we might be told it lost our lobby, but have the new match
  3203. /// assignment and be able to proceed without needing the lobby at all (as in normal cases where the GC
  3204. /// loses state after giving us the authority to run a match).
  3205. ///
  3206. /// Since the match in question hasn't started yet, and this is nearly impossible given the timing
  3207. /// window, I'm not doing the work to cache the lobby values we need in here just to let the
  3208. /// just-created match survive that edge case.
  3209. const CTFGSLobby* pLobby = GetLobby();
  3210. if ( !pLobby || m_flWaitingForNewMatchTime == 0.f || !m_pMatchInfo || \
  3211. m_pMatchInfo->BMatchTerminated() || m_pMatchInfo->m_bServerCreated )
  3212. {
  3213. // You need to prepare for the switch with RequestNewMatchForLobby first. Should not have gotten here if we have
  3214. // a terminated or server created match -- Must still be managed by the GC in order to roll into a new match.
  3215. Assert( false );
  3216. MMLog( "!! Attempting to launch a new match for a lobby without valid state\n" );
  3217. AbortInvalidMatchState();
  3218. }
  3219. m_flWaitingForNewMatchTime = 0.f;
  3220. CMatchInfo* pNewMatchInfo = new CMatchInfo( pLobby );
  3221. // The old match info is holding the vote-winning map name
  3222. pNewMatchInfo->m_strMapName = m_pMatchInfo->m_strMapName;
  3223. EMatchGroup eMatchGroup = pLobby->GetMatchGroup();
  3224. // We still need a new match ID from the GC. Mark that this new match is
  3225. // created by us so that: 1) If we do get a response for a new match ID
  3226. // we know what to do with it
  3227. // 2) The GC knows to assign it a match ID if it
  3228. // gets a match result for it before (1) occurs
  3229. if ( m_bWaitingForNewMatchID )
  3230. {
  3231. // Mark that we're going rogue
  3232. pNewMatchInfo->m_bServerCreated = true;
  3233. pNewMatchInfo->m_nMatchID = 0; // Don't inherit the stale one from the lobby
  3234. if ( !CanChangeMatchPlayerTeams() )
  3235. {
  3236. // Server created speculative matches are counting on the GC approving this when it wakes up, and also
  3237. // approving our override of player teams below. If we want a mode that does rolling matches but has no
  3238. // authority to override teams, we'd need to just cancel the pending match here instead of using
  3239. // m_bServerCreated
  3240. AbortInvalidMatchState();
  3241. }
  3242. }
  3243. for( int idx = 0; idx < m_pMatchInfo->GetNumTotalMatchPlayers(); idx++ )
  3244. {
  3245. const CMatchInfo::PlayerMatchData_t* pPlayerMatchData = m_pMatchInfo->GetMatchDataForPlayer( idx );
  3246. // We don't need record of dropped players for the new match
  3247. if ( pPlayerMatchData->bDropped )
  3248. { continue; }
  3249. // We stop doing maintenance on lobby->match sync during the pending-new-match period, but we don't want to
  3250. // include players who would be dropped on the first think -- we'd have erroneous record that they were
  3251. // officially part of the match for some period, when they were not.
  3252. //
  3253. // XXX(JohnS): Technically, we could create a speculative match, then when the new match ID arrives, some
  3254. // members vanished -- those members were never actually part of the lobby from the GC
  3255. // perspective. We might need to cull these people on the first post-new-matchID-think if having
  3256. // record of them is causing problems. (a bWasEverConfirmedByGC flag?)
  3257. if ( !pLobby->GetMemberDetails( pPlayerMatchData->steamID ) )
  3258. { continue; }
  3259. // AddPlayer needs to know if they are connected/active right now
  3260. int nEntIndex = 0;
  3261. bool bActive = false;
  3262. if ( pPlayerMatchData->bConnected )
  3263. {
  3264. if ( pPlayerMatchData->nConnectingButNotActiveIndex )
  3265. {
  3266. // Connected, not active
  3267. bActive = false;
  3268. nEntIndex = pPlayerMatchData->nConnectingButNotActiveIndex;
  3269. }
  3270. else
  3271. {
  3272. // Connected and active
  3273. bActive = true;
  3274. // We could null check this but we'd just use the information to call AbortInvalidMatchState().
  3275. nEntIndex = UTIL_PlayerBySteamID( pPlayerMatchData->steamID )->entindex();
  3276. }
  3277. }
  3278. pNewMatchInfo->AddPlayer( *pPlayerMatchData, nEntIndex, bActive );
  3279. }
  3280. delete m_pMatchInfo;
  3281. m_pMatchInfo = pNewMatchInfo;
  3282. // If we are going ahead with a server-created match, queue a ChangeMatchPlayerTeams message in sequence with our
  3283. // pending new match request -- the GC will process, in order:
  3284. //
  3285. // - Give us a new match!
  3286. // -> Okay here's new match & teams
  3287. // - Set everyone's teams to (the previous match teams)!
  3288. // -> Okay here's new lobby with teams that match your state
  3289. //
  3290. // ... And since we don't run UpdateConnectedPlayers() while messages are in queue, by time we run our next
  3291. // look-at-the-lobby think, we'll be in sync again.
  3292. CUtlVector< PlayerTeamPair_t > vecPlayerTeams;
  3293. for( int idx = 0; idx < m_pMatchInfo->GetNumTotalMatchPlayers(); idx++ )
  3294. {
  3295. const CMatchInfo::PlayerMatchData_t *pPlayer = m_pMatchInfo->GetMatchDataForPlayer( idx );
  3296. vecPlayerTeams.AddToTail( { pPlayer->steamID, pPlayer->eGCTeam } );
  3297. }
  3298. ChangeMatchPlayerTeams( vecPlayerTeams );
  3299. GTFGCClientSystem()->DumpLobby();
  3300. if ( eMatchGroup == EMatchGroup::k_nMatchGroup_Invalid ||
  3301. !GetMatchGroupDescription( eMatchGroup )->InitServerSettingsForMatch( pLobby ) )
  3302. {
  3303. AbortInvalidMatchState();
  3304. }
  3305. }
  3306. //-----------------------------------------------------------------------------
  3307. // Purpose: Activate / deactive GC hosting mode
  3308. //-----------------------------------------------------------------------------
  3309. void OnMMServerModeChanged( IConVar *pConVar, const char *pOldString, float flOldValue )
  3310. {
  3311. GTFGCClientSystem()->MMServerModeChanged();
  3312. }
  3313. //-----------------------------------------------------------------------------
  3314. // Purpose:
  3315. //-----------------------------------------------------------------------------
  3316. void OnMMServerModeTrustedChanged( IConVar *pConVar, const char *pOldString, float flOldValue )
  3317. {
  3318. OnMMServerModeChanged( pConVar, pOldString, flOldValue );
  3319. }
  3320. ConVar tf_mm_servermode( "tf_mm_servermode", "0", FCVAR_NOTIFY,
  3321. "Activates / deactivates Lobby-based hosting mode.\n"
  3322. " 0 = not active\n"
  3323. " 1 = Put in matchmaking pool (Lobby will control current map)\n",
  3324. true,
  3325. 0.f,
  3326. true,
  3327. 1.f,
  3328. OnMMServerModeChanged );
  3329. ConVar tf_mm_strict( "tf_mm_strict", "0", FCVAR_NOTIFY,
  3330. " 0 = Show in server browser, and allow ad-hoc joins\n"
  3331. " 1 = Hide from server browser and only allow joins coordinated through GC matchmaking\n"
  3332. " 2 = Hide from server browser, but allow ad-hoc joins\n",
  3333. OnMMServerModeChanged );
  3334. ConVar tf_mm_trusted( "tf_mm_trusted", "0", FCVAR_NOTIFY | FCVAR_HIDDEN,
  3335. "Set to 1 on Valve servers to requested trusted status. (Yes, it is authenticated on the backend, and attempts by non-valve servers are logged.)\n",
  3336. OnMMServerModeTrustedChanged );
  3337. #endif // #ifdef ENABLE_GC_MATCHMAKING