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