//========= 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 "matchmaking.h" #include "matchmakingqos.h" #include "Session.h" #include "vgui_baseui_interface.h" #include "cdll_engine_int.h" #include "convar.h" #include "cmd.h" #include "iclient.h" #include "server.h" #include "host.h" #if defined( _X360 ) #include "xbox/xbox_win32stubs.h" #include "audio/private/snd_dev_xaudio.h" #include "audio_pch.h" #endif // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" static CMatchmaking s_Matchmaking; CMatchmaking *g_pMatchmaking = &s_Matchmaking; extern IVEngineClient *engineClient; extern IXboxSystem *g_pXboxSystem; // Expose an interface for GameUI EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CMatchmaking, IMatchmaking, VENGINE_MATCHMAKING_VERSION, s_Matchmaking ); bool Channel_LessFunc( const uint &a, const uint &b ) { return a < b; } //----------------------------------------------------------------------------- // Purpose: Constructor //----------------------------------------------------------------------------- CMatchmaking::CMatchmaking() : m_Channels( Channel_LessFunc ) { m_bPreventFullServerStartup = false; m_bCleanup = false; m_bEnteredLobby = false; m_nTotalTeams = 0; m_pSearchResults = NULL; m_pSessionKeys = new KeyValues( "SessionKeys" ); m_Session.SetParent( this ); m_CurrentState = MMSTATE_INITIAL; m_InviteState = INVITE_NONE; memset( &m_InviteWaitingInfo, 0, sizeof( m_InviteWaitingInfo ) ); } CMatchmaking::~CMatchmaking() { Cleanup(); m_pSessionKeys->deleteThis(); } //----------------------------------------------------------------------------- // Purpose: Cleanup the matchmaking class to enable re-entry //----------------------------------------------------------------------------- void CMatchmaking::Cleanup() { m_bInitialized = false; m_bCleanup = false; m_bEnteredLobby = false; m_Host.Clear(); #ifdef _X360 if ( Audio_GetXVoice() ) { CClientInfo *pLocal = NULL; if ( m_bCreatedLocalTalker ) { pLocal = &m_Local; } Audio_GetXVoice()->RemoveAllTalkers( pLocal ); } #endif m_bCreatedLocalTalker = false; SetPreventFullServerStartup( false, "Cleanup\n" ); m_Session.ResetSession(); // TODO: Check on overlapped operations and cancel them // g_pXboxSystem->CancelAsyncOperations(); ClearSearchResults(); m_pSessionKeys->Clear(); m_pGameServer = NULL; Q_memset( m_Mutelist, 0, sizeof( m_Mutelist ) ); for ( int i = 0; i < MAX_PLAYERS_PER_CLIENT; ++i ) { m_MutedBy[i].Purge(); } m_Channels.RemoveAll(); m_SessionContexts.Purge(); m_SessionProperties.Purge(); m_PlayerStats.Purge(); m_Remote.PurgeAndDeleteElements(); m_nGameSize = 0; m_nPrivateSlots = 0; m_nSendCount = 0; m_fHeartbeatInterval = HEARTBEAT_INTERVAL_SHORT; m_fNextHeartbeatTime = GetTime(); } int CMatchmaking::FindOrCreateContext( const uint id ) { int idx = m_SessionContexts.InvalidIndex(); for ( int i = 0; i < m_SessionContexts.Count(); ++i ) { if ( m_SessionContexts[i].dwContextId == id ) { idx = i; } } if ( !m_SessionContexts.IsValidIndex( idx ) ) { idx = m_SessionContexts.AddToTail(); } return idx; } int CMatchmaking::FindOrCreateProperty( const uint id ) { int idx = m_SessionProperties.InvalidIndex(); for ( int i = 0; i < m_SessionProperties.Count(); ++i ) { if ( m_SessionProperties[i].dwPropertyId == id ) { idx = i; } } if ( !m_SessionProperties.IsValidIndex( idx ) ) { idx = m_SessionProperties.AddToTail(); } return idx; } //----------------------------------------------------------------------------- // Purpose: Add an additional property to the current session //----------------------------------------------------------------------------- void CMatchmaking::AddSessionProperty( const uint nType, const char *pID, const char *pValue, const char *pValueType ) { KeyValues *pProperty = m_pSessionKeys->FindKey( pID, true ); pProperty->SetName( pID ); pProperty->SetInt( "type", nType ); pProperty->SetString( "valuestring", pValue ); pProperty->SetString( "valuetype", pValueType ); AddSessionPropertyInternal( pProperty ); } //----------------------------------------------------------------------------- // Purpose: Set properties and contexts for the current session //----------------------------------------------------------------------------- void CMatchmaking::SetSessionProperties( KeyValues *pPropertyKeys ) { m_SessionContexts.RemoveAll(); m_SessionProperties.RemoveAll(); m_pSessionKeys->Clear(); pPropertyKeys->CopySubkeys( m_pSessionKeys ); for ( KeyValues *pProperty = m_pSessionKeys->GetFirstSubKey(); pProperty != NULL; pProperty = pProperty->GetNextKey() ) { AddSessionPropertyInternal( pProperty ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CMatchmaking::AddSessionPropertyInternal( KeyValues *pProperty ) { const char *pID = pProperty->GetName(); const char *pValue = pProperty->GetString( "valuestring" ); switch( pProperty->GetInt( "type" ) ) { case SESSION_CONTEXT: { Msg( "Adding Context: %s : %s\n", pID, pValue ); int id = g_ClientDLL->GetPresenceID( pID ); int val = g_ClientDLL->GetPresenceID( pValue ); int idx = FindOrCreateContext( id ); XUSER_CONTEXT &ctx = m_SessionContexts[idx]; ctx.dwContextId = id; ctx.dwValue = val; // Set the display string for gameUI char szBuffer[MAX_PATH]; g_ClientDLL->GetPropertyDisplayString( ctx.dwContextId, ctx.dwValue, szBuffer, sizeof( szBuffer ) ); pProperty->SetString( "displaystring", szBuffer ); // X360TBD: Such game specifics as this shouldn't be hard-coded if ( m_CurrentState != MMSTATE_INITIAL && !Q_stricmp( pID, "CONTEXT_SCENARIO" ) ) { // Set the scenario in our host data structure Q_strncpy( m_HostData.scenario, szBuffer, sizeof( m_HostData.scenario ) ); UpdateSessionReplyData( XNET_QOS_LISTEN_SET_DATA ); } } break; case SESSION_PROPERTY: { Msg( "Adding Property: %s : %s\n", pID, pValue ); if ( !Q_stricmp( pID, "PROPERTY_PRIVATE_SLOTS" ) ) { // "Private Slots" is not a search criteria m_nPrivateSlots = atoi( pValue ); break; } int id = g_ClientDLL->GetPresenceID( pID ); int idx = FindOrCreateProperty( id ); XUSER_PROPERTY &prop = m_SessionProperties[idx]; prop.dwPropertyId = id; if ( !Q_stricmp( pProperty->GetString( "valuetype" ), "int" ) ) { prop.value.nData = atoi( pValue ); prop.value.type = XUSER_DATA_TYPE_INT32; } // Build out the property keyvalues for gameUI char szBuffer[MAX_PATH]; g_ClientDLL->GetPropertyDisplayString( prop.dwPropertyId, prop.value.nData, szBuffer, sizeof( szBuffer ) ); pProperty->SetString( "displaystring", szBuffer ); // X360TBD: Such game specifics as these shouldn't be so hard-coded if ( !Q_stricmp( pID, "PROPERTY_GAME_SIZE" ) ) { m_nGameSize = atoi( pValue ); } if ( !Q_stricmp( pID, "PROPERTY_NUMBER_OF_TEAMS" ) ) { m_nTotalTeams = atoi( pValue ); } if ( m_CurrentState != MMSTATE_INITIAL && !Q_stricmp( pID, "PROPERTY_MAX_GAME_TIME" ) ) { // Set the game time in our host data structure m_HostData.gameTime = prop.value.nData; UpdateSessionReplyData( XNET_QOS_LISTEN_SET_DATA ); } } break; case SESSION_FLAG: m_Session.SetFlag( g_ClientDLL->GetPresenceID( pID ) ); break; default: Warning( "Session option type %d not recognized/n", pProperty->GetInt( "type" ) ); break; } } KeyValues *CMatchmaking::GetSessionProperties() { return m_pSessionKeys; } double CMatchmaking::GetTime() { return Plat_FloatTime(); } //----------------------------------------------------------------------------- // Purpose: At netchannel connection, register the messages //----------------------------------------------------------------------------- void CMatchmaking::ConnectionStart( INetChannel *chan ) { REGISTER_MM_MSG( JoinResponse ); REGISTER_MM_MSG( ClientInfo ); REGISTER_MM_MSG( RegisterResponse ); REGISTER_MM_MSG( Migrate ); REGISTER_MM_MSG( Mutelist ); REGISTER_MM_MSG( Checkpoint ); REGISTER_MM_MSG( Heartbeat ); REGISTER_CLC_MSG( VoiceData ); } //----------------------------------------------------------------------------- // Purpose: Process a networked voice packet //----------------------------------------------------------------------------- bool CMatchmaking::ProcessVoiceData( CLC_VoiceData *pVoice ) { char chReceived[4096]; DWORD dwLength = pVoice->m_nLength; pVoice->m_DataIn.ReadBits( chReceived, dwLength ); if ( m_Session.IsHost() ) { char chCopyBuffer[4096]; // Forward this message on to everyone else pVoice->m_DataOut.StartWriting( chCopyBuffer, sizeof ( chCopyBuffer ) ); Q_memcpy( chCopyBuffer, chReceived, sizeof( chCopyBuffer ) ); pVoice->m_DataOut.SeekToBit( dwLength ); SendToRemoteClients( pVoice, true, pVoice->m_xuid ); } // Playback the voice data locally through xaudio #if defined ( _X360 ) if ( pVoice->m_xuid != m_Local.m_xuids[0] ) { Audio_GetXVoice()->PlayIncomingVoiceData( pVoice->m_xuid, (byte*)chReceived, dwLength ); } #endif return true; } //----------------------------------------------------------------------------- // Purpose: Delete channels that have been marked for deletion //----------------------------------------------------------------------------- void CMatchmaking::CleanupMarkedChannels() { // Clean up net channels that need to be deleted for ( int i = 0; i < m_ChannelsToRemove.Count(); ++i ) { INetChannel *pNetChannel = FindChannel( m_ChannelsToRemove[i] ); if ( pNetChannel ) { if ( !m_Channels.Remove( m_ChannelsToRemove[i] ) ) { Warning( "CleanupMarkedChannels: Failed to remove a channel!\n" ); } } } m_ChannelsToRemove.Purge(); } //----------------------------------------------------------------------------- // Purpose: Channels can be flagged for deletion during packet processing. // Now that processing is finished, delete them. //----------------------------------------------------------------------------- void CMatchmaking::PacketEnd() { CleanupMarkedChannels(); } //----------------------------------------------------------------------------- // Purpose: Find a specific client from a netchannel //----------------------------------------------------------------------------- CClientInfo *CMatchmaking::FindClient( netadr_t *adr ) { CClientInfo *pClient = NULL; unsigned int ip = adr->GetIPNetworkByteOrder(); if ( ip == m_Host.m_adr.GetIPNetworkByteOrder() ) { pClient = &m_Host; } else { for ( int i = 0; i < m_Remote.Count(); ++i ) { if ( ip == m_Remote[i]->m_adr.GetIPNetworkByteOrder() ) { pClient = m_Remote[i]; break; } } } return pClient; } //----------------------------------------------------------------------------- // Purpose: Find a specific client by his XUID //----------------------------------------------------------------------------- CClientInfo *CMatchmaking::FindClientByXUID( XUID xuid ) { CClientInfo *pClient = NULL; if ( xuid == m_Host.m_xuids[0] ) { pClient = &m_Host; } else { for ( int i = 0; i < m_Remote.Count(); ++i ) { if ( xuid == m_Remote[i]->m_xuids[0] ) { pClient = m_Remote[i]; break; } } } return pClient; } //----------------------------------------------------------------------------- // Purpose: Find a specific client's netchannel //----------------------------------------------------------------------------- INetChannel *CMatchmaking::FindChannel( const unsigned int ip ) { INetChannel *pChannel = NULL; int idx = m_Channels.Find( ip ); if ( idx != m_Channels.InvalidIndex() ) { pChannel = m_Channels.Element( idx ); } return pChannel; } //----------------------------------------------------------------------------- // Purpose: Create a new netchannel //----------------------------------------------------------------------------- INetChannel *CMatchmaking::CreateNetChannel( netadr_t *adr ) { INetChannel *pNewChannel = FindChannel( adr->GetIPNetworkByteOrder() ); if ( !pNewChannel ) { pNewChannel = NET_CreateNetChannel( NS_MATCHMAKING, adr, "MATCHMAKING", this ); } if( pNewChannel ) { // Set a rate limit and other relevant properties pNewChannel->SetTimeout( HEARTBEAT_TIMEOUT ); } return pNewChannel; } //----------------------------------------------------------------------------- // Purpose: Add a netchannel for a session client //----------------------------------------------------------------------------- INetChannel *CMatchmaking::AddRemoteChannel( netadr_t *adr ) { INetChannel *pNetChannel = CreateNetChannel( adr ); if ( pNetChannel ) { // Save this new channel m_Channels.Insert( adr->GetIPNetworkByteOrder(), pNetChannel ); } return pNetChannel; } //----------------------------------------------------------------------------- // Purpose: Remove a netchannel for a session client //----------------------------------------------------------------------------- void CMatchmaking::RemoveRemoteChannel( netadr_t *adr, const char *pReason ) { INetChannel *pNetChannel = FindChannel( adr->GetIPNetworkByteOrder() ); if ( pNetChannel ) { m_Channels.Remove( adr->GetIPNetworkByteOrder() ); } } //----------------------------------------------------------------------------- // Purpose: Mark a net channel to be removed //----------------------------------------------------------------------------- void CMatchmaking::MarkChannelForRemoval( netadr_t *adr ) { m_ChannelsToRemove.AddToTail( adr->GetIPNetworkByteOrder() ); } //----------------------------------------------------------------------------- // Purpose: Set the timeout for a net channel //----------------------------------------------------------------------------- void CMatchmaking::SetChannelTimeout( netadr_t *adr, int timeout ) { INetChannel *pChannel = FindChannel( adr->GetIPNetworkByteOrder() ); if ( pChannel ) { Msg( "Setting new timeout for ip %d: %d\n", adr->GetIPNetworkByteOrder(), timeout ); pChannel->SetTimeout( timeout ); } } //----------------------------------------------------------------------------- // Purpose: Send a net message to a specific address //----------------------------------------------------------------------------- void CMatchmaking::SendMessage( INetMessage *msg, netadr_t *adr, bool bVoice ) { // Find the matching net channel INetChannel *pChannel = FindChannel( adr->GetIPNetworkByteOrder() ); if ( pChannel ) { pChannel->SendNetMsg( *msg, false, bVoice ); if ( !pChannel->Transmit() ) { Msg( "Transmit failed\n" ); } } } //----------------------------------------------------------------------------- // Purpose: Send a net message to a specific client //----------------------------------------------------------------------------- void CMatchmaking::SendMessage( INetMessage *msg, CClientInfo *pClient, bool bVoice ) { // Find the matching net channel INetChannel *pChannel = FindChannel( pClient->m_adr.GetIPNetworkByteOrder() ); if ( pChannel ) { pChannel->SendNetMsg( *msg, false, bVoice ); if ( !pChannel->Transmit() ) { Msg( "Transmit failed\n" ); } } } //----------------------------------------------------------------------------- // Purpose: Send a net message to all remote clients //----------------------------------------------------------------------------- void CMatchmaking::SendToRemoteClients( INetMessage *msg, bool bVoice, XUID excludeXUID ) { for ( int i = 0; i < m_Remote.Count(); ++i ) { CClientInfo *pInfo = m_Remote[i]; if ( excludeXUID != -1 && pInfo->m_xuids[0] == excludeXUID ) continue; SendMessage( msg, m_Remote[i], true ); } } //----------------------------------------------------------------------------- // Purpose: Send a heartbeat to a specific client //----------------------------------------------------------------------------- bool CMatchmaking::SendHeartbeat( CClientInfo *pClient ) { if ( pClient->m_adr.GetIPNetworkByteOrder() == 0 ) return false; // Check for timeout INetChannel *pChannel = FindChannel( pClient->m_adr.GetIPNetworkByteOrder() ); if ( pChannel ) { // Msg( "Sending HB\n" ); if ( pChannel->IsTimedOut() ) { ClientDropped( pClient ); return false; } // Send a heartbeat to the client MM_Heartbeat beat; pChannel->SendNetMsg( beat ); pChannel->Transmit(); } return true; } //----------------------------------------------------------------------------- // Purpose: Transmit regular messages to keep the connection alive //----------------------------------------------------------------------------- void CMatchmaking::SendHeartbeat() { double time = GetTime(); if ( time < m_fNextHeartbeatTime ) return; m_fNextHeartbeatTime = time + m_fHeartbeatInterval; if ( m_Session.IsHost() ) { for ( int i = 0; i < m_Remote.Count(); ++i ) { SendHeartbeat( m_Remote[i] ); } } else { SendHeartbeat( &m_Host ); } } //----------------------------------------------------------------------------- // Purpose: Look up a player by name //----------------------------------------------------------------------------- static uint64 FindPlayerByName( CClientInfo *pClient, const char *pName ) { for ( int i = 0; i < XUSER_MAX_COUNT; ++i ) { if ( pClient->m_xuids[i] && !Q_stricmp( pClient->m_szGamertags[i], pName ) ) { return pClient->m_xuids[i]; } } return 0; } //----------------------------------------------------------------------------- // Purpose: Get an xuid from a CBasePlayer id //----------------------------------------------------------------------------- uint64 CMatchmaking::PlayerIdToXuid( int playerId ) { uint64 ret = 0; player_info_t info; if ( engineClient->GetPlayerInfo( playerId, &info ) ) { // find the client with a matching name for ( int i = 0; i < m_Remote.Count(); ++i ) { ret = FindPlayerByName( m_Remote[i], info.name ); if ( ret ) { break; } } } if ( !ret ) { // Try ourselves ret = FindPlayerByName( &m_Local, info.name ); } if ( !ret ) { // Try the host ret = FindPlayerByName( &m_Host, info.name ); } return ret; } bool CMatchmaking::GameIsActive() { return m_CurrentState > MMSTATE_GAME_ACTIVE; } bool CMatchmaking::GameIsLocked() { return ( m_Session.IsArbitrated() && m_CurrentState > MMSTATE_GAME_LOCKED ); } bool CMatchmaking::ConnectedToServer() { return engineClient->IsConnected(); } bool CMatchmaking::IsInMigration() { return ( m_CurrentState >= MMSTATE_HOSTMIGRATE_STARTINGMIGRATION && m_CurrentState <= MMSTATE_HOSTMIGRATE_WAITINGFORHOST ); } bool CMatchmaking::IsAcceptingConnections() { if ( !m_Session.IsHost() || m_CurrentState < MMSTATE_ACCEPTING_CONNECTIONS || m_CurrentState == MMSTATE_PREGAME || m_CurrentState == MMSTATE_LOADING || GameIsLocked() ) { return false; } return true; } bool CMatchmaking::IsServer() { // for now, the host is the server return m_Session.IsHost(); } //----------------------------------------------------------------------------- // Purpose: Helpers to convert between CClientInfo and MM_ClientInfo //----------------------------------------------------------------------------- void CMatchmaking::ClientInfoToNetMessage( MM_ClientInfo *pInfo, const CClientInfo *pClient ) { pInfo->m_id = pClient->m_id; pInfo->m_xnaddr = pClient->m_xnaddr; pInfo->m_cPlayers = pClient->m_cPlayers; pInfo->m_bInvited = pClient->m_bInvited; Q_memcpy( pInfo->m_xuids, pClient->m_xuids, sizeof( pInfo->m_xuids ) ); Q_memcpy( pInfo->m_cVoiceState, pClient->m_cVoiceState, sizeof( pInfo->m_cVoiceState ) ); Q_memcpy( pInfo->m_iTeam, pClient->m_iTeam, sizeof( pInfo->m_iTeam ) ); Q_memcpy( pInfo->m_iControllers, pClient->m_iControllers, sizeof( pInfo->m_iControllers ) ); Q_memcpy( pInfo->m_szGamertags, pClient->m_szGamertags, sizeof( pInfo->m_szGamertags ) ); } void CMatchmaking::NetMessageToClientInfo( CClientInfo *pClient, const MM_ClientInfo *pInfo ) { pClient->m_id = pInfo->m_id; pClient->m_xnaddr = pInfo->m_xnaddr; pClient->m_cPlayers = pInfo->m_cPlayers; pClient->m_bInvited = pInfo->m_bInvited; #if defined( _X360 ) IN_ADDR winaddr; XNKID xid = m_Session.GetSessionId(); if ( XNetXnAddrToInAddr( &pClient->m_xnaddr, &xid, &winaddr ) != 0 ) { Warning( "Error resolving client IP\n" ); } pClient->m_adr.SetType( NA_IP ); pClient->m_adr.SetIPAndPort( winaddr.S_un.S_addr, PORT_MATCHMAKING ); #endif Q_memcpy( pClient->m_xuids, pInfo->m_xuids, sizeof( pClient->m_xuids ) ); Q_memcpy( pClient->m_cVoiceState, pInfo->m_cVoiceState, sizeof( pClient->m_cVoiceState ) ); Q_memcpy( pClient->m_iTeam, pInfo->m_iTeam, sizeof( pClient->m_iTeam ) ); Q_memcpy( pClient->m_iControllers, pInfo->m_iControllers, sizeof( pClient->m_iControllers ) ); Q_memcpy( pClient->m_szGamertags, pInfo->m_szGamertags, sizeof( pClient->m_szGamertags ) ); } //---------------------------------------- // // Host/Client Shared // //---------------------------------------- //----------------------------------------------------------------------------- // Purpose: Set up the properties of the local client machine //----------------------------------------------------------------------------- bool CMatchmaking::InitializeLocalClient( bool bIsHost ) { Q_memset( &m_Local, 0, sizeof( m_Local ) ); m_Local.m_bInvited = bIsHost; #if defined( _X360 ) while( XNetGetTitleXnAddr( &m_Local.m_xnaddr ) == XNET_GET_XNADDR_PENDING ) ; // machine id if ( 0 != XNetXnAddrToMachineId( &m_Local.m_xnaddr, &m_Local.m_id ) ) { // User isn't signed in to live, use their xuid instead XUserGetXUID( XBX_GetPrimaryUserId(), &m_Local.m_id ); } m_Local.m_cPlayers = 0; // Set up the players for ( uint i = 0; i < MAX_PLAYERS_PER_CLIENT; ++i ) { // We currently only allow one player per console if ( i != XBX_GetPrimaryUserId() ) { continue; } // xuid uint ret = XUserGetXUID( i, &m_Local.m_xuids[m_Local.m_cPlayers] ); if ( ret == ERROR_NO_SUCH_USER ) { continue; } else if ( ret != ERROR_SUCCESS ) { return false; } // gamertag ret = XUserGetName( XBX_GetPrimaryUserId(), m_Local.m_szGamertags[m_Local.m_cPlayers], MAX_PLAYER_NAME_LENGTH ); if ( ret != ERROR_SUCCESS ) { return false; } m_Local.m_szGamertags[m_Local.m_cPlayers][MAX_PLAYER_NAME_LENGTH - 1] = '\0'; // Set the player's name in the game char szNameCmd[MAX_PLAYER_NAME_LENGTH + 16]; Q_snprintf( szNameCmd, sizeof( szNameCmd ), "name %s", m_Local.m_szGamertags[m_Local.m_cPlayers] ); engineClient->ClientCmd( szNameCmd ); m_Local.m_iControllers[m_Local.m_cPlayers] = i; m_Local.m_iTeam[m_Local.m_cPlayers] = -1; m_Local.m_cVoiceState[m_Local.m_cPlayers] = 0; // number of players on this console ++m_Local.m_cPlayers; } // Source can only support one player per console. if( m_Local.m_cPlayers > 1 ) { Warning( "Too many players on this console\n" ); return false; } // Set up the host data that gets sent back to searching clients // By default, the first player is considered the host Q_strncpy( m_HostData.hostName, m_Local.m_szGamertags[0], sizeof( m_HostData.hostName ) ); m_HostData.gameState = GAMESTATE_INLOBBY; m_HostData.xuid = m_Local.m_xuids[ XBX_GetPrimaryUserId() ]; #endif m_bInitialized = true; return true; } //----------------------------------------------------------------------------- // Purpose: Connection to the game server has been established, so we can // add the local players to the teams that were setup in the lobby. //----------------------------------------------------------------------------- void CMatchmaking::AddLocalPlayersToTeams() { if ( !m_bInitialized || XBX_GetPrimaryUserId() == INVALID_USER_ID ) return; if ( m_Local.m_iTeam[0] == -1 ) return; // Convert the team number into a team name char szTeamName[32]; uint id = g_ClientDLL->GetPresenceID( "PROPERTY_TEAM" ); g_ClientDLL->GetPropertyDisplayString( id, m_Local.m_iTeam[0], szTeamName, sizeof( szTeamName ) ); Msg( "Joining team: %s\n", szTeamName ); char cmd[32]; Q_snprintf( cmd, sizeof( cmd ), "jointeam_nomenus %s", szTeamName ); engineClient->ClientCmd( cmd ); } //----------------------------------------------------------------------------- // Purpose: Map loading is completed - restore full communication //----------------------------------------------------------------------------- void CMatchmaking::OnLevelLoadingFinished() { // This functions gets called from some odd places if ( m_CurrentState != MMSTATE_CONNECTED_TO_SERVER ) return; // Test code to force a disconnect at end of map load // if ( !IsServer() ) // { // char cmd[MAX_PATH]; // Q_snprintf( cmd, sizeof( cmd ), "connect 127.0.0.1\n" ); // Cbuf_AddText( cmd ); // return; // } SwitchToState( MMSTATE_INGAME ); MM_Checkpoint msg; msg.m_Checkpoint = MM_Checkpoint::CHECKPOINT_LOADING_COMPLETE; if ( m_Session.IsHost() ) { if ( !m_Session.IsArbitrated() ) { // Re-enable response to probes m_HostData.gameState = GAMESTATE_INPROGRESS; UpdateSessionReplyData( XNET_QOS_LISTEN_ENABLE|XNET_QOS_LISTEN_SET_DATA ); } // Reset netchannel timeouts for any clients that are also finished loading for ( int i = 0; i < m_Remote.Count(); ++i ) { CClientInfo *pClient = m_Remote[i]; if ( pClient->m_bLoaded ) { // Send a reply and reset the netchannel timeout SendMessage( &msg, pClient ); SetChannelTimeout( &pClient->m_adr, HEARTBEAT_TIMEOUT ); } } } else { // Tell the host we're finished loading SendMessage( &msg, &m_Host ); } } //----------------------------------------------------------------------------- // Purpose: Process packet from another client //----------------------------------------------------------------------------- bool CMatchmaking::ProcessConnectionlessPacket( netpacket_t *pPacket ) { Assert( pPacket ); bf_read &msg = pPacket->message; int type = msg.ReadByte(); switch( type ) { case PTH_SYSTEMLINK_SEARCH: HandleSystemLinkSearch( pPacket ); break; case HTP_SYSTEMLINK_REPLY: HandleSystemLinkReply( pPacket ); break; case PTH_CONNECT: HandleJoinRequest( pPacket ); break; default: break; } return true; } //----------------------------------------------------------------------------- // Purpose: Process an info update about another client //----------------------------------------------------------------------------- bool CMatchmaking::ProcessClientInfo( MM_ClientInfo *pInfo ) { CClientInfo *pClient = NULL; bool bHost = false; if ( m_CurrentState == MMSTATE_INITIAL ) { // Session has been reset, this is a stale message Msg( "Received MM_ClientInfo with MMSTATE_INITIAL\n" ); return true; } if ( pInfo->m_id == m_Local.m_id ) { if ( pInfo->m_cPlayers == 0 ) { if ( m_CurrentState != MMSTATE_SESSION_DISCONNECTING ) { // We've been kicked KickPlayerFromSession( 0 ); SessionNotification( SESSION_NOTIFY_CLIENT_KICKED ); } return true; } else { pClient = &m_Local; } } // Check against our host id if ( pInfo->m_id == m_Host.m_id ) { pClient = &m_Host; bHost = true; } else { // Look for the client in our remote list for ( int i = 0; i < m_Remote.Count(); ++i ) { if ( m_Remote[i]->m_id == pInfo->m_id ) { pClient = m_Remote[i]; break; } } } // If we didn't find it, this must be a new client if ( !pClient && pInfo->m_cPlayers != 0 ) { Msg( "New client. %s\n", pInfo->ToString() ); pClient = new CClientInfo(); m_Remote.AddToTail( pClient ); // Copy the new client info NetMessageToClientInfo( pClient, pInfo ); AddPlayersToSession( pClient ); SendPlayerInfoToLobby( pClient ); } else { // We're updating an existing client if ( pInfo->m_cPlayers ) { // Cache off the old client info, as pClient gets updated through this function CClientInfo tempClient = *pClient; // Check for player changes if ( Q_memcmp( &tempClient.m_xuids, pInfo->m_xuids, sizeof( tempClient.m_xuids ) ) ) { // Remove the old players and add the new RemovePlayersFromSession( pClient ); NetMessageToClientInfo( pClient, pInfo ); AddPlayersToSession( pClient ); } // Check for team changes for ( int i = 0; i < pInfo->m_cPlayers; ++i ) { if ( pInfo->m_iTeam[i] != tempClient.m_iTeam[i] || pInfo->m_cVoiceState[i] != tempClient.m_cVoiceState[i] ) { // X360TBD: send real "ready" setting, or remove entirely? EngineVGui()->UpdatePlayerInfo( pInfo->m_xuids[i], pInfo->m_szGamertags[i], pInfo->m_iTeam[i], pInfo->m_cVoiceState[i], GetPlayersNeeded(), bHost ); } } // Store the new info NetMessageToClientInfo( pClient, pInfo ); if ( m_Session.IsHost() ) { SendToRemoteClients( pInfo ); } } else { // A client has been dropped ClientDropped( pClient ); } } return true; } //----------------------------------------------------------------------------- // Purpose: A connection was lost - respond accordingly //----------------------------------------------------------------------------- void CMatchmaking::ClientDropped( CClientInfo *pClient ) { if ( !pClient ) { Warning( "Null client pointer in ClientDropped!\n" ); return; } if ( m_CurrentState == MMSTATE_SESSION_CONNECTING ) { // Not really dropped, we just failed to connect to the host SessionNotification( SESSION_NOTIFY_CONNECT_NOTAVAILABLE ); return; } Warning( "Dropped player: %llu!", pClient->m_id ); // Do this first, before the team assignment gets cleared RemovePlayersFromSession( pClient ); // Remove all players from the lobby for ( int i = 0; i < pClient->m_cPlayers; ++i ) { pClient->m_iTeam[i] = -1; } SendPlayerInfoToLobby( pClient ); MarkChannelForRemoval( &pClient->m_adr ); if ( pClient == &m_Host ) { // The host was lost if ( m_Session.IsSystemLink() ) { // Can't migrate system link sessions SessionNotification( SESSION_NOTIFY_LOST_HOST ); } else { // X360TBD: Migration still doesn't work correctly SessionNotification( SESSION_NOTIFY_LOST_HOST ); /* // Start migrating if ( !IsInMigration() ) { m_PreMigrateState = m_CurrentState; StartHostMigration(); } */ } } else { if ( m_Session.IsHost() ) { // Send a disconnect ack back to the client MM_Checkpoint msg; msg.m_Checkpoint = MM_Checkpoint::CHECKPOINT_SESSION_DISCONNECT; SendMessage( &msg, &pClient->m_adr ); // Tell everyone else this client is gone MM_ClientInfo droppedPlayer; ClientInfoToNetMessage( &droppedPlayer, pClient ); droppedPlayer.m_cPlayers = 0; SendToRemoteClients( &droppedPlayer ); for ( int i = 0; i < sv.GetClientCount(); i++ ) { IClient *pIClient = sv.GetClient(i); bool bFound = false; if ( pIClient ) { for ( int j = 0; j < pClient->m_cPlayers; ++j ) { if ( pClient->m_xuids[j] == 0 ) continue; if ( Q_stricmp( pIClient->GetClientName(), pClient->m_szGamertags[j] ) == 0 ) { bFound = true; pIClient->Disconnect( "Timed Out" ); break; } } } if ( bFound == true ) break; } } } if ( m_Remote.FindAndRemove( pClient ) ) { delete pClient; } } //----------------------------------------------------------------------------- // Purpose: A player is leaving the session //----------------------------------------------------------------------------- void CMatchmaking::PerformDisconnect() { if ( m_CurrentState == MMSTATE_SESSION_DISCONNECTING ) { if ( ConnectedToServer() ) { engineClient->ExecuteClientCmd( "disconnect" ); EngineVGui()->ActivateGameUI(); } if ( m_bEnteredLobby ) { EngineVGui()->SessionNotification( SESSION_NOTIFY_WELCOME ); } SwitchToState( MMSTATE_INITIAL ); } } //----------------------------------------------------------------------------- // Purpose: A player is leaving the session //----------------------------------------------------------------------------- void CMatchmaking::KickPlayerFromSession( uint64 id ) { MM_ClientInfo droppedPlayer; if ( m_Local.m_xuids[0] == id || id == 0 ) { // We've been kicked, or voluntarily left the session ClientInfoToNetMessage( &droppedPlayer, &m_Local ); droppedPlayer.m_cPlayers = 0; if ( m_Session.IsHost() ) { // Tell all the clients SendToRemoteClients( &droppedPlayer ); } else { // tell the host to drop us SendMessage( &droppedPlayer, &m_Host ); } // Prepare to close the session and reset SwitchToState( MMSTATE_SESSION_DISCONNECTING ); } else if ( m_Session.IsHost() ) { // Host wants to kick a client for ( int i = 0; i < m_Remote.Count(); ++i ) { CClientInfo *pClient = m_Remote[i]; for ( int j = 0; j < pClient->m_cPlayers; ++j ) { if ( pClient->m_xuids[j] == id ) { ClientInfoToNetMessage( &droppedPlayer, pClient ); droppedPlayer.m_cPlayers = 0; SendMessage( &droppedPlayer, pClient ); return; } } } } } //----------------------------------------------------------------------------- // Purpose: Add players to the session //----------------------------------------------------------------------------- void CMatchmaking::AddPlayersToSession( CClientInfo *pClient ) { bool bIsLocal = false; if ( &m_Local == pClient ) { m_Session.JoinLocal( pClient ); bIsLocal = true; } else { m_Session.JoinRemote( pClient ); } #if defined ( _X360 ) if ( Audio_GetXVoice() ) { Audio_GetXVoice()->AddPlayerToVoiceList( pClient, bIsLocal ); if ( bIsLocal ) { m_bCreatedLocalTalker = true; } } #endif UpdateMuteList(); } //----------------------------------------------------------------------------- // Purpose: Remove players from the session //----------------------------------------------------------------------------- void CMatchmaking::RemovePlayersFromSession( CClientInfo *pClient ) { if ( !pClient->m_cPlayers ) return; bool bIsLocal = false; if ( &m_Local == pClient ) { m_Session.RemoveLocal( pClient ); bIsLocal = true; } else { m_Session.RemoveRemote( pClient ); } #if defined ( _X360 ) if ( Audio_GetXVoice() ) { Audio_GetXVoice()->RemovePlayerFromVoiceList( pClient, bIsLocal ); } #endif UpdateMuteList(); } //----------------------------------------------------------------------------- // Purpose: Check if a client is muted //----------------------------------------------------------------------------- bool CMatchmaking::IsPlayerMuted( int iUserId, XUID playerId ) { for ( int i = 0; i < MAX_PLAYERS; ++i ) { if ( m_Mutelist[iUserId][i] == playerId ) { return true; } } if ( m_MutedBy[iUserId].HasElement( playerId ) ) return true; return false; } //----------------------------------------------------------------------------- // Purpose: Determine which clients should be muted for the local client //----------------------------------------------------------------------------- void CMatchmaking::UpdateMuteList() { // Process our mute list MM_Mutelist msg; GenerateMutelist( &msg ); // Send that to everyone else if ( !m_Session.IsHost() ) { SendMessage( &msg, &m_Host ); } else { ProcessMutelist( &msg ); } } void Con_PrintTalkers( const CCommand &args ) { g_pMatchmaking->PrintVoiceStatus(); } void CMatchmaking::PrintVoiceStatus( void ) { #ifdef _X360 int iRemoteClient = 0; int numRemoteTalkers; XUID remoteTalkers[MAX_PLAYERS]; Audio_GetXVoice()->GetRemoteTalkers( &numRemoteTalkers, remoteTalkers ); CClientInfo *pClient = &m_Local; if ( m_Session.IsHost() == false ) { pClient = &m_Host; } Msg( "Num Remote Talkers: %d\n", numRemoteTalkers ); bool bFound = false; while ( pClient ) { if ( pClient != &m_Local ) { for ( int iRemote = 0; iRemote < numRemoteTalkers; iRemote++ ) { if ( pClient->m_xuids[0] == remoteTalkers[iRemote] ) { bFound = true; break; } } if ( bFound == true ) { Msg( "Found a Talker: %s\n", pClient->m_szGamertags[0] ); } else { Msg( "ALERT!!! %s not in Talker list\n", pClient->m_szGamertags[0] ); } } if ( iRemoteClient < m_Remote.Count() ) { pClient = m_Remote[iRemoteClient]; iRemoteClient++; } else { pClient = NULL; } } #endif } static ConCommand voice_printtalkers( "voice_printtalkers", Con_PrintTalkers, "voice debug.", FCVAR_DONTRECORD ); //----------------------------------------------------------------------------- // Purpose: Update a client's mute list //----------------------------------------------------------------------------- void CMatchmaking::GenerateMutelist( MM_Mutelist *pMsg ) { #if defined( _X360 ) // Get our remote talker list if ( !Audio_GetXVoice() ) return; int numRemoteTalkers; XUID remoteTalkers[MAX_PLAYERS]; Audio_GetXVoice()->GetRemoteTalkers( &numRemoteTalkers, remoteTalkers ); pMsg->m_cPlayers = 0; // Loop through local players and update mutes for ( int iLocal = 0; iLocal < m_Local.m_cPlayers; ++iLocal ) { pMsg->m_cMuted[iLocal] = 0; pMsg->m_xuid[pMsg->m_cPlayers] = m_Local.m_xuids[iLocal]; for ( int iRemote = 0; iRemote < numRemoteTalkers; ++iRemote ) { BOOL bIsMuted = false; DWORD ret = XUserMuteListQuery( m_Local.m_iControllers[iLocal], remoteTalkers[iRemote], &bIsMuted ); if( ERROR_SUCCESS != ret ) { Warning( "Warning: XUserMuteListQuery() returned 0x%08x for user %d\n", ret, iLocal ); } if( bIsMuted ) { pMsg->m_Muted[pMsg->m_cPlayers].AddToTail( remoteTalkers[iRemote ] ); ++pMsg->m_cMuted[pMsg->m_cPlayers]; } else { bIsMuted = m_MutedBy[iLocal].HasElement( remoteTalkers[iRemote] ); } if( bIsMuted ) { Audio_GetXVoice()->SetPlaybackPriority( remoteTalkers[iRemote], m_Local.m_iControllers[iLocal], XHV_PLAYBACK_PRIORITY_NEVER ); } else { Audio_GetXVoice()->SetPlaybackPriority( remoteTalkers[iRemote], m_Local.m_iControllers[iLocal], XHV_PLAYBACK_PRIORITY_MAX ); } } pMsg->m_cRemoteTalkers[pMsg->m_cPlayers] = m_MutedBy[pMsg->m_cPlayers].Count(); ++pMsg->m_cPlayers; } #endif } //----------------------------------------------------------------------------- // Purpose: Handle the mutelist from another client //----------------------------------------------------------------------------- bool CMatchmaking::ProcessMutelist( MM_Mutelist *pMsg ) { #if defined( _X360 ) // local players for( int i = 0; i < m_Local.m_cPlayers; ++i ) { // remote players for( int j = 0; j < pMsg->m_cPlayers; ++j ) { m_MutedBy[i].FindAndRemove( pMsg->m_xuid[j] ); // players muted by remote player for( int k = 0; k < pMsg->m_cMuted[j]; ++k ) { if( m_Local.m_xuids[i] == pMsg->m_Muted[j][k] ) { m_MutedBy[i].AddToTail( pMsg->m_xuid[j] ); } } BOOL bIsMuted = m_MutedBy[i].HasElement( pMsg->m_xuid[j] ); if ( !bIsMuted ) { XUserMuteListQuery( m_Local.m_iControllers[i], pMsg->m_xuid[j], &bIsMuted ); } if( bIsMuted ) { Audio_GetXVoice()->SetPlaybackPriority( pMsg->m_xuid[j], m_Local.m_iControllers[i], XHV_PLAYBACK_PRIORITY_NEVER ); } else { Audio_GetXVoice()->SetPlaybackPriority( pMsg->m_xuid[j], m_Local.m_iControllers[i], XHV_PLAYBACK_PRIORITY_MAX ); } } } if ( m_Session.IsHost() ) { // Pass this along to everyone else SendToRemoteClients( pMsg ); } #endif return true; } //----------------------------------------------------------------------------- // Purpose: A client is changing to another team //----------------------------------------------------------------------------- void CMatchmaking::ChangeTeam( const char *pTeamName ) { if ( !pTeamName ) { // Automatic switch to next team if ( m_Session.IsHost() ) { if ( m_CurrentState == MMSTATE_ACCEPTING_CONNECTIONS ) { // Put ourselves on another team and // tell the other players SwitchToNextOpenTeam( &m_Local ); } } else { // Send a request to the host MM_Checkpoint msg; msg.m_Checkpoint = MM_Checkpoint::CHECKPOINT_CHANGETEAM; SendMessage( &msg, &m_Host ); } } else { // Find a team name that matches, and tell everyone our new team number char szTeamName[32]; uint id = g_ClientDLL->GetPresenceID( "PROPERTY_TEAM" ); for ( int iTeam = 0; iTeam < m_nTotalTeams; ++iTeam ) { g_ClientDLL->GetPropertyDisplayString( id, iTeam, szTeamName, sizeof( szTeamName ) ); if ( !Q_stricmp( szTeamName, pTeamName ) ) { bool bChanged = false; MM_ClientInfo info; ClientInfoToNetMessage( &info, &m_Local ); for ( int i = 0; i < m_Local.m_cPlayers; ++i ) { if ( info.m_iTeam[i] != iTeam ) { bChanged = true; } info.m_iTeam[i] = iTeam; } if ( !bChanged ) { return; } if ( m_Session.IsHost() ) { ProcessClientInfo( &info ); } else { SendMessage( &info, &m_Host ); } break; } } } } //----------------------------------------------------------------------------- // Purpose: Handle various matchmaking checkpoint messages //----------------------------------------------------------------------------- bool CMatchmaking::ProcessCheckpoint( MM_Checkpoint *pMsg ) { switch( pMsg->m_Checkpoint ) { case MM_Checkpoint::CHECKPOINT_CHANGETEAM: if ( m_Session.IsHost() ) { // if the countdown has started, deny if ( m_CurrentState == MMSTATE_ACCEPTING_CONNECTIONS ) { netadr_t adr = pMsg->GetNetChannel()->GetRemoteAddress(); CClientInfo *pClient = FindClient( &adr ); if ( pClient ) { SwitchToNextOpenTeam( pClient ); } } } break; case MM_Checkpoint::CHECKPOINT_PREGAME: if ( m_Session.IsArbitrated() && !m_Session.IsHost() ) { m_Session.RegisterForArbitration(); } // Start the countdown timer to map load SwitchToState( MMSTATE_PREGAME ); break; case MM_Checkpoint::CHECKPOINT_GAME_LOBBY: // returning to game lobby, pregame canceled // reset the countdown SessionNotification( SESSION_NOTIFY_COUNTDOWN, -1 ); if ( m_Session.IsHost() ) { SwitchToState( MMSTATE_ACCEPTING_CONNECTIONS ); } else { SwitchToState( MMSTATE_SESSION_CONNECTED ); } break; case MM_Checkpoint::CHECKPOINT_LOADING_COMPLETE: if ( m_Session.IsHost() ) { // Mark this client as loaded netadr_t adr = pMsg->GetNetChannel()->GetRemoteAddress(); CClientInfo *pClient = FindClient( &adr ); if ( pClient ) { pClient->m_bLoaded = true; if ( GameIsActive() ) { // Send a reply and reset the netchannel timeout SendMessage( pMsg, pClient ); SetChannelTimeout( &pClient->m_adr, HEARTBEAT_TIMEOUT ); } } } else { // The host is also loaded, so reset the netchannel timeout SetChannelTimeout( &m_Host.m_adr, HEARTBEAT_TIMEOUT ); } break; case MM_Checkpoint::CHECKPOINT_CONNECT: // If we're already connected or in the game, don't connect again. if ( m_CurrentState == MMSTATE_CONNECTED_TO_SERVER || m_CurrentState == MMSTATE_INGAME ) { break; } if ( m_Session.IsHost() ) { // Send the message to everyone SendToRemoteClients( pMsg ); } else { // The host is asking us to connect to the game server if ( m_CurrentState != MMSTATE_LOADING ) { // Set the longer timeout for loading SetChannelTimeout( &m_Host.m_adr, HEARTBEAT_TIMEOUT_LOADING ); SwitchToState( MMSTATE_LOADING ); } } // Make sure we're not preventing full startup SetPreventFullServerStartup( false, "CHECKPOINT_CONNECT\n" ); if ( !IsServer() ) { char cmd[MAX_PATH]; Q_snprintf( cmd, sizeof( cmd ), "connect %d.%d.%d.%d", m_Host.m_adr.ip[0], m_Host.m_adr.ip[1], m_Host.m_adr.ip[2], m_Host.m_adr.ip[3] ); Cbuf_AddText( cmd ); SessionNotification( SESSION_NOTIFY_CONNECTED_TOSERVER ); } break; case MM_Checkpoint::CHECKPOINT_SESSION_DISCONNECT: PerformDisconnect(); break; case MM_Checkpoint::CHECKPOINT_REPORT_STATS: // Start stats reporting g_ClientDLL->StartStatsReporting( m_Session.GetSessionHandle(), m_Session.IsArbitrated() ); m_Local.m_bReportedStats = true; m_fHeartbeatInterval = HEARTBEAT_INTERVAL_SHORT; SwitchToState( MMSTATE_REPORTING_STATS ); // Host needs to wait for clients to finish reporting if ( !m_Session.IsHost() ) { EndStatsReporting(); } break; case MM_Checkpoint::CHECKPOINT_REPORTING_COMPLETE: { // Mark this client as finished reporting stats bool bFinished = false; netadr_t adr = pMsg->GetNetChannel()->GetRemoteAddress(); for ( int i = 0; i < m_Remote.Count(); ++i ) { CClientInfo *pClient = m_Remote[i]; if ( pClient->m_adr.CompareAdr( adr ) ) { pClient->m_bReportedStats = true; } if ( !pClient->m_bReportedStats ) { bFinished = false; } } if ( bFinished && m_Local.m_bReportedStats ) { EndStatsReporting(); } } break; case MM_Checkpoint::CHECKPOINT_POSTGAME: g_pXboxSystem->SessionEnd( m_Session.GetSessionHandle(), false ); engineClient->ClientCmd( "disconnect" ); EngineVGui()->ActivateGameUI(); if ( m_Session.IsArbitrated() ) { // Tell gameui to return to the main menu SessionNotification( SESSION_NOTIFY_ENDGAME_RANKED ); SwitchToState( MMSTATE_INITIAL ); } else { if ( m_Session.IsHost() ) { // Make ourselves available to queries again m_HostData.gameState = GAMESTATE_INLOBBY; UpdateSessionReplyData( XNET_QOS_LISTEN_ENABLE|XNET_QOS_LISTEN_SET_DATA ); } // Tell gameui to activate the lobby SessionNotification( m_Session.IsHost() ? SESSION_NOTIFY_ENDGAME_HOST : SESSION_NOTIFY_ENDGAME_CLIENT ); // Fill the lobby with all of the clients if ( !m_Session.IsHost() ) { SendPlayerInfoToLobby( &m_Host, m_nHostOwnerId ); SendPlayerInfoToLobby( &m_Local ); } else { SendPlayerInfoToLobby( &m_Local, 0 ); } for ( int i = 0; i < m_Remote.Count(); ++i ) { SendPlayerInfoToLobby( m_Remote[i] ); } if ( m_Session.IsHost() ) { SwitchToState( MMSTATE_ACCEPTING_CONNECTIONS ); } else { SwitchToState( MMSTATE_SESSION_CONNECTED ); } } break; } return true; } //----------------------------------------------------------------------------- // Purpose: Stop any asynchronous operation that is currently running //----------------------------------------------------------------------------- void CMatchmaking::CancelCurrentOperation() { switch( m_CurrentState ) { case MMSTATE_CREATING: m_Session.CancelCreateSession(); break; case MMSTATE_SEARCHING: CancelSearch(); break; case MMSTATE_WAITING_QOS: CancelQosLookup(); break; case MMSTATE_SESSION_CONNECTING: break; default: break; } } //----------------------------------------------------------------------------- // Purpose: Send player info to be displayed in the session lobby //----------------------------------------------------------------------------- void CMatchmaking::SendPlayerInfoToLobby( CClientInfo *pClient, int iHostIdx ) { for ( int i = 0; i < pClient->m_cPlayers; ++i ) { EngineVGui()->UpdatePlayerInfo( pClient->m_xuids[i], pClient->m_szGamertags[i], pClient->m_iTeam[i], pClient->m_cVoiceState[i], GetPlayersNeeded(), iHostIdx == i ); } } //----------------------------------------------------------------------------- // Purpose: Update the start game countdown timer //----------------------------------------------------------------------------- void CMatchmaking::UpdatePregame() { int elapsedTime = GetTime() - m_fCountdownStartTime; if ( elapsedTime > REGISTRATION_MAXWAITTIME ) { // Check the registration timer. if ( m_Session.IsHost() && m_Session.IsArbitrated() && !m_Local.m_bRegistered ) { // Time's up, register ourselves m_Local.m_bRegistered = true; m_Session.RegisterForArbitration(); } } // Check the countdown timer. When it's zero, start the game. if ( elapsedTime < STARTGAME_COUNTDOWN ) { SessionNotification( SESSION_NOTIFY_COUNTDOWN, STARTGAME_COUNTDOWN - elapsedTime ); return; } // Send the zero count SessionNotification( SESSION_NOTIFY_COUNTDOWN, 0 ); // Set a longer timeout for loading if ( m_Session.IsHost() ) { for ( int i = 0; i < m_Remote.Count(); ++i ) { SetChannelTimeout( &m_Remote[i]->m_adr, HEARTBEAT_TIMEOUT_LOADING ); } // Set commentary & cheats off ConVarRef commentary( "commentary" ); commentary.SetValue( false ); ConVarRef sv_cheats( "sv_cheats" ); sv_cheats.SetValue( 0 ); } else { SetChannelTimeout( &m_Host.m_adr, HEARTBEAT_TIMEOUT_LOADING ); } g_pXboxSystem->SessionStart( m_Session.GetSessionHandle(), 0, false ); if ( !IsServer() ) { SetPreventFullServerStartup( true, "SESSION_NOTIFY_COUNTDOWN == 0 and not the host\n" ); } SwitchToState( MMSTATE_LOADING ); } //----------------------------------------------------------------------------- // Purpose: Receive notifications from the session //----------------------------------------------------------------------------- void CMatchmaking::SessionNotification( const SESSION_NOTIFY notification, const int param ) { // Notify GameUI EngineVGui()->SessionNotification( notification, param ); switch( notification ) { case SESSION_NOTIFY_CREATED_HOST: m_bEnteredLobby = true; OnHostSessionCreated(); break; case SESSION_NOTIFY_CREATED_CLIENT: Msg( "Client: CreateSession successful\n" ); // Session has been created according to the advertised specifications. // Now send a connection request to the session host. SwitchToState( MMSTATE_SESSION_CONNECTING ); break; case SESSION_NOFIFY_MODIFYING_SESSION: SwitchToState( MMSTATE_MODIFYING ); break; case SESSION_NOTIFY_MODIFYING_COMPLETED_HOST: SwitchToState( MMSTATE_ACCEPTING_CONNECTIONS ); break; case SESSION_NOTIFY_MODIFYING_COMPLETED_CLIENT: SwitchToState( MMSTATE_SESSION_CONNECTED ); break; case SESSION_NOTIFY_SEARCH_COMPLETED: // Waiting for the player to choose a session SwitchToState( MMSTATE_BROWSING ); break; case SESSION_NOTIFY_CONNECTED_TOSESSION: m_bEnteredLobby = true; SwitchToState( MMSTATE_SESSION_CONNECTED ); break; case SESSION_NOTIFY_CONNECTED_TOSERVER: SwitchToState( MMSTATE_CONNECTED_TO_SERVER ); break; case SESSION_NOTIFY_MIGRATION_COMPLETED: if ( !m_Session.IsHost() ) { // Finished EndMigration(); } else { // Get ready to send join requests too peers for ( int i = 0; i < m_Remote.Count(); ++i ) { m_Remote[i]->m_bMigrated = false; AddRemoteChannel( &m_Remote[i]->m_adr ); } m_nSendCount = 0; m_fSendTimer = 0; SwitchToState( MMSTATE_HOSTMIGRATE_WAITINGFORCLIENTS ); } break; case SESSION_NOTIFY_FAIL_MIGRATE: // X360TBD: How to handle this error? Warning( "Migrate Failed\n" ); break; case SESSION_NOTIFY_REGISTER_COMPLETED: if( !m_Session.IsHost() ) { // Tell the host we're registered MM_RegisterResponse msg; SendMessage( &msg, &m_Host.m_adr ); } else { // Process the results of registration ProcessRegistrationResults(); } break; case SESSION_NOTIFY_ENDGAME_HOST: case SESSION_NOTIFY_ENDGAME_CLIENT: m_fHeartbeatInterval = HEARTBEAT_INTERVAL_SHORT; break; case SESSION_NOTIFY_LOST_HOST: case SESSION_NOTIFY_LOST_SERVER: SwitchToState( MMSTATE_SESSION_DISCONNECTING ); break; case SESSION_NOTIFY_FAIL_REGISTER: case SESSION_NOTIFY_FAIL_CREATE: case SESSION_NOTIFY_FAIL_SEARCH: case SESSION_NOTIFY_CONNECT_SESSIONFULL: case SESSION_NOTIFY_CONNECT_NOTAVAILABLE: case SESSION_NOTIFY_CONNECT_FAILED: // Reset the session SwitchToState( MMSTATE_INITIAL ); break; default: break; } } //----------------------------------------------------------------------------- // Purpose: Switch between matchmaking states //----------------------------------------------------------------------------- void CMatchmaking::SwitchToState( int newState ) { // Clean up from the previous state switch( m_CurrentState ) { case MMSTATE_INITIAL: break; case MMSTATE_BROWSING: // Clean up the search results ClearSearchResults(); break; default: break; } // Initialize the next state switch( newState ) { case MMSTATE_INITIAL: m_bCleanup = true; break; case MMSTATE_CREATING: case MMSTATE_SEARCHING: case MMSTATE_ACCEPTING_CONNECTIONS: break; case MMSTATE_MODIFYING: m_fSendTimer = GetTime(); break; case MMSTATE_WAITING_QOS: m_fWaitTimer = GetTime(); break; case MMSTATE_SESSION_CONNECTING: ConnectToHost(); break; case MMSTATE_PREGAME: m_fCountdownStartTime = GetTime(); break; case MMSTATE_LOADING: m_fHeartbeatInterval = HEARTBEAT_INTERVAL_LONG; break; case MMSTATE_SESSION_DISCONNECTING: m_fWaitTimer = GetTime(); break; case MMSTATE_REPORTING_STATS: m_fWaitTimer = GetTime(); break; default: break; } m_CurrentState = newState; } void CMatchmaking::UpdateVoiceStatus( void ) { #if defined( _X360 ) if ( m_flVoiceBlinkTime > GetTime() ) return; m_flVoiceBlinkTime = GetTime() + VOICE_ICON_BLINK_TIME; CClientInfo *pClient = &m_Local; bool bIsHost = m_Session.IsHost(); bool bShouldSendInfo = false; if ( pClient ) { for ( int i = 0; i < pClient->m_cPlayers; ++i ) { if ( pClient->m_xuids[i] == 0 ) continue; byte cOldVoiceState = pClient->m_cVoiceState[i]; if ( Audio_GetXVoice()->IsHeadsetPresent( pClient->m_iControllers[i] ) == false ) { pClient->m_cVoiceState[i] = VOICE_STATUS_OFF; } else { if ( Audio_GetXVoice()->IsPlayerTalking( pClient->m_xuids[i], true ) ) { pClient->m_cVoiceState[i] = VOICE_STATUS_TALKING; } else { pClient->m_cVoiceState[i] = VOICE_STATUS_IDLE; } } if ( cOldVoiceState != pClient->m_cVoiceState[i] ) { bShouldSendInfo = true; } if ( bShouldSendInfo == true ) { EngineVGui()->UpdatePlayerInfo( pClient->m_xuids[i], pClient->m_szGamertags[i], pClient->m_iTeam[i], pClient->m_cVoiceState[i], GetPlayersNeeded(), bIsHost ); } } if ( bShouldSendInfo ) { MM_ClientInfo info; ClientInfoToNetMessage( &info, pClient ); if ( bIsHost == true ) { // Tell all the clients ProcessClientInfo( &info ); } else { // Tell all the clients SendMessage( &info, &m_Host ); } } } #endif } //----------------------------------------------------------------------------- // Update matchmaking and any active session //----------------------------------------------------------------------------- void CMatchmaking::RunFrame() { if ( !m_bInitialized ) { RunFrameInvite(); return; } if ( NET_IsMultiplayer() ) { NET_ProcessSocket( NS_MATCHMAKING, this ); if ( m_Session.IsSystemLink() ) { NET_ProcessSocket( NS_SYSTEMLINK, this ); } } #if defined( _X360 ) if ( Audio_GetXVoice() != NULL ) { if ( !GameIsActive() ) { if ( Audio_GetXVoice()->VoiceUpdateData() == true ) { CLC_VoiceData voice; Audio_GetXVoice()->GetVoiceData( &voice ); if ( m_Session.IsHost() ) { // Send this message on to everyone else SendToRemoteClients( &voice, true ); } else { // Send to the host SendMessage( &voice, &m_Host ); } Audio_GetXVoice()->VoiceResetLocalData(); } UpdateVoiceStatus(); } } #endif // Tell the session to run its update m_Session.RunFrame(); // Check state: switch( m_CurrentState ) { case MMSTATE_CREATING: // Waiting for success or failure from CreateSession() // GameUI is displaying a "Creating Game" dialog. break; case MMSTATE_ACCEPTING_CONNECTIONS: // Host is sitting in the Lobby waiting for connection requests. Once the game // is full enough (player count >= min players) the host will be able to start the game. UpdateAcceptingConnections(); break; case MMSTATE_SEARCHING: UpdateSearch(); break; case MMSTATE_WAITING_QOS: UpdateQosLookup(); break; case MMSTATE_SESSION_CONNECTING: UpdateConnecting(); break; case MMSTATE_PREGAME: UpdatePregame(); break; case MMSTATE_MODIFYING: UpdateSessionModify(); break; case MMSTATE_HOSTMIGRATE_WAITINGFORCLIENTS: if ( GetTime() - m_fSendTimer > HOSTMIGRATION_RETRYINTERVAL ) { if ( m_nSendCount > HOSTMIGRATION_MAXRETRIES ) { EndMigration(); } else { TellClientsToMigrate(); } } break; case MMSTATE_HOSTMIGRATE_WAITINGFORHOST: if ( GetTime() - m_fWaitTimer > HOSTMIGRATION_MAXWAITTIME ) { // Give up on that host and try the next one in the list Msg( "Giving up on this host\n" ); ClientDropped( m_pNewHost ); StartHostMigration(); } break; case MMSTATE_SESSION_DISCONNECTING: // Wait for the host reply, or timeout if ( GetTime() - m_fWaitTimer > DISCONNECT_WAITTIME ) { PerformDisconnect(); } break; case MMSTATE_REPORTING_STATS: if ( GetTime() - m_fWaitTimer > REPORTSTATS_WAITTIME ) { EndStatsReporting(); } break; } CleanupMarkedChannels(); SendHeartbeat(); if ( m_bCleanup ) { Cleanup(); } RunFrameInvite(); } //----------------------------------------------------------------------------- // Purpose: Let the invite system determine a good moment to start connecting to inviter's host //----------------------------------------------------------------------------- void CMatchmaking::RunFrameInvite() { if ( m_InviteState != INVITE_NONE ) { JoinInviteSession( &m_InviteSessionInfo ); } } //----------------------------------------------------------------------------- // Purpose: Get Quality-of-Service with LIVE //----------------------------------------------------------------------------- MM_QOS_t CMatchmaking::GetQosWithLIVE() { return MM_GetQos(); } //----------------------------------------------------------------------------- // Debugging helpers //----------------------------------------------------------------------------- void CMatchmaking::ShowSessionInfo() { Msg( "[MM] Filled Slots:\n[MM] Public: %d of %d\n[MM] Private: %d of %d\n", m_Session.GetSessionSlots( SLOTS_FILLEDPUBLIC ), m_Session.GetSessionSlots( SLOTS_TOTALPUBLIC ), m_Session.GetSessionSlots( SLOTS_FILLEDPRIVATE ), m_Session.GetSessionSlots( SLOTS_TOTALPRIVATE ) ); Msg( "[MM] Current state: %d\n", m_CurrentState ); Msg( "[MM] Send timer: %f\n", GetTime() - m_fSendTimer ); Msg( "[MM] Wait timer: %f\n", GetTime() - m_fWaitTimer ); int total = 0; for ( int i = 0; i < m_nTotalTeams; ++i ) { total += CountPlayersOnTeam( i ); } Msg( "[MM] Total players: %d\n", total ); EngineVGui()->SessionNotification( SESSION_NOTIFY_DUMPSTATS ); } //----------------------------------------------------------------------------- // Debugging helpers //----------------------------------------------------------------------------- void CMatchmaking::TestSendMessage() { for ( int i = 0; i < m_Remote.Count(); ++i ) { AddRemoteChannel( &m_Remote[i]->m_adr ); } NET_StringCmd msg; SendToRemoteClients( &msg ); } bool CMatchmaking::PreventFullServerStartup() { return m_bPreventFullServerStartup; } void CMatchmaking::SetPreventFullServerStartup( bool bState, char const *fmt, ... ) { char desc[ 256 ]; va_list argptr; va_start( argptr, fmt ); Q_vsnprintf( desc, sizeof( desc ), fmt, argptr ); va_end( argptr ); DevMsg( 1, "Setting state from prevent %s to prevent %s: %s", m_bPreventFullServerStartup ? "yes" : "no", bState ? "yes" : "no", desc ); m_bPreventFullServerStartup = bState; } CON_COMMAND( mm_session_info, "Dump session information" ) { if ( g_pMatchmaking ) { g_pMatchmaking->ShowSessionInfo(); } } CON_COMMAND( mm_message, "Send a message to all remote clients" ) { if ( g_pMatchmaking ) { g_pMatchmaking->TestSendMessage(); } } CON_COMMAND( mm_stats, "" ) { if ( g_pMatchmaking ) { g_pMatchmaking->TestStats(); } }