/* =================== CG_TransitionSnapshot The transition point from snap to nextSnap has passed =================== */ static void CG_TransitionSnapshot( void ) { centity_t *cent; snapshot_t *oldFrame; int i; if ( !cg.snap ) { CG_Error( "CG_TransitionSnapshot: NULL cg.snap" ); } if ( !cg.nextSnap ) { CG_Error( "CG_TransitionSnapshot: NULL cg.nextSnap" ); } // execute any server string commands before transitioning entities CG_ExecuteNewServerCommands( cg.nextSnap->serverCommandSequence ); // clear the currentValid flag for all entities in the existing snapshot for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { cent = &cg_entities[ cg.snap->entities[ i ].number ]; cent->currentValid = qfalse; } // move nextSnap to snap and do the transitions oldFrame = cg.snap; cg.snap = cg.nextSnap; BG_PlayerStateToEntityState( &cg.snap->ps, &cg_entities[ cg.snap->ps.clientNum ].currentState, qfalse ); cg_entities[ cg.snap->ps.clientNum ].interpolate = qfalse; for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { cent = &cg_entities[ cg.snap->entities[ i ].number ]; CG_TransitionEntity( cent ); // remember time of snapshot this entity was last updated in cent->snapShotTime = cg.snap->serverTime; } cg.nextSnap = NULL; // check for playerstate transition events if ( oldFrame ) { playerState_t *ops, *ps; ops = &oldFrame->ps; ps = &cg.snap->ps; // teleporting checks are irrespective of prediction if ( ( ps->eFlags ^ ops->eFlags ) & EF_TELEPORT_BIT ) { cg.thisFrameTeleport = qtrue; // will be cleared by prediction code } // if we are not doing client side movement prediction for any // reason, then the client events and view changes will be issued now if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) || cg_nopredict.integer || cg_synchronousClients.integer ) { CG_TransitionPlayerState( ps, ops ); } } }
/* =================== CG_TransitionSnapshot The transition point from snap to nextSnap has passed =================== */ void CG_TransitionSnapshot( void ) { centity_t *cent; snapshot_t *oldFrame; int i; if ( !cg.snap ) { CG_Error( "CG_TransitionSnapshot: NULL cg.snap" ); } if ( !cg.nextSnap ) { CG_Error( "CG_TransitionSnapshot: NULL cg.nextSnap" ); } // execute any server string commands before transitioning entities CG_ExecuteNewServerCommands( cg.nextSnap->serverCommandSequence ); // clear the currentValid flag for all entities in the existing snapshot for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { cent = &cg_entities[ cg.snap->entities[ i ].number ]; cent->currentValid = qfalse; } // move nextSnap to snap and do the transitions oldFrame = cg.snap; cg.snap = cg.nextSnap; // sort out solid entities //CG_BuildSolidList(); for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { if ( 1 )//cg.snap->entities[ i ].number != 0 ) // I guess the player adds his/her events elsewhere, so doing this also gives us double events for the player! { cent = &cg_entities[ cg.snap->entities[ i ].number ]; CG_TransitionEntity( cent ); } } cg.nextSnap = NULL; // check for playerstate transition events if ( oldFrame ) { // if we are not doing client side movement prediction for any // reason, then the client events and view changes will be issued now if ( cg_timescale.value >= 1.0f ) { CG_TransitionPlayerState( &cg.snap->ps, &oldFrame->ps ); } } }
/* =================== CG_TransitionSnapshot The transition point from snap to nextSnap has passed =================== */ static void CG_TransitionSnapshot( void ) { centity_t *cent; snapshot_t *oldFrame; int i, id; if ( !cg.snap ) { CG_Error( "CG_TransitionSnapshot: NULL cg.snap" ); } if ( !cg.nextSnap ) { CG_Error( "CG_TransitionSnapshot: NULL cg.nextSnap" ); } // execute any server string commands before transitioning entities CG_ExecuteNewServerCommands( cg.nextSnap->serverCommandSequence ); // if we had a map_restart, set everthing with initial if ( !(cg.snap) || !(cg.nextSnap) ) { return; } // rain - I hate doing things like this for enums. Oh well. memset(&oldValid, 0, sizeof(oldValid)); // clear the currentValid flag for all entities in the existing snapshot for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { cent = &cg_entities[ cg.snap->entities[ i ].number ]; cent->currentValid = qfalse; oldValid[cg.snap->entities[i].number] = qtrue; } // OSP -- check for MV updates from new snapshot info #ifdef MV_SUPPORT if(cg.snap->ps.powerups[PW_MVCLIENTLIST] != cg.mvClientList) { CG_mvProcessClientList(); } #endif // move nextSnap to snap and do the transitions oldFrame = cg.snap; cg.snap = cg.nextSnap; if( cg.snap->ps.clientNum == cg.clientNum ) { if( cg.xp < cg.snap->ps.stats[STAT_XP] ) { cg.xpChangeTime = cg.time; } cg.xp = cg.snap->ps.stats[STAT_XP]; } BG_PlayerStateToEntityState( &cg.snap->ps, &cg_entities[ cg.snap->ps.clientNum ].currentState, qfalse ); cg_entities[ cg.snap->ps.clientNum ].interpolate = qfalse; for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { id = cg.snap->entities[ i ].number; CG_TransitionEntity( &cg_entities[ id ] ); // rain - #374 - ent doesn't exist in this frame, reset it. // this is to fix the silent landmines bug, which is caused // by a stale miscTime in the cent if (cg_entities[id].currentValid == qfalse && oldValid[id] == qtrue) { CG_ResetEntity(&cg_entities[id]); } if(cg.mvTotalClients > 0 && CG_mvMergedClientLocate(id)) { CG_mvUpdateClientInfo(id); } } if(cg.mvTotalClients > 0) { CG_mvTransitionPlayerState(&cg.snap->ps); } cg.nextSnap = NULL; // check for playerstate transition events if ( oldFrame ) { playerState_t *ops, *ps; ops = &oldFrame->ps; ps = &cg.snap->ps; // teleporting checks are irrespective of prediction if ( ( ps->eFlags ^ ops->eFlags ) & EF_TELEPORT_BIT ) { cg.thisFrameTeleport = qtrue; // will be cleared by prediction code } // if we are not doing client side movement prediction for any // reason, then the client events and view changes will be issued now if ( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) || cg_nopredict.integer #ifdef ALLOW_GSYNC || cg_synchronousClients.integer #endif // ALLOW_GSYNC ) { CG_TransitionPlayerState( ps, ops ); } } }
/* =================== CG_TransitionSnapshot The transition point from snap to nextSnap has passed =================== */ static void CG_TransitionSnapshot(void) { snapshot_t *oldFrame; int i; if (!cg.snap) { CG_Error("CG_TransitionSnapshot: NULL cg.snap"); } if (!cg.nextSnap) { CG_Error("CG_TransitionSnapshot: NULL cg.nextSnap"); } // execute any server string commands before transitioning entities CG_ExecuteNewServerCommands(cg.nextSnap->serverCommandSequence); // if we had a map_restart, set everthing with initial if (!cg.snap || !cg.nextSnap) { return; } // rain - I hate doing things like this for enums. Oh well. memset(&oldValid, 0, sizeof (oldValid)); // clear the currentValid flag for all entities in the existing snapshot for (i = 0 ; i < cg.snap->numEntities ; ++i) { centity_t *cent = &cg_entities[cg.snap->entities[i].number]; cent->currentValid = qfalse; oldValid[cg.snap->entities[i].number] = qtrue; } // move nextSnap to snap and do the transitions oldFrame = cg.snap; cg.snap = cg.nextSnap; BG_PlayerStateToEntityState(&cg.snap->ps, &cg_entities[cg.snap->ps.clientNum].currentState, qfalse); cg_entities[cg.snap->ps.clientNum].interpolate = qfalse; for (i = 0 ; i < cg.snap->numEntities ; ++i) { int id = cg.snap->entities[i].number; CG_TransitionEntity(&cg_entities[id]); // rain - #374 - ent doesn't exist in this frame, reset it. // this is to fix the silent landmines bug, which is caused // by a stale miscTime in the cent if (cg_entities[id].currentValid == qfalse && oldValid[id] == qtrue) { CG_ResetEntity(&cg_entities[id]); } } cg.nextSnap = NULL; // check for playerstate transition events if (oldFrame) { playerState_t *ops, *ps; ops = &oldFrame->ps; ps = &cg.snap->ps; // teleporting checks are irrespective of prediction if ((ps->eFlags ^ ops->eFlags) & EF_TELEPORT_BIT) { cg.thisFrameTeleport = qtrue; // will be cleared by prediction code } // if we are not doing client side movement prediction for any // reason, then the client events and view changes will be issued now if (cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) || cg_nopredict.integer) { CG_TransitionPlayerState(ps, ops); } } }
/* =================== CG_TransitionSnapshot The transition point from snap to nextSnap has passed =================== */ static void CG_TransitionSnapshot() { centity_t *cent; snapshot_t *oldFrame; int oldWeapon; if ( !cg.snap ) { Sys::Drop( "CG_TransitionSnapshot: NULL cg.snap" ); } if ( !cg.nextSnap ) { Sys::Drop( "CG_TransitionSnapshot: NULL cg.nextSnap" ); } // execute any server string commands before transitioning entities CG_ExecuteServerCommands( cg.nextSnap ); // clear the currentValid flag for all entities in the existing snapshot for ( unsigned i = 0; i < cg.snap->entities.size(); i++ ) { cent = &cg_entities[ cg.snap->entities[ i ].number ]; cent->currentValid = false; } // move nextSnap to snap and do the transitions oldFrame = cg.snap; cg.snap = cg.nextSnap; // Need to store the previous weapon because BG_PlayerStateToEntityState might change it // so the CG_OnPlayerWeaponChange callback is never called oldWeapon = oldFrame->ps.weapon; BG_PlayerStateToEntityState( &cg.snap->ps, &cg_entities[ cg.snap->ps.clientNum ].currentState, false ); cg_entities[ cg.snap->ps.clientNum ].interpolate = false; for ( unsigned i = 0; i < cg.snap->entities.size(); i++ ) { cent = &cg_entities[ cg.snap->entities[ i ].number ]; CG_TransitionEntity( cent ); // remember time of snapshot this entity was last updated in cent->snapShotTime = cg.snap->serverTime; } cg.nextSnap = nullptr; // check for playerstate transition events playerState_t *ops = &oldFrame->ps, *ps = &cg.snap->ps; // teleporting checks are irrespective of prediction if ((ps->eFlags ^ ops->eFlags) & EF_TELEPORT_BIT) { cg.thisFrameTeleport = true; // will be cleared by prediction code } // if we are not doing client side movement prediction for any // reason, then the client events and view changes will be issued now if (cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) || cg_nopredict.integer || cg.pmoveParams.synchronous) { CG_TransitionPlayerState(ps, ops); } // Callbacks for changes in playerState like weapon/class/team if (oldWeapon != ps->weapon) { CG_OnPlayerWeaponChange(); } if (ops->stats[STAT_ITEMS] != ps->stats[STAT_ITEMS]) { CG_OnPlayerUpgradeChange(); } }
/* ================= CG_PredictPlayerState Generates cg.cur_lc->predictedPlayerState for the current cg.time cg.cur_lc->predictedPlayerState is guaranteed to be valid after exiting. For demo playback, this will be an interpolation between two valid playerState_t. For normal gameplay, it will be the result of predicted usercmd_t on top of the most recent playerState_t received from the server. Each new snapshot will usually have one or more new usercmd over the last, but we simulate all unacknowledged commands each time, not just the new ones. This means that on an internet connection, quite a few pmoves may be issued each frame. OPTIMIZE: don't re-simulate unless the newly arrived snapshot playerState_t differs from the predicted one. Would require saving all intermediate playerState_t during prediction. We detect prediction errors and allow them to be decayed off over several frames to ease the jerk. ================= */ void CG_PredictPlayerState( void ) { int cmdNum, current; playerState_t oldPlayerState; qboolean moved; usercmd_t oldestCmd; usercmd_t latestCmd; cg.cur_lc->hyperspace = qfalse; // will be set if touching a trigger_teleport // if this is the first frame we must guarantee // predictedPlayerState is valid even if there is some // other error condition if ( !cg.cur_lc->validPPS ) { cg.cur_lc->validPPS = qtrue; cg.cur_lc->predictedPlayerState = *cg.cur_ps; } // demo playback just copies the moves if ( cg.demoPlayback || (cg.cur_ps->pm_flags & PMF_FOLLOW) ) { CG_InterpolatePlayerState( qfalse ); return; } // non-predicting local movement will grab the latest angles if ( cg_nopredict.integer || cg_synchronousClients.integer ) { CG_InterpolatePlayerState( qtrue ); return; } // prepare for pmove cg_pmove.ps = &cg.cur_lc->predictedPlayerState; if (cg.cur_lc->predictedPlayerState.collisionType == CT_CAPSULE) { cg_pmove.trace = CG_TraceCapsule; } else { cg_pmove.trace = CG_Trace; } cg_pmove.pointcontents = CG_PointContents; if ( cg_pmove.ps->pm_type == PM_DEAD ) { cg_pmove.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; } else { cg_pmove.tracemask = MASK_PLAYERSOLID; } if ( cg.cur_ps->persistant[PERS_TEAM] == TEAM_SPECTATOR ) { cg_pmove.tracemask &= ~CONTENTS_BODY; // spectators can fly through bodies } cg_pmove.noFootsteps = ( cgs.dmflags & DF_NO_FOOTSTEPS ) > 0; // save the state before the pmove so we can detect transitions oldPlayerState = cg.cur_lc->predictedPlayerState; current = trap_GetCurrentCmdNumber(); // if we don't have the commands right after the snapshot, we // can't accurately predict a current position, so just freeze at // the last good position we had cmdNum = current - CMD_BACKUP + 1; trap_GetUserCmd( cmdNum, &oldestCmd, cg.cur_localPlayerNum ); if ( oldestCmd.serverTime > cg.cur_ps->commandTime && oldestCmd.serverTime < cg.time ) { // special check for map_restart if ( cg_showmiss.integer ) { CG_Printf ("exceeded PACKET_BACKUP on commands\n"); } return; } // get the latest command so we can know which commands are from previous map_restarts trap_GetUserCmd( current, &latestCmd, cg.cur_localPlayerNum ); // get the most recent information we have, even if // the server time is beyond our current cg.time, // because predicted player positions are going to // be ahead of everything else anyway if ( cg.nextSnap && !cg.nextFrameTeleport && !cg.thisFrameTeleport && cg.nextSnap->playerNums[cg.cur_localPlayerNum] != -1) { cg.cur_lc->predictedPlayerState = cg.nextSnap->pss[cg.cur_localPlayerNum]; cg.physicsTime = cg.nextSnap->serverTime; } else { cg.cur_lc->predictedPlayerState = *cg.cur_ps; cg.physicsTime = cg.snap->serverTime; } if ( pmove_msec.integer < 8 ) { trap_Cvar_SetValue( "pmove_msec", 8 ); } else if ( pmove_msec.integer > 33 ) { trap_Cvar_SetValue( "pmove_msec", 33 ); } cg_pmove.pmove_fixed = pmove_fixed.integer;// | cg_pmove_fixed.integer; cg_pmove.pmove_msec = pmove_msec.integer; cg_pmove.pmove_overbounce = pmove_overbounce.integer; // run cmds moved = qfalse; for ( cmdNum = current - CMD_BACKUP + 1 ; cmdNum <= current ; cmdNum++ ) { // get the command trap_GetUserCmd( cmdNum, &cg_pmove.cmd, cg.cur_localPlayerNum ); if ( cg_pmove.pmove_fixed ) { PM_UpdateViewAngles( cg_pmove.ps, &cg_pmove.cmd ); } // don't do anything if the time is before the snapshot player time if ( cg_pmove.cmd.serverTime <= cg.cur_lc->predictedPlayerState.commandTime ) { continue; } // don't do anything if the command was from a previous map_restart if ( cg_pmove.cmd.serverTime > latestCmd.serverTime ) { continue; } // check for a prediction error from last frame // on a lan, this will often be the exact value // from the snapshot, but on a wan we will have // to predict several commands to get to the point // we want to compare if ( cg.cur_lc->predictedPlayerState.commandTime == oldPlayerState.commandTime ) { vec3_t delta; float len; if ( cg.thisFrameTeleport ) { // a teleport will not cause an error decay VectorClear( cg.cur_lc->predictedError ); if ( cg_showmiss.integer ) { CG_Printf( "PredictionTeleport\n" ); } cg.thisFrameTeleport = qfalse; } else { vec3_t adjusted, new_angles; CG_AdjustPositionForMover( cg.cur_lc->predictedPlayerState.origin, cg.cur_lc->predictedPlayerState.groundEntityNum, cg.physicsTime, cg.oldTime, adjusted, cg.cur_lc->predictedPlayerState.viewangles, new_angles); if ( cg_showmiss.integer ) { if (!VectorCompare( oldPlayerState.origin, adjusted )) { CG_Printf("prediction error\n"); } } VectorSubtract( oldPlayerState.origin, adjusted, delta ); len = VectorLength( delta ); if ( len > 0.1 ) { if ( cg_showmiss.integer ) { CG_Printf("Prediction miss: %f\n", len); } if ( cg_errorDecay.integer ) { int t; float f; t = cg.time - cg.cur_lc->predictedErrorTime; f = ( cg_errorDecay.value - t ) / cg_errorDecay.value; if ( f < 0 ) { f = 0; } if ( f > 0 && cg_showmiss.integer ) { CG_Printf("Double prediction decay: %f\n", f); } VectorScale( cg.cur_lc->predictedError, f, cg.cur_lc->predictedError ); } else { VectorClear( cg.cur_lc->predictedError ); } VectorAdd( delta, cg.cur_lc->predictedError, cg.cur_lc->predictedError ); cg.cur_lc->predictedErrorTime = cg.oldTime; } } } // don't predict gauntlet firing, which is only supposed to happen // when it actually inflicts damage cg_pmove.gauntletHit = qfalse; if ( cg_pmove.pmove_fixed ) { cg_pmove.cmd.serverTime = ((cg_pmove.cmd.serverTime + pmove_msec.integer-1) / pmove_msec.integer) * pmove_msec.integer; } Pmove (&cg_pmove); moved = qtrue; // add push trigger movement effects CG_TouchTriggerPrediction(); // check for predictable events that changed from previous predictions //CG_CheckChangedPredictableEvents(&cg.cur_lc->predictedPlayerState); } if ( cg_showmiss.integer > 1 ) { CG_Printf( "[%i : %i] ", cg_pmove.cmd.serverTime, cg.time ); } if ( !moved ) { if ( cg_showmiss.integer ) { CG_Printf( "not moved\n" ); } return; } // adjust for the movement of the groundentity CG_AdjustPositionForMover( cg.cur_lc->predictedPlayerState.origin, cg.cur_lc->predictedPlayerState.groundEntityNum, cg.physicsTime, cg.time, cg.cur_lc->predictedPlayerState.origin, cg.cur_lc->predictedPlayerState.viewangles,cg.cur_lc->predictedPlayerState.viewangles); if ( cg_showmiss.integer ) { if (cg.cur_lc->predictedPlayerState.eventSequence > oldPlayerState.eventSequence + MAX_PS_EVENTS) { CG_Printf("WARNING: dropped event\n"); } } // fire events and other transition triggered things CG_TransitionPlayerState( &cg.cur_lc->predictedPlayerState, &oldPlayerState ); if ( cg_showmiss.integer ) { if (cg.cur_lc->eventSequence > cg.cur_lc->predictedPlayerState.eventSequence) { CG_Printf("WARNING: double event\n"); cg.cur_lc->eventSequence = cg.cur_lc->predictedPlayerState.eventSequence; } } }
/* =================== CG_TransitionSnapshot The transition point from snap to nextSnap has passed =================== */ static void CG_TransitionSnapshot( void ) { centity_t *cent; snapshot_t *oldFrame; int i; server_sound_t *snd; if ( !cg.snap ) { CG_Error( "CG_TransitionSnapshot: NULL cg.snap" ); } if ( !cg.nextSnap ) { CG_Error( "CG_TransitionSnapshot: NULL cg.nextSnap" ); } // execute any server string commands before transitioning entities CG_ExecuteNewServerCommands( cg.nextSnap->serverCommandSequence ); // if we had a map_restart, set everthing with initial if ( !cg.snap ) { } // clear the currentValid flag for all entities in the existing snapshot for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { cent = &cg_entities[ cg.snap->entities[ i ].number ]; cent->currentValid = qfalse; } // move nextSnap to snap and do the transitions oldFrame = cg.snap; cg.snap = cg.nextSnap; BG_PlayerStateToEntityState( &cg.snap->ps, &cg_entities[ cg.snap->ps.clientNum ].currentState, qfalse ); cg_entities[ cg.snap->ps.clientNum ].interpolate = qfalse; for ( i = 0 ; i < cg.snap->numEntities ; i++ ) { cent = &cg_entities[ cg.snap->entities[ i ].number ]; CG_TransitionEntity( cent ); // remember time of snapshot this entity was last updated in cent->snapShotTime = cg.snap->serverTime; } for ( i=0;i<cg.snap->number_of_sounds;i++ ) { snd = &cg.snap->sounds[i]; if ( snd->sound_index == 0 ) // wombat: we get these sometimes, no clue why continue; if (snd->stop_flag) { trap_S_StopLoopingSound( snd->entity_number ); } else { trap_S_StartSound( snd->origin, snd->entity_number, snd->channel, cgs.gameSounds[snd->sound_index] ); } } cg.nextSnap = NULL; // check for playerstate transition events if ( oldFrame ) { playerState_t *ops, *ps; ops = &oldFrame->ps; ps = &cg.snap->ps; // teleporting checks are irrespective of prediction //if ( ( ps->eFlags ^ ops->eFlags ) & EF_TELEPORT_BIT ) { // cg.thisFrameTeleport = qtrue; // will be cleared by prediction code //} // if we are not doing client side movement prediction for any // reason, then the client events and view changes will be issued now if ( cg.demoPlayback /*|| (cg.snap->ps.pm_flags & PMF_FOLLOW)*/ || cg_nopredict.integer || cg_synchronousClients.integer ) { CG_TransitionPlayerState( ps, ops ); } } }