123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- /*
- ===========================================================================
- Doom 3 BFG Edition GPL Source Code
- Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company.
- This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code").
- Doom 3 BFG Edition Source Code is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- Doom 3 BFG Edition Source Code is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with Doom 3 BFG Edition Source Code. If not, see <http://www.gnu.org/licenses/>.
- In addition, the Doom 3 BFG Edition Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 BFG Edition Source Code. If not, please request a copy in writing from id Software at the address below.
- If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
- ===========================================================================
- */
- #pragma hdrstop
- #include "../idlib/precompiled.h"
- #include "sys_lobby.h"
- idCVar net_migration_debug( "net_migration_debug", "0", CVAR_BOOL, "debug" );
- idCVar net_migration_disable( "net_migration_disable", "0", CVAR_BOOL, "debug" );
- idCVar net_migration_forcePeerAsHost( "net_migration_forcePeerAsHost", "-1", CVAR_INTEGER, "When set to >-1, it forces that peer number to be the new host during migration" );
- /*
- ========================
- idLobby::IsBetterHost
- ========================
- */
- bool idLobby::IsBetterHost( int ping1, lobbyUserID_t userId1, int ping2, lobbyUserID_t userId2 ) {
- if ( lobbyType == TYPE_PARTY ) {
- return userId1 < userId2; // Only use user id for party, since ping doesn't matter
- }
- if ( ping1 < ping2 ) {
- // Better ping wins
- return true;
- } else if ( ping1 == ping2 && userId1 < userId2 ) {
- // User id is tie breaker
- return true;
- }
- return false;
- }
- /*
- ========================
- idLobby::FindMigrationInviteIndex
- ========================
- */
- int idLobby::FindMigrationInviteIndex( lobbyAddress_t & address ) {
- if ( migrationInfo.state == MIGRATE_NONE ) {
- return -1;
- }
- for ( int i = 0; i < migrationInfo.invites.Num(); i++ ) {
- if ( migrationInfo.invites[i].address.Compare( address, true ) ) {
- return i;
- }
- }
- return -1;
- }
- /*
- ========================
- 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::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::PickNewHost
- ========================
- */
- void idLobby::PickNewHost( bool forceMe, bool inviteOldHost ) {
- if ( IsHost() ) {
- idLib::Printf( "PickNewHost: Already host of session %s\n", GetLobbyName() );
- return;
- }
- sessionCB->PrePickNewHost( *this, forceMe, inviteOldHost );
- }
- /*
- ========================
- 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::BecomeHost
- ========================
- */
- void idLobby::BecomeHost() {
- if ( !verify( migrationInfo.state == MIGRATE_PICKING_HOST ) ) {
- idLib::Printf( "BecomeHost: Must be called from PickNewHost.\n" );
- EndMigration();
- return;
- }
- if ( IsHost() ) {
- idLib::Printf( "BecomeHost: Already host of session.\n" );
- EndMigration();
- return;
- }
- if ( !sessionCB->BecomingHost( *this ) ) {
- EndMigration();
- return;
- }
- idLib::Printf( "BecomeHost: Sending %i invites on %s.\n", migrationInfo.invites.Num(), GetLobbyName() );
- migrationInfo.state = MIGRATE_BECOMING_HOST;
- migrationInfo.migrationStartTime = Sys_Milliseconds();
- if ( lobbyBackend == NULL ) {
- // If we don't have a lobbyBackend, then just create one
- Shutdown();
- StartCreating();
- return;
- }
- // Shutdown the current lobby, but keep the lobbyBackend (we'll migrate it)
- Shutdown( true );
- // Migrate the lobbyBackend to host
- lobbyBackend->BecomeHost( migrationInfo.invites.Num() );
- // Wait for it to complete
- SetState( STATE_CREATE_LOBBY_BACKEND );
- }
- /*
- ========================
- idLobby::EndMigration
- This gets called when we are done migrating, and invites will no longer be sent out.
- ========================
- */
- void idLobby::EndMigration() {
- if ( migrationInfo.state == MIGRATE_NONE ) {
- idLib::Printf( "idSessionLocal::EndMigration: Not migrating.\n" );
- return;
- }
- sessionCB->MigrationEnded( *this );
-
- if ( lobbyBackend != NULL ) {
- lobbyBackend->FinishBecomeHost();
- }
- migrationInfo.state = MIGRATE_NONE;
- migrationInfo.invites.Clear();
- }
- /*
- ========================
- idLobby::ResetAllMigrationState
- This will reset all state related to host migration. Should be called
- at match end so our next game is not treated as a migrated game
- ========================
- */
- void idLobby::ResetAllMigrationState() {
- migrationInfo.state = MIGRATE_NONE;
- migrationInfo.invites.Clear();
- migrationInfo.persistUntilGameEndsData.Clear();
- migrateMsgFlags = 0;
- common->Dialog().ClearDialog( GDM_MIGRATING );
- common->Dialog().ClearDialog( GDM_MIGRATING_WAITING );
- common->Dialog().ClearDialog( GDM_MIGRATING_RELAUNCHING );
- }
- /*
- ========================
- idLobby::GetMigrationGameData
- This will setup the passed in idBitMsg to either read or write from the global migration game data buffer
- ========================
- */
- bool idLobby::GetMigrationGameData( idBitMsg &msg, bool reading ) {
- if ( reading ) {
- if ( !IsMigratedStatsGame() || !migrationInfo.persistUntilGameEndsData.wasMigratedHost ) {
- // This was not a migrated session, we have no migration data
- return false;
- }
- msg.InitRead( migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
- } else {
- migrationInfo.persistUntilGameEndsData.hasGameData = true;
- memset( migrationInfo.persistUntilGameEndsData.gameData, 0, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
- msg.InitWrite( migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
- }
- return true;
- }
- /*
- ========================
- idLobby::GetMigrationGameDataUser
- This will setup the passed in idBitMsg to either read or write from the user's migration game data buffer
- ========================
- */
- bool idLobby::GetMigrationGameDataUser( lobbyUserID_t lobbyUserID, idBitMsg & msg, bool reading ) {
- const int userNum = GetLobbyUserIndexByID( lobbyUserID );
- if ( !verify( userNum >=0 && userNum < MAX_PLAYERS ) ) {
- return false;
- }
- lobbyUser_t * u = GetLobbyUser( userNum );
- if ( u != NULL ) {
- if ( reading ) {
- if ( !IsMigratedStatsGame() || !migrationInfo.persistUntilGameEndsData.wasMigratedHost ) {
- // This was not a migrated session, we have no migration data
- return false;
- }
- if ( u->migrationGameData >= 0 && u->migrationGameData < MAX_PLAYERS ) {
- msg.InitRead( migrationInfo.persistUntilGameEndsData.gameDataUser[ u->migrationGameData ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[ 0 ] ) );
- } else {
- // We don't have migration data for this user
- idLib::Warning( "No migration data for user %d in a migrated game (%d)", userNum, u->migrationGameData );
- return false;
- }
- } else {
- // Writing
- migrationInfo.persistUntilGameEndsData.hasGameData = true;
- u->migrationGameData = userNum;
- memset( migrationInfo.persistUntilGameEndsData.gameDataUser[ userNum ], 0, sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[0] ) );
- msg.InitWrite( migrationInfo.persistUntilGameEndsData.gameDataUser[ userNum ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[0] ) );
-
- }
- return true;
- }
- return false;
- }
- /*
- ========================
- 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::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;
- }
|