//========= Copyright © 1996-2009, Valve Corporation, All rights reserved. ============// // // Purpose: // //=====================================================================================// #ifndef _X360 #include "xbox/xboxstubs.h" #endif #include "mm_framework.h" #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) #include "steam/matchmakingtypes.h" #endif #include "proto_oob.h" #include "fmtstr.h" // NOTE: This has to be the last file included! #include "tier0/memdbgon.h" #pragma warning (disable : 4355 ) size_t const mm_filter_max_size = 128; // Maximum size of a filter key, it's key+value+3 static ConVar mm_server_search_update_interval( "mm_server_search_update_interval", "60", FCVAR_DEVELOPMENTONLY, "Interval between servers updates." ); static ConVar mm_server_search_inet_ping_interval( "mm_server_search_inet_ping_interval", "1.0", FCVAR_DEVELOPMENTONLY, "How long to wait between pinging internet server details." ); static ConVar mm_server_search_inet_ping_timeout( "mm_server_search_inet_ping_timeout", "3.0", FCVAR_DEVELOPMENTONLY, "How long to wait for internet server details." ); static ConVar mm_server_search_inet_ping_window( "mm_server_search_inet_ping_window", "10", FCVAR_DEVELOPMENTONLY, "How many servers can be pinged for server details in a batch." ); static ConVar mm_server_search_inet_ping_refresh( "mm_server_search_inet_ping_refresh", "15", FCVAR_DEVELOPMENTONLY, "How often to refresh a listed server." ); static ConVar mm_server_search_server_lifetime( "mm_server_search_server_lifetime", "180", FCVAR_DEVELOPMENTONLY, "How long until a server is no longer returned by the master till we remove it." ); static ConVar mm_server_search_lan_ping_interval( "mm_server_search_lan_ping_interval", "0.4", FCVAR_DEVELOPMENTONLY, "Interval between LAN discovery pings." ); static ConVar mm_server_search_lan_ping_duration( "mm_server_search_lan_ping_duration", "1.0", FCVAR_DEVELOPMENTONLY, "Duration of LAN discovery ping phase." ); static ConVar mm_server_search_lan_ports( "mm_server_search_lan_ports", "27015,27016,27017,27018,27019,27020", FCVAR_RELEASE | FCVAR_ARCHIVE, "Ports to scan during LAN games discovery. Also used to discover and correctly connect to dedicated LAN servers behind NATs." ); // // Server implementation // class CServerPinging : public CServer { public: CServerPinging() : m_flTimeout( 0 ) {} public: #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) gameserveritem_t m_gsi; #endif float m_flTimeout; }; CServer::CServer() : m_flLastRefresh( Plat_FloatTime() ), m_xuid( 0ull ), m_pGameDetails( NULL ) { } CServer::~CServer() { if ( m_pGameDetails ) m_pGameDetails->deleteThis(); m_pGameDetails = NULL; } XUID CServer::GetOnlineId() { return m_xuid; } KeyValues * CServer::GetGameDetails() { return m_pGameDetails; } bool CServer::IsJoinable() { return m_pGameDetails != NULL; } void CServer::Join() { char const *szConnectString = m_pGameDetails->GetString( "server/connectstring", NULL ); if ( !szConnectString || !*szConnectString ) return; g_pMatchExtensions->GetIVEngineClient()->ClientCmd( CFmtStr( "connect %s\n", szConnectString ) ); } // // Server manager implementation // CServerManager::CServerManager() { #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) m_hRequest = NULL; #endif m_bUpdateEnabled = false; m_flNextUpdateTime = 0.0f; m_flNextServerUpdateTime = 0.0f; m_eState = STATE_IDLE; } CServerManager::~CServerManager() { m_Servers.PurgeAndDeleteElements(); m_ServersPinging.PurgeAndDeleteElements(); #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) if ( m_hRequest ) steamapicontext->SteamMatchmakingServers()->ReleaseRequest( m_hRequest ); m_hRequest = NULL; #endif } static CServerManager g_ServerManager; CServerManager *g_pServerManager = &g_ServerManager; void CServerManager::EnableServersUpdate( bool bEnable ) { if ( bEnable && ( g_pMatchFramework->GetMatchTitle()->GetTitleSettingsFlags() & MATCHTITLE_SERVERMGR_DISABLED ) ) bEnable = false; m_bUpdateEnabled = bEnable; m_flNextUpdateTime = 0.0f; // If enabled the search, we'll pick it up next update if ( bEnable ) return; // Otherwise search is being disabled m_Servers.PurgeAndDeleteElements(); m_ServersPinging.PurgeAndDeleteElements(); #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) if ( m_eState == STATE_FETCHING_SERVERS && m_hRequest ) { steamapicontext->SteamMatchmakingServers()->ReleaseRequest( m_hRequest ); m_hRequest = NULL; } #endif // Will clean up all servers and go IDLE OnAllDataFetched(); } int CServerManager::GetNumServers() { return m_Servers.Count(); } IMatchServer * CServerManager::GetServerByIndex( int iServerIdx ) { return m_Servers.IsValidIndex( iServerIdx ) ? m_Servers[ iServerIdx ] : NULL; } IMatchServer * CServerManager::GetServerByOnlineId( XUID xuidServerOnline ) { return GetServerRecordByOnlineId( m_Servers, xuidServerOnline ); } CServer * CServerManager::GetServerRecordByOnlineId( CUtlVector< CServer * > &arr, XUID xuidServerOnline ) { for ( int k = 0; k < arr.Count(); ++ k ) { CServer *pServer = arr[ k ]; if ( pServer && pServer->GetOnlineId() == xuidServerOnline ) return pServer; } return NULL; } void CServerManager::OnEvent( KeyValues *pEvent ) { if ( IsX360() ) return; char const *szName = pEvent->GetName(); if ( !Q_stricmp( szName, "OnNetLanConnectionlessPacket" ) ) { char const * arrServerProbeKeys[] = { "LanSearchServerPing", "ConnectServerDetailsRequest", "InetSearchServerDetails" }; for ( int k = 0; k < ARRAYSIZE( arrServerProbeKeys ); ++ k ) { KeyValues *pProbeKey = pEvent->FindKey( arrServerProbeKeys[k] ); if ( !pProbeKey ) continue; KeyValues *pDetails = g_pMatchFramework->GetMatchNetworkMsgController()->GetActiveServerGameDetails( pEvent ); KeyValues::AutoDelete autodelete_pDetails( pDetails ); if ( !pDetails ) return; if ( !pDetails->FindKey( "server" ) ) return; pDetails->FindKey( arrServerProbeKeys[k], true )->MergeFrom( pProbeKey, KeyValues::MERGE_KV_UPDATE ); g_pConnectionlessLanMgr->SendPacket( pDetails, pEvent->GetString( "from" ), INetSupport::NS_SOCK_SERVER ); return; } if ( KeyValues *pGameDetailsServer = pEvent->FindKey( "GameDetailsServer" ) ) { // Incoming data: // // System // Game // Server // Members // LanSearchServerPing // timestamp = holds the time packet was sent for ping // Server ping int nPing = 0; if ( float flTimeSent = pGameDetailsServer->GetFloat( "LanSearchServerPing/timestamp" ) ) { float flSeconds = Plat_FloatTime() - flTimeSent; nPing = flSeconds * 1000; if ( nPing < 0 ) nPing = 0; if ( nPing >= 1000 ) nPing = 999; } else if ( uint64 xuidInetPing = pGameDetailsServer->GetUint64( "InetSearchServerDetails/pingxuid" ) ) { CServerPinging *pServerPinging = ( CServerPinging * ) GetServerRecordByOnlineId( m_ServersPinging, xuidInetPing ); if ( !pServerPinging ) return; if ( float flTimeSent = pGameDetailsServer->GetFloat( "InetSearchServerDetails/timestamp" ) ) { float flSeconds = Plat_FloatTime() - flTimeSent; nPing = flSeconds * 1000; if ( nPing < 0 ) nPing = 0; if ( nPing >= 1000 ) nPing = 999; } #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) else { nPing = pServerPinging->m_gsi.m_nPing; } #endif // Remove the server from outstanding pings list m_ServersPinging.FindAndFastRemove( pServerPinging ); delete pServerPinging; } else if ( char const *szDetailsAdr = pGameDetailsServer->GetString( "ConnectServerDetailsRequest/server" ) ) { g_pMatchExtensions->GetINetSupport()->OnMatchEvent( pEvent ); return; } else return; // Server address char const *szAddr = pGameDetailsServer->GetString( "server/adronline", NULL ); if ( !szAddr || !*szAddr ) return; // Determine server network address netadr_t netAddress; netAddress.SetType( NA_IP ); netAddress.SetPort( PORT_SERVER ); netAddress.SetFromString( szAddr ); // Check if this is not our local server INetSupport::ServerInfo_t si; g_pMatchExtensions->GetINetSupport()->GetServerInfo( &si ); if ( si.m_bActive && ( si.m_netAdr.CompareAdr( netAddress ) || si.m_netAdrOnline.CompareAdr( netAddress ) ) ) return; // Coalesce it into its XUID online id XUID xuidOnline = uint64( netAddress.GetIPNetworkByteOrder() ) | ( uint64( netAddress.GetPort() ) << 32ull ); // Prepare the settings KeyValues *pSettings = pGameDetailsServer->MakeCopy(); pSettings->SetName( "settings" ); if ( KeyValues *kvLanSearch = pSettings->FindKey( "LanSearch" ) ) { pSettings->RemoveSubKey( kvLanSearch ); kvLanSearch->deleteThis(); } pSettings->SetInt( "server/ping", nPing ); if ( char const *szPacketFrom = pEvent->GetString( "from", NULL ) ) pSettings->SetString( "server/connectstring", szPacketFrom ); // // Find the server or create a new one // IMatchServer *pExistingServer = GetServerByOnlineId( xuidOnline ); CServer *pServer = ( CServer * ) pExistingServer; if ( !pServer ) { pServer = new CServer(); pServer->m_xuid = xuidOnline; #if !defined( NO_STEAM ) && !defined( SWDS ) servernetadr_t serverNetAddr; serverNetAddr.Init( netAddress.GetIPHostByteOrder(), netAddress.GetPort(), netAddress.GetPort() ); pServer->m_netAdr = serverNetAddr; #endif m_Servers.AddToTail( pServer ); } if ( pServer->m_pGameDetails ) { // Average out the ping value int nKnownPing = pServer->m_pGameDetails->GetInt( "server/ping", 0 ); if ( nKnownPing < nPing ) { // we got a high ping, try to display the ping as low as possible nPing = ( nKnownPing * 9 + nPing * 1 ) / 10; pSettings->SetInt( "server/ping", nPing ); } pServer->m_pGameDetails->deleteThis(); } pServer->m_pGameDetails = pSettings; pServer->m_flLastRefresh = Plat_FloatTime(); // Signal that we have updated a server KeyValues *kvEvent = new KeyValues( "OnMatchServerMgrUpdate", "update", "server" ); kvEvent->SetUint64( "xuid", xuidOnline ); g_pMatchFramework->GetEventsSubscription()->BroadcastEvent( kvEvent ); } } else if ( !Q_stricmp( "Client::ResendGameDetailsRequest", szName ) ) { KeyValues *kv = new KeyValues( "ConnectServerDetailsRequest" ); KeyValues::AutoDelete autodelete( kv ); kv->SetString( "server", pEvent->GetString( "to", "" ) ); g_pConnectionlessLanMgr->SendPacket( kv, pEvent->GetString( "to", "" ) ); } } #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) void CServerManager::ServerResponded( HServerListRequest hReq, int iServer ) { gameserveritem_t *gsi = steamapicontext->SteamMatchmakingServers()->GetServerDetails( hReq, iServer ); if ( !gsi ) return; // Every time a server responds during groups query, bump this groups last request timeout m_lanSearchData.m_flLastBroadcastTime = Plat_FloatTime(); // Determine server network address netadr_t netAddress; netAddress.SetType( NA_IP ); netAddress.SetPort( PORT_SERVER ); netAddress.SetFromString( gsi->m_NetAdr.GetConnectionAddressString() ); // Coalesce it into its XUID online id XUID xuidOnline = uint64( netAddress.GetIPNetworkByteOrder() ) | ( uint64( netAddress.GetPort() ) << 32ull ); // Check if we have a pinging record for this server CServerPinging *pServer = ( CServerPinging * ) GetServerRecordByOnlineId( m_ServersPinging, xuidOnline ); if ( pServer ) { // We already have a pinging record for this server, just update its info pServer->m_gsi = *gsi; pServer->m_netAdr = gsi->m_NetAdr; return; } // Create a new pinging record pServer = new CServerPinging(); pServer->m_xuid = xuidOnline; pServer->m_gsi = *gsi; pServer->m_netAdr = gsi->m_NetAdr; m_ServersPinging.AddToTail( pServer ); } void CServerManager::RefreshComplete( HServerListRequest hReq, EMatchMakingServerResponse response ) { if ( m_eState == STATE_FETCHING_SERVERS ) m_eState = STATE_GROUP_FETCHED; steamapicontext->SteamMatchmakingServers()->ReleaseRequest( m_hRequest ); m_hRequest = NULL; } #endif void CServerManager::Update() { #if !( !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) ) return; #endif float now = Plat_FloatTime(); switch ( m_eState ) { case STATE_IDLE: if ( m_bUpdateEnabled && !IsLocalClientConnectedToServer() ) { if ( now > m_flNextUpdateTime ) { MarkOldServersAndBeginSearch(); } else if ( now > m_flNextServerUpdateTime ) { float nextUpdatePeriod = mm_server_search_inet_ping_refresh.GetFloat(); // any servers not in the pinging list but haven't been refreshed in a while should be refreshed for ( int i = 0; i < m_Servers.Count(); ++i ) { float timePassed = now - m_Servers[i]->m_flLastRefresh; if ( timePassed > mm_server_search_inet_ping_refresh.GetFloat() ) { CServerPinging *pServerPinging = ( CServerPinging * ) GetServerRecordByOnlineId( m_ServersPinging, m_Servers[i]->GetOnlineId() ); if ( !pServerPinging ) { pServerPinging = new CServerPinging(); pServerPinging->m_xuid = m_Servers[i]->GetOnlineId(); #if !defined( NO_STEAM ) && !defined( SWDS ) pServerPinging->m_netAdr = m_Servers[i]->m_netAdr; #endif m_ServersPinging.AddToTail( pServerPinging ); } } else { nextUpdatePeriod = MIN( nextUpdatePeriod, mm_server_search_inet_ping_refresh.GetFloat() - timePassed ); } } m_flNextServerUpdateTime = now + nextUpdatePeriod; m_eState = STATE_REQUESTING_DETAILS; break; } } break; case STATE_LAN_SEARCH: UpdateLanSearch(); break; case STATE_GROUP_SEARCH: if ( StartFetchingGroupServersData() ) break; // else -> // fall through case STATE_GROUP_FETCHED: OnGroupFetched(); break; #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) case STATE_FETCHING_SERVERS: if ( Plat_FloatTime() > m_lanSearchData.m_flLastBroadcastTime + mm_server_search_inet_ping_timeout.GetFloat() ) { steamapicontext->SteamMatchmakingServers()->ReleaseRequest( m_hRequest ); m_hRequest = NULL; m_eState = STATE_GROUP_FETCHED; } break; #endif case STATE_REQUESTING_DETAILS: UpdateRequestingDetails(); break; } } void CServerManager::MarkOldServersAndBeginSearch() { DevMsg( 2, "Server manager refreshing...\n" ); // Signal that we are starting a search g_pMatchFramework->GetEventsSubscription()->BroadcastEvent( new KeyValues( "OnMatchServerMgrUpdate", "update", "searchstarted" ) ); m_ServersPinging.PurgeAndDeleteElements(); m_groupSearchData.Reset(); m_lanSearchData = SLanSearchData_t(); m_eState = STATE_LAN_SEARCH; // If broadcasts are disallowed, then go straight to group search state extern ConVar net_allow_multicast; if ( !net_allow_multicast.GetBool() ) m_eState = STATE_GROUP_SEARCH; } void CServerManager::UpdateLanSearch() { if ( m_lanSearchData.m_flStartTime && m_lanSearchData.m_flLastBroadcastTime ) { if ( Plat_FloatTime() > m_lanSearchData.m_flStartTime + mm_server_search_lan_ping_duration.GetFloat() ) { m_eState = STATE_GROUP_SEARCH; return; } if ( Plat_FloatTime() < m_lanSearchData.m_flLastBroadcastTime + mm_server_search_lan_ping_interval.GetFloat() ) { // waiting out interval between pings return; } } else { // Initialize the start time of the lan broadcast m_lanSearchData.m_flStartTime = Plat_FloatTime(); } // // Send the packet // m_lanSearchData.m_flLastBroadcastTime = Plat_FloatTime(); KeyValues *kv = new KeyValues( "LanSearchServerPing" ); KeyValues::AutoDelete autodelete( kv ); kv->SetFloat( "timestamp", Plat_FloatTime() ); if ( mm_server_search_lan_ports.GetString()[0] ) { // Build the list of ports to scan CSplitString arrPorts( mm_server_search_lan_ports.GetString(), "," ); for ( int i = 0; i < arrPorts.Count(); i++ ) { // Port number int nPort = Q_atoi( arrPorts[i] ); if ( nPort <= 0 ) continue; g_pConnectionlessLanMgr->SendPacket( kv, CFmtStr( "*:%d", nPort ) ); } } } void CServerManager::RemoveOldServers() { float now = Plat_FloatTime(); for ( int k = 0; k < m_Servers.Count(); ++ k ) { CServer *pServer = m_Servers[ k ]; if ( pServer && now - pServer->m_flLastRefresh < mm_server_search_server_lifetime.GetFloat() ) continue; m_Servers.Remove( k -- ); } } bool CServerManager::StartFetchingGroupServersData() { #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) ISteamUser *pUser = steamapicontext->SteamUser(); ISteamFriends *pFriends = steamapicontext->SteamFriends(); if ( !pUser || !pFriends ) return false; int iGroupCount = pFriends->GetClanCount(); if ( !iGroupCount ) return false; m_groupSearchData.m_UserGroupAccountIDs.SetCount( iGroupCount ); for ( int k = 0; k < iGroupCount; ++ k ) { m_groupSearchData.m_UserGroupAccountIDs[ k ] = pFriends->GetClanByIndex( k ).GetAccountID(); } return FetchGroupServers(); #else return false; #endif } bool CServerManager::FetchGroupServers() { #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) static const char gamedataFilterType[] = "gamedataor"; char gamedataFitler[ mm_filter_max_size ]; *gamedataFitler = 0; size_t roomLeft = mm_filter_max_size - strlen( gamedataFilterType ) - 3; // Add as many groups as will fit while ( m_groupSearchData.m_idxSearchGroupId < m_groupSearchData.m_UserGroupAccountIDs.Count() ) { const char *tag = CFmtStr( *gamedataFitler ? ",grp:%ui" : "grp:%ui", m_groupSearchData.m_UserGroupAccountIDs[ m_groupSearchData.m_idxSearchGroupId ] ); if ( roomLeft < strlen( tag ) ) { break; } Q_strncat( gamedataFitler, tag, mm_filter_max_size ); roomLeft -= strlen( tag ); ++m_groupSearchData.m_idxSearchGroupId; } MatchMakingKeyValuePair_t filters[ 2 ] = { // filter by game MatchMakingKeyValuePair_t( "gamedir", COM_GetModDirectory() ), // look for group servers MatchMakingKeyValuePair_t( gamedataFilterType, gamedataFitler ) }; // request the server list. We will get called back at ServerResponded, ServerFailedToRespond, and RefreshComplete m_eState = STATE_FETCHING_SERVERS; m_lanSearchData.m_flLastBroadcastTime = Plat_FloatTime(); MatchMakingKeyValuePair_t *pFilter = filters; DevMsg( 2, "Requesting group server list for groups %s...\n", gamedataFitler ); if ( m_hRequest ) steamapicontext->SteamMatchmakingServers()->ReleaseRequest( m_hRequest ); m_hRequest = steamapicontext->SteamMatchmakingServers()->RequestInternetServerList( ( AppId_t ) g_pMatchFramework->GetMatchTitle()->GetTitleID(), &pFilter, ARRAYSIZE( filters ), this ); return true; #else return false; #endif } void CServerManager::OnGroupFetched() { if ( m_bUpdateEnabled && m_groupSearchData.m_UserGroupAccountIDs.IsValidIndex( m_groupSearchData.m_idxSearchGroupId ) && FetchGroupServers() ) return; OnAllGroupsFetched(); } void CServerManager::OnAllGroupsFetched() { m_flNextUpdateTime = Plat_FloatTime() + mm_server_search_update_interval.GetInt(); if ( !m_ServersPinging.Count() ) { OnAllDataFetched(); return; } m_eState = STATE_REQUESTING_DETAILS; m_lanSearchData.m_flStartTime = Plat_FloatTime(); RequestPingingDetails(); } void CServerManager::RequestPingingDetails() { // Ping every server that we deferred to ping KeyValues *kv = new KeyValues( "InetSearchServerDetails" ); KeyValues::AutoDelete autodelete( kv ); for ( int k = 0; k < m_ServersPinging.Count() && k < mm_server_search_inet_ping_window.GetInt(); ++ k ) { CServerPinging *pServerPinging = ( CServerPinging * ) m_ServersPinging[k]; if ( pServerPinging->m_flTimeout && Plat_FloatTime() > pServerPinging->m_flTimeout ) { m_ServersPinging.FastRemove( k -- ); // server timed out delete pServerPinging; continue; } kv->SetFloat( "timestamp", Plat_FloatTime() ); kv->SetUint64( "pingxuid", pServerPinging->m_xuid ); #if !defined( _X360 ) && !defined( NO_STEAM ) && !defined( SWDS ) g_pConnectionlessLanMgr->SendPacket( kv, pServerPinging->m_netAdr.GetConnectionAddressString() ); #else DevWarning( "Cannot request internet pinging server details.\n" ); #endif if ( !pServerPinging->m_flTimeout ) pServerPinging->m_flTimeout = Plat_FloatTime() + mm_server_search_inet_ping_timeout.GetFloat(); } m_lanSearchData.m_flLastBroadcastTime = Plat_FloatTime(); DevMsg( 2, "Server manager waiting for game details from %d servers...\n", m_ServersPinging.Count() ); } void CServerManager::UpdateRequestingDetails() { if ( !m_ServersPinging.Count() ) { // We have no more servers to ping m_ServersPinging.PurgeAndDeleteElements(); OnAllDataFetched(); return; } if ( m_lanSearchData.m_flLastBroadcastTime + mm_server_search_inet_ping_interval.GetFloat() < Plat_FloatTime() ) { RequestPingingDetails(); } } void CServerManager::OnAllDataFetched() { DevMsg( 2, "Server manager refresh completed.\n" ); m_eState = STATE_IDLE; if ( !m_bUpdateEnabled ) { m_Servers.PurgeAndDeleteElements(); m_ServersPinging.PurgeAndDeleteElements(); } else { RemoveOldServers(); } if ( g_pMatchFramework ) { g_pMatchFramework->GetEventsSubscription()->BroadcastEvent( new KeyValues( "OnMatchServerMgrUpdate", "update", "searchfinished" ) ); } }