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.

993 lines
31 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose: Handles joining clients together in a matchmaking session before a multiplayer
  4. // game, tracking new players and dropped players during the game, and reporting
  5. // game results and stats after the game is complete.
  6. //
  7. //=============================================================================//
  8. #include "proto_oob.h"
  9. #include "vgui_baseui_interface.h"
  10. #include "cdll_engine_int.h"
  11. #include "matchmaking.h"
  12. #include "Session.h"
  13. #include "convar.h"
  14. #include "cmd.h"
  15. extern IVEngineClient *engineClient;
  16. // TODO: remove when UI sets all properties
  17. #include "hl2orange.spa.h"
  18. // memdbgon must be the last include file in a .cpp file!!!
  19. #include "tier0/memdbgon.h"
  20. extern IXboxSystem *g_pXboxSystem;
  21. //-----------------------------------------------------------------------------
  22. // Purpose: Start a matchmaking client
  23. //-----------------------------------------------------------------------------
  24. void CMatchmaking::StartClient( bool bSystemLink )
  25. {
  26. NET_SetMutiplayer( true );
  27. InitializeLocalClient( false );
  28. m_Session.SetIsSystemLink( bSystemLink );
  29. // SearchForSession is an async call
  30. if ( !SearchForSession() )
  31. {
  32. // The call failed
  33. SessionNotification( SESSION_NOTIFY_FAIL_CREATE );
  34. return;
  35. }
  36. // Session search is underway
  37. SwitchToState( MMSTATE_SEARCHING );
  38. }
  39. //-----------------------------------------------------------------------------
  40. // Purpose: Set up to search for a system link host
  41. //-----------------------------------------------------------------------------
  42. bool CMatchmaking::StartSystemLinkSearch()
  43. {
  44. // Create an random identifier and reset the send timer
  45. #if defined( _X360 )
  46. XNetRandom( (byte*)&m_Nonce, sizeof( m_Nonce ) );
  47. #endif
  48. m_fSendTimer = GetTime();
  49. m_nSendCount = 0;
  50. return true;
  51. }
  52. //-----------------------------------------------------------------------------
  53. // Purpose: Handle replies from system link servers
  54. //-----------------------------------------------------------------------------
  55. void CMatchmaking::HandleSystemLinkReply( netpacket_t *pPacket )
  56. {
  57. if ( !m_Session.IsSystemLink() || m_Session.IsHost() )
  58. return;
  59. uint64 nonce = pPacket->message.ReadLongLong();
  60. if ( nonce != m_Nonce )
  61. {
  62. // Reply isn't a response to our request
  63. return;
  64. }
  65. // Store the session information
  66. bf_read &msg = pPacket->message;
  67. char *pData = new char[MAX_ROUTABLE_PAYLOAD];
  68. systemLinkInfo_s *pResultInfo = (systemLinkInfo_s*)pData;
  69. XSESSION_SEARCHRESULT *pResult = &pResultInfo->Result;
  70. msg.ReadBytes( &pResult->info, sizeof( pResult->info ) );
  71. // Don't accept multiple replies from the same host
  72. for ( int i = 0; i < m_pSystemLinkResults.Count(); ++i )
  73. {
  74. XSESSION_SEARCHRESULT *pCheck = &((systemLinkInfo_s*)m_pSystemLinkResults[i])->Result;
  75. if ( Q_memcmp( &pCheck->info.sessionID, &pResult->info.sessionID, sizeof( pResult->info.sessionID ) ) == 0 )
  76. {
  77. // Already have this session
  78. delete [] pData;
  79. return;
  80. }
  81. }
  82. pResult->dwOpenPublicSlots = msg.ReadByte();
  83. pResult->dwOpenPrivateSlots = msg.ReadByte();
  84. pResult->dwFilledPublicSlots = msg.ReadByte();
  85. pResult->dwFilledPrivateSlots = msg.ReadByte();
  86. m_nTotalTeams = msg.ReadByte();
  87. pResultInfo->gameState = msg.ReadByte();
  88. pResultInfo->gameTime = msg.ReadByte();
  89. msg.ReadBytes( &pResultInfo->szHostName, MAX_PLAYER_NAME_LENGTH );
  90. msg.ReadBytes( &pResultInfo->szScenario, MAX_MAP_NAME );
  91. pResult->cProperties = msg.ReadByte();
  92. pResult->cContexts = msg.ReadByte();
  93. const uint propSize = pResult->cProperties * sizeof( XUSER_PROPERTY );
  94. const uint ctxSize = pResult->cContexts * sizeof( XUSER_CONTEXT );
  95. pResult->pProperties = (XUSER_PROPERTY*)( (byte*)pResult + sizeof( XSESSION_SEARCHRESULT ) );
  96. pResult->pContexts = (XUSER_CONTEXT*)( (byte*)pResult->pProperties + propSize );
  97. msg.ReadBytes( pResult->pProperties, propSize );
  98. msg.ReadBytes( pResult->pContexts, ctxSize);
  99. pResultInfo->iScenarioIndex = msg.ReadByte();
  100. pResultInfo->xuid = msg.ReadLongLong();
  101. m_pSystemLinkResults.AddToTail( pData );
  102. Msg( "Found a matching game\n" );
  103. DevMsg( "Result #%d: %d open public, %d open private\n", m_pSystemLinkResults.Count()-1, pResult->dwOpenPublicSlots, pResult->dwOpenPrivateSlots );
  104. }
  105. //-----------------------------------------------------------------------------
  106. // Purpose: Search for a session to join
  107. //-----------------------------------------------------------------------------
  108. bool CMatchmaking::SearchForSession()
  109. {
  110. if ( m_Session.IsSystemLink() )
  111. {
  112. return StartSystemLinkSearch();
  113. }
  114. m_pSearchResults = NULL;
  115. uint cbResultsBytes = 0;
  116. uint ret = 0;
  117. // Call once to get the necessary buffer size
  118. ret = g_pXboxSystem->SessionSearch(
  119. SESSION_MATCH_QUERY_PLAYER_MATCH,
  120. XBX_GetPrimaryUserId(),
  121. MAX_SEARCHRESULTS,
  122. 1,
  123. 0,
  124. 0,
  125. NULL,
  126. NULL,
  127. &cbResultsBytes, // must be 0
  128. m_pSearchResults, // must be NULL
  129. false // synchronous
  130. );
  131. m_hSearchHandle = g_pXboxSystem->CreateAsyncHandle();
  132. // Allocate the buffer and call again
  133. m_pSearchResults = (XSESSION_SEARCHRESULT_HEADER*)malloc( cbResultsBytes );
  134. ret = g_pXboxSystem->SessionSearch(
  135. SESSION_MATCH_QUERY_PLAYER_MATCH, // Procedure index
  136. XBX_GetPrimaryUserId(), // User index
  137. MAX_SEARCHRESULTS, // Maximum results
  138. m_Local.m_cPlayers, // Number of local players
  139. m_SessionProperties.Count(), // Number of properties
  140. m_SessionContexts.Count(), // Number of contexts
  141. m_SessionProperties.Base(), // Properties
  142. m_SessionContexts.Base(), // Contexts
  143. &cbResultsBytes, // Size of result buffer
  144. m_pSearchResults, // Pointer to results
  145. true,
  146. &m_hSearchHandle
  147. );
  148. if ( ret != ERROR_IO_PENDING )
  149. {
  150. return false;
  151. }
  152. return true;
  153. }
  154. //-----------------------------------------------------------------------------
  155. // Purpose: Check for session search results
  156. //-----------------------------------------------------------------------------
  157. void CMatchmaking::UpdateSearch()
  158. {
  159. if ( !m_Session.IsSystemLink() )
  160. {
  161. // Check if the search has finished
  162. DWORD ret = g_pXboxSystem->GetOverlappedResult( m_hSearchHandle, NULL, false );
  163. if ( ret == ERROR_IO_INCOMPLETE )
  164. {
  165. // Still waiting
  166. return;
  167. }
  168. else
  169. {
  170. if ( ret == ERROR_SUCCESS && m_pSearchResults && m_pSearchResults->dwSearchResults )
  171. {
  172. // A list of matching sessions was found.
  173. Msg( "Found %d matching games\n", m_pSearchResults->dwSearchResults );
  174. #if defined( _X360 )
  175. for ( unsigned int i = 0; i < m_pSearchResults->dwSearchResults; ++i )
  176. {
  177. m_QoSxnaddr[i] = &(m_pSearchResults->pResults[i].info.hostAddress);
  178. m_QoSxnkid[i] = &(m_pSearchResults->pResults[i].info.sessionID);
  179. m_QoSxnkey[i] = &(m_pSearchResults->pResults[i].info.keyExchangeKey);
  180. }
  181. //
  182. // Note: XNetQosLookup requires only 2 successful probes to be received from the host.
  183. // This is much less than the recommended 8 probes because on a 10% data loss profile
  184. // it is impossible to find the host when requiring 8 probes to be received.
  185. XNetQosLookup( m_pSearchResults->dwSearchResults,
  186. m_QoSxnaddr,
  187. m_QoSxnkid,
  188. m_QoSxnkey,
  189. 0, // number of security gateways to probe
  190. NULL, // gateway ip addresses
  191. NULL, // gateway service ids
  192. 2, // number of probes
  193. 0, // upstream bandwith to use (0 = default)
  194. 0, // flags - not supported
  195. NULL, // signal event
  196. &m_pQoSResult );// results
  197. #endif
  198. SwitchToState( MMSTATE_WAITING_QOS );
  199. }
  200. else
  201. {
  202. SessionNotification( SESSION_NOTIFY_FAIL_SEARCH );
  203. }
  204. g_pXboxSystem->ReleaseAsyncHandle( m_hSearchHandle );
  205. }
  206. }
  207. else
  208. {
  209. if ( GetTime() - m_fSendTimer > SYSTEMLINK_RETRYINTERVAL && m_nSendCount < SYSTEMLINK_MAXRETRIES )
  210. {
  211. // Send out a search for lan servers
  212. ALIGN4 char msg_buffer[MAX_ROUTABLE_PAYLOAD] ALIGN4_POST;
  213. bf_write msg( msg_buffer, sizeof(msg_buffer) );
  214. msg.WriteLong( CONNECTIONLESS_HEADER );
  215. msg.WriteByte( PTH_SYSTEMLINK_SEARCH );
  216. msg.WriteLongLong( m_Nonce ); // 64 bit
  217. // Send message
  218. netadr_t adr;
  219. adr.SetType( NA_BROADCAST );
  220. adr.SetPort( PORT_SYSTEMLINK );
  221. NET_SendPacket( NULL, NS_SYSTEMLINK, adr, msg.GetData(), msg.GetNumBytesWritten() );
  222. m_fSendTimer = GetTime();
  223. ++m_nSendCount;
  224. }
  225. else if ( m_nSendCount >= SYSTEMLINK_MAXRETRIES )
  226. {
  227. if ( m_pSystemLinkResults.Count() )
  228. {
  229. SessionNotification( SESSION_NOTIFY_SEARCH_COMPLETED );
  230. // Send the session info to gameui
  231. for ( int i = 0; i < m_pSystemLinkResults.Count(); ++i )
  232. {
  233. systemLinkInfo_s *pSearchInfo = (systemLinkInfo_s*)m_pSystemLinkResults[i];
  234. XSESSION_SEARCHRESULT *pResult = &pSearchInfo->Result;
  235. hostData_s hostData;
  236. hostData.gameState = pSearchInfo->gameState;
  237. hostData.gameTime = pSearchInfo->gameTime;
  238. hostData.xuid = pSearchInfo->xuid;
  239. Q_strncpy( hostData.hostName, pSearchInfo->szHostName, sizeof( hostData.hostName ) );
  240. Q_strncpy( hostData.scenario, pSearchInfo->szScenario, sizeof( hostData.scenario ) );
  241. EngineVGui()->SessionSearchResult( i, &hostData, pResult, -1 );
  242. }
  243. }
  244. else
  245. {
  246. SessionNotification( SESSION_NOTIFY_FAIL_SEARCH );
  247. }
  248. }
  249. }
  250. }
  251. //-----------------------------------------------------------------------------
  252. // Purpose: Check for QOS Results
  253. //-----------------------------------------------------------------------------
  254. void CMatchmaking::UpdateQosLookup()
  255. {
  256. #if defined( _X360 )
  257. // Keep checking for results until the wait time expires
  258. if ( GetTime() - m_fWaitTimer < QOSLOOKUP_WAITTIME )
  259. {
  260. for ( uint i = 0; i < m_pSearchResults->dwSearchResults; ++i )
  261. {
  262. if ( (m_pQoSResult->axnqosinfo[0].bFlags & XNET_XNQOSINFO_COMPLETE) == 0 )
  263. return;
  264. }
  265. }
  266. bool bNotifiedGameUI = false;
  267. for ( unsigned int i = 0; i < m_pSearchResults->dwSearchResults; ++i )
  268. {
  269. // Make sure the host is available
  270. if ( !(m_pQoSResult->axnqosinfo[i].bFlags & XNET_XNQOSINFO_TARGET_CONTACTED) )
  271. {
  272. DevMsg( "Result #%d: Host unreachable (!XNET_XNQOSINFO_TARGET_CONTACTED)\n", i );
  273. continue;
  274. }
  275. else if ( (m_pQoSResult->axnqosinfo[i].bFlags & XNET_XNQOSINFO_TARGET_DISABLED) )
  276. {
  277. DevMsg( "Result #%d: Host disabled (XNET_XNQOSINFO_TARGET_DISABLED)\n", i );
  278. continue;
  279. }
  280. else if ( !(m_pQoSResult->axnqosinfo[i].bFlags & XNET_XNQOSINFO_DATA_RECEIVED) ||
  281. !m_pQoSResult->axnqosinfo[i].cbData ||
  282. !m_pQoSResult->axnqosinfo[i].pbData )
  283. {
  284. DevMsg( "Result #%d: No data received (!XNET_XNQOSINFO_DATA_RECEIVED)\n", i );
  285. continue;
  286. }
  287. // Check the ping before we accept this host
  288. // unsigned short ping = m_pQoSResult->axnqosinfo[i].wRttMedInMsecs;
  289. unsigned short ping = m_pQoSResult->axnqosinfo[i].wRttMinInMsecs; // Use min ping to suit better for traffic bursts and lossy connections
  290. DevMsg( "Result #%d: ping min %d ms, med %d ms\n", i, m_pQoSResult->axnqosinfo[i].wRttMinInMsecs, m_pQoSResult->axnqosinfo[i].wRttMedInMsecs );
  291. // On X360 ping calculations are reported between 4 and 5 times bigger
  292. // than the actual upstream/downstream latency of the connection to Xbox LIVE
  293. const int pingFactor = 5;
  294. ping /= pingFactor;
  295. if ( ping > PING_MAX_RED )
  296. {
  297. DevMsg( "Result #%d: Host connection too slow, ignoring\n", i );
  298. continue;
  299. }
  300. // Determine the QOS quality to show to user
  301. int pingDisplayedToUserIcon = -1;
  302. if ( ping <= PING_MAX_GREEN )
  303. {
  304. pingDisplayedToUserIcon = 0;
  305. }
  306. else if ( ping <= PING_MAX_YELLOW )
  307. {
  308. pingDisplayedToUserIcon = 1;
  309. }
  310. else if ( ping <= PING_MAX_RED )
  311. {
  312. pingDisplayedToUserIcon = 2;
  313. }
  314. // Retrieve the search result
  315. XSESSION_SEARCHRESULT &searchResult = m_pSearchResults->pResults[i];
  316. // The host should have given us some game data
  317. hostData_s hostData;
  318. Q_memcpy( &hostData, m_pQoSResult->axnqosinfo[i].pbData, sizeof( hostData ) );
  319. // This host is acceptable. Get the info and notify gameUI
  320. if ( !bNotifiedGameUI )
  321. {
  322. SessionNotification( SESSION_NOTIFY_SEARCH_COMPLETED );
  323. bNotifiedGameUI = true;
  324. }
  325. // Send the host info to GameUI
  326. EngineVGui()->SessionSearchResult( i, &hostData, &searchResult, pingDisplayedToUserIcon );
  327. DevMsg( "Result #%d: %d open public slots, %d open private slots\n", i, searchResult.dwOpenPublicSlots, searchResult.dwOpenPrivateSlots );
  328. }
  329. if ( !bNotifiedGameUI )
  330. {
  331. SessionNotification( SESSION_NOTIFY_FAIL_SEARCH );
  332. }
  333. #endif
  334. }
  335. //-----------------------------------------------------------------------------
  336. // Purpose: Cancel the current search operation
  337. //-----------------------------------------------------------------------------
  338. void CMatchmaking::CancelSearch()
  339. {
  340. SwitchToState( MMSTATE_INITIAL );
  341. }
  342. //-----------------------------------------------------------------------------
  343. // Purpose: Cancel the current search operation
  344. //-----------------------------------------------------------------------------
  345. void CMatchmaking::CancelQosLookup()
  346. {
  347. #if defined( _X360 )
  348. XNetQosRelease( m_pQoSResult );
  349. #endif
  350. SwitchToState( MMSTATE_INITIAL );
  351. }
  352. //-----------------------------------------------------------------------------
  353. // Purpose: User has selected a session to join, create a local session with the same properties
  354. //-----------------------------------------------------------------------------
  355. void CMatchmaking::SelectSession( uint idx )
  356. {
  357. XSESSION_SEARCHRESULT *pResult = NULL;
  358. if ( m_Session.IsSystemLink() )
  359. {
  360. systemLinkInfo_s* pInfo = (systemLinkInfo_s*)m_pSystemLinkResults[idx];
  361. pResult = &pInfo->Result;
  362. }
  363. else
  364. {
  365. pResult = &m_pSearchResults->pResults[idx];
  366. }
  367. if ( !pResult )
  368. return;
  369. m_Session.SetSessionInfo( &pResult->info );
  370. ApplySessionProperties( pResult->cContexts, pResult->cProperties, pResult->pContexts, pResult->pProperties );
  371. m_Session.SetIsHost( false );
  372. m_Session.SetSessionSlots( SLOTS_TOTALPUBLIC, pResult->dwOpenPublicSlots + pResult->dwFilledPublicSlots );
  373. m_Session.SetSessionSlots( SLOTS_TOTALPRIVATE, pResult->dwOpenPrivateSlots + pResult->dwFilledPrivateSlots );
  374. if ( !m_Session.CreateSession() )
  375. {
  376. SessionNotification( SESSION_NOTIFY_FAIL_CREATE );
  377. return;
  378. }
  379. // Waiting for session creation results
  380. SwitchToState( MMSTATE_CREATING );
  381. }
  382. //-----------------------------------------------------------------------------
  383. // Purpose: Join a session when invited to.
  384. //-----------------------------------------------------------------------------
  385. void CMatchmaking::JoinInviteSession( XSESSION_INFO *pHostInfo )
  386. {
  387. if ( !pHostInfo )
  388. {
  389. Msg( "[JoinInviteSession] resetting.\n" );
  390. InviteCancel();
  391. return;
  392. }
  393. // Fetch our current session id
  394. XNKID nSessionID = m_Session.GetSessionId();
  395. // Check our invite state
  396. switch ( m_InviteState )
  397. {
  398. case INVITE_NONE:
  399. // Initial invite call
  400. Msg( "[JoinInviteSession:INVITE_NONE] Initial call to join invite session.\n" );
  401. // Don't bother if we're invited to join the same session
  402. if ( !Q_memcmp( &(pHostInfo->sessionID), &(nSessionID), sizeof(nSessionID) ) )
  403. {
  404. Msg( "[JoinInviteSession:INVITE_NONE] Rejecting invite session since it is the current session.\n" );
  405. return;
  406. }
  407. // Leave our current session, if we have one
  408. KickPlayerFromSession( 0 );
  409. if ( &m_InviteSessionInfo != pHostInfo )
  410. memcpy( &m_InviteSessionInfo, pHostInfo, sizeof( XSESSION_INFO ) );
  411. m_InviteState = INVITE_PENDING;
  412. // If we are currently in progress of doing something, let it finish
  413. if ( m_bInitialized )
  414. {
  415. if ( MMSTATE_INITIAL != m_CurrentState )
  416. {
  417. Msg( "[JoinInviteSession:INVITE_NONE] Yielding, current state = %d.\n", m_CurrentState );
  418. return;
  419. }
  420. }
  421. else
  422. {
  423. // We can be uninitialized due to the commentary mode - perform the disconnect to be sure
  424. ConVarRef commentary( "commentary" );
  425. if ( commentary.IsValid() && commentary.GetBool() )
  426. {
  427. Msg( "[JoinInviteSession:INVITE_NONE] Stopping commentary mode first.\n" );
  428. engineClient->ClientCmd( "disconnect" );
  429. Cbuf_Execute();
  430. return;
  431. }
  432. }
  433. // otherwise fall through to join
  434. case INVITE_PENDING:
  435. // While the invite is pending and user changed an invite, obey the user
  436. if ( pHostInfo != &m_InviteSessionInfo )
  437. memcpy( &m_InviteSessionInfo, pHostInfo, sizeof( XSESSION_INFO ) );
  438. // Wait for the previous matchmaking session to finish and cleanup
  439. if ( m_bInitialized && ( MMSTATE_INITIAL != m_CurrentState ) )
  440. {
  441. Msg( "[JoinInviteSession:INVITE_PENDING] Waiting, current state = %d.\n", m_CurrentState );
  442. return;
  443. }
  444. #if defined( _X360 )
  445. // Switch into validating invite mode and do it right away
  446. m_InviteState = INVITE_VALIDATING;
  447. // fall through
  448. case INVITE_VALIDATING:
  449. // Validate user storage information
  450. Msg( "[JoinInviteSession:INVITE_VALIDATING] Validating user storage before accepting invite.\n" );
  451. //
  452. // Configure and validate waiting info in case user will have to pick storage device
  453. //
  454. m_InviteWaitingInfo.m_InviteStorageDeviceSelected = 0;
  455. m_InviteWaitingInfo.m_UserIdx = XBX_GetPrimaryUserId();
  456. if ( m_InviteWaitingInfo.m_UserIdx == INVALID_USER_ID )
  457. {
  458. Msg( "[JoinInviteSession:INVITE_VALIDATING] Invalid user id, aborting.\n" );
  459. InviteCancel();
  460. return;
  461. }
  462. m_InviteWaitingInfo.m_SignInState = XUserGetSigninState( m_InviteWaitingInfo.m_UserIdx );
  463. if ( ( m_InviteWaitingInfo.m_SignInState != eXUserSigninState_SignedInToLive ) ||
  464. ( ERROR_SUCCESS != XUserGetSigninInfo( m_InviteWaitingInfo.m_UserIdx, 0, &m_InviteWaitingInfo.m_SignInInfo ) ) ||
  465. ! ( m_InviteWaitingInfo.m_SignInInfo.dwInfoFlags & XUSER_INFO_FLAG_LIVE_ENABLED ) )
  466. {
  467. Msg( "[JoinInviteSession:INVITE_VALIDATING] Failed to get sign in to LIVE info, aborting.\n" );
  468. InviteCancel();
  469. return;
  470. }
  471. if ( ( ERROR_SUCCESS != XUserCheckPrivilege( m_InviteWaitingInfo.m_UserIdx, XPRIVILEGE_MULTIPLAYER_SESSIONS, &m_InviteWaitingInfo.m_PrivilegeMultiplayer ) ) ||
  472. ( !m_InviteWaitingInfo.m_PrivilegeMultiplayer ) )
  473. {
  474. Msg( "[JoinInviteSession:INVITE_VALIDATING] Privilege denied, aborting.\n" );
  475. InviteCancel();
  476. return;
  477. }
  478. //
  479. // Enqueue the call to track storage device once it gets selected
  480. //
  481. if ( m_InviteWaitingInfo.m_bAcceptingInvite ||
  482. EngineVGui()->ValidateStorageDevice( &m_InviteWaitingInfo.m_InviteStorageDeviceSelected ) )
  483. {
  484. m_InviteWaitingInfo.m_bAcceptingInvite = 0;
  485. Msg( "[JoinInviteSession:INVITE_VALIDATING] Storage %s, accepting.\n", m_InviteWaitingInfo.m_bAcceptingInvite ? "already queried" : "valid" );
  486. m_InviteState = INVITE_ACCEPTING;
  487. // fall through
  488. }
  489. else
  490. {
  491. // User doesn't have a device selected and has to pick one
  492. Msg( "[JoinInviteSession:INVITE_VALIDATING] Awaiting storage.\n" );
  493. m_InviteState = INVITE_AWAITING_STORAGE;
  494. return;
  495. }
  496. #else
  497. // Accept the invite right away
  498. m_InviteState = INVITE_ACCEPTING;
  499. // fall through
  500. #endif
  501. case INVITE_ACCEPTING:
  502. // Everything will finish this frame
  503. Msg( "[JoinInviteSession:INVITE_ACCEPTING] Accepting the invite.\n" );
  504. break;
  505. #if defined( _X360 )
  506. case INVITE_AWAITING_STORAGE:
  507. // Wait for user to select storage, but keep an eye on user change or logout or etc
  508. {
  509. InviteWaitingInfo_t InviteCurrentInfo;
  510. InviteCurrentInfo.m_UserIdx = XBX_GetPrimaryUserId();
  511. if ( InviteCurrentInfo.m_UserIdx != m_InviteWaitingInfo.m_UserIdx )
  512. {
  513. Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] User index changed, aborting.\n" );
  514. InviteCancel();
  515. return;
  516. }
  517. InviteCurrentInfo.m_SignInState = XUserGetSigninState( InviteCurrentInfo.m_UserIdx );
  518. if ( ( InviteCurrentInfo.m_SignInState != m_InviteWaitingInfo.m_SignInState ) ||
  519. ( ERROR_SUCCESS != XUserGetSigninInfo( InviteCurrentInfo.m_UserIdx, 0, &InviteCurrentInfo.m_SignInInfo ) ) ||
  520. ! ( InviteCurrentInfo.m_SignInInfo.dwInfoFlags & XUSER_INFO_FLAG_LIVE_ENABLED ) ||
  521. !IsEqualXUID( InviteCurrentInfo.m_SignInInfo.xuid, m_InviteWaitingInfo.m_SignInInfo.xuid ) )
  522. {
  523. Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] User xuid changed, aborting.\n" );
  524. InviteCancel();
  525. return;
  526. }
  527. if ( ( ERROR_SUCCESS != XUserCheckPrivilege( InviteCurrentInfo.m_UserIdx, XPRIVILEGE_MULTIPLAYER_SESSIONS, &InviteCurrentInfo.m_PrivilegeMultiplayer ) ) ||
  528. ( !InviteCurrentInfo.m_PrivilegeMultiplayer ) )
  529. {
  530. Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] Privilege denied, aborting.\n" );
  531. InviteCancel();
  532. return;
  533. }
  534. // Check if we should keep waiting
  535. switch ( m_InviteWaitingInfo.m_InviteStorageDeviceSelected )
  536. {
  537. case 0:
  538. // Keep waiting
  539. return;
  540. case 1:
  541. // Device selected, proceed
  542. Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] Device selected.\n" );
  543. break;
  544. case 2:
  545. Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] Device rejected.\n" );
  546. // User opts to run with no storage device, proceed
  547. break;
  548. default:
  549. // Device selection weird error
  550. InviteCancel();
  551. return;
  552. }
  553. }
  554. // Otherwise user has selected the storage device, try to accept the invite once again
  555. Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] Accepting.\n" );
  556. m_InviteWaitingInfo.m_bAcceptingInvite = 1;
  557. m_InviteState = INVITE_NONE;
  558. JoinInviteSession( pHostInfo );
  559. return;
  560. #endif
  561. default:
  562. Msg( "[JoinInviteSession:UnknownState=%d] Aborting.\n", m_InviteState );
  563. InviteCancel();
  564. return;
  565. }
  566. // Initialize our state to accept the new connection
  567. NET_SetMutiplayer( true );
  568. InitializeLocalClient( false );
  569. // Allow us to access private channels due to invite
  570. m_Local.m_bInvited = true;
  571. #if defined( _X360 )
  572. // "Spoof" certain information we don't yet know about the server, knowing that we'll modify it later once connected
  573. m_Session.SetIsSystemLink( false );
  574. m_Session.SetSessionInfo( pHostInfo );
  575. m_Session.SetIsHost( false );
  576. m_Session.SetContext( X_CONTEXT_GAME_TYPE, X_CONTEXT_GAME_TYPE_STANDARD, false );
  577. m_Session.SetSessionFlags( XSESSION_CREATE_LIVE_MULTIPLAYER_STANDARD );
  578. m_Session.SetSessionSlots( SLOTS_TOTALPUBLIC, 8 );
  579. m_Session.SetSessionSlots( SLOTS_TOTALPRIVATE, 0 );
  580. m_Session.SetSessionSlots( SLOTS_FILLEDPUBLIC, 0 );
  581. m_Session.SetSessionSlots( SLOTS_FILLEDPRIVATE, 0 );
  582. #endif
  583. // Create the session and kick off our UI
  584. if ( !m_Session.CreateSession() )
  585. {
  586. SessionNotification( SESSION_NOTIFY_FAIL_CREATE );
  587. return;
  588. }
  589. // Waiting for session creation results
  590. SwitchToState( MMSTATE_CREATING );
  591. InviteCancel();
  592. }
  593. void CMatchmaking::InviteCancel()
  594. {
  595. m_InviteState = INVITE_NONE;
  596. memset( &m_InviteWaitingInfo, 0, sizeof( m_InviteWaitingInfo ) );
  597. }
  598. //-----------------------------------------------------------------------------
  599. // Purpose: Search for a session by ID and connect to it (done for cross-game invites)
  600. //-----------------------------------------------------------------------------
  601. void CMatchmaking::JoinInviteSessionByID( XNKID nSessionID )
  602. {
  603. #ifdef _X360
  604. DWORD dwResultSize = 0;
  605. XSESSION_SEARCHRESULT_HEADER *pSearchResults = NULL;
  606. // Call this once to find the proper buffer size
  607. DWORD dwError = XSessionSearchByID( nSessionID, XBX_GetPrimaryUserId(), &dwResultSize, pSearchResults, NULL );
  608. if ( dwError != ERROR_INSUFFICIENT_BUFFER )
  609. return;
  610. // Create a buffer big enough to hold the requested information
  611. pSearchResults = (XSESSION_SEARCHRESULT_HEADER *) new byte[dwResultSize];
  612. ZeroMemory( pSearchResults, dwResultSize );
  613. // Now get the real results
  614. dwError = XSessionSearchByID( nSessionID, XBX_GetPrimaryUserId(), &dwResultSize, pSearchResults, NULL );
  615. if ( dwError != ERROR_SUCCESS )
  616. {
  617. delete[] pSearchResults;
  618. return;
  619. }
  620. // If we found something, connect to it
  621. if ( pSearchResults->dwSearchResults > 0 )
  622. {
  623. JoinInviteSession( &(pSearchResults->pResults[0].info) );
  624. }
  625. else
  626. {
  627. SessionNotification( SESSION_NOTIFY_CONNECT_NOTAVAILABLE );
  628. }
  629. // Done
  630. delete[] pSearchResults;
  631. #endif // _X360
  632. }
  633. //-----------------------------------------------------------------------------
  634. // Purpose: Tell a session host we'd like to join the session
  635. //-----------------------------------------------------------------------------
  636. void CMatchmaking::SendJoinRequest( netadr_t *adr )
  637. {
  638. ALIGN4 char msg_buffer[MAX_ROUTABLE_PAYLOAD] ALIGN4_POST;
  639. bf_write msg( msg_buffer, sizeof(msg_buffer) );
  640. // Send local player info
  641. msg.WriteLong( CONNECTIONLESS_HEADER );
  642. msg.WriteByte( PTH_CONNECT );
  643. msg.WriteLongLong( m_Local.m_id ); // 64 bit
  644. msg.WriteByte( m_Local.m_cPlayers );
  645. msg.WriteOneBit( m_Local.m_bInvited );
  646. msg.WriteBytes( &m_Local.m_xnaddr, sizeof( m_Local.m_xnaddr ) );
  647. for ( int i = 0; i < m_Local.m_cPlayers; ++i )
  648. {
  649. msg.WriteLongLong( m_Local.m_xuids[i] ); // 64 bit
  650. msg.WriteBytes( &m_Local.m_cVoiceState, sizeof( m_Local.m_cVoiceState ) ); // TODO: has voice
  651. msg.WriteString( m_Local.m_szGamertags[i] );
  652. }
  653. // Send message
  654. NET_SendPacket( NULL, NS_MATCHMAKING, *adr, msg.GetData(), msg.GetNumBytesWritten() );
  655. }
  656. //-----------------------------------------------------------------------------
  657. // Purpose: Process the session host's response to our join request
  658. //-----------------------------------------------------------------------------
  659. bool CMatchmaking::ProcessJoinResponse( MM_JoinResponse *pMsg )
  660. {
  661. switch( pMsg->m_ResponseType )
  662. {
  663. case MM_JoinResponse::JOINRESPONSE_NOTHOSTING:
  664. if ( m_CurrentState != MMSTATE_SESSION_CONNECTING )
  665. {
  666. return true;
  667. }
  668. Msg( "This game is no longer available.\n" );
  669. SessionNotification( SESSION_NOTIFY_CONNECT_NOTAVAILABLE );
  670. break;
  671. case MM_JoinResponse::JOINRESPONSE_SESSIONFULL:
  672. if ( m_CurrentState != MMSTATE_SESSION_CONNECTING )
  673. {
  674. return true;
  675. }
  676. Msg( "This game is full.\n" );
  677. SessionNotification( SESSION_NOTIFY_CONNECT_SESSIONFULL );
  678. break;
  679. case MM_JoinResponse::JOINRESPONSE_APPROVED:
  680. case MM_JoinResponse::JOINRESPONSE_APPROVED_JOINGAME:
  681. if ( m_CurrentState != MMSTATE_SESSION_CONNECTING )
  682. {
  683. return true;
  684. }
  685. // Fill in host data
  686. m_Host.m_id = pMsg->m_id; // 64 bit
  687. m_Session.SetSessionNonce( pMsg->m_Nonce ); // 64 bit
  688. m_Session.SetSessionFlags( pMsg->m_SessionFlags );
  689. m_Session.SetOwnerId( pMsg->m_nOwnerId );
  690. m_nHostOwnerId = pMsg->m_nOwnerId;
  691. ApplySessionProperties( pMsg->m_ContextCount, pMsg->m_PropertyCount, pMsg->m_SessionContexts.Base(), pMsg->m_SessionProperties.Base() );
  692. for ( int i = 0; i < m_Local.m_cPlayers; ++i )
  693. {
  694. m_Local.m_iTeam[i] = pMsg->m_iTeam;
  695. }
  696. m_nTotalTeams = pMsg->m_nTotalTeams;
  697. if ( pMsg->m_ResponseType == pMsg->JOINRESPONSE_APPROVED )
  698. {
  699. SessionNotification( SESSION_NOTIFY_CONNECTED_TOSESSION );
  700. SendPlayerInfoToLobby( &m_Local );
  701. }
  702. break;
  703. case MM_JoinResponse::JOINRESPONSE_MODIFY_SESSION:
  704. if ( !m_Session.IsHost() )
  705. {
  706. if ( m_CurrentState != MMSTATE_SESSION_CONNECTED )
  707. {
  708. return true;
  709. }
  710. // Host has sent us some new session properties
  711. ApplySessionProperties( pMsg->m_ContextCount, pMsg->m_PropertyCount, pMsg->m_SessionContexts.Base(), pMsg->m_SessionProperties.Base() );
  712. MM_JoinResponse response;
  713. response.m_ResponseType = MM_JoinResponse::JOINRESPONSE_MODIFY_SESSION;
  714. response.m_id = m_Local.m_id;
  715. SendMessage( &response, &m_Host );
  716. SessionNotification( SESSION_NOTIFY_MODIFYING_COMPLETED_CLIENT );
  717. }
  718. else
  719. {
  720. if ( m_CurrentState != MMSTATE_MODIFYING )
  721. {
  722. return true;
  723. }
  724. // Handle this client response
  725. bool bWaiting = false;
  726. for ( int i = 0; i < m_Remote.Count(); ++i )
  727. {
  728. if ( m_Remote[i]->m_id == pMsg->m_id )
  729. {
  730. m_Remote[i]->m_bModified = true;
  731. }
  732. else
  733. {
  734. if ( !m_Remote[i]->m_bModified )
  735. {
  736. bWaiting = true;
  737. }
  738. }
  739. }
  740. if ( !bWaiting )
  741. {
  742. // Everyone has modified their session
  743. EndSessionModify();
  744. }
  745. }
  746. break;
  747. default:
  748. break;
  749. }
  750. return true;
  751. }
  752. //-----------------------------------------------------------------------------
  753. // Purpose: Apply the contexts and properties that came from the host, and build out keyvalues for GameUI
  754. //-----------------------------------------------------------------------------
  755. void CMatchmaking::ApplySessionProperties( int numContexts, int numProperties, XUSER_CONTEXT *pContexts, XUSER_PROPERTY *pProperties )
  756. {
  757. // Clear our existing properties, as they should be completely replaced by these new ones
  758. m_SessionContexts.RemoveAll();
  759. m_SessionProperties.RemoveAll();
  760. char szBuffer[MAX_PATH];
  761. uint nGameTypeId = g_ClientDLL->GetPresenceID( "CONTEXT_GAME_TYPE" );
  762. // Update the session properties
  763. for ( int i = 0; i < numContexts; ++i )
  764. {
  765. XUSER_CONTEXT &ctx = pContexts[i];
  766. m_SessionContexts.AddToTail( ctx );
  767. const char *pID = g_ClientDLL->GetPropertyIdString( ctx.dwContextId );
  768. g_ClientDLL->GetPropertyDisplayString( ctx.dwContextId, ctx.dwValue, szBuffer, sizeof( szBuffer ) );
  769. // Set the display string for gameUI
  770. KeyValues *pContextKey = m_pSessionKeys->FindKey( pID, true );
  771. pContextKey->SetName( pID );
  772. pContextKey->SetString( "displaystring", szBuffer );
  773. // We need to set the game type
  774. if ( ctx.dwContextId == nGameTypeId )
  775. {
  776. m_Session.SetContext( ctx.dwContextId, ctx.dwValue, false );
  777. }
  778. }
  779. for ( int i = 0; i < numProperties; ++i )
  780. {
  781. XUSER_PROPERTY &prop = pProperties[i];
  782. m_SessionProperties.AddToTail( prop );
  783. const char *pID = g_ClientDLL->GetPropertyIdString( prop.dwPropertyId );
  784. g_ClientDLL->GetPropertyDisplayString( prop.dwPropertyId, prop.value.nData, szBuffer, sizeof( szBuffer ) );
  785. // Set the display string for gameUI
  786. KeyValues *pPropertyKey = m_pSessionKeys->FindKey( pID, true );
  787. pPropertyKey->SetName( pID );
  788. pPropertyKey->SetString( "displaystring", szBuffer );
  789. }
  790. }
  791. //-----------------------------------------------------------------------------
  792. // Purpose: Send a join request to the session host
  793. //-----------------------------------------------------------------------------
  794. bool CMatchmaking::ConnectToHost()
  795. {
  796. AddPlayersToSession( &m_Local );
  797. XSESSION_INFO info;
  798. m_Session.GetSessionInfo( &info );
  799. #if defined( _X360 )
  800. // Resolve the host's IP address
  801. XNADDR xaddr = info.hostAddress;
  802. XNKID xid = info.sessionID;
  803. IN_ADDR winaddr;
  804. if ( XNetXnAddrToInAddr( &xaddr, &xid, &winaddr ) != 0 )
  805. {
  806. Warning( "Error resolving host IP\n" );
  807. return false;
  808. }
  809. m_Host.m_adr.SetType( NA_IP );
  810. m_Host.m_adr.SetIPAndPort( winaddr.S_un.S_addr, PORT_MATCHMAKING );
  811. #endif
  812. // Initiate the network channel
  813. AddRemoteChannel( &m_Host.m_adr );
  814. SendJoinRequest( &m_Host.m_adr );
  815. m_fWaitTimer = GetTime();
  816. return true;
  817. }
  818. //-----------------------------------------------------------------------------
  819. // Purpose: Waiting for a connection response from the session host
  820. //-----------------------------------------------------------------------------
  821. void CMatchmaking::UpdateConnecting()
  822. {
  823. if ( GetTime() - m_fWaitTimer > JOINREPLY_WAITTIME )
  824. {
  825. SessionNotification( SESSION_NOTIFY_CONNECT_NOTAVAILABLE );
  826. }
  827. }
  828. //-----------------------------------------------------------------------------
  829. // Purpose: Clean up the search results arrays
  830. //-----------------------------------------------------------------------------
  831. void CMatchmaking::ClearSearchResults()
  832. {
  833. if ( m_pSearchResults )
  834. {
  835. free( m_pSearchResults );
  836. m_pSearchResults = NULL;
  837. }
  838. // This will call delete and we should technically be calling delete []
  839. m_pSystemLinkResults.PurgeAndDeleteElements();
  840. }
  841. CON_COMMAND( mm_select_session, "Select a session" )
  842. {
  843. if ( args.ArgC() >= 2 )
  844. {
  845. g_pMatchmaking->SelectSession( atoi( args[1] ) );
  846. }
  847. }