/* ======================== idLobby::BuildMigrationInviteList ======================== */ void idLobby::BuildMigrationInviteList( bool inviteOldHost ) { migrationInfo.invites.Clear(); // Build a list of addresses we will send invites to (gather all unique remote addresses from the session user list) for( int i = 0; i < GetNumLobbyUsers(); i++ ) { lobbyUser_t* user = GetLobbyUser( i ); if( !verify( user != NULL ) ) { continue; } if( user->IsDisconnected() ) { continue; } if( IsSessionUserIndexLocal( i ) ) { migrationInfo.ourPingMs = user->pingMs; migrationInfo.ourUserId = user->lobbyUserID; migrationInfo.persistUntilGameEndsData.ourGameData = user->migrationGameData; NET_VERBOSE_PRINT( "^2NET: Migration game data for local user is index %d \n", user->migrationGameData ); continue; // Only interested in remote users } if( !inviteOldHost && user->peerIndex == -1 ) { continue; // Don't invite old host if told not to do so } if( FindMigrationInviteIndex( user->address ) == -1 ) { migrationInvite_t invite; invite.address = user->address; invite.pingMs = user->pingMs; invite.userId = user->lobbyUserID; invite.migrationGameData = user->migrationGameData; invite.lastInviteTime = 0; NET_VERBOSE_PRINT( "^2NET: Migration game data for user %s is index %d \n", user->gamertag, user->migrationGameData ); migrationInfo.invites.Append( invite ); } } }
/* ======================== idLobby::HandleMigrationGameData ======================== */ void idLobby::HandleMigrationGameData( idBitMsg& msg ) { // Receives game migration data from the server. Just save off the raw data. If we ever become host we'll let the game code read // that chunk in (we can't do anything with it now anyways: we don't have entities or any server code to read it in to) migrationInfo.persistUntilGameEndsData.hasGameData = true; // Reset each user's migration game data. If we don't receive new data for them in this msg, we don't want to use the old data for( int i = 0; i < GetNumLobbyUsers(); i++ ) { lobbyUser_t* u = GetLobbyUser( i ); if( u != NULL ) { u->migrationGameData = -1; } } msg.ReadData( migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) ); int numUsers = msg.ReadByte(); int dataIndex = 0; for( int i = 0; i < numUsers; i++ ) { lobbyUserID_t lobbyUserID; lobbyUserID.ReadFromMsg( msg ); lobbyUser_t* user = GetLobbyUser( GetLobbyUserIndexByID( lobbyUserID ) ); if( user != NULL ) { NET_VERBOSE_PRINT( "NET: Got migration data[%d] for user %s\n", dataIndex, user->gamertag ); user->migrationGameData = dataIndex; msg.ReadData( migrationInfo.persistUntilGameEndsData.gameDataUser[ dataIndex ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[ dataIndex ] ) ); dataIndex++; } } }
/* ======================== idLobby::InitSessionUsersFromLocalUsers This functions just defaults the session users to the signin manager local users ======================== */ void idLobby::InitSessionUsersFromLocalUsers( bool onlineMatch ) { assert( lobbyBackend != NULL ); // First, clear all session users of this session type FreeAllUsers(); // Copy all local users from sign in mgr to the session user list for ( int i = 0; i < sessionCB->GetSignInManager().GetNumLocalUsers(); i++ ) { idLocalUser * localUser = sessionCB->GetSignInManager().GetLocalUserByIndex( i ); // Make sure this user can join lobbies if ( onlineMatch && !localUser->CanPlayOnline() ) { continue; } lobbyUser_t lobbyUser = CreateLobbyUserFromLocalUser( localUser ); // Append this new session user to the session user list lobbyUser_t * createdUser = AllocUser( lobbyUser ); // Get the migration game data if this is a migrated hosting if ( verify( createdUser != NULL ) && migrationInfo.persistUntilGameEndsData.wasMigratedHost ) { createdUser->migrationGameData = migrationInfo.persistUntilGameEndsData.ourGameData; NET_VERBOSE_PRINT( "NET: Migration game data set for local user %s at index %d \n", createdUser->gamertag, migrationInfo.persistUntilGameEndsData.ourGameData ); } } }
/* ======================== idLobby::CreateLobbyUserFromLocalUser This functions just defaults the session users to the signin manager local users ======================== */ lobbyUser_t idLobby::CreateLobbyUserFromLocalUser( const idLocalUser * localUser ) { lobbyUser_t lobbyUser; idStr::Copynz( lobbyUser.gamertag, localUser->GetGamerTag(), sizeof( lobbyUser.gamertag ) ); lobbyUser.peerIndex = -1; lobbyUser.lobbyUserID = lobbyUserID_t( localUser->GetLocalUserHandle(), lobbyType ); // Generate the lobby using a combination of local user id, and lobby type lobbyUser.disconnecting = false; // If we are in a game lobby (or dedicated game state lobby), and we have a party lobby running, assume we can grab the party token from our equivalent user in the party. if ( ( lobbyType == TYPE_GAME || lobbyType == TYPE_GAME_STATE ) && sessionCB->GetPartyLobby().IsLobbyActive() ) { if ( ( sessionCB->GetPartyLobby().parms.matchFlags & MATCH_REQUIRE_PARTY_LOBBY ) && !( sessionCB->GetPartyLobby().parms.matchFlags & MATCH_PARTY_INVITE_PLACEHOLDER ) ) { // copy some things from my party user const int myPartyUserIndex = sessionCB->GetPartyLobby().GetLobbyUserIndexByLocalUserHandle( lobbyUser.lobbyUserID.GetLocalUserHandle() ); if ( verify( myPartyUserIndex >= 0 ) ) { // Just in case lobbyUser_t * myPartyUser = sessionCB->GetPartyLobby().GetLobbyUser( myPartyUserIndex ); if ( myPartyUser != NULL ) { lobbyUser.partyToken = myPartyUser->partyToken; } } } } lobbyUser.UpdateClientMutableData( localUser ); NET_VERBOSE_PRINT( "NET: CreateLobbyUserFromLocalUser: party %08x name %s (%s)\n", lobbyUser.partyToken, lobbyUser.gamertag, GetLobbyName() ); return lobbyUser; }
/* ======================== idLobbyBackendDirect::StartHosting ======================== */ void idLobbyBackendDirect::StartHosting( const idMatchParameters & p, float skillLevel, lobbyBackendType_t type ) { NET_VERBOSE_PRINT( "idLobbyBackendDirect::StartHosting\n" ); isLocal = MatchTypeIsLocal( p.matchFlags ); isHost = true; state = STATE_READY; isLocal = true; }
/* ======================== idLobbyBackendDirect::JoinFromConnectInfo ======================== */ void idLobbyBackendDirect::JoinFromConnectInfo( const lobbyConnectInfo_t& connectInfo ) { if( lobbyToSessionCB->CanJoinLocalHost() ) { // TODO: "CanJoinLocalHost" == *must* join LocalHost ?! Sys_StringToNetAdr( "localhost", &address, true ); address.port = net_port.GetInteger(); NET_VERBOSE_PRINT( "NET: idLobbyBackendDirect::JoinFromConnectInfo(): canJoinLocalHost\n" ); } else { address = connectInfo.netAddr; NET_VERBOSE_PRINT( "NET: idLobbyBackendDirect::JoinFromConnectInfo(): %s\n", Sys_NetAdrToString( address ) ); } state = STATE_READY; isLocal = false; isHost = false; }
/* ======================== idLobby::AllocLobbyUserSlotForBot ======================== */ lobbyUserID_t idLobby::AllocLobbyUserSlotForBot( const char * botName ) { lobbyUser_t botSessionUser; botSessionUser.peerIndex = peerIndexOnHost; botSessionUser.isBot = true; botSessionUser.disconnecting = false; idStr::Copynz( botSessionUser.gamertag, botName, sizeof( botSessionUser.gamertag ) ); localUserHandle_t localUserHandle( session->GetSignInManager().GetUniqueLocalUserHandle( botSessionUser.gamertag ) ); botSessionUser.lobbyUserID = lobbyUserID_t( localUserHandle, lobbyType ); lobbyUser_t * botUser = NULL; int sessionUserIndex = -1; // First, try to replace a disconnected user for ( int i = 0; i < GetNumLobbyUsers(); ++i ) { if ( IsLobbyUserDisconnected( i ) ) { lobbyUser_t * user = GetLobbyUser( i ); if ( verify( user != NULL ) ) { *user = botSessionUser; botUser = user; sessionUserIndex = i; break; } } } if ( botUser == NULL ) { if ( freeUsers.Num() == 0 ) { idLib::Warning( "NET: Out Of Session Users - Can't Add Bot %s!", botName ); return lobbyUserID_t(); } botUser = AllocUser( botSessionUser ); sessionUserIndex = userList.Num() - 1; } if ( !verify( botUser != NULL ) ) { idLib::Warning( "NET: Can't Find Session Slot For Bot!" ); return lobbyUserID_t(); } else { NET_VERBOSE_PRINT( "NET: Created Bot %s At Index %d \n", botUser->gamertag, sessionUserIndex ); } SendNewUsersToPeers( peerIndexOnHost, userList.Num() - 1, 1 ); // bot has been added to the lobby user list - update the peers so that they can see the bot too. return GetLobbyUser( sessionUserIndex )->lobbyUserID; }
/* ======================== idSessionLocalWin::Shutdown ======================== */ void idSessionLocalWin::Shutdown() { NET_VERBOSE_PRINT( "NET: Shutdown\n" ); idSessionLocal::Shutdown(); MoveToMainMenu(); // Wait until we fully shutdown while( localState != STATE_IDLE && localState != STATE_PRESS_START ) { Pump(); } if( achievementSystem != NULL ) { achievementSystem->Shutdown(); delete achievementSystem; achievementSystem = NULL; } }
/* ======================== idLobby::GetAverageSessionLevel ======================== */ float idLobby::GetAverageSessionLevel() { float level = 0.0f; int numActiveMembers = 0; for ( int i = 0; i < GetNumLobbyUsers(); i++ ) { const lobbyUser_t * user = GetLobbyUser( i ); if ( user->IsDisconnected() ) { continue; } level += user->level; numActiveMembers++; } if ( numActiveMembers > 0 ) { level /= (float)numActiveMembers; } float ret = Max( level, 1.0f ); NET_VERBOSE_PRINT( "NET: GetAverageSessionLevel %g\n", ret ); return ret; }
/* ======================== idLobby::UpdateHostMigration ======================== */ void idLobby::UpdateHostMigration() { int time = Sys_Milliseconds(); // If we are picking a new host, then update that if( migrationInfo.state == MIGRATE_PICKING_HOST ) { const int MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS = 20; // FIXME: set back to 5 // Give other hosts 5 seconds if( time - migrationInfo.migrationStartTime > session->GetTitleStorageInt( "MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS", MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS ) * 1000 ) { // Just become the host if we haven't heard from a host in awhile BecomeHost(); } else { return; } } // See if we are a new migrated host that needs to invite the original members back if( migrationInfo.state != MIGRATE_BECOMING_HOST ) { return; } if( lobbyBackend == NULL || lobbyBackend->GetState() != idLobbyBackend::STATE_READY ) { return; } if( state != STATE_IDLE ) { return; } if( !IsHost() ) { return; } const int MIGRATION_TIMEOUT_IN_SECONDS = 30; // FIXME: setting to 30 for dev purposes. 10 seems more reasonable. Need to make unloading game / loading lobby async const int MIGRATION_INVITE_TIME_IN_SECONDS = 2; if( migrationInfo.invites.Num() == 0 || time - migrationInfo.migrationStartTime > session->GetTitleStorageInt( "MIGRATION_TIMEOUT_IN_SECONDS", MIGRATION_TIMEOUT_IN_SECONDS ) * 1000 ) { // Either everyone acked, or we timed out, just keep who we have, and stop sending invites EndMigration(); return; } // Send invites to anyone who hasn't responded for( int i = 0; i < migrationInfo.invites.Num(); i++ ) { if( time - migrationInfo.invites[i].lastInviteTime < session->GetTitleStorageInt( "MIGRATION_INVITE_TIME_IN_SECONDS", MIGRATION_INVITE_TIME_IN_SECONDS ) * 1000 ) { continue; // Not enough time passed } // Mark the time migrationInfo.invites[i].lastInviteTime = time; byte buffer[ idPacketProcessor::MAX_PACKET_SIZE - 2 ]; idBitMsg outmsg( buffer, sizeof( buffer ) ); // Have lobbyBackend fill out msg with connection info lobbyConnectInfo_t connectInfo = lobbyBackend->GetConnectInfo(); connectInfo.WriteToMsg( outmsg ); // Let them know whether or not this was from in game outmsg.WriteBool( migrationInfo.persistUntilGameEndsData.wasMigratedGame ); NET_VERBOSE_PRINT( "NET: Sending migration invite to %s\n", migrationInfo.invites[i].address.ToString() ); // Send the migration invite SendConnectionLess( migrationInfo.invites[i].address, OOB_MIGRATE_INVITE, outmsg.GetReadData(), outmsg.GetSize() ); } }
/* ======================== idLobby::SendMigrationGameData ======================== */ void idLobby::SendMigrationGameData() { if( net_migration_disable.GetBool() ) { return; } if( sessionCB->GetState() != idSession::INGAME ) { return; } if( !migrationInfo.persistUntilGameEndsData.hasGameData ) { // Haven't been given any migration game data yet return; } const int now = Sys_Milliseconds(); if( nextSendMigrationGameTime > now ) { return; } byte packetData[ idPacketProcessor::MAX_MSG_SIZE ]; idBitMsg msg( packetData, sizeof( packetData ) ); // Write global data msg.WriteData( &migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) ); msg.WriteByte( GetNumLobbyUsers() ); // Write user data for( int userIndex = 0; userIndex < GetNumLobbyUsers(); ++userIndex ) { lobbyUser_t* u = GetLobbyUser( userIndex ); if( u->IsDisconnected() || u->migrationGameData < 0 ) { continue; } u->lobbyUserID.WriteToMsg( msg ); msg.WriteData( migrationInfo.persistUntilGameEndsData.gameDataUser[ u->migrationGameData ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[ u->migrationGameData ] ) ); } // Send to 1 peer for( int i = 0; i < peers.Num(); i++ ) { int peerToSend = ( nextSendMigrationGamePeer + i ) % peers.Num(); if( peers[ peerToSend ].IsConnected() && peers[ peerToSend ].loaded ) { if( peers[ peerToSend ].packetProc->NumQueuedReliables() > idPacketProcessor::MAX_RELIABLE_QUEUE / 2 ) { // This is kind of a hack for development so we don't DC clients by sending them too many reliable migration messages // when they aren't responding. Doesn't seem like a horrible thing to have in a shipping product but is not necessary. NET_VERBOSE_PRINT( "NET: Skipping reliable game migration data msg because client reliable queue is > half full\n" ); } else { if( net_migration_debug.GetBool() ) { idLib::Printf( "NET: Sending migration game data to peer %d. size: %d\n", peerToSend, msg.GetSize() ); } QueueReliableMessage( peerToSend, RELIABLE_MIGRATION_GAME_DATA, msg.GetReadData(), msg.GetSize() ); } break; } } // Increment next send time / next send peer nextSendMigrationGamePeer++; if( nextSendMigrationGamePeer >= peers.Num() ) { nextSendMigrationGamePeer = 0; } nextSendMigrationGameTime = now + MIGRATION_GAME_DATA_INTERVAL_MS; }
/* ======================== idLobby::PickNewHostInternal ======================== */ void idLobby::PickNewHostInternal( bool forceMe, bool inviteOldHost ) { if( migrationInfo.state == MIGRATE_PICKING_HOST ) { return; // Already picking new host } idLib::Printf( "PickNewHost: Started picking new host %s.\n", GetLobbyName() ); if( IsHost() ) { idLib::Printf( "PickNewHost: Already host of session %s\n", GetLobbyName() ); return; } // Find the user with the lowest ping int bestUserIndex = -1; int bestPingMs = 0; lobbyUserID_t bestUserId; for( int i = 0; i < GetNumLobbyUsers(); i++ ) { lobbyUser_t* user = GetLobbyUser( i ); if( !verify( user != NULL ) ) { continue; } if( user->IsDisconnected() ) { continue; } if( user->peerIndex == -1 ) { continue; // Don't try and pick old host } if( bestUserIndex == -1 || IsBetterHost( user->pingMs, user->lobbyUserID, bestPingMs, bestUserId ) ) { bestUserIndex = i; bestPingMs = user->pingMs; bestUserId = user->lobbyUserID; } if( user->peerIndex == net_migration_forcePeerAsHost.GetInteger() ) { bestUserIndex = i; bestPingMs = user->pingMs; bestUserId = user->lobbyUserID; break; } } // Remember when we first started picking a new host migrationInfo.state = MIGRATE_PICKING_HOST; migrationInfo.migrationStartTime = Sys_Milliseconds(); migrationInfo.persistUntilGameEndsData.wasMigratedGame = sessionCB->GetState() == idSession::INGAME; if( bestUserIndex == -1 ) // This can happen if we call PickNewHost on an lobby that was Shutdown { NET_VERBOSE_PRINT( "MIGRATION: PickNewHost was called on an lobby that was Shutdown\n" ); BecomeHost(); return; } NET_VERBOSE_PRINT( "MIGRATION: Chose user index %d (%s) for new host\n", bestUserIndex, GetLobbyUser( bestUserIndex )->gamertag ); bool bestWasLocal = IsSessionUserIndexLocal( bestUserIndex ); // Check before shutting down the lobby migrateMsgFlags = parms.matchFlags; // Save off match parms // Build invite list BuildMigrationInviteList( inviteOldHost ); // If the best user is on this machine, then we become the host now, otherwise, wait for a new host to contact us if( forceMe || bestWasLocal ) { BecomeHost(); } }
/* ======================== idLobby::CheckPeerThrottle ======================== */ void idLobby::CheckPeerThrottle( int p ) { assert( lobbyType == GetActingGameStateLobbyType() ); if( !verify( p >= 0 && p < peers.Num() ) ) { return; } peer_t& peer = peers[p]; if( !peer.IsConnected() ) { return; } if( !IsHost() ) { return; } if( session->GetTitleStorageInt( "net_peer_throttle_mode", net_peer_throttle_mode.GetInteger() ) == 0 ) { return; } if( peer.receivedBps < 0.0f ) { return; } int time = Sys_Milliseconds(); if( !AllPeersHaveBaseState() ) { return; } if( verify( peer.snapProc != NULL ) ) { const int peer_throttle_minSnapSeq = session->GetTitleStorageInt( "net_peer_throttle_minSnapSeq", net_peer_throttle_minSnapSeq.GetInteger() ); if( peer.snapProc->GetFullSnapBaseSequence() <= idSnapshotProcessor::INITIAL_SNAP_SEQUENCE + peer_throttle_minSnapSeq ) { return; } } // This is bps throttling which compares the sent bytes per second to the reported received bps float peer_throttle_bps_host_threshold = session->GetTitleStorageFloat( "net_peer_throttle_bps_host_threshold", net_peer_throttle_bps_host_threshold.GetFloat() ); if( peer_throttle_bps_host_threshold > 0.0f ) { int deltaT = idMath::ClampInt( 0, 100, time - peer.receivedThrottleTime ); if( deltaT > 0 && peer.receivedThrottleTime > 0 && peer.receivedBpsIndex > 0 ) { bool throttled = false; float sentBps = peer.sentBpsHistory[ peer.receivedBpsIndex % MAX_BPS_HISTORY ]; // Min outgoing rate from server (don't throttle if we are sending < 1k) if( sentBps > peer_throttle_bps_host_threshold ) { float pct = peer.receivedBps / idMath::ClampFloat( 0.01f, static_cast<float>( BANDWIDTH_REPORTING_MAX ), sentBps ); // note the receivedBps is implicitly clamped on client end to 10k/sec /* static int lastSeq = 0; if ( peer.receivedBpsIndex != lastSeq ) { NET_VERBOSE_PRINT( "%ssentBpsHistory[%d] = %.2f received: %.2f PCT: %.2f \n", ( pct > 1.0f ? "^1" : "" ), peer.receivedBpsIndex, sentBps, peer.receivedBps, pct ); } lastSeq = peer.receivedBpsIndex; */ // Increase throttle time if peer is < % of what we are sending him if( pct < session->GetTitleStorageFloat( "net_peer_throttle_bps_peer_threshold_pct", net_peer_throttle_bps_peer_threshold_pct.GetFloat() ) ) { peer.receivedThrottle += ( float )deltaT; throttled = true; NET_VERBOSE_PRINT( "NET: throttled... %.2f ....pct %.2f receivedBps %.2f outgoingBps %.2f, peer %i, seq %i\n", peer.receivedThrottle, pct, peer.receivedBps, sentBps, p, peer.snapProc->GetFullSnapBaseSequence() ); } } if( !throttled ) { float decayRate = session->GetTitleStorageFloat( "net_peer_throttle_bps_decay", net_peer_throttle_bps_decay.GetFloat() ); peer.receivedThrottle = Max<float>( 0.0f, peer.receivedThrottle - ( ( ( float )deltaT ) * decayRate ) ); //NET_VERBOSE_PRINT("NET: !throttled... %.2f ....receivedBps %.2f outgoingBps %.2f\n", peer.receivedThrottle, peer.receivedBps, sentBps ); } float duration = session->GetTitleStorageFloat( "net_peer_throttle_bps_duration", net_peer_throttle_bps_duration.GetFloat() ); if( peer.receivedThrottle > duration ) { peer.maxSnapBps = peer.receivedBps * session->GetTitleStorageFloat( "net_snap_bw_test_throttle_max_scale", net_snap_bw_test_throttle_max_scale.GetFloat() ); int maxRate = common->GetSnapRate() * session->GetTitleStorageInt( "net_peer_throttle_maxSnapRate", net_peer_throttle_maxSnapRate.GetInteger() ); if( peer.throttledSnapRate == 0 ) { peer.throttledSnapRate = common->GetSnapRate() * 2; } else if( peer.throttledSnapRate < maxRate ) { peer.throttledSnapRate = idMath::ClampInt( common->GetSnapRate(), maxRate, peer.throttledSnapRate + common->GetSnapRate() ); } peer.receivedThrottle = 0.0f; // Start over, so we don't immediately throttle again } } peer.receivedThrottleTime = time; } }