//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Handles joining clients together in a matchmaking session before a multiplayer // game, tracking new players and dropped players during the game, and reporting // game results and stats after the game is complete. // //=============================================================================// #include "proto_oob.h" #include "vgui_baseui_interface.h" #include "cdll_engine_int.h" #include "matchmaking.h" #include "Session.h" #include "convar.h" #include "cmd.h" extern IVEngineClient *engineClient; // TODO: remove when UI sets all properties #include "hl2orange.spa.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" extern IXboxSystem *g_pXboxSystem; //----------------------------------------------------------------------------- // Purpose: Start a matchmaking client //----------------------------------------------------------------------------- void CMatchmaking::StartClient( bool bSystemLink ) { NET_SetMutiplayer( true ); InitializeLocalClient( false ); m_Session.SetIsSystemLink( bSystemLink ); // SearchForSession is an async call if ( !SearchForSession() ) { // The call failed SessionNotification( SESSION_NOTIFY_FAIL_CREATE ); return; } // Session search is underway SwitchToState( MMSTATE_SEARCHING ); } //----------------------------------------------------------------------------- // Purpose: Set up to search for a system link host //----------------------------------------------------------------------------- bool CMatchmaking::StartSystemLinkSearch() { // Create an random identifier and reset the send timer #if defined( _X360 ) XNetRandom( (byte*)&m_Nonce, sizeof( m_Nonce ) ); #endif m_fSendTimer = GetTime(); m_nSendCount = 0; return true; } //----------------------------------------------------------------------------- // Purpose: Handle replies from system link servers //----------------------------------------------------------------------------- void CMatchmaking::HandleSystemLinkReply( netpacket_t *pPacket ) { if ( !m_Session.IsSystemLink() || m_Session.IsHost() ) return; uint64 nonce = pPacket->message.ReadLongLong(); if ( nonce != m_Nonce ) { // Reply isn't a response to our request return; } // Store the session information bf_read &msg = pPacket->message; char *pData = new char[MAX_ROUTABLE_PAYLOAD]; systemLinkInfo_s *pResultInfo = (systemLinkInfo_s*)pData; XSESSION_SEARCHRESULT *pResult = &pResultInfo->Result; msg.ReadBytes( &pResult->info, sizeof( pResult->info ) ); // Don't accept multiple replies from the same host for ( int i = 0; i < m_pSystemLinkResults.Count(); ++i ) { XSESSION_SEARCHRESULT *pCheck = &((systemLinkInfo_s*)m_pSystemLinkResults[i])->Result; if ( Q_memcmp( &pCheck->info.sessionID, &pResult->info.sessionID, sizeof( pResult->info.sessionID ) ) == 0 ) { // Already have this session delete [] pData; return; } } pResult->dwOpenPublicSlots = msg.ReadByte(); pResult->dwOpenPrivateSlots = msg.ReadByte(); pResult->dwFilledPublicSlots = msg.ReadByte(); pResult->dwFilledPrivateSlots = msg.ReadByte(); m_nTotalTeams = msg.ReadByte(); pResultInfo->gameState = msg.ReadByte(); pResultInfo->gameTime = msg.ReadByte(); msg.ReadBytes( &pResultInfo->szHostName, MAX_PLAYER_NAME_LENGTH ); msg.ReadBytes( &pResultInfo->szScenario, MAX_MAP_NAME ); pResult->cProperties = msg.ReadByte(); pResult->cContexts = msg.ReadByte(); const uint propSize = pResult->cProperties * sizeof( XUSER_PROPERTY ); const uint ctxSize = pResult->cContexts * sizeof( XUSER_CONTEXT ); pResult->pProperties = (XUSER_PROPERTY*)( (byte*)pResult + sizeof( XSESSION_SEARCHRESULT ) ); pResult->pContexts = (XUSER_CONTEXT*)( (byte*)pResult->pProperties + propSize ); msg.ReadBytes( pResult->pProperties, propSize ); msg.ReadBytes( pResult->pContexts, ctxSize); pResultInfo->iScenarioIndex = msg.ReadByte(); pResultInfo->xuid = msg.ReadLongLong(); m_pSystemLinkResults.AddToTail( pData ); Msg( "Found a matching game\n" ); DevMsg( "Result #%d: %d open public, %d open private\n", m_pSystemLinkResults.Count()-1, pResult->dwOpenPublicSlots, pResult->dwOpenPrivateSlots ); } //----------------------------------------------------------------------------- // Purpose: Search for a session to join //----------------------------------------------------------------------------- bool CMatchmaking::SearchForSession() { if ( m_Session.IsSystemLink() ) { return StartSystemLinkSearch(); } m_pSearchResults = NULL; uint cbResultsBytes = 0; uint ret = 0; // Call once to get the necessary buffer size ret = g_pXboxSystem->SessionSearch( SESSION_MATCH_QUERY_PLAYER_MATCH, XBX_GetPrimaryUserId(), MAX_SEARCHRESULTS, 1, 0, 0, NULL, NULL, &cbResultsBytes, // must be 0 m_pSearchResults, // must be NULL false // synchronous ); m_hSearchHandle = g_pXboxSystem->CreateAsyncHandle(); // Allocate the buffer and call again m_pSearchResults = (XSESSION_SEARCHRESULT_HEADER*)malloc( cbResultsBytes ); ret = g_pXboxSystem->SessionSearch( SESSION_MATCH_QUERY_PLAYER_MATCH, // Procedure index XBX_GetPrimaryUserId(), // User index MAX_SEARCHRESULTS, // Maximum results m_Local.m_cPlayers, // Number of local players m_SessionProperties.Count(), // Number of properties m_SessionContexts.Count(), // Number of contexts m_SessionProperties.Base(), // Properties m_SessionContexts.Base(), // Contexts &cbResultsBytes, // Size of result buffer m_pSearchResults, // Pointer to results true, &m_hSearchHandle ); if ( ret != ERROR_IO_PENDING ) { return false; } return true; } //----------------------------------------------------------------------------- // Purpose: Check for session search results //----------------------------------------------------------------------------- void CMatchmaking::UpdateSearch() { if ( !m_Session.IsSystemLink() ) { // Check if the search has finished DWORD ret = g_pXboxSystem->GetOverlappedResult( m_hSearchHandle, NULL, false ); if ( ret == ERROR_IO_INCOMPLETE ) { // Still waiting return; } else { if ( ret == ERROR_SUCCESS && m_pSearchResults && m_pSearchResults->dwSearchResults ) { // A list of matching sessions was found. Msg( "Found %d matching games\n", m_pSearchResults->dwSearchResults ); #if defined( _X360 ) for ( unsigned int i = 0; i < m_pSearchResults->dwSearchResults; ++i ) { m_QoSxnaddr[i] = &(m_pSearchResults->pResults[i].info.hostAddress); m_QoSxnkid[i] = &(m_pSearchResults->pResults[i].info.sessionID); m_QoSxnkey[i] = &(m_pSearchResults->pResults[i].info.keyExchangeKey); } // // Note: XNetQosLookup requires only 2 successful probes to be received from the host. // This is much less than the recommended 8 probes because on a 10% data loss profile // it is impossible to find the host when requiring 8 probes to be received. XNetQosLookup( m_pSearchResults->dwSearchResults, m_QoSxnaddr, m_QoSxnkid, m_QoSxnkey, 0, // number of security gateways to probe NULL, // gateway ip addresses NULL, // gateway service ids 2, // number of probes 0, // upstream bandwith to use (0 = default) 0, // flags - not supported NULL, // signal event &m_pQoSResult );// results #endif SwitchToState( MMSTATE_WAITING_QOS ); } else { SessionNotification( SESSION_NOTIFY_FAIL_SEARCH ); } g_pXboxSystem->ReleaseAsyncHandle( m_hSearchHandle ); } } else { if ( GetTime() - m_fSendTimer > SYSTEMLINK_RETRYINTERVAL && m_nSendCount < SYSTEMLINK_MAXRETRIES ) { // Send out a search for lan servers ALIGN4 char msg_buffer[MAX_ROUTABLE_PAYLOAD] ALIGN4_POST; bf_write msg( msg_buffer, sizeof(msg_buffer) ); msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( PTH_SYSTEMLINK_SEARCH ); msg.WriteLongLong( m_Nonce ); // 64 bit // Send message netadr_t adr; adr.SetType( NA_BROADCAST ); adr.SetPort( PORT_SYSTEMLINK ); NET_SendPacket( NULL, NS_SYSTEMLINK, adr, msg.GetData(), msg.GetNumBytesWritten() ); m_fSendTimer = GetTime(); ++m_nSendCount; } else if ( m_nSendCount >= SYSTEMLINK_MAXRETRIES ) { if ( m_pSystemLinkResults.Count() ) { SessionNotification( SESSION_NOTIFY_SEARCH_COMPLETED ); // Send the session info to gameui for ( int i = 0; i < m_pSystemLinkResults.Count(); ++i ) { systemLinkInfo_s *pSearchInfo = (systemLinkInfo_s*)m_pSystemLinkResults[i]; XSESSION_SEARCHRESULT *pResult = &pSearchInfo->Result; hostData_s hostData; hostData.gameState = pSearchInfo->gameState; hostData.gameTime = pSearchInfo->gameTime; hostData.xuid = pSearchInfo->xuid; Q_strncpy( hostData.hostName, pSearchInfo->szHostName, sizeof( hostData.hostName ) ); Q_strncpy( hostData.scenario, pSearchInfo->szScenario, sizeof( hostData.scenario ) ); EngineVGui()->SessionSearchResult( i, &hostData, pResult, -1 ); } } else { SessionNotification( SESSION_NOTIFY_FAIL_SEARCH ); } } } } //----------------------------------------------------------------------------- // Purpose: Check for QOS Results //----------------------------------------------------------------------------- void CMatchmaking::UpdateQosLookup() { #if defined( _X360 ) // Keep checking for results until the wait time expires if ( GetTime() - m_fWaitTimer < QOSLOOKUP_WAITTIME ) { for ( uint i = 0; i < m_pSearchResults->dwSearchResults; ++i ) { if ( (m_pQoSResult->axnqosinfo[0].bFlags & XNET_XNQOSINFO_COMPLETE) == 0 ) return; } } bool bNotifiedGameUI = false; for ( unsigned int i = 0; i < m_pSearchResults->dwSearchResults; ++i ) { // Make sure the host is available if ( !(m_pQoSResult->axnqosinfo[i].bFlags & XNET_XNQOSINFO_TARGET_CONTACTED) ) { DevMsg( "Result #%d: Host unreachable (!XNET_XNQOSINFO_TARGET_CONTACTED)\n", i ); continue; } else if ( (m_pQoSResult->axnqosinfo[i].bFlags & XNET_XNQOSINFO_TARGET_DISABLED) ) { DevMsg( "Result #%d: Host disabled (XNET_XNQOSINFO_TARGET_DISABLED)\n", i ); continue; } else if ( !(m_pQoSResult->axnqosinfo[i].bFlags & XNET_XNQOSINFO_DATA_RECEIVED) || !m_pQoSResult->axnqosinfo[i].cbData || !m_pQoSResult->axnqosinfo[i].pbData ) { DevMsg( "Result #%d: No data received (!XNET_XNQOSINFO_DATA_RECEIVED)\n", i ); continue; } // Check the ping before we accept this host // unsigned short ping = m_pQoSResult->axnqosinfo[i].wRttMedInMsecs; unsigned short ping = m_pQoSResult->axnqosinfo[i].wRttMinInMsecs; // Use min ping to suit better for traffic bursts and lossy connections DevMsg( "Result #%d: ping min %d ms, med %d ms\n", i, m_pQoSResult->axnqosinfo[i].wRttMinInMsecs, m_pQoSResult->axnqosinfo[i].wRttMedInMsecs ); // On X360 ping calculations are reported between 4 and 5 times bigger // than the actual upstream/downstream latency of the connection to Xbox LIVE const int pingFactor = 5; ping /= pingFactor; if ( ping > PING_MAX_RED ) { DevMsg( "Result #%d: Host connection too slow, ignoring\n", i ); continue; } // Determine the QOS quality to show to user int pingDisplayedToUserIcon = -1; if ( ping <= PING_MAX_GREEN ) { pingDisplayedToUserIcon = 0; } else if ( ping <= PING_MAX_YELLOW ) { pingDisplayedToUserIcon = 1; } else if ( ping <= PING_MAX_RED ) { pingDisplayedToUserIcon = 2; } // Retrieve the search result XSESSION_SEARCHRESULT &searchResult = m_pSearchResults->pResults[i]; // The host should have given us some game data hostData_s hostData; Q_memcpy( &hostData, m_pQoSResult->axnqosinfo[i].pbData, sizeof( hostData ) ); // This host is acceptable. Get the info and notify gameUI if ( !bNotifiedGameUI ) { SessionNotification( SESSION_NOTIFY_SEARCH_COMPLETED ); bNotifiedGameUI = true; } // Send the host info to GameUI EngineVGui()->SessionSearchResult( i, &hostData, &searchResult, pingDisplayedToUserIcon ); DevMsg( "Result #%d: %d open public slots, %d open private slots\n", i, searchResult.dwOpenPublicSlots, searchResult.dwOpenPrivateSlots ); } if ( !bNotifiedGameUI ) { SessionNotification( SESSION_NOTIFY_FAIL_SEARCH ); } #endif } //----------------------------------------------------------------------------- // Purpose: Cancel the current search operation //----------------------------------------------------------------------------- void CMatchmaking::CancelSearch() { SwitchToState( MMSTATE_INITIAL ); } //----------------------------------------------------------------------------- // Purpose: Cancel the current search operation //----------------------------------------------------------------------------- void CMatchmaking::CancelQosLookup() { #if defined( _X360 ) XNetQosRelease( m_pQoSResult ); #endif SwitchToState( MMSTATE_INITIAL ); } //----------------------------------------------------------------------------- // Purpose: User has selected a session to join, create a local session with the same properties //----------------------------------------------------------------------------- void CMatchmaking::SelectSession( uint idx ) { XSESSION_SEARCHRESULT *pResult = NULL; if ( m_Session.IsSystemLink() ) { systemLinkInfo_s* pInfo = (systemLinkInfo_s*)m_pSystemLinkResults[idx]; pResult = &pInfo->Result; } else { pResult = &m_pSearchResults->pResults[idx]; } if ( !pResult ) return; m_Session.SetSessionInfo( &pResult->info ); ApplySessionProperties( pResult->cContexts, pResult->cProperties, pResult->pContexts, pResult->pProperties ); m_Session.SetIsHost( false ); m_Session.SetSessionSlots( SLOTS_TOTALPUBLIC, pResult->dwOpenPublicSlots + pResult->dwFilledPublicSlots ); m_Session.SetSessionSlots( SLOTS_TOTALPRIVATE, pResult->dwOpenPrivateSlots + pResult->dwFilledPrivateSlots ); if ( !m_Session.CreateSession() ) { SessionNotification( SESSION_NOTIFY_FAIL_CREATE ); return; } // Waiting for session creation results SwitchToState( MMSTATE_CREATING ); } //----------------------------------------------------------------------------- // Purpose: Join a session when invited to. //----------------------------------------------------------------------------- void CMatchmaking::JoinInviteSession( XSESSION_INFO *pHostInfo ) { if ( !pHostInfo ) { Msg( "[JoinInviteSession] resetting.\n" ); InviteCancel(); return; } // Fetch our current session id XNKID nSessionID = m_Session.GetSessionId(); // Check our invite state switch ( m_InviteState ) { case INVITE_NONE: // Initial invite call Msg( "[JoinInviteSession:INVITE_NONE] Initial call to join invite session.\n" ); // Don't bother if we're invited to join the same session if ( !Q_memcmp( &(pHostInfo->sessionID), &(nSessionID), sizeof(nSessionID) ) ) { Msg( "[JoinInviteSession:INVITE_NONE] Rejecting invite session since it is the current session.\n" ); return; } // Leave our current session, if we have one KickPlayerFromSession( 0 ); if ( &m_InviteSessionInfo != pHostInfo ) memcpy( &m_InviteSessionInfo, pHostInfo, sizeof( XSESSION_INFO ) ); m_InviteState = INVITE_PENDING; // If we are currently in progress of doing something, let it finish if ( m_bInitialized ) { if ( MMSTATE_INITIAL != m_CurrentState ) { Msg( "[JoinInviteSession:INVITE_NONE] Yielding, current state = %d.\n", m_CurrentState ); return; } } else { // We can be uninitialized due to the commentary mode - perform the disconnect to be sure ConVarRef commentary( "commentary" ); if ( commentary.IsValid() && commentary.GetBool() ) { Msg( "[JoinInviteSession:INVITE_NONE] Stopping commentary mode first.\n" ); engineClient->ClientCmd( "disconnect" ); Cbuf_Execute(); return; } } // otherwise fall through to join case INVITE_PENDING: // While the invite is pending and user changed an invite, obey the user if ( pHostInfo != &m_InviteSessionInfo ) memcpy( &m_InviteSessionInfo, pHostInfo, sizeof( XSESSION_INFO ) ); // Wait for the previous matchmaking session to finish and cleanup if ( m_bInitialized && ( MMSTATE_INITIAL != m_CurrentState ) ) { Msg( "[JoinInviteSession:INVITE_PENDING] Waiting, current state = %d.\n", m_CurrentState ); return; } #if defined( _X360 ) // Switch into validating invite mode and do it right away m_InviteState = INVITE_VALIDATING; // fall through case INVITE_VALIDATING: // Validate user storage information Msg( "[JoinInviteSession:INVITE_VALIDATING] Validating user storage before accepting invite.\n" ); // // Configure and validate waiting info in case user will have to pick storage device // m_InviteWaitingInfo.m_InviteStorageDeviceSelected = 0; m_InviteWaitingInfo.m_UserIdx = XBX_GetPrimaryUserId(); if ( m_InviteWaitingInfo.m_UserIdx == INVALID_USER_ID ) { Msg( "[JoinInviteSession:INVITE_VALIDATING] Invalid user id, aborting.\n" ); InviteCancel(); return; } m_InviteWaitingInfo.m_SignInState = XUserGetSigninState( m_InviteWaitingInfo.m_UserIdx ); if ( ( m_InviteWaitingInfo.m_SignInState != eXUserSigninState_SignedInToLive ) || ( ERROR_SUCCESS != XUserGetSigninInfo( m_InviteWaitingInfo.m_UserIdx, 0, &m_InviteWaitingInfo.m_SignInInfo ) ) || ! ( m_InviteWaitingInfo.m_SignInInfo.dwInfoFlags & XUSER_INFO_FLAG_LIVE_ENABLED ) ) { Msg( "[JoinInviteSession:INVITE_VALIDATING] Failed to get sign in to LIVE info, aborting.\n" ); InviteCancel(); return; } if ( ( ERROR_SUCCESS != XUserCheckPrivilege( m_InviteWaitingInfo.m_UserIdx, XPRIVILEGE_MULTIPLAYER_SESSIONS, &m_InviteWaitingInfo.m_PrivilegeMultiplayer ) ) || ( !m_InviteWaitingInfo.m_PrivilegeMultiplayer ) ) { Msg( "[JoinInviteSession:INVITE_VALIDATING] Privilege denied, aborting.\n" ); InviteCancel(); return; } // // Enqueue the call to track storage device once it gets selected // if ( m_InviteWaitingInfo.m_bAcceptingInvite || EngineVGui()->ValidateStorageDevice( &m_InviteWaitingInfo.m_InviteStorageDeviceSelected ) ) { m_InviteWaitingInfo.m_bAcceptingInvite = 0; Msg( "[JoinInviteSession:INVITE_VALIDATING] Storage %s, accepting.\n", m_InviteWaitingInfo.m_bAcceptingInvite ? "already queried" : "valid" ); m_InviteState = INVITE_ACCEPTING; // fall through } else { // User doesn't have a device selected and has to pick one Msg( "[JoinInviteSession:INVITE_VALIDATING] Awaiting storage.\n" ); m_InviteState = INVITE_AWAITING_STORAGE; return; } #else // Accept the invite right away m_InviteState = INVITE_ACCEPTING; // fall through #endif case INVITE_ACCEPTING: // Everything will finish this frame Msg( "[JoinInviteSession:INVITE_ACCEPTING] Accepting the invite.\n" ); break; #if defined( _X360 ) case INVITE_AWAITING_STORAGE: // Wait for user to select storage, but keep an eye on user change or logout or etc { InviteWaitingInfo_t InviteCurrentInfo; InviteCurrentInfo.m_UserIdx = XBX_GetPrimaryUserId(); if ( InviteCurrentInfo.m_UserIdx != m_InviteWaitingInfo.m_UserIdx ) { Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] User index changed, aborting.\n" ); InviteCancel(); return; } InviteCurrentInfo.m_SignInState = XUserGetSigninState( InviteCurrentInfo.m_UserIdx ); if ( ( InviteCurrentInfo.m_SignInState != m_InviteWaitingInfo.m_SignInState ) || ( ERROR_SUCCESS != XUserGetSigninInfo( InviteCurrentInfo.m_UserIdx, 0, &InviteCurrentInfo.m_SignInInfo ) ) || ! ( InviteCurrentInfo.m_SignInInfo.dwInfoFlags & XUSER_INFO_FLAG_LIVE_ENABLED ) || !IsEqualXUID( InviteCurrentInfo.m_SignInInfo.xuid, m_InviteWaitingInfo.m_SignInInfo.xuid ) ) { Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] User xuid changed, aborting.\n" ); InviteCancel(); return; } if ( ( ERROR_SUCCESS != XUserCheckPrivilege( InviteCurrentInfo.m_UserIdx, XPRIVILEGE_MULTIPLAYER_SESSIONS, &InviteCurrentInfo.m_PrivilegeMultiplayer ) ) || ( !InviteCurrentInfo.m_PrivilegeMultiplayer ) ) { Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] Privilege denied, aborting.\n" ); InviteCancel(); return; } // Check if we should keep waiting switch ( m_InviteWaitingInfo.m_InviteStorageDeviceSelected ) { case 0: // Keep waiting return; case 1: // Device selected, proceed Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] Device selected.\n" ); break; case 2: Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] Device rejected.\n" ); // User opts to run with no storage device, proceed break; default: // Device selection weird error InviteCancel(); return; } } // Otherwise user has selected the storage device, try to accept the invite once again Msg( "[JoinInviteSession:INVITE_AWAITING_STORAGE] Accepting.\n" ); m_InviteWaitingInfo.m_bAcceptingInvite = 1; m_InviteState = INVITE_NONE; JoinInviteSession( pHostInfo ); return; #endif default: Msg( "[JoinInviteSession:UnknownState=%d] Aborting.\n", m_InviteState ); InviteCancel(); return; } // Initialize our state to accept the new connection NET_SetMutiplayer( true ); InitializeLocalClient( false ); // Allow us to access private channels due to invite m_Local.m_bInvited = true; #if defined( _X360 ) // "Spoof" certain information we don't yet know about the server, knowing that we'll modify it later once connected m_Session.SetIsSystemLink( false ); m_Session.SetSessionInfo( pHostInfo ); m_Session.SetIsHost( false ); m_Session.SetContext( X_CONTEXT_GAME_TYPE, X_CONTEXT_GAME_TYPE_STANDARD, false ); m_Session.SetSessionFlags( XSESSION_CREATE_LIVE_MULTIPLAYER_STANDARD ); m_Session.SetSessionSlots( SLOTS_TOTALPUBLIC, 8 ); m_Session.SetSessionSlots( SLOTS_TOTALPRIVATE, 0 ); m_Session.SetSessionSlots( SLOTS_FILLEDPUBLIC, 0 ); m_Session.SetSessionSlots( SLOTS_FILLEDPRIVATE, 0 ); #endif // Create the session and kick off our UI if ( !m_Session.CreateSession() ) { SessionNotification( SESSION_NOTIFY_FAIL_CREATE ); return; } // Waiting for session creation results SwitchToState( MMSTATE_CREATING ); InviteCancel(); } void CMatchmaking::InviteCancel() { m_InviteState = INVITE_NONE; memset( &m_InviteWaitingInfo, 0, sizeof( m_InviteWaitingInfo ) ); } //----------------------------------------------------------------------------- // Purpose: Search for a session by ID and connect to it (done for cross-game invites) //----------------------------------------------------------------------------- void CMatchmaking::JoinInviteSessionByID( XNKID nSessionID ) { #ifdef _X360 DWORD dwResultSize = 0; XSESSION_SEARCHRESULT_HEADER *pSearchResults = NULL; // Call this once to find the proper buffer size DWORD dwError = XSessionSearchByID( nSessionID, XBX_GetPrimaryUserId(), &dwResultSize, pSearchResults, NULL ); if ( dwError != ERROR_INSUFFICIENT_BUFFER ) return; // Create a buffer big enough to hold the requested information pSearchResults = (XSESSION_SEARCHRESULT_HEADER *) new byte[dwResultSize]; ZeroMemory( pSearchResults, dwResultSize ); // Now get the real results dwError = XSessionSearchByID( nSessionID, XBX_GetPrimaryUserId(), &dwResultSize, pSearchResults, NULL ); if ( dwError != ERROR_SUCCESS ) { delete[] pSearchResults; return; } // If we found something, connect to it if ( pSearchResults->dwSearchResults > 0 ) { JoinInviteSession( &(pSearchResults->pResults[0].info) ); } else { SessionNotification( SESSION_NOTIFY_CONNECT_NOTAVAILABLE ); } // Done delete[] pSearchResults; #endif // _X360 } //----------------------------------------------------------------------------- // Purpose: Tell a session host we'd like to join the session //----------------------------------------------------------------------------- void CMatchmaking::SendJoinRequest( netadr_t *adr ) { ALIGN4 char msg_buffer[MAX_ROUTABLE_PAYLOAD] ALIGN4_POST; bf_write msg( msg_buffer, sizeof(msg_buffer) ); // Send local player info msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( PTH_CONNECT ); msg.WriteLongLong( m_Local.m_id ); // 64 bit msg.WriteByte( m_Local.m_cPlayers ); msg.WriteOneBit( m_Local.m_bInvited ); msg.WriteBytes( &m_Local.m_xnaddr, sizeof( m_Local.m_xnaddr ) ); for ( int i = 0; i < m_Local.m_cPlayers; ++i ) { msg.WriteLongLong( m_Local.m_xuids[i] ); // 64 bit msg.WriteBytes( &m_Local.m_cVoiceState, sizeof( m_Local.m_cVoiceState ) ); // TODO: has voice msg.WriteString( m_Local.m_szGamertags[i] ); } // Send message NET_SendPacket( NULL, NS_MATCHMAKING, *adr, msg.GetData(), msg.GetNumBytesWritten() ); } //----------------------------------------------------------------------------- // Purpose: Process the session host's response to our join request //----------------------------------------------------------------------------- bool CMatchmaking::ProcessJoinResponse( MM_JoinResponse *pMsg ) { switch( pMsg->m_ResponseType ) { case MM_JoinResponse::JOINRESPONSE_NOTHOSTING: if ( m_CurrentState != MMSTATE_SESSION_CONNECTING ) { return true; } Msg( "This game is no longer available.\n" ); SessionNotification( SESSION_NOTIFY_CONNECT_NOTAVAILABLE ); break; case MM_JoinResponse::JOINRESPONSE_SESSIONFULL: if ( m_CurrentState != MMSTATE_SESSION_CONNECTING ) { return true; } Msg( "This game is full.\n" ); SessionNotification( SESSION_NOTIFY_CONNECT_SESSIONFULL ); break; case MM_JoinResponse::JOINRESPONSE_APPROVED: case MM_JoinResponse::JOINRESPONSE_APPROVED_JOINGAME: if ( m_CurrentState != MMSTATE_SESSION_CONNECTING ) { return true; } // Fill in host data m_Host.m_id = pMsg->m_id; // 64 bit m_Session.SetSessionNonce( pMsg->m_Nonce ); // 64 bit m_Session.SetSessionFlags( pMsg->m_SessionFlags ); m_Session.SetOwnerId( pMsg->m_nOwnerId ); m_nHostOwnerId = pMsg->m_nOwnerId; ApplySessionProperties( pMsg->m_ContextCount, pMsg->m_PropertyCount, pMsg->m_SessionContexts.Base(), pMsg->m_SessionProperties.Base() ); for ( int i = 0; i < m_Local.m_cPlayers; ++i ) { m_Local.m_iTeam[i] = pMsg->m_iTeam; } m_nTotalTeams = pMsg->m_nTotalTeams; if ( pMsg->m_ResponseType == pMsg->JOINRESPONSE_APPROVED ) { SessionNotification( SESSION_NOTIFY_CONNECTED_TOSESSION ); SendPlayerInfoToLobby( &m_Local ); } break; case MM_JoinResponse::JOINRESPONSE_MODIFY_SESSION: if ( !m_Session.IsHost() ) { if ( m_CurrentState != MMSTATE_SESSION_CONNECTED ) { return true; } // Host has sent us some new session properties ApplySessionProperties( pMsg->m_ContextCount, pMsg->m_PropertyCount, pMsg->m_SessionContexts.Base(), pMsg->m_SessionProperties.Base() ); MM_JoinResponse response; response.m_ResponseType = MM_JoinResponse::JOINRESPONSE_MODIFY_SESSION; response.m_id = m_Local.m_id; SendMessage( &response, &m_Host ); SessionNotification( SESSION_NOTIFY_MODIFYING_COMPLETED_CLIENT ); } else { if ( m_CurrentState != MMSTATE_MODIFYING ) { return true; } // Handle this client response bool bWaiting = false; for ( int i = 0; i < m_Remote.Count(); ++i ) { if ( m_Remote[i]->m_id == pMsg->m_id ) { m_Remote[i]->m_bModified = true; } else { if ( !m_Remote[i]->m_bModified ) { bWaiting = true; } } } if ( !bWaiting ) { // Everyone has modified their session EndSessionModify(); } } break; default: break; } return true; } //----------------------------------------------------------------------------- // Purpose: Apply the contexts and properties that came from the host, and build out keyvalues for GameUI //----------------------------------------------------------------------------- void CMatchmaking::ApplySessionProperties( int numContexts, int numProperties, XUSER_CONTEXT *pContexts, XUSER_PROPERTY *pProperties ) { // Clear our existing properties, as they should be completely replaced by these new ones m_SessionContexts.RemoveAll(); m_SessionProperties.RemoveAll(); char szBuffer[MAX_PATH]; uint nGameTypeId = g_ClientDLL->GetPresenceID( "CONTEXT_GAME_TYPE" ); // Update the session properties for ( int i = 0; i < numContexts; ++i ) { XUSER_CONTEXT &ctx = pContexts[i]; m_SessionContexts.AddToTail( ctx ); const char *pID = g_ClientDLL->GetPropertyIdString( ctx.dwContextId ); g_ClientDLL->GetPropertyDisplayString( ctx.dwContextId, ctx.dwValue, szBuffer, sizeof( szBuffer ) ); // Set the display string for gameUI KeyValues *pContextKey = m_pSessionKeys->FindKey( pID, true ); pContextKey->SetName( pID ); pContextKey->SetString( "displaystring", szBuffer ); // We need to set the game type if ( ctx.dwContextId == nGameTypeId ) { m_Session.SetContext( ctx.dwContextId, ctx.dwValue, false ); } } for ( int i = 0; i < numProperties; ++i ) { XUSER_PROPERTY &prop = pProperties[i]; m_SessionProperties.AddToTail( prop ); const char *pID = g_ClientDLL->GetPropertyIdString( prop.dwPropertyId ); g_ClientDLL->GetPropertyDisplayString( prop.dwPropertyId, prop.value.nData, szBuffer, sizeof( szBuffer ) ); // Set the display string for gameUI KeyValues *pPropertyKey = m_pSessionKeys->FindKey( pID, true ); pPropertyKey->SetName( pID ); pPropertyKey->SetString( "displaystring", szBuffer ); } } //----------------------------------------------------------------------------- // Purpose: Send a join request to the session host //----------------------------------------------------------------------------- bool CMatchmaking::ConnectToHost() { AddPlayersToSession( &m_Local ); XSESSION_INFO info; m_Session.GetSessionInfo( &info ); #if defined( _X360 ) // Resolve the host's IP address XNADDR xaddr = info.hostAddress; XNKID xid = info.sessionID; IN_ADDR winaddr; if ( XNetXnAddrToInAddr( &xaddr, &xid, &winaddr ) != 0 ) { Warning( "Error resolving host IP\n" ); return false; } m_Host.m_adr.SetType( NA_IP ); m_Host.m_adr.SetIPAndPort( winaddr.S_un.S_addr, PORT_MATCHMAKING ); #endif // Initiate the network channel AddRemoteChannel( &m_Host.m_adr ); SendJoinRequest( &m_Host.m_adr ); m_fWaitTimer = GetTime(); return true; } //----------------------------------------------------------------------------- // Purpose: Waiting for a connection response from the session host //----------------------------------------------------------------------------- void CMatchmaking::UpdateConnecting() { if ( GetTime() - m_fWaitTimer > JOINREPLY_WAITTIME ) { SessionNotification( SESSION_NOTIFY_CONNECT_NOTAVAILABLE ); } } //----------------------------------------------------------------------------- // Purpose: Clean up the search results arrays //----------------------------------------------------------------------------- void CMatchmaking::ClearSearchResults() { if ( m_pSearchResults ) { free( m_pSearchResults ); m_pSearchResults = NULL; } // This will call delete and we should technically be calling delete [] m_pSystemLinkResults.PurgeAndDeleteElements(); } CON_COMMAND( mm_select_session, "Select a session" ) { if ( args.ArgC() >= 2 ) { g_pMatchmaking->SelectSession( atoi( args[1] ) ); } }