/* =========================================================================== 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 "Common_local.h" idCVar net_clientMaxPrediction( "net_clientMaxPrediction", "5000", CVAR_SYSTEM | CVAR_INTEGER | CVAR_NOCHEAT, "maximum number of milliseconds a client can predict ahead of server." ); idCVar net_snapRate( "net_snapRate", "100", CVAR_SYSTEM | CVAR_INTEGER, "How many milliseconds between sending snapshots" ); idCVar net_ucmdRate( "net_ucmdRate", "40", CVAR_SYSTEM | CVAR_INTEGER, "How many milliseconds between sending usercmds" ); idCVar net_debug_snapShotTime( "net_debug_snapShotTime", "0", CVAR_BOOL | CVAR_ARCHIVE, "" ); idCVar com_forceLatestSnap( "com_forceLatestSnap", "0", CVAR_BOOL, "" ); // Enables effective snap rate: dynamically adjust the client snap rate based on: // -client FPS // -server FPS (interpolated game time received / interval it was received over) // -local buffered time (leave a cushion to absorb spikes, slow down when infront of it, speed up when behind it) ie: net_minBufferedSnapPCT_Static idCVar net_effectiveSnapRateEnable( "net_effectiveSnapRateEnable", "1", CVAR_BOOL, "Dynamically adjust client snaprate"); idCVar net_effectiveSnapRateDebug( "net_effectiveSnapRateDebug", "0", CVAR_BOOL, "Debug"); // Min buffered snapshot time to keep as a percentage of the effective snaprate // -ie we want to keep 50% of the amount of time difference between last two snaps. // -we need to scale this because we may get throttled at the snaprate may change // -Acts as a buffer to absorb spikes idCVar net_minBufferedSnapPCT_Static( "net_minBufferedSnapPCT_Static", "1.0", CVAR_FLOAT, "Min amount of snapshot buffer time we want need to buffer"); idCVar net_maxBufferedSnapMS( "net_maxBufferedSnapMS", "336", CVAR_INTEGER, "Max time to allow for interpolation cushion"); idCVar net_minBufferedSnapWinPCT_Static( "net_minBufferedSnapWinPCT_Static", "1.0", CVAR_FLOAT, "Min amount of snapshot buffer time we want need to buffer"); // Factor at which we catch speed up interpolation if we fall behind our optimal interpolation window // -This is a static factor. We may experiment with a dynamic one that would be faster the farther you are from the ideal window idCVar net_interpolationCatchupRate( "net_interpolationCatchupRate", "1.3", CVAR_FLOAT, "Scale interpolationg rate when we fall behind"); idCVar net_interpolationFallbackRate( "net_interpolationFallbackRate", "0.95", CVAR_FLOAT, "Scale interpolationg rate when we fall behind"); idCVar net_interpolationBaseRate( "net_interpolationBaseRate", "1.0", CVAR_FLOAT, "Scale interpolationg rate when we fall behind"); // Enabled a dynamic ideal snap buffer window: we will scale the distance and size idCVar net_optimalDynamic( "net_optimalDynamic", "1", CVAR_BOOL, "How fast to add to our optimal time buffer when we are playing snapshots faster than server is feeding them to us"); // These values are used instead if net_optimalDynamic is 0 (don't scale by actual snap rate/interval) idCVar net_optimalSnapWindow( "net_optimalSnapWindow", "112", CVAR_FLOAT, ""); idCVar net_optimalSnapTime( "net_optimalSnapTime", "112", CVAR_FLOAT, "How fast to add to our optimal time buffer when we are playing snapshots faster than server is feeding them to us"); // this is at what percentage of being ahead of the interpolation buffer that we start slowing down (we ramp down from 1.0 to 0.0 starting here) // this is a percentage of the total cushion time. idCVar net_interpolationSlowdownStart( "net_interpolationSlowdownStart", "0.5", CVAR_FLOAT, "Scale interpolation rate when we fall behind"); // Extrapolation is now disabled idCVar net_maxExtrapolationInMS( "net_maxExtrapolationInMS", "0", CVAR_INTEGER, "Max time in MS that extrapolation is allowed to occur."); static const int SNAP_USERCMDS = 8192; /* =============== idCommonLocal::IsMultiplayer =============== */ bool idCommonLocal::IsMultiplayer() { idLobbyBase & lobby = session->GetPartyLobbyBase(); return ( ( ( lobby.GetMatchParms().matchFlags & MATCH_ONLINE ) != 0 ) && ( session->GetState() > idSession::IDLE ) ); } /* =============== idCommonLocal::IsServer =============== */ bool idCommonLocal::IsServer() { return IsMultiplayer() && session->GetActingGameStateLobbyBase().IsHost(); } /* =============== idCommonLocal::IsClient =============== */ bool idCommonLocal::IsClient() { return IsMultiplayer() && session->GetActingGameStateLobbyBase().IsPeer(); } /* =============== idCommonLocal::SendSnapshots =============== */ int idCommonLocal::GetSnapRate() { return net_snapRate.GetInteger(); } /* =============== idCommonLocal::SendSnapshots =============== */ void idCommonLocal::SendSnapshots() { if ( !mapSpawned ) { return; } int currentTime = Sys_Milliseconds(); if ( currentTime < nextSnapshotSendTime ) { return; } idLobbyBase & lobby = session->GetActingGameStateLobbyBase(); if ( !lobby.IsHost() ) { return; } if ( !lobby.HasActivePeers() ) { return; } idSnapShot ss; game->ServerWriteSnapshot( ss ); session->SendSnapshot( ss ); nextSnapshotSendTime = MSEC_ALIGN_TO_FRAME( currentTime + net_snapRate.GetInteger() ); } /* =============== idCommonLocal::NetReceiveSnapshot =============== */ void idCommonLocal::NetReceiveSnapshot( class idSnapShot & ss ) { ss.SetRecvTime( Sys_Milliseconds() ); // If we are about to overwrite the oldest snap, then force a read, which will cause a pop on screen, but we have to do this. if ( writeSnapshotIndex - readSnapshotIndex >= RECEIVE_SNAPSHOT_BUFFER_SIZE ) { idLib::Printf( "Overwritting oldest snapshot %d with new snapshot %d\n", readSnapshotIndex, writeSnapshotIndex ); assert( writeSnapshotIndex % RECEIVE_SNAPSHOT_BUFFER_SIZE == readSnapshotIndex % RECEIVE_SNAPSHOT_BUFFER_SIZE ); ProcessNextSnapshot(); } receivedSnaps[ writeSnapshotIndex % RECEIVE_SNAPSHOT_BUFFER_SIZE ] = ss; writeSnapshotIndex++; // Force read the very first 2 snapshots if ( readSnapshotIndex < 2 ) { ProcessNextSnapshot(); } } /* =============== idCommonLocal::SendUsercmd =============== */ void idCommonLocal::SendUsercmds( int localClientNum ) { if ( !mapSpawned ) { return; } int currentTime = Sys_Milliseconds(); if ( currentTime < nextUsercmdSendTime ) { return; } idLobbyBase & lobby = session->GetActingGameStateLobbyBase(); if ( lobby.IsHost() ) { return; } // We always send the last NUM_USERCMD_SEND usercmds // Which may result in duplicate usercmds being sent in the case of a low net_ucmdRate // But the LZW compressor means the extra usercmds are not large and the redundancy can smooth packet loss byte buffer[idPacketProcessor::MAX_FINAL_PACKET_SIZE]; idBitMsg msg( buffer, sizeof( buffer ) ); idSerializer ser( msg, true ); usercmd_t empty; usercmd_t * last = ∅ usercmd_t * cmdBuffer[NUM_USERCMD_SEND]; const int numCmds = userCmdMgr.GetPlayerCmds( localClientNum, cmdBuffer, NUM_USERCMD_SEND ); msg.WriteByte( numCmds ); for ( int i = 0; i < numCmds; i++ ) { cmdBuffer[i]->Serialize( ser, *last ); last = cmdBuffer[i]; } session->SendUsercmds( msg ); nextUsercmdSendTime = MSEC_ALIGN_TO_FRAME( currentTime + net_ucmdRate.GetInteger() ); } /* =============== idCommonLocal::NetReceiveUsercmds =============== */ void idCommonLocal::NetReceiveUsercmds( int peer, idBitMsg & msg ) { int clientNum = Game()->MapPeerToClient( peer ); if ( clientNum == -1 ) { idLib::Warning( "NetReceiveUsercmds: Could not find client for peer %d", peer ); return; } NetReadUsercmds( clientNum, msg ); } /* =============== idCommonLocal::NetReceiveReliable =============== */ void idCommonLocal::NetReceiveReliable( int peer, int type, idBitMsg & msg ) { int clientNum = Game()->MapPeerToClient( peer ); // Only servers care about the client num. Band-aid for problems related to the host's peerIndex being -1 on clients. if ( common->IsServer() && clientNum == -1 ) { idLib::Warning( "NetReceiveReliable: Could not find client for peer %d", peer ); return; } const byte * msgData = msg.GetReadData() + msg.GetReadCount(); int msgSize = msg.GetRemainingData(); reliableMsg_t & reliable = reliableQueue.Alloc(); reliable.client = clientNum; reliable.type = type; reliable.dataSize = msgSize; reliable.data = (byte *)Mem_Alloc( msgSize, TAG_NETWORKING ); memcpy( reliable.data, msgData, msgSize ); } /* ======================== idCommonLocal::ProcessSnapshot ======================== */ void idCommonLocal::ProcessSnapshot( idSnapShot & ss ) { int time = Sys_Milliseconds(); snapTime = time; snapPrevious = snapCurrent; snapCurrent.serverTime = ss.GetTime(); snapRate = snapCurrent.serverTime - snapPrevious.serverTime; static int lastReceivedLocalTime = 0; int timeSinceLastSnap = ( time - lastReceivedLocalTime ); if ( net_debug_snapShotTime.GetBool() ) { idLib::Printf( "^2ProcessSnapshot. delta serverTime: %d delta localTime: %d \n", ( snapCurrent.serverTime-snapPrevious.serverTime ), timeSinceLastSnap ); } lastReceivedLocalTime = time; /* JAF ? for ( int i = 0; i < MAX_PLAYERS; i++ ) { idBitMsg msg; if ( ss.GetObjectMsgByID( idSession::SS_PLAYER + i, msg ) ) { if ( msg.GetSize() == 0 ) { snapCurrent.players[ i ].valid = false; continue; } idSerializer ser( msg, false ); SerializePlayer( ser, snapCurrent.players[ i ] ); snapCurrent.players[ i ].valid = true; extern idCVar com_drawSnapshots; if ( com_drawSnapshots.GetInteger() == 3 ) { console->AddSnapObject( "players", msg.GetSize(), ss.CompareObject( &oldss, idSession::SS_PLAYER + i ) ); } } } */ // Read usercmds from other players for ( int p = 0; p < MAX_PLAYERS; p++ ) { if ( p == game->GetLocalClientNum() ) { continue; } idBitMsg msg; if ( ss.GetObjectMsgByID( SNAP_USERCMDS + p, msg ) ) { NetReadUsercmds( p, msg ); } } // Set server game time here so that it accurately reflects the time when this frame was saved out, in case any serialize function needs it. int oldTime = Game()->GetServerGameTimeMs(); Game()->SetServerGameTimeMs( snapCurrent.serverTime ); Game()->ClientReadSnapshot( ss ); //, &oldss ); // Restore server game time Game()->SetServerGameTimeMs( oldTime ); snapTimeDelta = ss.GetRecvTime() - oldss.GetRecvTime(); oldss = ss; } /* ======================== idCommonLocal::NetReadUsercmds ======================== */ void idCommonLocal::NetReadUsercmds( int clientNum, idBitMsg & msg ) { if ( clientNum == -1 ) { idLib::Warning( "NetReadUsercmds: Trying to read commands from invalid clientNum %d", clientNum ); return; } // TODO: This shouldn't actually happen. Figure out why it does. // Seen on clients when another client leaves a match. if ( msg.GetReadData() == NULL ) { return; } idSerializer ser( msg, false ); usercmd_t fakeCmd; usercmd_t * base = &fakeCmd; usercmd_t lastCmd; bool gotNewCmd = false; idStaticList< usercmd_t, NUM_USERCMD_RELAY > newCmdBuffer; usercmd_t baseCmd = userCmdMgr.NewestUserCmdForPlayer( clientNum ); int curMilliseconds = baseCmd.clientGameMilliseconds; const int numCmds = msg.ReadByte(); for ( int i = 0; i < numCmds; i++ ) { usercmd_t newCmd; newCmd.Serialize( ser, *base ); lastCmd = newCmd; base = &lastCmd; int newMilliseconds = newCmd.clientGameMilliseconds; if ( newMilliseconds > curMilliseconds ) { if ( verify( i < NUM_USERCMD_RELAY ) ) { newCmdBuffer.Append( newCmd ); gotNewCmd = true; curMilliseconds = newMilliseconds; } } } // Push the commands into the buffer. for ( int i = 0; i < newCmdBuffer.Num(); ++i ) { userCmdMgr.PutUserCmdForPlayer( clientNum, newCmdBuffer[i] ); } } /* ======================== idCommonLocal::ProcessNextSnapshot ======================== */ void idCommonLocal::ProcessNextSnapshot() { if ( readSnapshotIndex == writeSnapshotIndex ) { idLib::Printf("No snapshots to process.\n"); return; // No snaps to process } ProcessSnapshot( receivedSnaps[ readSnapshotIndex % RECEIVE_SNAPSHOT_BUFFER_SIZE ] ); readSnapshotIndex++; } /* ======================== idCommonLocal::CalcSnapTimeBuffered Return the amount of game time left of buffered snapshots totalBufferedTime - total amount of snapshot time (includng what we've already past in current interpolate) totalRecvTime - total real time (sys_milliseconds) all of totalBufferedTime was received over ======================== */ int idCommonLocal::CalcSnapTimeBuffered( int & totalBufferedTime, int & totalRecvTime ) { totalBufferedTime = snapRate; totalRecvTime = snapTimeDelta; // oldSS = last ss we deserialized int lastBuffTime = oldss.GetTime(); int lastRecvTime = oldss.GetRecvTime(); // receivedSnaps[readSnapshotIndex % RECEIVE_SNAPSHOT_BUFFER_SIZE] = next buffered snapshot we haven't processed yet (might not exist) for ( int i = readSnapshotIndex; i < writeSnapshotIndex; i++ ) { int buffTime = receivedSnaps[i % RECEIVE_SNAPSHOT_BUFFER_SIZE].GetTime(); int recvTime = receivedSnaps[i % RECEIVE_SNAPSHOT_BUFFER_SIZE].GetRecvTime(); totalBufferedTime += buffTime - lastBuffTime; totalRecvTime += recvTime - lastRecvTime; lastRecvTime = recvTime; lastBuffTime = buffTime; } totalRecvTime = Max( 1, totalRecvTime ); totalRecvTime = static_cast( initialBaseTicksPerSec ) * static_cast( totalRecvTime / 1000.0f ); // convert realMS to gameMS // remove time we've already interpolated over int timeLeft = totalBufferedTime - Min< int >( snapRate, snapCurrentTime ); //idLib::Printf( "CalcSnapTimeBuffered. timeLeft: %d totalRecvTime: %d, totalTimeBuffered: %d\n", timeLeft, totalRecvTime, totalBufferedTime ); return timeLeft; } /* ======================== idCommonLocal::InterpolateSnapshot ======================== */ void idCommonLocal::InterpolateSnapshot( netTimes_t & prev, netTimes_t & next, float fraction, bool predict ) { int serverTime = Lerp( prev.serverTime, next.serverTime, fraction ); Game()->SetServerGameTimeMs( serverTime ); // Set the global server time to the interpolated time of the server Game()->SetInterpolation( fraction, serverTime, prev.serverTime, next.serverTime ); //Game()->RunFrame( &userCmdMgr, &ret, true ); } /* ======================== idCommonLocal::RunNetworkSnapshotFrame ======================== */ void idCommonLocal::RunNetworkSnapshotFrame() { // Process any reliable messages we've received for ( int i = 0; i < reliableQueue.Num(); i++ ) { game->ProcessReliableMessage( reliableQueue[i].client, reliableQueue[i].type, idBitMsg( (const byte *)reliableQueue[i].data, reliableQueue[i].dataSize ) ); Mem_Free( reliableQueue[i].data ); } reliableQueue.Clear(); // abuse the game timing to time presentable thinking on clients time_gameFrame = Sys_Microseconds(); time_maxGameFrame = 0; count_numGameFrames = 0; if ( snapPrevious.serverTime >= 0 ) { int msec_interval = 1 + idMath::Ftoi( (float)initialBaseTicksPerSec ); static int clientTimeResidual = 0; static int lastTime = Sys_Milliseconds(); int currentTime = Sys_Milliseconds(); int deltaFrameTime = idMath::ClampInt( 1, 33, currentTime - lastTime ); clientTimeResidual += idMath::ClampInt( 0, 50, currentTime - lastTime ); lastTime = currentTime; extern idCVar com_fixedTic; if ( com_fixedTic.GetBool() ) { clientTimeResidual = 0; } do { // If we are extrapolating and have fresher snapshots, then use the freshest one while ( ( snapCurrentTime >= snapRate || com_forceLatestSnap.GetBool() ) && readSnapshotIndex < writeSnapshotIndex ) { snapCurrentTime -= snapRate; ProcessNextSnapshot(); } // this only matters when running < 60 fps // JAF Game()->GetRenderWorld()->UpdateDeferredPositions(); // Clamp the current time so that it doesn't fall outside of our extrapolation bounds snapCurrentTime = idMath::ClampInt( 0, snapRate + Min( (int)snapRate, (int)net_maxExtrapolationInMS.GetInteger() ), snapCurrentTime ); if ( snapRate <= 0 ) { idLib::Warning("snapRate <= 0. Resetting to 100"); snapRate = 100; } float fraction = (float)snapCurrentTime / (float)snapRate; if ( !IsValid( fraction ) ) { idLib::Warning("Interpolation Fraction invalid: snapCurrentTime %d / snapRate %d", (int)snapCurrentTime, (int)snapRate ); fraction = 0.0f; } InterpolateSnapshot( snapPrevious, snapCurrent, fraction, true ); // Default to a snap scale of 1 float snapRateScale = net_interpolationBaseRate.GetFloat(); snapTimeBuffered = CalcSnapTimeBuffered( totalBufferedTime, totalRecvTime ); effectiveSnapRate = static_cast< float > ( totalBufferedTime ) / static_cast< float > ( totalRecvTime ); if ( net_minBufferedSnapPCT_Static.GetFloat() > 0.0f ) { optimalPCTBuffer = session->GetTitleStorageFloat( "net_minBufferedSnapPCT_Static", net_minBufferedSnapPCT_Static.GetFloat() ); } // Calculate optimal amount of buffered time we want if ( net_optimalDynamic.GetBool() ) { optimalTimeBuffered = idMath::ClampInt( 0, net_maxBufferedSnapMS.GetInteger(), snapRate * optimalPCTBuffer ); optimalTimeBufferedWindow = snapRate * net_minBufferedSnapWinPCT_Static.GetFloat(); } else { optimalTimeBuffered = net_optimalSnapTime.GetFloat(); optimalTimeBufferedWindow = net_optimalSnapWindow.GetFloat(); } // Scale snapRate based on where we are in the buffer if ( snapTimeBuffered <= optimalTimeBuffered ) { if ( snapTimeBuffered <= idMath::FLT_SMALLEST_NON_DENORMAL ) { snapRateScale = 0; } else { snapRateScale = net_interpolationFallbackRate.GetFloat(); // When we interpolate past our cushion of buffered snapshot, we want to slow smoothly slow the // rate of interpolation. frac will go from 1.0 to 0.0 (if snapshots stop coming in). float startSlowdown = ( net_interpolationSlowdownStart.GetFloat() * optimalTimeBuffered ); if ( startSlowdown > 0 && snapTimeBuffered < startSlowdown ) { float frac = idMath::ClampFloat( 0.0f, 1.0f, snapTimeBuffered / startSlowdown ); if ( !IsValid( frac ) ) { frac = 0.0f; } snapRateScale = Square( frac ) * snapRateScale; if ( !IsValid( snapRateScale ) ) { snapRateScale = 0.0f; } } } } else if ( snapTimeBuffered > optimalTimeBuffered + optimalTimeBufferedWindow ) { // Go faster snapRateScale = net_interpolationCatchupRate.GetFloat(); } float delta_interpolate = (float)initialBaseTicksPerSec * snapRateScale; if ( net_effectiveSnapRateEnable.GetBool() ) { float deltaFrameGameMS = static_cast( initialBaseTicksPerSec ) * static_cast( deltaFrameTime / 1000.0f ); delta_interpolate = ( deltaFrameGameMS * snapRateScale * effectiveSnapRate ) + snapCurrentResidual; if ( !IsValid( delta_interpolate ) ) { delta_interpolate = 0.0f; } snapCurrentResidual = idMath::Frac( delta_interpolate ); // fixme: snapCurrentTime should just be a float, but would require changes in d4 too if ( !IsValid( snapCurrentResidual ) ) { snapCurrentResidual = 0.0f; } if ( net_effectiveSnapRateDebug.GetBool() ) { idLib::Printf("%d/%.2f snapRateScale: %.2f effectiveSR: %.2f d.interp: %.2f snapTimeBuffered: %.2f res: %.2f\n", deltaFrameTime, deltaFrameGameMS, snapRateScale, effectiveSnapRate, delta_interpolate, snapTimeBuffered, snapCurrentResidual ); } } assert( IsValid( delta_interpolate ) ); int interpolate_interval = idMath::Ftoi( delta_interpolate ); snapCurrentTime += interpolate_interval; // advance interpolation time by the scaled interpolate_interval clientTimeResidual -= msec_interval; // advance local client residual time (fixed step) } while ( clientTimeResidual >= msec_interval ); if ( clientTimeResidual < 0 ) { clientTimeResidual = 0; } } time_gameFrame = Sys_Microseconds() - time_gameFrame; } /* ======================== idCommonLocal::ExecuteReliableMessages ======================== */ void idCommonLocal::ExecuteReliableMessages() { // Process any reliable messages we've received for ( int i = 0; i < reliableQueue.Num(); i++ ) { reliableMsg_t & reliable = reliableQueue[i]; game->ProcessReliableMessage( reliable.client, reliable.type, idBitMsg( (const byte *)reliable.data, reliable.dataSize ) ); Mem_Free( reliable.data ); } reliableQueue.Clear(); } /* ======================== idCommonLocal::ResetNetworkingState ======================== */ void idCommonLocal::ResetNetworkingState() { snapTime = 0; snapTimeWrite = 0; snapCurrentTime = 0; snapCurrentResidual = 0.0f; snapTimeBuffered = 0.0f; effectiveSnapRate = 0.0f; totalBufferedTime = 0; totalRecvTime = 0; readSnapshotIndex = 0; writeSnapshotIndex = 0; snapRate = 100000; optimalTimeBuffered = 0.0f; optimalPCTBuffer = 0.5f; optimalTimeBufferedWindow = 0.0; // Clear snapshot queue for ( int i = 0; i < RECEIVE_SNAPSHOT_BUFFER_SIZE; i++ ) { receivedSnaps[i].Clear(); } userCmdMgr.SetDefaults(); snapCurrent.localTime = -1; snapPrevious.localTime = -1; snapCurrent.serverTime = -1; snapPrevious.serverTime = -1; // Make sure our current snap state is cleared so state from last game doesn't carry over into new game oldss.Clear(); gameFrame = 0; clientPrediction = 0; nextUsercmdSendTime = 0; nextSnapshotSendTime = 0; }