/* =========================================================================== 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 . 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. =========================================================================== */ #include "../idlib/precompiled.h" #pragma hdrstop #include "Game_local.h" #include "..\framework\Common_local.h" static const int SNAP_GAMESTATE = 0; static const int SNAP_SHADERPARMS = 1; static const int SNAP_PORTALS = 2; static const int SNAP_PLAYERSTATE = SNAP_PORTALS + 1; static const int SNAP_PLAYERSTATE_END = SNAP_PLAYERSTATE + MAX_PLAYERS; static const int SNAP_ENTITIES = SNAP_PLAYERSTATE_END; static const int SNAP_ENTITIES_END = SNAP_ENTITIES + MAX_GENTITIES; static const int SNAP_LAST_CLIENT_FRAME = SNAP_ENTITIES_END; static const int SNAP_LAST_CLIENT_FRAME_END = SNAP_LAST_CLIENT_FRAME + MAX_PLAYERS; /* =============================================================================== Client running game code: - entity events don't work and should not be issued - entities should never be spawned outside idGameLocal::ClientReadSnapshot =============================================================================== */ idCVar net_clientSmoothing( "net_clientSmoothing", "0.8", CVAR_GAME | CVAR_FLOAT, "smooth other clients angles and position.", 0.0f, 0.95f ); idCVar net_clientSelfSmoothing( "net_clientSelfSmoothing", "0.6", CVAR_GAME | CVAR_FLOAT, "smooth self position if network causes prediction error.", 0.0f, 0.95f ); extern idCVar net_clientMaxPrediction; idCVar cg_predictedSpawn_debug( "cg_predictedSpawn_debug", "0", CVAR_BOOL, "Debug predictive spawning of presentables" ); idCVar g_clientFire_checkLineOfSightDebug( "g_clientFire_checkLineOfSightDebug", "0", CVAR_BOOL, "" ); /* ================ idGameLocal::InitAsyncNetwork ================ */ void idGameLocal::InitAsyncNetwork() { eventQueue.Init(); savedEventQueue.Init(); entityDefBits = -( idMath::BitsForInteger( declManager->GetNumDecls( DECL_ENTITYDEF ) ) + 1 ); realClientTime = 0; fast.Set( 0, 0, 0 ); slow.Set( 0, 0, 0 ); isNewFrame = true; clientSmoothing = net_clientSmoothing.GetFloat(); lastCmdRunTimeOnClient.Zero(); lastCmdRunTimeOnServer.Zero(); usercmdLastClientMilliseconds.Zero(); } /* ================ idGameLocal::ShutdownAsyncNetwork ================ */ void idGameLocal::ShutdownAsyncNetwork() { eventQueue.Shutdown(); savedEventQueue.Shutdown(); } /* ================ idGameLocal::ServerRemapDecl ================ */ int idGameLocal::ServerRemapDecl( int clientNum, declType_t type, int index ) { return index; } /* ================ idGameLocal::ClientRemapDecl ================ */ int idGameLocal::ClientRemapDecl( declType_t type, int index ) { return index; } /* ================ idGameLocal::SyncPlayersWithLobbyUsers ================ */ void idGameLocal::SyncPlayersWithLobbyUsers( bool initial ) { idLobbyBase & lobby = session->GetActingGameStateLobbyBase(); if ( !lobby.IsHost() ) { return; } idStaticList< lobbyUserID_t, MAX_CLIENTS > newLobbyUsers; // First, loop over lobby users, and see if we find a lobby user that we haven't registered for ( int i = 0; i < lobby.GetNumLobbyUsers(); i++ ) { lobbyUserID_t lobbyUserID1 = lobby.GetLobbyUserIdByOrdinal( i ); if ( !lobbyUserID1.IsValid() ) { continue; } if ( !initial && !lobby.IsLobbyUserLoaded( lobbyUserID1 ) ) { continue; } // Now, see if we find this lobby user in our list bool found = false; for ( int j = 0; j < MAX_PLAYERS; j++ ) { idPlayer * player = static_cast( entities[ j ] ); if ( player == NULL ) { continue; } lobbyUserID_t lobbyUserID2 = lobbyUserIDs[j]; if ( lobbyUserID1 == lobbyUserID2 ) { found = true; break; } } if ( !found ) { // If we didn't find it, we need to create a player and assign it to this new lobby user newLobbyUsers.Append( lobbyUserID1 ); } } // Validate connected players for ( int i = 0; i < MAX_PLAYERS; i++ ) { idPlayer * player = static_cast( entities[ i ] ); if ( player == NULL ) { continue; } lobbyUserID_t lobbyUserID = lobbyUserIDs[i]; if ( !lobby.IsLobbyUserValid( lobbyUserID ) ) { delete entities[ i ]; mpGame.DisconnectClient( i ); lobbyUserIDs[i] = lobbyUserID_t(); continue; } lobby.EnableSnapshotsForLobbyUser( lobbyUserID ); } while ( newLobbyUsers.Num() > 0 ) { // Find a free player data slot to use for this new player int freePlayerDataIndex = -1; for ( int i = 0; i < MAX_PLAYERS; ++i ) { idPlayer * player = static_cast( entities[ i ] ); if ( player == NULL ) { freePlayerDataIndex = i; break; } } if ( freePlayerDataIndex == -1 ) { break; // No player data slots (this shouldn't happen) } lobbyUserID_t lobbyUserID = newLobbyUsers[0]; newLobbyUsers.RemoveIndex( 0 ); mpGame.ServerClientConnect( freePlayerDataIndex ); Printf( "client %d connected.\n", freePlayerDataIndex ); lobbyUserIDs[ freePlayerDataIndex ] = lobbyUserID; // Clear this player's old usercmds. common->ResetPlayerInput( freePlayerDataIndex ); common->UpdateLevelLoadPacifier(); // spawn the player SpawnPlayer( freePlayerDataIndex ); common->UpdateLevelLoadPacifier(); ServerWriteInitialReliableMessages( freePlayerDataIndex, lobbyUserID ); } } /* ================ idGameLocal::ServerSendNetworkSyncCvars ================ */ void idGameLocal::ServerSendNetworkSyncCvars() { if ( ( cvarSystem->GetModifiedFlags() & CVAR_NETWORKSYNC ) == 0 ) { return; } cvarSystem->ClearModifiedFlags( CVAR_NETWORKSYNC ); idBitMsg outMsg; byte msgBuf[MAX_GAME_MESSAGE_SIZE]; idLobbyBase & lobby = session->GetActingGameStateLobbyBase(); outMsg.InitWrite( msgBuf, sizeof( msgBuf ) ); outMsg.BeginWriting(); idDict syncedCvars; cvarSystem->MoveCVarsToDict( CVAR_NETWORKSYNC, syncedCvars, true ); outMsg.WriteDeltaDict( syncedCvars, NULL ); lobby.SendReliable( GAME_RELIABLE_MESSAGE_SYNCEDCVARS, outMsg, false ); idLib::Printf( "Sending networkSync cvars:\n" ); syncedCvars.Print(); } /* ================ idGameLocal::ServerWriteInitialReliableMessages Send reliable messages to initialize the client game up to a certain initial state. ================ */ void idGameLocal::ServerWriteInitialReliableMessages( int clientNum, lobbyUserID_t lobbyUserID ) { if ( clientNum == GetLocalClientNum() ) { // We don't need to send messages to ourself return; } idBitMsg outMsg; byte msgBuf[MAX_GAME_MESSAGE_SIZE]; idLobbyBase & lobby = session->GetActingGameStateLobbyBase(); outMsg.InitWrite( msgBuf, sizeof( msgBuf ) ); outMsg.BeginWriting(); idDict syncedCvars; cvarSystem->MoveCVarsToDict( CVAR_NETWORKSYNC, syncedCvars, true ); outMsg.WriteDeltaDict( syncedCvars, NULL ); lobby.SendReliableToLobbyUser( lobbyUserID, GAME_RELIABLE_MESSAGE_SYNCEDCVARS, outMsg ); idLib::Printf( "Sending initial networkSync cvars:\n" ); syncedCvars.Print(); // send all saved events for ( entityNetEvent_t * event = savedEventQueue.Start(); event; event = event->next ) { outMsg.InitWrite( msgBuf, sizeof( msgBuf ) ); outMsg.BeginWriting(); outMsg.WriteBits( event->spawnId, 32 ); outMsg.WriteByte( event->event ); outMsg.WriteLong( event->time ); outMsg.WriteBits( event->paramsSize, idMath::BitsForInteger( MAX_EVENT_PARAM_SIZE ) ); if ( event->paramsSize ) { outMsg.WriteData( event->paramsBuf, event->paramsSize ); } lobby.SendReliableToLobbyUser( lobbyUserID, GAME_RELIABLE_MESSAGE_EVENT, outMsg ); } mpGame.ServerWriteInitialReliableMessages( clientNum, lobbyUserID ); } /* ================ idGameLocal::SaveEntityNetworkEvent ================ */ void idGameLocal::SaveEntityNetworkEvent( const idEntity *ent, int eventId, const idBitMsg *msg ) { entityNetEvent_t * event = savedEventQueue.Alloc(); event->spawnId = GetSpawnId( ent ); event->event = eventId; event->time = time; if ( msg ) { event->paramsSize = msg->GetSize(); memcpy( event->paramsBuf, msg->GetReadData(), msg->GetSize() ); } else { event->paramsSize = 0; } savedEventQueue.Enqueue( event, idEventQueue::OUTOFORDER_IGNORE ); } /* ================ idGameLocal::ServerWriteSnapshot Write a snapshot of the current game state ================ */ void idGameLocal::ServerWriteSnapshot( idSnapShot & ss ) { ss.SetTime( fast.time ); byte buffer[ MAX_ENTITY_STATE_SIZE ]; idBitMsg msg; // First write the generic game state to the snapshot msg.InitWrite( buffer, sizeof( buffer ) ); mpGame.WriteToSnapshot( msg ); ss.S_AddObject( SNAP_GAMESTATE, ~0U, msg, "Game State" ); // Update global shader parameters msg.InitWrite( buffer, sizeof( buffer ) ); for ( int i = 0; i < MAX_GLOBAL_SHADER_PARMS; i++ ) { msg.WriteFloat( globalShaderParms[i] ); } ss.S_AddObject( SNAP_SHADERPARMS, ~0U, msg, "Shader Parms" ); // update portals for opened doors msg.InitWrite( buffer, sizeof( buffer ) ); int numPortals = gameRenderWorld->NumPortals(); msg.WriteLong( numPortals ); for ( int i = 0; i < numPortals; i++ ) { msg.WriteBits( gameRenderWorld->GetPortalState( (qhandle_t) (i+1) ) , NUM_RENDER_PORTAL_BITS ); } ss.S_AddObject( SNAP_PORTALS, ~0U, msg, "Portal State" ); idEntity * skyEnt = portalSkyEnt.GetEntity(); pvsHandle_t portalSkyPVS; portalSkyPVS.i = -1; if ( skyEnt != NULL ) { portalSkyPVS = pvs.SetupCurrentPVS( skyEnt->GetPVSAreas(), skyEnt->GetNumPVSAreas() ); } // Build PVS data for each player and write their player state to the snapshot as well pvsHandle_t pvsHandles[ MAX_PLAYERS ]; for ( int i = 0; i < MAX_PLAYERS; i++ ) { idPlayer * player = static_cast( entities[ i ] ); if ( player == NULL ) { pvsHandles[i].i = -1; continue; } idPlayer * spectated = player; if ( player->spectating && player->spectator != i && entities[ player->spectator ] ) { spectated = static_cast< idPlayer * >( entities[ player->spectator ] ); } msg.InitWrite( buffer, sizeof( buffer ) ); spectated->WritePlayerStateToSnapshot( msg ); ss.S_AddObject( SNAP_PLAYERSTATE + i, ~0U, msg, "Player State" ); int sourceAreas[ idEntity::MAX_PVS_AREAS ]; int numSourceAreas = gameRenderWorld->BoundsInAreas( spectated->GetPlayerPhysics()->GetAbsBounds(), sourceAreas, idEntity::MAX_PVS_AREAS ); pvsHandles[i] = pvs.SetupCurrentPVS( sourceAreas, numSourceAreas, PVS_NORMAL ); if ( portalSkyPVS.i >= 0 ) { pvsHandle_t tempPVS = pvs.MergeCurrentPVS( pvsHandles[i], portalSkyPVS ); pvs.FreeCurrentPVS( pvsHandles[i] ); pvsHandles[i] = tempPVS; } // Write the last usercmd processed by the server so that clients know // when to stop predicting. msg.BeginWriting(); msg.WriteLong( usercmdLastClientMilliseconds[i] ); ss.S_AddObject( SNAP_LAST_CLIENT_FRAME + i, ~0U, msg, "Last client frame" ); } if ( portalSkyPVS.i >= 0 ) { pvs.FreeCurrentPVS( portalSkyPVS ); } // Add all entities to the snapshot for ( idEntity * ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { if ( ent->GetSkipReplication() ) { continue; } msg.InitWrite( buffer, sizeof( buffer ) ); msg.WriteBits( spawnIds[ ent->entityNumber ], 32 - GENTITYNUM_BITS ); msg.WriteBits( ent->GetType()->typeNum, idClass::GetTypeNumBits() ); msg.WriteBits( ServerRemapDecl( -1, DECL_ENTITYDEF, ent->entityDefNumber ), entityDefBits ); msg.WriteBits( ent->GetPredictedKey(), 32 ); if ( ent->fl.networkSync ) { // write the class specific data to the snapshot ent->WriteToSnapshot( msg ); } ss.S_AddObject( SNAP_ENTITIES + ent->entityNumber, ~0U, msg, ent->GetName() ); } // Free PVS handles for all the players for ( int i = 0; i < MAX_PLAYERS; i++ ) { if ( pvsHandles[i].i < 0 ) { continue; } pvs.FreeCurrentPVS( pvsHandles[i] ); } } /* ================ idGameLocal::NetworkEventWarning ================ */ void idGameLocal::NetworkEventWarning( const entityNetEvent_t *event, const char *fmt, ... ) { char buf[1024]; int length = 0; va_list argptr; int entityNum = event->spawnId & ( ( 1 << GENTITYNUM_BITS ) - 1 ); int id = event->spawnId >> GENTITYNUM_BITS; length += idStr::snPrintf( buf+length, sizeof(buf)-1-length, "event %d for entity %d %d: ", event->event, entityNum, id ); va_start( argptr, fmt ); length = idStr::vsnPrintf( buf+length, sizeof(buf)-1-length, fmt, argptr ); va_end( argptr ); idStr::Append( buf, sizeof(buf), "\n" ); common->DWarning( buf ); } /* ================ idGameLocal::ServerProcessEntityNetworkEventQueue ================ */ void idGameLocal::ServerProcessEntityNetworkEventQueue() { while ( eventQueue.Start() ) { entityNetEvent_t * event = eventQueue.Start(); if ( event->time > time ) { break; } idEntityPtr< idEntity > entPtr; if( !entPtr.SetSpawnId( event->spawnId ) ) { NetworkEventWarning( event, "Entity does not exist any longer, or has not been spawned yet." ); } else { idEntity * ent = entPtr.GetEntity(); assert( ent ); idBitMsg eventMsg; eventMsg.InitRead( event->paramsBuf, sizeof( event->paramsBuf ) ); eventMsg.SetSize( event->paramsSize ); eventMsg.BeginReading(); if ( !ent->ServerReceiveEvent( event->event, event->time, eventMsg ) ) { NetworkEventWarning( event, "unknown event" ); } } entityNetEvent_t* freedEvent = eventQueue.Dequeue(); verify( freedEvent == event ); eventQueue.Free( event ); } } /* ================ idGameLocal::ProcessReliableMessage ================ */ void idGameLocal::ProcessReliableMessage( int clientNum, int type, const idBitMsg &msg ) { if ( session->GetActingGameStateLobbyBase().IsPeer() ) { ClientProcessReliableMessage( type, msg ); } else { ServerProcessReliableMessage( clientNum, type, msg ); } } /* ================ idGameLocal::ServerProcessReliableMessage ================ */ void idGameLocal::ServerProcessReliableMessage( int clientNum, int type, const idBitMsg &msg ) { if ( clientNum < 0 ) { return; } switch( type ) { case GAME_RELIABLE_MESSAGE_CHAT: case GAME_RELIABLE_MESSAGE_TCHAT: { char name[128]; char text[128]; msg.ReadString( name, sizeof( name ) ); msg.ReadString( text, sizeof( text ) ); mpGame.ProcessChatMessage( clientNum, type == GAME_RELIABLE_MESSAGE_TCHAT, name, text, NULL ); break; } case GAME_RELIABLE_MESSAGE_VCHAT: { int index = msg.ReadLong(); bool team = msg.ReadBits( 1 ) != 0; mpGame.ProcessVoiceChat( clientNum, team, index ); break; } case GAME_RELIABLE_MESSAGE_DROPWEAPON: { mpGame.DropWeapon( clientNum ); break; } case GAME_RELIABLE_MESSAGE_EVENT: { // allocate new event entityNetEvent_t * event = eventQueue.Alloc(); eventQueue.Enqueue( event, idEventQueue::OUTOFORDER_DROP ); event->spawnId = msg.ReadBits( 32 ); event->event = msg.ReadByte(); event->time = msg.ReadLong(); event->paramsSize = msg.ReadBits( idMath::BitsForInteger( MAX_EVENT_PARAM_SIZE ) ); if ( event->paramsSize ) { if ( event->paramsSize > MAX_EVENT_PARAM_SIZE ) { NetworkEventWarning( event, "invalid param size" ); return; } msg.ReadByteAlign(); msg.ReadData( event->paramsBuf, event->paramsSize ); } break; } case GAME_RELIABLE_MESSAGE_SPECTATE: { bool spec = msg.ReadBool(); idPlayer * player = GetClientByNum( clientNum ); if ( serverInfo.GetBool( "si_spectators" ) ) { // never let spectators go back to game while sudden death is on if ( mpGame.GetGameState() == idMultiplayerGame::SUDDENDEATH && !spec && player->wantSpectate ) { // Don't allow the change } else { if ( player->wantSpectate && !spec ) { player->forceRespawn = true; } player->wantSpectate = spec; } } else { // If the server turned off si_spectators while a player is spectating, then any spectate message forces the player out of spectate mode if ( player->wantSpectate ) { player->forceRespawn = true; } player->wantSpectate = false; } break; } case GAME_RELIABLE_MESSAGE_CLIENT_HITSCAN_HIT: { const int attackerNum = msg.ReadShort(); const int victimNum = msg.ReadShort(); idVec3 dir; msg.ReadVectorFloat( dir ); const int damageDefIndex = msg.ReadLong(); const float damageScale = msg.ReadFloat(); const int location = msg.ReadLong(); if ( gameLocal.entities[victimNum] == NULL ) { break; } if ( gameLocal.entities[attackerNum] == NULL ) { break; } idPlayer & victim = static_cast< idPlayer & >( *gameLocal.entities[victimNum] ); idPlayer & attacker = static_cast< idPlayer & >( *gameLocal.entities[attackerNum] ); if ( victim.GetPhysics() == NULL ) { break; } if ( attacker.weapon.GetEntity() == NULL ) { break; } if ( location == INVALID_JOINT ) { break; } // Line of sight check. As a basic precaution against cheating, // the server performs a ray intersection from the client's position // to the joint he hit on the target. idVec3 muzzleOrigin; idMat3 muzzleAxis; attacker.weapon.GetEntity()->GetProjectileLaunchOriginAndAxis( muzzleOrigin, muzzleAxis ); idVec3 targetLocation = victim.GetRenderEntity()->origin + victim.GetRenderEntity()->joints[location].ToVec3() * victim.GetRenderEntity()->axis; trace_t tr; gameLocal.clip.Translation( tr, muzzleOrigin, targetLocation, NULL, mat3_identity, MASK_SHOT_RENDERMODEL, &attacker ); idEntity * hitEnt = gameLocal.entities[ tr.c.entityNum ]; if ( hitEnt != &victim ) { break; } const idDeclEntityDef *damageDef = static_cast( declManager->DeclByIndex( DECL_ENTITYDEF, damageDefIndex, false ) ); if ( damageDef != NULL ) { victim.Damage( NULL, gameLocal.entities[attackerNum], dir, damageDef->GetName(), damageScale, location ); } break; } default: { Warning( "Unknown reliable message (%d) from client %d", type, clientNum ); break; } } } /* ================ idGameLocal::ClientReadSnapshot ================ */ void idGameLocal::ClientReadSnapshot( const idSnapShot & ss ) { if ( GetLocalClientNum() < 0 ) { return; } // if prediction is off, enable local client smoothing //localPlayer->SetSelfSmooth( dupeUsercmds > 2 ); // clear any debug lines from a previous frame gameRenderWorld->DebugClearLines( time ); // clear any debug polygons from a previous frame gameRenderWorld->DebugClearPolygons( time ); SelectTimeGroup( false ); // so that StartSound/StopSound doesn't risk skipping isNewFrame = true; // clear the snapshot entity list snapshotEntities.Clear(); // read all entities from the snapshot for ( int o = 0; o < ss.NumObjects(); o++ ) { idBitMsg msg; int snapObjectNum = ss.GetObjectMsgByIndex( o, msg ); if ( snapObjectNum < 0 ) { assert( false ); continue; } if ( snapObjectNum == SNAP_GAMESTATE ) { mpGame.ReadFromSnapshot( msg ); continue; } if ( snapObjectNum == SNAP_SHADERPARMS ) { for ( int i = 0; i < MAX_GLOBAL_SHADER_PARMS; i++ ) { globalShaderParms[i] = msg.ReadFloat(); } continue; } if ( snapObjectNum == SNAP_PORTALS ) { // update portals for opened doors int numPortals = msg.ReadLong(); assert( numPortals == gameRenderWorld->NumPortals() ); for ( int i = 0; i < numPortals; i++ ) { gameRenderWorld->SetPortalState( (qhandle_t) (i+1), msg.ReadBits( NUM_RENDER_PORTAL_BITS ) ); } continue; } if ( snapObjectNum >= SNAP_PLAYERSTATE && snapObjectNum < SNAP_PLAYERSTATE_END ) { int playerNumber = snapObjectNum - SNAP_PLAYERSTATE; idPlayer * otherPlayer = static_cast< idPlayer * >( entities[ playerNumber ] ); // Don't process Player Snapshots that are disconnected. const int lobbyIndex = session->GetActingGameStateLobbyBase().GetLobbyUserIndexFromLobbyUserID( lobbyUserIDs[ playerNumber ] ); if( lobbyIndex < 0 || session->GetActingGameStateLobbyBase().IsLobbyUserConnected( lobbyIndex ) == false ) { continue; } if ( otherPlayer != NULL ) { otherPlayer->ReadPlayerStateFromSnapshot( msg ); if ( otherPlayer != entities[ GetLocalClientNum() ] ) { // This happens when we spectate another player idWeapon * weap = otherPlayer->weapon.GetEntity(); if ( weap && ( weap->GetRenderEntity()->bounds[0] == weap->GetRenderEntity()->bounds[1] ) ) { // update the weapon's viewmodel bounds so that the model doesn't flicker in the spectator's view weap->GetAnimator()->GetBounds( gameLocal.time, weap->GetRenderEntity()->bounds ); weap->UpdateVisuals(); } } } continue; } if ( snapObjectNum >= SNAP_LAST_CLIENT_FRAME && snapObjectNum < SNAP_LAST_CLIENT_FRAME_END ) { int playerNumber = snapObjectNum - SNAP_LAST_CLIENT_FRAME; // Don't process Player Snapshots that are disconnected. const int lobbyIndex = session->GetActingGameStateLobbyBase().GetLobbyUserIndexFromLobbyUserID( lobbyUserIDs[ playerNumber ] ); if( lobbyIndex < 0 || session->GetActingGameStateLobbyBase().IsLobbyUserConnected( lobbyIndex ) == false ) { continue; } usercmdLastClientMilliseconds[playerNumber] = msg.ReadLong(); continue; } if ( snapObjectNum < SNAP_ENTITIES || snapObjectNum >= SNAP_ENTITIES_END ) { continue; } int entityNumber = snapObjectNum - SNAP_ENTITIES; if ( msg.GetSize() == 0 ) { delete entities[entityNumber]; continue; } bool debug = false; int spawnId = msg.ReadBits( 32 - GENTITYNUM_BITS ); int typeNum = msg.ReadBits( idClass::GetTypeNumBits() ); int entityDefNumber = ClientRemapDecl( DECL_ENTITYDEF, msg.ReadBits( entityDefBits ) ); const int predictedKey = msg.ReadBits( 32 ); idTypeInfo * typeInfo = idClass::GetType( typeNum ); if ( !typeInfo ) { idLib::Error( "Unknown type number %d for entity %d with class number %d", typeNum, entityNumber, entityDefNumber ); } // If there is no entity on this client, but the server's entity matches a predictionKey, move the client's // predicted entity to the normal, replicated area in the entity list. if ( entities[entityNumber] == NULL ) { if ( predictedKey != idEntity::INVALID_PREDICTION_KEY ) { idLib::PrintfIf( debug, "Looking for predicted key %d.\n", predictedKey ); idEntity * predictedEntity = FindPredictedEntity( predictedKey, typeInfo ); if ( predictedEntity != NULL ) { // This presentable better be in the proper place in the list or bad things will happen if we move this presentable around assert( predictedEntity->GetEntityNumber() >= ENTITYNUM_FIRST_NON_REPLICATED ); continue; #if 0 idProjectile * predictedProjectile = idProjectile::CastTo( predictedEntity ); if ( predictedProjectile != NULL ) { for ( int i = 0; i < MAX_PLAYERS; i++ ) { if ( entities[i] == NULL ) { continue; } idPlayer * player = idPlayer::CastTo( entities[i] ); if ( player != NULL ) { if ( player->GetUniqueProjectile() == predictedProjectile ) { // Set new spawn id player->TrackUniqueProjectile( predictedProjectile ); } } } } idLib::PrintfIf( debug, "Found predicted EntNum old:%i new:%i spawnID:%i\n", predictedEntity->GetEntityNumber(), entityNumber, spawnId >> GENTITYNUM_BITS ); // move the entity RemoveEntityFromHash( predictedEntity->name.c_str(), predictedEntity ); UnregisterEntity( predictedEntity ); assert( entities[predictedEntity->GetEntityNumber()] == NULL ); predictedEntity->spawnArgs.SetInt( "spawn_entnum", entityNumber ); RegisterEntity( predictedEntity, spawnId, predictedEntity->spawnArgs ); predictedEntity->SetName( "" ); // now mark us as no longer predicted predictedEntity->BecomeReplicated(); #endif } //TODO make this work with non-client preditced entities /* else { idLib::Warning( "Could not find predicted entity - key: %d. EntityIndex: %d", predictedKey, entityNum ); } */ } } idEntity * ent = entities[entityNumber]; // if there is no entity or an entity of the wrong type if ( !ent || ent->GetType()->typeNum != typeNum || ent->entityDefNumber != entityDefNumber || spawnId != spawnIds[ entityNumber ] ) { delete ent; spawnCount = spawnId; if ( entityNumber < MAX_CLIENTS ) { commonLocal.GetUCmdMgr().ResetPlayer( entityNumber ); SpawnPlayer( entityNumber ); ent = entities[ entityNumber ]; ent->FreeModelDef(); } else { idDict args; args.SetInt( "spawn_entnum", entityNumber ); args.Set( "name", va( "entity%d", entityNumber ) ); if ( entityDefNumber >= 0 ) { if ( entityDefNumber >= declManager->GetNumDecls( DECL_ENTITYDEF ) ) { Error( "server has %d entityDefs instead of %d", entityDefNumber, declManager->GetNumDecls( DECL_ENTITYDEF ) ); } const char * classname = declManager->DeclByIndex( DECL_ENTITYDEF, entityDefNumber, false )->GetName(); args.Set( "classname", classname ); if ( !SpawnEntityDef( args, &ent ) || !entities[entityNumber] || entities[entityNumber]->GetType()->typeNum != typeNum ) { Error( "Failed to spawn entity with classname '%s' of type '%s'", classname, typeInfo->classname ); } } else { ent = SpawnEntityType( *typeInfo, &args, true ); if ( !entities[entityNumber] || entities[entityNumber]->GetType()->typeNum != typeNum ) { Error( "Failed to spawn entity of type '%s'", typeInfo->classname ); } } if ( ent != NULL ) { // Fixme: for now, force all think flags on. We'll need to figure out how we want dormancy to work on clients // (but for now since clientThink is so light weight, this is ok) ent->BecomeActive( TH_ANIMATE ); ent->BecomeActive( TH_THINK ); ent->BecomeActive( TH_PHYSICS ); } if ( entityNumber < MAX_CLIENTS && entityNumber >= numClients ) { numClients = entityNumber + 1; } } } if ( ss.ObjectIsStaleByIndex( o ) ) { if ( ent->entityNumber >= MAX_CLIENTS && ent->entityNumber < mapSpawnCount && !ent->spawnArgs.GetBool("net_dynamic", "0")) { //_D3XP // server says it's not in PVS // if that happens on map entities, most likely something is wrong // I can see that moving pieces along several PVS could be a legit situation though // this is a band aid, which means something is not done right elsewhere common->DWarning( "map entity 0x%x (%s) is stale", ent->entityNumber, ent->name.c_str() ); } else { ent->snapshotStale = true; ent->FreeModelDef(); // possible fix for left over lights on CTF flag ent->FreeLightDef(); ent->UpdateVisuals(); ent->GetPhysics()->UnlinkClip(); } } else { // add the entity to the snapshot list ent->snapshotNode.AddToEnd( snapshotEntities ); int snapshotChanged = ss.ObjectChangedCountByIndex( o ); msg.SetHasChanged( ent->snapshotChanged != snapshotChanged ); ent->snapshotChanged = snapshotChanged; ent->FlagNewSnapshot(); // read the class specific data from the snapshot if ( msg.GetRemainingReadBits() > 0 ) { ent->ReadFromSnapshot_Ex( msg ); ent->snapshotBits = msg.GetSize(); } // Set after ReadFromSnapshot so we can detect coming unstale ent->snapshotStale = false; } } // process entity events ClientProcessEntityNetworkEventQueue(); } /* ================ idGameLocal::ClientProcessEntityNetworkEventQueue ================ */ void idGameLocal::ClientProcessEntityNetworkEventQueue() { while( eventQueue.Start() ) { entityNetEvent_t * event = eventQueue.Start(); // only process forward, in order if ( event->time > this->serverTime ) { break; } idEntityPtr< idEntity > entPtr; if( !entPtr.SetSpawnId( event->spawnId ) ) { if( !gameLocal.entities[ event->spawnId & ( ( 1 << GENTITYNUM_BITS ) - 1 ) ] ) { // if new entity exists in this position, silently ignore NetworkEventWarning( event, "Entity does not exist any longer, or has not been spawned yet." ); } } else { idEntity * ent = entPtr.GetEntity(); assert( ent ); idBitMsg eventMsg; eventMsg.InitRead( event->paramsBuf, sizeof( event->paramsBuf ) ); eventMsg.SetSize( event->paramsSize ); eventMsg.BeginReading(); if ( !ent->ClientReceiveEvent( event->event, event->time, eventMsg ) ) { NetworkEventWarning( event, "unknown event" ); } } verify( eventQueue.Dequeue() == event ); eventQueue.Free( event ); } } /* ================ idGameLocal::ClientProcessReliableMessage ================ */ void idGameLocal::ClientProcessReliableMessage( int type, const idBitMsg &msg ) { switch( type ) { case GAME_RELIABLE_MESSAGE_SYNCEDCVARS: { idDict syncedCvars; msg.ReadDeltaDict( syncedCvars, NULL ); idLib::Printf( "Got networkSync cvars:\n" ); syncedCvars.Print(); cvarSystem->ResetFlaggedVariables( CVAR_NETWORKSYNC ); cvarSystem->SetCVarsFromDict( syncedCvars ); break; } case GAME_RELIABLE_MESSAGE_CHAT: case GAME_RELIABLE_MESSAGE_TCHAT: { // (client should never get a TCHAT though) char name[128]; char text[128]; msg.ReadString( name, sizeof( name ) ); msg.ReadString( text, sizeof( text ) ); mpGame.AddChatLine( "%s^0: %s\n", name, text ); break; } case GAME_RELIABLE_MESSAGE_SOUND_EVENT: { snd_evt_t snd_evt = (snd_evt_t)msg.ReadByte(); mpGame.PlayGlobalSound( -1, snd_evt ); break; } case GAME_RELIABLE_MESSAGE_SOUND_INDEX: { int index = gameLocal.ClientRemapDecl( DECL_SOUND, msg.ReadLong() ); if ( index >= 0 && index < declManager->GetNumDecls( DECL_SOUND ) ) { const idSoundShader *shader = declManager->SoundByIndex( index ); mpGame.PlayGlobalSound( -1, SND_COUNT, shader->GetName() ); } break; } case GAME_RELIABLE_MESSAGE_DB: { idMultiplayerGame::msg_evt_t msg_evt = (idMultiplayerGame::msg_evt_t)msg.ReadByte(); int parm1, parm2; parm1 = msg.ReadByte( ); parm2 = msg.ReadByte( ); mpGame.PrintMessageEvent( msg_evt, parm1, parm2 ); break; } case GAME_RELIABLE_MESSAGE_EVENT: { // allocate new event entityNetEvent_t * event = eventQueue.Alloc(); eventQueue.Enqueue( event, idEventQueue::OUTOFORDER_IGNORE ); event->spawnId = msg.ReadBits( 32 ); event->event = msg.ReadByte(); event->time = msg.ReadLong(); event->paramsSize = msg.ReadBits( idMath::BitsForInteger( MAX_EVENT_PARAM_SIZE ) ); if ( event->paramsSize ) { if ( event->paramsSize > MAX_EVENT_PARAM_SIZE ) { NetworkEventWarning( event, "invalid param size" ); return; } msg.ReadByteAlign(); msg.ReadData( event->paramsBuf, event->paramsSize ); } break; } case GAME_RELIABLE_MESSAGE_RESTART: { MapRestart(); break; } case GAME_RELIABLE_MESSAGE_TOURNEYLINE: { int line = msg.ReadByte( ); idPlayer * p = static_cast< idPlayer * >( entities[ GetLocalClientNum() ] ); if ( !p ) { break; } p->tourneyLine = line; break; } case GAME_RELIABLE_MESSAGE_STARTSTATE: { mpGame.ClientReadStartState( msg ); break; } case GAME_RELIABLE_MESSAGE_WARMUPTIME: { mpGame.ClientReadWarmupTime( msg ); break; } case GAME_RELIABLE_MESSAGE_LOBBY_COUNTDOWN: { int timeRemaining = msg.ReadLong(); Shell_UpdateClientCountdown( timeRemaining ); break; } case GAME_RELIABLE_MESSAGE_RESPAWN_AVAILABLE: { idPlayer * p = static_cast< idPlayer * >( entities[ GetLocalClientNum() ] ); if ( p ) { p->ShowRespawnHudMessage(); } break; } case GAME_RELIABLE_MESSAGE_MATCH_STARTED_TIME: { mpGame.ClientReadMatchStartedTime( msg ); break; } case GAME_RELIABLE_MESSAGE_ACHIEVEMENT_UNLOCK: { mpGame.ClientReadAchievementUnlock( msg ); break; } default: { Error( "Unknown reliable message (%d) from host", type ); break; } } } /* ================ idGameLocal::ClientRunFrame ================ */ void idGameLocal::ClientRunFrame( idUserCmdMgr & cmdMgr, bool lastPredictFrame, gameReturn_t & ret ) { idEntity *ent; // update the game time previousTime = FRAME_TO_MSEC( framenum ); framenum++; time = FRAME_TO_MSEC( framenum ); idPlayer * player = static_cast( entities[GetLocalClientNum()] ); if ( !player ) { // service any pending events idEvent::ServiceEvents(); return; } // check for local client lag idLobbyBase & lobby = session->GetActingGameStateLobbyBase(); if ( lobby.GetPeerTimeSinceLastPacket( lobby.PeerIndexForHost() ) >= net_clientMaxPrediction.GetInteger() ) { player->isLagged = true; } else { player->isLagged = false; } // update the real client time and the new frame flag if ( time > realClientTime ) { realClientTime = time; isNewFrame = true; } else { isNewFrame = false; } slow.Set( time, previousTime, realClientTime ); fast.Set( time, previousTime, realClientTime ); // run prediction on all active entities for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) { ent->thinkFlags |= TH_PHYSICS; if ( ent->entityNumber != GetLocalClientNum() ) { ent->ClientThink( netInterpolationInfo.serverGameMs, netInterpolationInfo.pct, true ); } else { RunAllUserCmdsForPlayer( cmdMgr, ent->entityNumber ); } } // service any pending events idEvent::ServiceEvents(); // show any debug info for this frame if ( isNewFrame ) { RunDebugInfo(); D_DrawDebugLines(); } BuildReturnValue( ret ); } /* =============== idGameLocal::Tokenize =============== */ void idGameLocal::Tokenize( idStrList &out, const char *in ) { char buf[ MAX_STRING_CHARS ]; char *token, *next; idStr::Copynz( buf, in, MAX_STRING_CHARS ); token = buf; next = strchr( token, ';' ); while ( token ) { if ( next ) { *next = '\0'; } idStr::ToLower( token ); out.Append( token ); if ( next ) { token = next + 1; next = strchr( token, ';' ); } else { token = NULL; } } } /* ======================== idGameLocal::FindPredictedEntity ======================== */ idEntity * idGameLocal::FindPredictedEntity( uint32 predictedKey, idTypeInfo * type ) { for ( idEntity * predictedEntity = activeEntities.Next(); predictedEntity != NULL; predictedEntity = predictedEntity->activeNode.Next() ) { if ( !verify( predictedEntity != NULL ) ) { continue; } if ( !predictedEntity->IsReplicated() && predictedEntity->GetPredictedKey() == predictedKey ) { if ( predictedEntity->GetType() != type ) { idLib::Warning("Mismatched presentable type. Predicted: %s Actual: %s", predictedEntity->GetType()->classname, type->classname ); } return predictedEntity; } } return NULL; } /* ======================== idGameLocal::GeneratePredictionKey ======================== */ uint32 idGameLocal::GeneratePredictionKey( idWeapon * weapon, idPlayer * playerAttacker, int overrideKey ) { if ( overrideKey != -1 ) { uint32 predictedKey = overrideKey; int peerIndex = -1; if ( common->IsServer() ) { peerIndex = session->GetActingGameStateLobbyBase().PeerIndexFromLobbyUser( lobbyUserIDs[ playerAttacker->entityNumber ] ); } else { peerIndex = session->GetActingGameStateLobbyBase().PeerIndexOnHost(); } predictedKey |= ( peerIndex << 28 ); return predictedKey; } uint32 predictedKey = idEntity::INVALID_PREDICTION_KEY; int peerIndex = -1; // Get key - fireCount or throwCount //if ( weapon != NULL ) { if ( common->IsClient() ) { predictedKey = playerAttacker->GetClientFireCount(); } else { predictedKey = playerAttacker->usercmd.fireCount; } //} else { // predictedKey = ( playerAttacker->GetThrowCount() ); //} // Get peer index if ( common->IsServer() ) { peerIndex = session->GetActingGameStateLobbyBase().PeerIndexFromLobbyUser( lobbyUserIDs[ playerAttacker->entityNumber ] ); } else { peerIndex = session->GetActingGameStateLobbyBase().PeerIndexOnHost(); } if ( cg_predictedSpawn_debug.GetBool() ) { idLib::Printf("GeneratePredictionKey. predictedKey: %d peedIndex: %d\n", predictedKey, peerIndex ); } predictedKey |= ( peerIndex << 28 ); return predictedKey; } /* =============== idEventQueue::Alloc =============== */ entityNetEvent_t* idEventQueue::Alloc() { entityNetEvent_t* event = eventAllocator.Alloc(); event->prev = NULL; event->next = NULL; return event; } /* =============== idEventQueue::Free =============== */ void idEventQueue::Free( entityNetEvent_t *event ) { // should only be called on an unlinked event! assert( !event->next && !event->prev ); eventAllocator.Free( event ); } /* =============== idEventQueue::Shutdown =============== */ void idEventQueue::Shutdown() { eventAllocator.Shutdown(); this->Init(); } /* =============== idEventQueue::Init =============== */ void idEventQueue::Init() { start = NULL; end = NULL; } /* =============== idEventQueue::Dequeue =============== */ entityNetEvent_t* idEventQueue::Dequeue() { entityNetEvent_t* event = start; if ( !event ) { return NULL; } start = start->next; if ( !start ) { end = NULL; } else { start->prev = NULL; } event->next = NULL; event->prev = NULL; return event; } /* =============== idEventQueue::RemoveLast =============== */ entityNetEvent_t* idEventQueue::RemoveLast() { entityNetEvent_t *event = end; if ( !event ) { return NULL; } end = event->prev; if ( !end ) { start = NULL; } else { end->next = NULL; } event->next = NULL; event->prev = NULL; return event; } /* =============== idEventQueue::Enqueue =============== */ void idEventQueue::Enqueue( entityNetEvent_t *event, outOfOrderBehaviour_t behaviour ) { if ( behaviour == OUTOFORDER_DROP ) { // go backwards through the queue and determine if there are // any out-of-order events while ( end && end->time > event->time ) { entityNetEvent_t *outOfOrder = RemoveLast(); common->DPrintf( "WARNING: new event with id %d ( time %d ) caused removal of event with id %d ( time %d ), game time = %d.\n", event->event, event->time, outOfOrder->event, outOfOrder->time, gameLocal.time ); Free( outOfOrder ); } } else if ( behaviour == OUTOFORDER_SORT && end ) { // NOT TESTED -- sorting out of order packets hasn't been // tested yet... wasn't strictly necessary for // the patch fix. entityNetEvent_t *cur = end; // iterate until we find a time < the new event's while ( cur && cur->time > event->time ) { cur = cur->prev; } if ( !cur ) { // add to start event->next = start; event->prev = NULL; start = event; } else { // insert event->prev = cur; event->next = cur->next; cur->next = event; } return; } // add the new event event->next = NULL; event->prev = NULL; if ( end ) { end->next = event; event->prev = end; } else { start = event; } end = event; }