//===== Copyright 1996-2009, Valve Corporation, All rights reserved. ======// // // Purpose: // //===========================================================================// #include "mm_framework.h" #include "vstdlib/random.h" #include "fmtstr.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" static ConVar mm_ignored_sessions_forget_time( "mm_ignored_sessions_forget_time", "600", FCVAR_DEVELOPMENTONLY ); static ConVar mm_ignored_sessions_forget_pass( "mm_ignored_sessions_forget_pass", "5", FCVAR_DEVELOPMENTONLY ); class CIgnoredSessionsMgr { public: CIgnoredSessionsMgr(); public: void Reset(); void OnSearchStarted(); bool IsIgnored( XNKID xid ); void Ignore( XNKID xid ); protected: struct SessionSearchPass_t { double m_flTime; int m_nSearchCounter; }; static bool XNKID_LessFunc( const XNKID &lhs, const XNKID &rhs ) { return ( (uint64 const&) lhs ) < ( (uint64 const&) rhs ); } CUtlMap< XNKID, SessionSearchPass_t > m_IgnoredSessionsAndTime; int m_nSearchCounter; }; static CIgnoredSessionsMgr g_IgnoredSessionsMgr; static CUtlMap< uint32, float > g_mapValidatedWhitelistCacheTimestamps( DefLessFunc( uint32 ) ); CON_COMMAND_F( mm_ignored_sessions_reset, "Reset ignored sessions", FCVAR_DEVELOPMENTONLY ) { g_IgnoredSessionsMgr.Reset(); DevMsg( "Reset ignored sessions" ); } CIgnoredSessionsMgr::CIgnoredSessionsMgr() : m_IgnoredSessionsAndTime( XNKID_LessFunc ), m_nSearchCounter( 0 ) { } void CIgnoredSessionsMgr::Reset() { m_nSearchCounter = 0; m_IgnoredSessionsAndTime.RemoveAll(); } void CIgnoredSessionsMgr::OnSearchStarted() { ++ m_nSearchCounter; double fNow = Plat_FloatTime(); double const fKeepIgnoredTime = mm_ignored_sessions_forget_time.GetFloat(); int const numIgnoredSearches = mm_ignored_sessions_forget_pass.GetInt(); // Keep sessions for only so long... for ( int x = m_IgnoredSessionsAndTime.FirstInorder(); x != m_IgnoredSessionsAndTime.InvalidIndex(); ) { SessionSearchPass_t ssp = m_IgnoredSessionsAndTime.Element( x ); int xNext = m_IgnoredSessionsAndTime.NextInorder( x ); if ( fabs( fNow - ssp.m_flTime ) > fKeepIgnoredTime && m_nSearchCounter - ssp.m_nSearchCounter > numIgnoredSearches ) { m_IgnoredSessionsAndTime.RemoveAt( x ); } x = xNext; } } bool CIgnoredSessionsMgr::IsIgnored( XNKID xid ) { return ( m_IgnoredSessionsAndTime.Find( xid ) != m_IgnoredSessionsAndTime.InvalidIndex() ); } void CIgnoredSessionsMgr::Ignore( XNKID xid ) { if ( ( const uint64 & )xid == 0ull ) return; SessionSearchPass_t ssp = { Plat_FloatTime(), m_nSearchCounter }; m_IgnoredSessionsAndTime.InsertOrReplace( xid, ssp ); } // // CMatchSessionOnlineSearch // // Implementation of an online session search (aka matchmaking) // CMatchSessionOnlineSearch::CMatchSessionOnlineSearch( KeyValues *pSettings ) : m_pSettings( pSettings->MakeCopy() ), m_autodelete_pSettings( m_pSettings ), m_eState( STATE_INIT ), m_pSysSession( NULL ), m_pMatchSearcher( NULL ), m_result( RESULT_UNDEFINED ), m_pSysSessionConTeam (NULL), #if !defined( NO_STEAM ) m_pServerListListener( NULL ), #endif m_flInitializeTimestamp( 0.0f ) { DevMsg( "Created CMatchSessionOnlineSearch:\n" ); KeyValuesDumpAsDevMsg( m_pSettings, 1 ); } CMatchSessionOnlineSearch::CMatchSessionOnlineSearch() : m_pSettings( NULL ), m_autodelete_pSettings( (KeyValues*)NULL ), m_eState( STATE_INIT ), m_pSysSession( NULL ), m_pMatchSearcher( NULL ), m_result( RESULT_UNDEFINED ), m_pSysSessionConTeam (NULL), m_flInitializeTimestamp( 0.0f ) { } CMatchSessionOnlineSearch::~CMatchSessionOnlineSearch() { if ( m_pMatchSearcher ) m_pMatchSearcher->Destroy(); m_pMatchSearcher = NULL; DevMsg( "Destroying CMatchSessionOnlineSearch:\n" ); KeyValuesDumpAsDevMsg( m_pSettings, 1 ); } KeyValues * CMatchSessionOnlineSearch::GetSessionSettings() { return m_pSettings; } void CMatchSessionOnlineSearch::UpdateSessionSettings( KeyValues *pSettings ) { Warning( "CMatchSessionOnlineSearch::UpdateSessionSettings is unavailable in state %d!\n", m_eState ); Assert( !"CMatchSessionOnlineSearch::UpdateSessionSettings is unavailable!\n" ); } void CMatchSessionOnlineSearch::Command( KeyValues *pCommand ) { Warning( "CMatchSessionOnlineSearch::Command is unavailable!\n" ); Assert( !"CMatchSessionOnlineSearch::Command is unavailable!\n" ); } uint64 CMatchSessionOnlineSearch::GetSessionID() { return 0; } #if !defined( NO_STEAM ) extern volatile uint32 *g_hRankingSetupCallHandle; void CMatchSessionOnlineSearch::SetupSteamRankingConfiguration() { KeyValues *kvNotification = new KeyValues( "SetupSteamRankingConfiguration" ); kvNotification->SetPtr( "settingsptr", m_pSettings ); kvNotification->SetPtr( "callhandleptr", ( void * ) &g_hRankingSetupCallHandle ); g_pMatchEventsSubscription->BroadcastEvent( kvNotification ); } bool CMatchSessionOnlineSearch::IsSteamRankingConfigured() const { return !g_hRankingSetupCallHandle || !*g_hRankingSetupCallHandle; } #endif extern ConVar mm_session_sys_ranking_timeout; extern ConVar mm_session_search_qos_timeout; void CMatchSessionOnlineSearch::Update() { switch ( m_eState ) { case STATE_INIT: if ( !m_flInitializeTimestamp ) { m_flInitializeTimestamp = Plat_FloatTime(); #if !defined( NO_STEAM ) SetupSteamRankingConfiguration(); #endif } #if !defined( NO_STEAM ) if ( !IsSteamRankingConfigured() && ( Plat_FloatTime() < m_flInitializeTimestamp + mm_session_sys_ranking_timeout.GetFloat() ) ) break; #endif m_eState = STATE_SEARCHING; // Kick off the search g_IgnoredSessionsMgr.OnSearchStarted(); m_pMatchSearcher = OnStartSearching(); // Update our settings with match searcher m_pSettings->deleteThis(); m_pSettings = m_pMatchSearcher->GetSearchSettings()->MakeCopy(); m_autodelete_pSettings.Assign( m_pSettings ); // Run the first frame update on the searcher m_pMatchSearcher->Update(); break; case STATE_SEARCHING: // Waiting for session search to complete m_pMatchSearcher->Update(); break; case STATE_JOIN_NEXT: StartJoinNextFoundSession(); break; #if !defined( NO_STEAM ) case STATE_VALIDATING_WHITELIST: if ( Plat_FloatTime() > m_flInitializeTimestamp + mm_session_search_qos_timeout.GetFloat() ) { DevWarning( "Steam whitelist validation timed out.\n" ); Steam_OnDedicatedServerListFetched(); } break; #endif case STATE_JOINING: // Waiting for the join negotiation if ( m_pSysSession ) { m_pSysSession->Update(); } if (m_pSysSessionConTeam) { m_pSysSessionConTeam->Update(); switch ( m_pSysSessionConTeam->GetResult() ) { case CSysSessionConTeamHost::RESULT_SUCCESS: OnSearchCompletedSuccess( NULL, m_pSettings ); break; case CSysSessionConTeamHost::RESULT_FAIL: m_pSysSessionConTeam->Destroy(); m_pSysSessionConTeam = NULL; // Try next session m_eState = STATE_JOIN_NEXT; break; } } break; } } void CMatchSessionOnlineSearch::Destroy() { // Stop the search if ( m_pMatchSearcher ) { m_pMatchSearcher->Destroy(); m_pMatchSearcher = NULL; } // If we are in the middle of connecting, // abort if ( m_pSysSession ) { m_pSysSession->Destroy(); m_pSysSession = NULL; } if ( m_pSysSessionConTeam ) { m_pSysSessionConTeam->Destroy(); m_pSysSessionConTeam = NULL; } #if !defined( NO_STEAM ) if ( m_pServerListListener ) { m_pServerListListener->Destroy(); m_pServerListListener = NULL; } #endif delete this; } void CMatchSessionOnlineSearch::DebugPrint() { DevMsg( "CMatchSessionOnlineSearch [ state=%d ]\n", m_eState ); DevMsg( "System data:\n" ); KeyValuesDumpAsDevMsg( GetSessionSystemData(), 1 ); DevMsg( "Settings data:\n" ); KeyValuesDumpAsDevMsg( GetSessionSettings(), 1 ); if ( m_pSysSession ) m_pSysSession->DebugPrint(); else DevMsg( "SysSession is NULL\n" ); DevMsg( "Search results outstanding: %d\n", m_arrSearchResults.Count() ); } void CMatchSessionOnlineSearch::OnEvent( KeyValues *pEvent ) { char const *szEvent = pEvent->GetName(); if ( !Q_stricmp( "mmF->SysSessionUpdate", szEvent ) ) { if ( m_pSysSession && pEvent->GetPtr( "syssession", NULL ) == m_pSysSession ) { // This is our session switch ( m_eState ) { case STATE_JOINING: // Session was creating if ( char const *szError = pEvent->GetString( "error", NULL ) ) { // Destroy the session m_pSysSession->Destroy(); m_pSysSession = NULL; // Go ahead and join next available session m_eState = STATE_JOIN_NEXT; } else { // We have received an entirely new "settings" data and copied that to our "settings" data m_eState = STATE_CLOSING; // Now we need to create a new client session CSysSessionClient *pSysSession = m_pSysSession; KeyValues *pSettings = m_pSettings; // Release ownership of the resources since new match session now owns them m_pSysSession = NULL; m_pSettings = NULL; m_autodelete_pSettings.Assign( NULL ); OnSearchCompletedSuccess( pSysSession, pSettings ); return; } break; } } } } void CMatchSessionOnlineSearch::OnSearchEvent( KeyValues *pNotify ) { g_pMatchEventsSubscription->BroadcastEvent( pNotify ); } CSysSessionClient * CMatchSessionOnlineSearch::OnBeginJoiningSearchResult() { return new CSysSessionClient( m_pSettings ); } void CMatchSessionOnlineSearch::OnSearchDoneNoResultsMatch() { m_eState = STATE_CLOSING; // Reset ignored session tracker g_IgnoredSessionsMgr.Reset(); // Just go ahead and create the session KeyValues *pSettings = m_pSettings; m_pSettings = NULL; m_autodelete_pSettings.Assign( NULL ); OnSearchCompletedEmpty( pSettings ); } void CMatchSessionOnlineSearch::OnSearchCompletedSuccess( CSysSessionClient *pSysSession, KeyValues *pSettings ) { m_result = RESULT_SUCCESS; // Note that m_pSysSessionConTeam will be NULL if this is an individual joining a // match that was started with con team KeyValues *teamMatch = pSettings->FindKey( "options/conteammatch" ); if ( teamMatch && m_pSysSessionConTeam ) { DevMsg( "OnlineSearch - ConTeam host reserved session\n" ); KeyValuesDumpAsDevMsg( pSettings ); int numPlayers, sides[10]; uint64 playerIds[10]; if ( !m_pSysSessionConTeam->GetPlayerSidesAssignment( &numPlayers, playerIds, sides ) ) { // Something went badly wrong, bail m_result = RESULT_FAIL; } else { // Send out a "joinsession" event. This is picked up by the host and sent out // to all machines machines in lobby. KeyValues *joinSession = new KeyValues( "OnMatchSessionUpdate", "state", "joinconteamsession" ); #if defined (_X360) uint64 sessionId = pSettings->GetUint64( "options/sessionid", 0 ); const char *sessionInfo = pSettings->GetString( "options/sessioninfo", "" ); joinSession->SetUint64( "sessionid", sessionId ); joinSession->SetString( "sessioninfo", sessionInfo ); // Unpack sessionHostData KeyValues *pSessionHostData = (KeyValues*)pSettings->GetPtr( "options/sessionHostData" ); if ( pSessionHostData ) { KeyValues *pSessionHostDataUnpacked = joinSession->CreateNewKey(); pSessionHostDataUnpacked->SetName("sessionHostDataUnpacked"); pSessionHostData->CopySubkeys( pSessionHostDataUnpacked ); } #else joinSession->SetUint64( "sessionid", m_pSysSessionConTeam->GetSessionID() ); #endif KeyValues *pTeamMembers = joinSession->CreateNewKey(); pTeamMembers->SetName( "teamMembers" ); pTeamMembers->SetInt( "numPlayers", numPlayers ); // Assign players to different teams for ( int i = 0; i < numPlayers; i++ ) { KeyValues *pTeamPlayer = pTeamMembers->CreateNewKey(); pTeamPlayer->SetName( CFmtStr( "player%d", i ) ); pTeamPlayer->SetUint64( "xuid", playerIds[i] ); pTeamPlayer->SetInt( "team", sides[i] ); } OnSearchEvent( joinSession ); } } else { // Destroy our instance and point at the new match interface CMatchSessionOnlineClient *pNewSession = new CMatchSessionOnlineClient( pSysSession, pSettings ); g_pMMF->SetCurrentMatchSession( pNewSession ); this->Destroy(); DevMsg( "OnlineSearch - client fully connected to session, search finished.\n" ); pNewSession->OnClientFullyConnectedToSession(); } } void CMatchSessionOnlineSearch::OnSearchCompletedEmpty( KeyValues *pSettings ) { KeyValues::AutoDelete autodelete_pSettings( pSettings ); m_result = RESULT_FAIL; KeyValues *notify = new KeyValues( "OnMatchSessionUpdate", "state", "progress", "progress", "searchempty" ); notify->SetPtr( "settingsptr", pSettings ); OnSearchEvent( notify ); if ( !Q_stricmp( pSettings->GetString( "options/searchempty" ), "close" ) ) { g_pMatchFramework->CloseSession(); return; } // If this is a team session then stop here and let the team host decide what // to do next KeyValues *teamMatch = pSettings->FindKey( "options/conteammatch" ); if ( teamMatch ) { return; } // Preserve the "options/bypasslobby" key bool bypassLobby = pSettings->GetBool( "options/bypasslobby", false ); // Preserve the "options/server" key char serverType[64]; const char *prevServerType = pSettings->GetString( "options/server", NULL ); if ( prevServerType ) { Q_strncpy( serverType, prevServerType, sizeof( serverType ) ); } // Remove "options" key if ( KeyValues *kvOptions = pSettings->FindKey( "options" ) ) { pSettings->RemoveSubKey( kvOptions ); kvOptions->deleteThis(); } pSettings->SetString( "options/createreason", "searchempty" ); if ( bypassLobby ) { pSettings->SetBool( "options/bypasslobby", bypassLobby ); } if ( prevServerType ) { pSettings->SetString( "options/server", serverType ); } DevMsg( "Search completed empty - creating a new session\n" ); KeyValuesDumpAsDevMsg( pSettings ); g_pMatchFramework->CreateSession( pSettings ); } void CMatchSessionOnlineSearch::UpdateTeamProperties( KeyValues *pTeamProperties ) { } void CMatchSessionOnlineSearch::StartJoinNextFoundSession() { if ( !m_arrSearchResults.Count() ) { OnSearchDoneNoResultsMatch(); return; } // Session is joining KeyValues *notify = new KeyValues( "OnMatchSessionUpdate", "state", "progress", "progress", "searchresult" ); notify->SetInt( "numResults", m_arrSearchResults.Count() ); OnSearchEvent( notify ); // Peek at the next search result CMatchSearcher::SearchResult_t const &sr = *m_arrSearchResults.Head(); // Register it into the ignored session pool g_IgnoredSessionsMgr.Ignore( sr.GetXNKID() ); // Make a validation query ValidateSearchResultWhitelist(); } static uint32 OfficialWhitelistClientCachedAddress( uint32 uiServerIP ) { /** Removed for partner depot **/ return 0; } void CMatchSessionOnlineSearch::ValidateSearchResultWhitelist() { #if !defined( NO_STEAM ) // In case of official matchmaking we need to validate that the server // that we are about to join actually is whitelisted official server if ( !m_pSettings->GetInt( "game/hosted" ) ) { // Peek at the next search result CMatchSearcher::SearchResult_t const &sr = *m_arrSearchResults.Head(); if ( ( g_mapValidatedWhitelistCacheTimestamps.Find( sr.m_svAdr.GetIPHostByteOrder() ) == g_mapValidatedWhitelistCacheTimestamps.InvalidIndex() ) && !OfficialWhitelistClientCachedAddress( sr.m_svAdr.GetIPHostByteOrder() ) ) { // This server needs to be validated m_flInitializeTimestamp = Plat_FloatTime(); m_eState = STATE_VALIDATING_WHITELIST; CUtlVector< MatchMakingKeyValuePair_t > filters; filters.EnsureCapacity( 10 ); filters.AddToTail( MatchMakingKeyValuePair_t( "gamedir", COM_GetModDirectory() ) ); filters.AddToTail( MatchMakingKeyValuePair_t( "addr", sr.m_svAdr.ToString() ) ); filters.AddToTail( MatchMakingKeyValuePair_t( "white", "1" ) ); m_pServerListListener = new CServerListListener( this, filters ); return; } } #endif ConnectJoinLobbyNextFoundSession(); } #if !defined( NO_STEAM ) CMatchSessionOnlineSearch::CServerListListener::CServerListListener( CMatchSessionOnlineSearch *pDsSearcher, CUtlVector< MatchMakingKeyValuePair_t > &filters ) : m_pOuter( pDsSearcher ), m_hRequest( NULL ) { MatchMakingKeyValuePair_t *pFilter = filters.Base(); DevMsg( 1, "Requesting dedicated whitelist validation...\n" ); for (int i = 0; i < filters.Count(); i++) { DevMsg("Filter %d: %s=%s\n", i, filters.Element(i).m_szKey, filters.Element(i).m_szValue); } m_hRequest = steamapicontext->SteamMatchmakingServers()->RequestInternetServerList( ( AppId_t ) g_pMatchFramework->GetMatchTitle()->GetTitleID(), &pFilter, filters.Count(), this ); } void CMatchSessionOnlineSearch::CServerListListener::Destroy() { m_pOuter = NULL; if ( m_hRequest ) steamapicontext->SteamMatchmakingServers()->ReleaseRequest( m_hRequest ); m_hRequest = NULL; delete this; } void CMatchSessionOnlineSearch::CServerListListener::HandleServerResponse( HServerListRequest hReq, int iServer, bool bResponded ) { // Register the result if ( bResponded ) { gameserveritem_t *pServer = steamapicontext->SteamMatchmakingServers() ->GetServerDetails( hReq, iServer ); DevMsg( 1, "Successfully validated whitelist for %s...\n", pServer->m_NetAdr.GetConnectionAddressString() ); g_mapValidatedWhitelistCacheTimestamps.InsertOrReplace( pServer->m_NetAdr.GetIP(), Plat_FloatTime() ); } } void CMatchSessionOnlineSearch::CServerListListener::RefreshComplete( HServerListRequest hReq, EMatchMakingServerResponse response ) { if ( m_pOuter ) { m_pOuter->Steam_OnDedicatedServerListFetched(); } } void CMatchSessionOnlineSearch::Steam_OnDedicatedServerListFetched() { if ( m_pServerListListener ) { m_pServerListListener->Destroy(); m_pServerListListener = NULL; } // Peek at the next search result if ( m_arrSearchResults.Count() ) { CMatchSearcher::SearchResult_t const &sr = *m_arrSearchResults.Head(); if ( g_mapValidatedWhitelistCacheTimestamps.Find( sr.m_svAdr.GetIPHostByteOrder() ) == g_mapValidatedWhitelistCacheTimestamps.InvalidIndex() ) { DevWarning( 1, "Failed to validate whitelist for %s...\n", sr.m_svAdr.ToString( true ) ); FOR_EACH_VEC_BACK( m_arrSearchResults, itSR ) { if ( m_arrSearchResults[itSR]->m_svAdr.GetIPHostByteOrder() == sr.m_svAdr.GetIPHostByteOrder() ) { m_arrSearchResults.Remove( itSR ); } } } } m_eState = STATE_JOIN_NEXT; } #endif void CMatchSessionOnlineSearch::ConnectJoinLobbyNextFoundSession() { // Pop the search result CMatchSearcher::SearchResult_t const &sr = *m_arrSearchResults.Head(); m_arrSearchResults.RemoveMultipleFromHead( 1 ); // Set the settings to connect with if ( KeyValues *kvOptions = m_pSettings->FindKey( "options", true ) ) { #ifdef _X360 kvOptions->SetUint64( "sessionid", ( const uint64 & ) sr.m_info.sessionID ); char chSessionInfo[ XSESSION_INFO_STRING_LENGTH ] = {0}; MMX360_SessionInfoToString( sr.m_info, chSessionInfo ); kvOptions->SetString( "sessioninfo", chSessionInfo ); kvOptions->SetPtr( "sessionHostData", sr.GetGameDetails() ); KeyValuesDumpAsDevMsg( sr.GetGameDetails(), 1, 2 ); #else kvOptions->SetUint64( "sessionid", sr.m_uiLobbyId ); #endif } // Trigger client session creation Msg( "[MM] Joining session %llx, %d search results remaining...\n", m_pSettings->GetUint64( "options/sessionid", 0ull ), m_arrSearchResults.Count() ); KeyValues *teamMatch = m_pSettings->FindKey( "options/conteammatch" ); if ( teamMatch ) { m_pSysSessionConTeam = new CSysSessionConTeamHost( m_pSettings ); } else { m_pSysSession = OnBeginJoiningSearchResult(); } m_eState = STATE_JOINING; } CMatchSearcher_OnlineSearch::CMatchSearcher_OnlineSearch( CMatchSessionOnlineSearch *pSession, KeyValues *pSettings ) : CMatchSearcher( pSettings ), m_pSession( pSession ) { } void CMatchSearcher_OnlineSearch::OnSearchEvent( KeyValues *pNotify ) { m_pSession->OnSearchEvent( pNotify ); } void CMatchSearcher_OnlineSearch::OnSearchDone() { // Let the base searcher finalize results CMatchSearcher::OnSearchDone(); // Iterate over search results for ( int k = 0, kNum = GetNumSearchResults(); k < kNum; ++ k ) { SearchResult_t const &sr = GetSearchResult( k ); if ( !g_IgnoredSessionsMgr.IsIgnored( sr.GetXNKID() ) ) m_pSession->m_arrSearchResults.AddToTail( &sr ); } if ( !m_pSession->m_arrSearchResults.Count() ) { m_pSession->OnSearchDoneNoResultsMatch(); return; } // Go ahead and start joining the results DevMsg( "Establishing connection with %d search results.\n", m_pSession->m_arrSearchResults.Count() ); m_pSession->m_eState = m_pSession->STATE_JOIN_NEXT; } CMatchSearcher * CMatchSessionOnlineSearch::OnStartSearching() { CMatchSearcher *pMS = new CMatchSearcher_OnlineSearch( this, m_pSettings->MakeCopy() ); // Let the title extend the game settings g_pMMF->GetMatchTitleGameSettingsMgr()->InitializeGameSettings( pMS->GetSearchSettings(), "search_online" ); DevMsg( "CMatchSearcher_OnlineSearch title adjusted settings:\n" ); KeyValuesDumpAsDevMsg( pMS->GetSearchSettings(), 1 ); return pMS; }