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 }