DOOM-3-BFG

DOOM 3 BFG Edition
Log | Files | Refs

sys_lobby_migrate.cpp (18032B)


      1 /*
      2 ===========================================================================
      3 
      4 Doom 3 BFG Edition GPL Source Code
      5 Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company. 
      6 
      7 This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code").  
      8 
      9 Doom 3 BFG Edition Source Code is free software: you can redistribute it and/or modify
     10 it under the terms of the GNU General Public License as published by
     11 the Free Software Foundation, either version 3 of the License, or
     12 (at your option) any later version.
     13 
     14 Doom 3 BFG Edition Source Code is distributed in the hope that it will be useful,
     15 but WITHOUT ANY WARRANTY; without even the implied warranty of
     16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     17 GNU General Public License for more details.
     18 
     19 You should have received a copy of the GNU General Public License
     20 along with Doom 3 BFG Edition Source Code.  If not, see <http://www.gnu.org/licenses/>.
     21 
     22 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.
     23 
     24 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.
     25 
     26 ===========================================================================
     27 */
     28 #pragma hdrstop
     29 #include "../idlib/precompiled.h"
     30 #include "sys_lobby.h"
     31 
     32 idCVar net_migration_debug( "net_migration_debug", "0", CVAR_BOOL, "debug" ); 
     33 idCVar net_migration_disable( "net_migration_disable", "0", CVAR_BOOL, "debug" ); 
     34 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" );
     35 
     36 
     37 /*
     38 ========================
     39 idLobby::IsBetterHost
     40 ========================
     41 */
     42 bool idLobby::IsBetterHost( int ping1, lobbyUserID_t userId1, int ping2, lobbyUserID_t userId2 ) {
     43 	if ( lobbyType == TYPE_PARTY ) {
     44 		return userId1 < userId2;			// Only use user id for party, since ping doesn't matter
     45 	}
     46 
     47 	if ( ping1 < ping2 ) {
     48 		// Better ping wins
     49 		return true;
     50 	} else if ( ping1 == ping2 && userId1 < userId2 ) {
     51 		// User id is tie breaker
     52 		return true;
     53 	}
     54 
     55 	return false;
     56 }
     57 
     58 /*
     59 ========================
     60 idLobby::FindMigrationInviteIndex
     61 ========================
     62 */
     63 int idLobby::FindMigrationInviteIndex( lobbyAddress_t & address ) {
     64 	if ( migrationInfo.state == MIGRATE_NONE ) {
     65 		return -1;
     66 	}
     67 
     68 	for ( int i = 0; i < migrationInfo.invites.Num(); i++ ) {
     69 		if ( migrationInfo.invites[i].address.Compare( address, true ) ) {
     70 			return i;
     71 		}
     72 	}
     73 
     74 	return -1;
     75 }
     76 
     77 /*
     78 ========================
     79 idLobby::UpdateHostMigration
     80 ========================
     81 */
     82 void idLobby::UpdateHostMigration() {
     83 	
     84 	int time = Sys_Milliseconds();
     85 
     86 	// If we are picking a new host, then update that
     87 	if ( migrationInfo.state == MIGRATE_PICKING_HOST ) {
     88 		const int MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS = 20;		// FIXME: set back to 5 // Give other hosts 5 seconds
     89 
     90 		if ( time - migrationInfo.migrationStartTime > session->GetTitleStorageInt( "MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS", MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS ) * 1000 ) {
     91 			// Just become the host if we haven't heard from a host in awhile
     92 			BecomeHost();
     93 		} else {
     94 			return;
     95 		}
     96 	}
     97 
     98 	// See if we are a new migrated host that needs to invite the original members back
     99 	if ( migrationInfo.state != MIGRATE_BECOMING_HOST ) {
    100 		return;
    101 	}
    102 
    103 	if ( lobbyBackend == NULL || lobbyBackend->GetState() != idLobbyBackend::STATE_READY ) {
    104 		return;
    105 	}
    106 
    107 	if ( state != STATE_IDLE ) {
    108 		return;
    109 	}
    110 
    111 	if ( !IsHost() ) {
    112 		return;
    113 	}
    114 
    115 	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
    116 	const int MIGRATION_INVITE_TIME_IN_SECONDS	= 2;
    117 
    118 	if ( migrationInfo.invites.Num() == 0 || time - migrationInfo.migrationStartTime > session->GetTitleStorageInt( "MIGRATION_TIMEOUT_IN_SECONDS", MIGRATION_TIMEOUT_IN_SECONDS ) * 1000 ) {
    119 		// Either everyone acked, or we timed out, just keep who we have, and stop sending invites
    120 		EndMigration();
    121 		return;
    122 	}
    123 
    124 	// Send invites to anyone who hasn't responded
    125 	for ( int i = 0; i < migrationInfo.invites.Num(); i++ ) {
    126 		if ( time - migrationInfo.invites[i].lastInviteTime < session->GetTitleStorageInt( "MIGRATION_INVITE_TIME_IN_SECONDS", MIGRATION_INVITE_TIME_IN_SECONDS ) * 1000 ) {
    127 			continue;		// Not enough time passed
    128 		}
    129 
    130 		// Mark the time
    131 		migrationInfo.invites[i].lastInviteTime = time;
    132 
    133 		byte buffer[ idPacketProcessor::MAX_PACKET_SIZE - 2 ];
    134 		idBitMsg outmsg( buffer, sizeof( buffer ) );
    135 
    136 		// Have lobbyBackend fill out msg with connection info
    137 		lobbyConnectInfo_t connectInfo = lobbyBackend->GetConnectInfo();
    138 		connectInfo.WriteToMsg( outmsg );
    139 
    140 		// Let them know whether or not this was from in game
    141 		outmsg.WriteBool( migrationInfo.persistUntilGameEndsData.wasMigratedGame );
    142 
    143 		NET_VERBOSE_PRINT( "NET: Sending migration invite to %s\n", migrationInfo.invites[i].address.ToString() );
    144 		
    145 		// Send the migration invite
    146 		SendConnectionLess( migrationInfo.invites[i].address, OOB_MIGRATE_INVITE, outmsg.GetReadData(), outmsg.GetSize() ); 
    147 	}
    148 }
    149 
    150 /*
    151 ========================
    152 idLobby::BuildMigrationInviteList
    153 ========================
    154 */
    155 void idLobby::BuildMigrationInviteList( bool inviteOldHost ) {
    156 	migrationInfo.invites.Clear();
    157 
    158 	// Build a list of addresses we will send invites to (gather all unique remote addresses from the session user list)
    159 	for ( int i = 0; i < GetNumLobbyUsers(); i++ ) {
    160 		lobbyUser_t * user = GetLobbyUser( i );
    161 
    162 		if ( !verify( user != NULL ) ) {
    163 			continue;
    164 		}
    165 
    166 		if ( user->IsDisconnected() ) {
    167 			continue;
    168 		}
    169 
    170 		if ( IsSessionUserIndexLocal( i ) ) {
    171 			migrationInfo.ourPingMs = user->pingMs;
    172 			migrationInfo.ourUserId = user->lobbyUserID;
    173 			migrationInfo.persistUntilGameEndsData.ourGameData = user->migrationGameData;
    174 			NET_VERBOSE_PRINT( "^2NET: Migration game data for local user is index %d \n", user->migrationGameData );
    175 
    176 			continue;		// Only interested in remote users
    177 		}
    178 
    179 		if ( !inviteOldHost && user->peerIndex == -1 ) {
    180 			continue;		// Don't invite old host if told not to do so
    181 		}
    182 
    183 		if ( FindMigrationInviteIndex( user->address ) == -1 ) {
    184 			migrationInvite_t invite;
    185 			invite.address			= user->address;
    186 			invite.pingMs			= user->pingMs;
    187 			invite.userId			= user->lobbyUserID;
    188 			invite.migrationGameData = user->migrationGameData;
    189 			invite.lastInviteTime	= 0;
    190 
    191 			NET_VERBOSE_PRINT( "^2NET: Migration game data for user %s is index %d \n", user->gamertag, user->migrationGameData );
    192 
    193 			migrationInfo.invites.Append( invite );
    194 		}
    195 	}
    196 }
    197 
    198 /*
    199 ========================
    200 idLobby::PickNewHost
    201 ========================
    202 */
    203 void idLobby::PickNewHost( bool forceMe, bool inviteOldHost ) {
    204 	if ( IsHost() ) {
    205 		idLib::Printf( "PickNewHost: Already host of session %s\n", GetLobbyName() );
    206 		return;
    207 	}
    208 
    209 	sessionCB->PrePickNewHost( *this, forceMe, inviteOldHost );
    210 }
    211 
    212 /*
    213 ========================
    214 idLobby::PickNewHostInternal
    215 ========================
    216 */
    217 void idLobby::PickNewHostInternal( bool forceMe, bool inviteOldHost ) {
    218 
    219 	if ( migrationInfo.state == MIGRATE_PICKING_HOST ) {
    220 		return;		// Already picking new host
    221 	}
    222 
    223 	idLib::Printf( "PickNewHost: Started picking new host %s.\n", GetLobbyName() );
    224 
    225 	if ( IsHost() ) {
    226 		idLib::Printf( "PickNewHost: Already host of session %s\n", GetLobbyName() );
    227 		return;
    228 	}
    229 
    230 	// Find the user with the lowest ping
    231 	int bestUserIndex			= -1;
    232 	int bestPingMs				= 0;
    233 	lobbyUserID_t bestUserId;
    234 
    235 	for ( int i = 0; i < GetNumLobbyUsers(); i++ ) {
    236 		lobbyUser_t * user = GetLobbyUser( i );
    237 
    238 		if ( !verify( user != NULL ) ) {
    239 			continue;
    240 		}
    241 
    242 		if ( user->IsDisconnected() ) {
    243 			continue;
    244 		}
    245 
    246 		if ( user->peerIndex == -1 ) {
    247 			continue;		// Don't try and pick old host
    248 		}
    249 
    250 		if ( bestUserIndex == -1 || IsBetterHost( user->pingMs, user->lobbyUserID, bestPingMs, bestUserId ) ) {
    251 			bestUserIndex	= i;
    252 			bestPingMs		= user->pingMs;
    253 			bestUserId		= user->lobbyUserID;
    254 		}
    255 
    256 		if ( user->peerIndex == net_migration_forcePeerAsHost.GetInteger() ) {
    257 			bestUserIndex	= i;
    258 			bestPingMs		= user->pingMs;
    259 			bestUserId		= user->lobbyUserID;
    260 			break;
    261 		}
    262 	}
    263 
    264 	// Remember when we first started picking a new host
    265 	migrationInfo.state						= MIGRATE_PICKING_HOST;
    266 	migrationInfo.migrationStartTime		= Sys_Milliseconds();	
    267 
    268 	migrationInfo.persistUntilGameEndsData.wasMigratedGame = sessionCB->GetState() == idSession::INGAME;
    269 
    270 	if ( bestUserIndex == -1 ) {	// This can happen if we call PickNewHost on an lobby that was Shutdown
    271 		NET_VERBOSE_PRINT( "MIGRATION: PickNewHost was called on an lobby that was Shutdown\n" );
    272 		BecomeHost();
    273 		return;
    274 	}
    275 
    276 	NET_VERBOSE_PRINT( "MIGRATION: Chose user index %d (%s) for new host\n", bestUserIndex, GetLobbyUser( bestUserIndex )->gamertag );
    277 
    278 	bool bestWasLocal = IsSessionUserIndexLocal( bestUserIndex );		// Check before shutting down the lobby
    279 	migrateMsgFlags = parms.matchFlags;						// Save off match parms 
    280 
    281 	// Build invite list
    282 	BuildMigrationInviteList( inviteOldHost );
    283 
    284 	// If the best user is on this machine, then we become the host now, otherwise, wait for a new host to contact us
    285 	if ( forceMe || bestWasLocal ) {
    286 		BecomeHost();
    287 	}
    288 }
    289 
    290 /*
    291 ========================
    292 idLobby::BecomeHost
    293 ========================
    294 */
    295 void idLobby::BecomeHost() {
    296 
    297 	if ( !verify( migrationInfo.state == MIGRATE_PICKING_HOST ) ) {
    298 		idLib::Printf( "BecomeHost: Must be called from PickNewHost.\n" );
    299 		EndMigration();
    300 		return;
    301 	}
    302 
    303 	if ( IsHost() ) {
    304 		idLib::Printf( "BecomeHost: Already host of session.\n" );
    305 		EndMigration();
    306 		return;
    307 	}
    308 
    309 	if ( !sessionCB->BecomingHost( *this ) ) {
    310 		EndMigration();
    311 		return;
    312 	}
    313 
    314 	idLib::Printf( "BecomeHost: Sending %i invites on %s.\n", migrationInfo.invites.Num(), GetLobbyName() );
    315 
    316 	migrationInfo.state					= MIGRATE_BECOMING_HOST;
    317 	migrationInfo.migrationStartTime	= Sys_Milliseconds();
    318 
    319 	if ( lobbyBackend == NULL ) {
    320 		// If we don't have a lobbyBackend, then just create one
    321 		Shutdown();
    322 		StartCreating();
    323 		return;
    324 	}
    325 
    326 	// Shutdown the current lobby, but keep the lobbyBackend (we'll migrate it)
    327 	Shutdown( true );
    328 
    329 	// Migrate the lobbyBackend to host
    330 	lobbyBackend->BecomeHost( migrationInfo.invites.Num() );
    331 
    332 	// Wait for it to complete
    333 	SetState( STATE_CREATE_LOBBY_BACKEND );
    334 }
    335 
    336 /*
    337 ========================
    338 idLobby::EndMigration
    339 This gets called when we are done migrating, and invites will no longer be sent out.
    340 ========================
    341 */
    342 void idLobby::EndMigration() {
    343 	if ( migrationInfo.state == MIGRATE_NONE ) {
    344 		idLib::Printf( "idSessionLocal::EndMigration: Not migrating.\n" );
    345 		return;
    346 	}
    347 
    348 	sessionCB->MigrationEnded( *this );
    349 	
    350 	if ( lobbyBackend != NULL ) {
    351 		lobbyBackend->FinishBecomeHost();
    352 	}
    353 
    354 	migrationInfo.state = MIGRATE_NONE;
    355 	migrationInfo.invites.Clear();
    356 }
    357 
    358 /*
    359 ========================
    360 idLobby::ResetAllMigrationState
    361 This will reset all state related to host migration. Should be called
    362 at match end so our next game is not treated as a migrated game
    363 ========================
    364 */
    365 void idLobby::ResetAllMigrationState() {
    366 	migrationInfo.state = MIGRATE_NONE;
    367 	migrationInfo.invites.Clear();
    368 	migrationInfo.persistUntilGameEndsData.Clear();
    369 
    370 	migrateMsgFlags		= 0;
    371 
    372 	common->Dialog().ClearDialog( GDM_MIGRATING );
    373 	common->Dialog().ClearDialog( GDM_MIGRATING_WAITING );
    374 	common->Dialog().ClearDialog( GDM_MIGRATING_RELAUNCHING );
    375 }
    376 
    377 /*
    378 ========================
    379 idLobby::GetMigrationGameData
    380 This will setup the passed in idBitMsg to either read or write from the global migration game data buffer
    381 ========================
    382 */
    383 bool idLobby::GetMigrationGameData( idBitMsg &msg, bool reading ) {
    384 	if ( reading ) {
    385 		if ( !IsMigratedStatsGame() || !migrationInfo.persistUntilGameEndsData.wasMigratedHost ) {
    386 			// This was not a migrated session, we have no migration data
    387 			return false;
    388 		}
    389 		msg.InitRead( migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
    390 	} else {
    391 		migrationInfo.persistUntilGameEndsData.hasGameData = true;
    392 		memset( migrationInfo.persistUntilGameEndsData.gameData, 0, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
    393 		msg.InitWrite( migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
    394 	}
    395 
    396 	return true;
    397 }
    398 
    399 /*
    400 ========================
    401 idLobby::GetMigrationGameDataUser
    402 This will setup the passed in idBitMsg to either read or write from the user's migration game data buffer
    403 ========================
    404 */
    405 bool idLobby::GetMigrationGameDataUser( lobbyUserID_t lobbyUserID, idBitMsg & msg, bool reading ) {
    406 	const int userNum = GetLobbyUserIndexByID( lobbyUserID );
    407 
    408 	if ( !verify( userNum >=0 && userNum < MAX_PLAYERS ) ) {
    409 		return false;
    410 	}
    411 
    412 	lobbyUser_t * u = GetLobbyUser( userNum );
    413 	if ( u != NULL ) {
    414 		if ( reading ) {
    415 
    416 			if ( !IsMigratedStatsGame() || !migrationInfo.persistUntilGameEndsData.wasMigratedHost ) {
    417 				// This was not a migrated session, we have no migration data
    418 				return false;
    419 			}
    420 
    421 			if ( u->migrationGameData >= 0 && u->migrationGameData < MAX_PLAYERS ) {
    422 				msg.InitRead( migrationInfo.persistUntilGameEndsData.gameDataUser[ u->migrationGameData ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[ 0 ] ) );
    423 			} else {
    424 				// We don't have migration data for this user
    425 				idLib::Warning( "No migration data for user %d in a migrated game (%d)", userNum, u->migrationGameData );
    426 				return false;
    427 			}
    428 		} else {
    429 			// Writing
    430 			migrationInfo.persistUntilGameEndsData.hasGameData = true;
    431 			u->migrationGameData = userNum;
    432 			memset( migrationInfo.persistUntilGameEndsData.gameDataUser[ userNum ], 0, sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[0] ) );
    433 			msg.InitWrite( migrationInfo.persistUntilGameEndsData.gameDataUser[ userNum ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[0] ) );
    434 			
    435 		}
    436 		return true;
    437 	}
    438 	return false;
    439 }
    440 
    441 /*
    442 ========================
    443 idLobby::HandleMigrationGameData
    444 ========================
    445 */
    446 void idLobby::HandleMigrationGameData( idBitMsg & msg ) {
    447 	// 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
    448 	// 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)
    449 	migrationInfo.persistUntilGameEndsData.hasGameData = true;
    450 
    451 	// 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
    452 	for ( int i=0; i < GetNumLobbyUsers(); i++ ) {
    453 		lobbyUser_t * u = GetLobbyUser( i );
    454 		if ( u != NULL ) {
    455 			u->migrationGameData = -1;
    456 		}
    457 	}
    458 	
    459 	msg.ReadData( migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
    460 	int numUsers = msg.ReadByte();
    461 	int dataIndex=0;
    462 	for ( int i=0; i < numUsers; i++ ) {
    463 		lobbyUserID_t lobbyUserID;
    464 		lobbyUserID.ReadFromMsg( msg );
    465 		lobbyUser_t * user = GetLobbyUser( GetLobbyUserIndexByID( lobbyUserID ) );
    466 		if ( user != NULL ) {
    467 
    468 			NET_VERBOSE_PRINT( "NET:    Got migration data[%d] for user %s\n", dataIndex, user->gamertag );
    469 
    470 			user->migrationGameData = dataIndex;
    471 			msg.ReadData( migrationInfo.persistUntilGameEndsData.gameDataUser[ dataIndex ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[ dataIndex ] ) );
    472 			dataIndex++;			
    473 		}
    474 	}
    475 }
    476 
    477 /*
    478 ========================
    479 idLobby::SendMigrationGameData
    480 ========================
    481 */
    482 void idLobby::SendMigrationGameData() {
    483 	if ( net_migration_disable.GetBool() ) {
    484 		return;
    485 	}
    486 
    487 	if ( sessionCB->GetState() != idSession::INGAME ) {
    488 		return;
    489 	}
    490 
    491 	if ( !migrationInfo.persistUntilGameEndsData.hasGameData ) {
    492 		// Haven't been given any migration game data yet
    493 		return;
    494 	}
    495 
    496 	const int now = Sys_Milliseconds();
    497 	if ( nextSendMigrationGameTime > now ) {
    498 		return;
    499 	}
    500 
    501 	byte	packetData[ idPacketProcessor::MAX_MSG_SIZE ];
    502 	idBitMsg msg( packetData, sizeof(packetData) );
    503 
    504 	// Write global data
    505 	msg.WriteData( &migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
    506 	msg.WriteByte( GetNumLobbyUsers() );
    507 
    508 	// Write user data
    509 	for ( int userIndex = 0; userIndex < GetNumLobbyUsers(); ++userIndex ) {
    510 		lobbyUser_t * u = GetLobbyUser( userIndex );
    511 		if ( u->IsDisconnected() || u->migrationGameData < 0 ) {
    512 			continue;
    513 		}
    514 
    515 		u->lobbyUserID.WriteToMsg( msg );
    516 		msg.WriteData( migrationInfo.persistUntilGameEndsData.gameDataUser[ u->migrationGameData ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[ u->migrationGameData ] ) );
    517 	}
    518 
    519 	// Send to 1 peer
    520 	for ( int i=0; i < peers.Num(); i++ ) {
    521 		int peerToSend = ( nextSendMigrationGamePeer + i ) % peers.Num();
    522 
    523 		if ( peers[ peerToSend ].IsConnected() && peers[ peerToSend ].loaded ) {
    524 			if ( peers[ peerToSend ].packetProc->NumQueuedReliables() > idPacketProcessor::MAX_RELIABLE_QUEUE / 2 ) {
    525 				// This is kind of a hack for development so we don't DC clients by sending them too many reliable migration messages
    526 				// when they aren't responding. Doesn't seem like a horrible thing to have in a shipping product but is not necessary.
    527 				NET_VERBOSE_PRINT("NET: Skipping reliable game migration data msg because client reliable queue is > half full\n");
    528 
    529 			} else {
    530 				if ( net_migration_debug.GetBool() ) {
    531 					idLib::Printf( "NET: Sending migration game data to peer %d. size: %d\n", peerToSend, msg.GetSize() );
    532 				}
    533 				QueueReliableMessage( peerToSend, RELIABLE_MIGRATION_GAME_DATA, msg.GetReadData(), msg.GetSize() );
    534 			}
    535 			break;
    536 		}
    537 	}
    538 	
    539 	// Increment next send time / next send peer
    540 	nextSendMigrationGamePeer++;
    541 	if ( nextSendMigrationGamePeer >= peers.Num() ) {
    542 		nextSendMigrationGamePeer = 0;
    543 	}
    544 
    545 	nextSendMigrationGameTime = now + MIGRATION_GAME_DATA_INTERVAL_MS;
    546 }