csgo-2018-source/matchmaking/mm_session_online_search.cpp
2021-07-24 21:11:47 -07:00

772 lines
21 KiB
C++

//===== 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;
}