You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1847 lines
52 KiB
1847 lines
52 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
#include "cbase.h" |
|
#include "prediction.h" |
|
#include "igamemovement.h" |
|
#include "prediction_private.h" |
|
#include "ivrenderview.h" |
|
#include "iinput.h" |
|
#include "usercmd.h" |
|
#include <vgui_controls/Controls.h> |
|
#include <vgui/ISurface.h> |
|
#include <vgui/IScheme.h> |
|
#include "hud.h" |
|
#include "iclientvehicle.h" |
|
#include "in_buttons.h" |
|
#include "con_nprint.h" |
|
#include "hud_pdump.h" |
|
#include "datacache/imdlcache.h" |
|
|
|
#ifdef HL2_CLIENT_DLL |
|
#include "c_basehlplayer.h" |
|
#endif |
|
|
|
#include "tier0/vprof.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
IPredictionSystem *IPredictionSystem::g_pPredictionSystems = NULL; |
|
|
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
|
|
ConVar cl_predictweapons ( "cl_predictweapons","1", FCVAR_USERINFO | FCVAR_NOT_CONNECTED, "Perform client side prediction of weapon effects." ); |
|
ConVar cl_lagcompensation ( "cl_lagcompensation","1", FCVAR_USERINFO | FCVAR_NOT_CONNECTED, "Perform server side lag compensation of weapon firing events." ); |
|
ConVar cl_showerror ( "cl_showerror", "0", 0, "Show prediction errors, 2 for above plus detailed field deltas." ); |
|
|
|
static ConVar cl_idealpitchscale ( "cl_idealpitchscale", "0.8", FCVAR_ARCHIVE ); |
|
static ConVar cl_predictionlist ( "cl_predictionlist", "0", FCVAR_CHEAT, "Show which entities are predicting\n" ); |
|
|
|
static ConVar cl_predictionentitydump( "cl_pdump", "-1", FCVAR_CHEAT, "Dump info about this entity to screen." ); |
|
static ConVar cl_predictionentitydumpbyclass( "cl_pclass", "", FCVAR_CHEAT, "Dump entity by prediction classname." ); |
|
static ConVar cl_pred_optimize( "cl_pred_optimize", "2", 0, "Optimize for not copying data if didn't receive a network update (1), and also for not repredicting if there were no errors (2)." ); |
|
|
|
#ifdef STAGING_ONLY |
|
// Do not ship this - testing a fix |
|
static ConVar cl_pred_optimize_prefer_server_data( "cl_pred_optimize_prefer_server_data", "0", 0, "In the case where we have both server data and predicted data up to the same tick, choose server data over predicted data." ); |
|
// |
|
#endif // STAGING_ONLY |
|
|
|
#endif |
|
|
|
extern IGameMovement *g_pGameMovement; |
|
extern CMoveData *g_pMoveData; |
|
|
|
void COM_Log( char *pszFile, const char *fmt, ...); |
|
typedescription_t *FindFieldByName( const char *fieldname, datamap_t *dmap ); |
|
|
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
//----------------------------------------------------------------------------- |
|
// Purpose: For debugging, find predictable by classname |
|
// Input : *classname - |
|
// Output : static C_BaseEntity |
|
//----------------------------------------------------------------------------- |
|
static C_BaseEntity *FindPredictableByGameClass( const char *classname ) |
|
{ |
|
// Walk backward due to deletion from UtlVector |
|
int c = predictables->GetPredictableCount(); |
|
int i; |
|
for ( i = 0; i < c; i++ ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( i ); |
|
if ( !ent ) |
|
continue; |
|
|
|
// Don't do anything to truly predicted things (like player and weapons ) |
|
if ( !FClassnameIs( ent, classname ) ) |
|
continue; |
|
|
|
return ent; |
|
} |
|
|
|
return NULL; |
|
} |
|
#endif |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CPrediction::CPrediction( void ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
m_bInPrediction = false; |
|
m_bFirstTimePredicted = false; |
|
|
|
m_nIncomingPacketNumber = 0; |
|
m_flIdealPitch = 0.0f; |
|
|
|
m_nPreviousStartFrame = -1; |
|
|
|
m_nCommandsPredicted = 0; |
|
m_nServerCommandsAcknowledged = 0; |
|
m_bPreviousAckHadErrors = false; |
|
#endif |
|
} |
|
|
|
CPrediction::~CPrediction( void ) |
|
{ |
|
} |
|
|
|
void CPrediction::Init( void ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
m_bOldCLPredictValue = cl_predict->GetInt(); |
|
#endif |
|
} |
|
|
|
void CPrediction::Shutdown( void ) |
|
{ |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
|
|
void CPrediction::CheckError( int commands_acknowledged ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
C_BasePlayer *player; |
|
Vector origin; |
|
Vector delta; |
|
float len; |
|
static int pos = 0; |
|
|
|
// Not in the game yet |
|
if ( !engine->IsInGame() ) |
|
return; |
|
|
|
// Not running prediction |
|
if ( !cl_predict->GetInt() ) |
|
return; |
|
|
|
player = C_BasePlayer::GetLocalPlayer(); |
|
if ( !player ) |
|
return; |
|
|
|
// Not predictable yet (flush entity packet?) |
|
if ( !player->IsIntermediateDataAllocated() ) |
|
return; |
|
|
|
origin = player->GetNetworkOrigin(); |
|
|
|
const void *slot = player->GetPredictedFrame( commands_acknowledged - 1 ); |
|
if ( !slot ) |
|
return; |
|
|
|
// Find the origin field in the database |
|
typedescription_t *td = FindFieldByName( "m_vecNetworkOrigin", player->GetPredDescMap() ); |
|
Assert( td ); |
|
if ( !td ) |
|
return; |
|
|
|
Vector predicted_origin; |
|
|
|
memcpy( (Vector *)&predicted_origin, (Vector *)( (byte *)slot + td->fieldOffset[ PC_DATA_PACKED ] ), sizeof( Vector ) ); |
|
|
|
// Compare what the server returned with what we had predicted it to be |
|
VectorSubtract ( predicted_origin, origin, delta ); |
|
|
|
len = VectorLength( delta ); |
|
if (len > MAX_PREDICTION_ERROR ) |
|
{ |
|
// A teleport or something, clear out error |
|
len = 0; |
|
} |
|
else |
|
{ |
|
if ( len > MIN_PREDICTION_EPSILON ) |
|
{ |
|
player->NotePredictionError( delta ); |
|
|
|
if ( cl_showerror.GetInt() >= 1 ) |
|
{ |
|
con_nprint_t np; |
|
np.fixed_width_font = true; |
|
np.color[0] = 1.0f; |
|
np.color[1] = 0.95f; |
|
np.color[2] = 0.7f; |
|
np.index = 20 + ( ++pos % 20 ); |
|
np.time_to_live = 2.0f; |
|
|
|
engine->Con_NXPrintf( &np, "pred error %6.3f units (%6.3f %6.3f %6.3f)", len, delta.x, delta.y, delta.z ); |
|
} |
|
} |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::ShutdownPredictables( void ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
// Transfer intermediate data from other predictables |
|
int c = predictables->GetPredictableCount(); |
|
int i; |
|
|
|
int shutdown_count = 0; |
|
int release_count = 0; |
|
|
|
for ( i = c - 1; i >= 0 ; i-- ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( i ); |
|
if ( !ent ) |
|
continue; |
|
|
|
// Shutdown predictables |
|
if ( ent->GetPredictable() ) |
|
{ |
|
ent->ShutdownPredictable(); |
|
shutdown_count++; |
|
} |
|
// Otherwise, release client created entities |
|
else |
|
{ |
|
ent->Release(); |
|
release_count++; |
|
} |
|
} |
|
|
|
if ( ( release_count > 0 ) || |
|
( shutdown_count > 0 ) ) |
|
{ |
|
Msg( "Shutdown %i predictable entities and %i client-created entities\n", |
|
shutdown_count, |
|
release_count ); |
|
} |
|
|
|
// All gone now... |
|
Assert( predictables->GetPredictableCount() == 0 ); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::ReinitPredictables( void ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
// Go through all entities and init any eligible ones |
|
int i; |
|
int c = ClientEntityList().GetHighestEntityIndex(); |
|
for ( i = 0; i <= c; i++ ) |
|
{ |
|
C_BaseEntity *e = ClientEntityList().GetBaseEntity( i ); |
|
if ( !e ) |
|
continue; |
|
|
|
if ( e->GetPredictable() ) |
|
continue; |
|
|
|
e->CheckInitPredictable( "ReinitPredictables" ); |
|
} |
|
|
|
Msg( "Reinitialized %i predictable entities\n", |
|
predictables->GetPredictableCount() ); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::OnReceivedUncompressedPacket( void ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
m_nCommandsPredicted = 0; |
|
m_nServerCommandsAcknowledged = 0; |
|
m_nPreviousStartFrame = -1; |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : commands_acknowledged - |
|
// current_world_update_packet - |
|
// Output : void CPrediction::PreEntityPacketReceived |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::PreEntityPacketReceived ( int commands_acknowledged, int current_world_update_packet ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
#if defined( _DEBUG ) |
|
char sz[ 32 ]; |
|
Q_snprintf( sz, sizeof( sz ), "preentitypacket%d", commands_acknowledged ); |
|
PREDICTION_TRACKVALUECHANGESCOPE( sz ); |
|
#endif |
|
VPROF( "CPrediction::PreEntityPacketReceived" ); |
|
|
|
// Cache off incoming packet # |
|
m_nIncomingPacketNumber = current_world_update_packet; |
|
|
|
// Don't screw up memory of current player from history buffers if not filling in history buffers |
|
// during prediction!!! |
|
if ( !cl_predict->GetInt() ) |
|
{ |
|
ShutdownPredictables(); |
|
return; |
|
} |
|
|
|
C_BasePlayer *current = C_BasePlayer::GetLocalPlayer(); |
|
// No local player object? |
|
if ( !current ) |
|
return; |
|
|
|
// Transfer intermediate data from other predictables |
|
int c = predictables->GetPredictableCount(); |
|
int i; |
|
for ( i = 0; i < c; i++ ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( i ); |
|
if ( !ent ) |
|
continue; |
|
|
|
if ( !ent->GetPredictable() ) |
|
continue; |
|
|
|
ent->PreEntityPacketReceived( commands_acknowledged ); |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called for every packet received( could be multiple times per frame) |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::PostEntityPacketReceived( void ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
PREDICTION_TRACKVALUECHANGESCOPE( "postentitypacket" ); |
|
VPROF( "CPrediction::PostEntityPacketReceived" ); |
|
|
|
// Don't screw up memory of current player from history buffers if not filling in history buffers |
|
// during prediction!!! |
|
if ( !cl_predict->GetInt() ) |
|
return; |
|
|
|
C_BasePlayer *current = C_BasePlayer::GetLocalPlayer(); |
|
// No local player object? |
|
if ( !current ) |
|
return; |
|
|
|
// Transfer intermediate data from other predictables |
|
int c = predictables->GetPredictableCount(); |
|
int i; |
|
for ( i = 0; i < c; i++ ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( i ); |
|
if ( !ent ) |
|
continue; |
|
|
|
if ( !ent->GetPredictable() ) |
|
continue; |
|
|
|
ent->PostEntityPacketReceived(); |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *ent - |
|
// Output : static bool |
|
//----------------------------------------------------------------------------- |
|
bool CPrediction::ShouldDumpEntity( C_BaseEntity *ent ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
int dump_entity = cl_predictionentitydump.GetInt(); |
|
if ( dump_entity != -1 ) |
|
{ |
|
bool dump = false; |
|
if ( ent->entindex() == -1 ) |
|
{ |
|
dump = ( dump_entity == ent->entindex() ) ? true : false; |
|
} |
|
else |
|
{ |
|
dump = ( ent->entindex() == dump_entity ) ? true : false; |
|
} |
|
|
|
if ( !dump ) |
|
{ |
|
return false; |
|
} |
|
} |
|
else |
|
{ |
|
if ( cl_predictionentitydumpbyclass.GetString()[ 0 ] == 0 ) |
|
return false; |
|
|
|
if ( !FClassnameIs( ent, cl_predictionentitydumpbyclass.GetString() ) ) |
|
return false; |
|
} |
|
return true; |
|
#else |
|
return false; |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called at the end of the frame if any packets were received |
|
// Input : error_check - |
|
// last_predicted - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::PostNetworkDataReceived( int commands_acknowledged ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::PostNetworkDataReceived" ); |
|
|
|
bool error_check = ( commands_acknowledged > 0 ) ? true : false; |
|
#if defined( _DEBUG ) |
|
char szDebug[32]; |
|
Q_snprintf( szDebug, sizeof( szDebug ), "postnetworkdata%d", commands_acknowledged ); |
|
PREDICTION_TRACKVALUECHANGESCOPE( szDebug ); |
|
#endif |
|
#ifndef _XBOX |
|
CPDumpPanel *dump = GetPDumpPanel(); |
|
#endif |
|
//Msg( "%i/%i ack %i commands/slot\n", |
|
// gpGlobals->framecount, |
|
// gpGlobals->tickcount, |
|
// commands_acknowledged - 1 ); |
|
|
|
m_nServerCommandsAcknowledged += commands_acknowledged; |
|
m_bPreviousAckHadErrors = false; |
|
|
|
bool entityDumped = false; |
|
|
|
C_BasePlayer *current = C_BasePlayer::GetLocalPlayer(); |
|
// No local player object? |
|
if ( !current ) |
|
return; |
|
|
|
// Don't screw up memory of current player from history buffers if not filling in history buffers |
|
// during prediction!!! |
|
if ( cl_predict->GetInt() ) |
|
{ |
|
int showlist = cl_predictionlist.GetInt(); |
|
int totalsize = 0; |
|
int totalsize_intermediate = 0; |
|
|
|
con_nprint_t np; |
|
np.fixed_width_font = true; |
|
np.color[0] = 0.8f; |
|
np.color[1] = 1.0f; |
|
np.color[2] = 1.0f; |
|
np.time_to_live = 2.0f; |
|
|
|
// Transfer intermediate data from other predictables |
|
int c = predictables->GetPredictableCount(); |
|
int i; |
|
for ( i = 0; i < c; i++ ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( i ); |
|
if ( !ent ) |
|
continue; |
|
|
|
if ( ent->GetPredictable() ) |
|
{ |
|
if ( ent->PostNetworkDataReceived( m_nServerCommandsAcknowledged ) ) |
|
{ |
|
m_bPreviousAckHadErrors = true; |
|
} |
|
} |
|
|
|
if ( showlist ) |
|
{ |
|
char sz[32]; |
|
if ( ent->entindex() == -1 ) |
|
{ |
|
Q_snprintf( sz, sizeof( sz ), "handle %u", (unsigned int)ent->GetClientHandle().ToInt() ); |
|
} |
|
else |
|
{ |
|
Q_snprintf( sz, sizeof( sz ), "%i", ent->entindex() ); |
|
} |
|
|
|
np.index = i; |
|
|
|
if ( showlist >= 2 ) |
|
{ |
|
int size = GetClassMap().GetClassSize( ent->GetClassname() ); |
|
int intermediate_size = ent->GetIntermediateDataSize() * ( MULTIPLAYER_BACKUP + 1 ); |
|
|
|
engine->Con_NXPrintf( &np, "%15s %30s (%5i / %5i bytes): %15s", |
|
sz, |
|
ent->GetClassname(), |
|
size, |
|
intermediate_size, |
|
ent->GetPredictable() ? "predicted" : "client created" ); |
|
|
|
totalsize += size; |
|
totalsize_intermediate += intermediate_size; |
|
} |
|
else |
|
{ |
|
engine->Con_NXPrintf( &np, "%15s %30s: %15s", |
|
sz, |
|
ent->GetClassname(), |
|
ent->GetPredictable() ? "predicted" : "client created" ); |
|
} |
|
} |
|
#ifndef _XBOX |
|
if ( error_check && |
|
!entityDumped && |
|
dump && |
|
ShouldDumpEntity( ent ) ) |
|
{ |
|
entityDumped = true; |
|
dump->DumpEntity( ent, m_nServerCommandsAcknowledged ); |
|
} |
|
#endif |
|
} |
|
|
|
if ( showlist >= 2 ) |
|
{ |
|
np.index = i++; |
|
char sz1[32]; |
|
char sz2[32]; |
|
|
|
Q_strncpy( sz1, Q_pretifymem( (float)totalsize ), sizeof( sz1 ) ); |
|
Q_strncpy( sz2, Q_pretifymem( (float)totalsize_intermediate ), sizeof( sz2 ) ); |
|
|
|
engine->Con_NXPrintf( &np, "%15s %27s (%s / %s) %14s", |
|
"totals:", |
|
"", |
|
sz1, |
|
sz2, |
|
"" ); |
|
} |
|
|
|
// Zero out rest of list |
|
if ( showlist ) |
|
{ |
|
while ( i < 20 ) |
|
{ |
|
engine->Con_NPrintf( i, "" ); |
|
i++; |
|
} |
|
} |
|
|
|
if ( error_check ) |
|
{ |
|
CheckError( m_nServerCommandsAcknowledged ); |
|
} |
|
} |
|
|
|
// Can also look at regular entities |
|
#ifndef _XBOX |
|
int dumpentindex = cl_predictionentitydump.GetInt(); |
|
if ( dump && error_check && !entityDumped && dumpentindex != -1 ) |
|
{ |
|
int last_entity = ClientEntityList().GetHighestEntityIndex(); |
|
if ( dumpentindex >= 0 && dumpentindex <= last_entity ) |
|
{ |
|
C_BaseEntity *ent = ClientEntityList().GetBaseEntity( dumpentindex ); |
|
if ( ent ) |
|
{ |
|
dump->DumpEntity( ent, m_nServerCommandsAcknowledged ); |
|
entityDumped = true; |
|
} |
|
} |
|
} |
|
#endif |
|
if ( cl_predict->GetBool() != m_bOldCLPredictValue ) |
|
{ |
|
if ( !m_bOldCLPredictValue ) |
|
{ |
|
ReinitPredictables(); |
|
} |
|
|
|
m_nCommandsPredicted = 0; |
|
m_nServerCommandsAcknowledged = 0; |
|
m_nPreviousStartFrame = -1; |
|
} |
|
|
|
m_bOldCLPredictValue = cl_predict->GetInt(); |
|
|
|
#ifndef _XBOX |
|
if ( dump && error_check && !entityDumped ) |
|
{ |
|
dump->Clear(); |
|
} |
|
#endif |
|
#endif |
|
|
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Prepare for running prediction code |
|
// Input : *ucmd - |
|
// *from - |
|
// *pHelper - |
|
// &moveInput - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::SetupMove( C_BasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::SetupMove" ); |
|
|
|
move->m_bFirstRunOfFunctions = IsFirstTimePredicted(); |
|
|
|
move->m_nPlayerHandle = player->GetClientHandle(); |
|
move->m_vecVelocity = player->GetAbsVelocity(); |
|
move->SetAbsOrigin( player->GetNetworkOrigin() ); |
|
move->m_vecOldAngles = move->m_vecAngles; |
|
move->m_nOldButtons = player->m_Local.m_nOldButtons; |
|
move->m_flOldForwardMove = player->m_Local.m_flOldForwardMove; |
|
move->m_flClientMaxSpeed = player->m_flMaxspeed; |
|
|
|
move->m_vecAngles = ucmd->viewangles; |
|
move->m_vecViewAngles = ucmd->viewangles; |
|
move->m_nImpulseCommand = ucmd->impulse; |
|
move->m_nButtons = ucmd->buttons; |
|
|
|
CBaseEntity *pMoveParent = player->GetMoveParent(); |
|
if (!pMoveParent) |
|
{ |
|
move->m_vecAbsViewAngles = move->m_vecViewAngles; |
|
} |
|
else |
|
{ |
|
matrix3x4_t viewToParent, viewToWorld; |
|
AngleMatrix( move->m_vecViewAngles, viewToParent ); |
|
ConcatTransforms( pMoveParent->EntityToWorldTransform(), viewToParent, viewToWorld ); |
|
MatrixAngles( viewToWorld, move->m_vecAbsViewAngles ); |
|
} |
|
|
|
|
|
// Ingore buttons for movement if at controls |
|
if (player->GetFlags() & FL_ATCONTROLS) |
|
{ |
|
move->m_flForwardMove = 0; |
|
move->m_flSideMove = 0; |
|
move->m_flUpMove = 0; |
|
} |
|
else |
|
{ |
|
move->m_flForwardMove = ucmd->forwardmove; |
|
move->m_flSideMove = ucmd->sidemove; |
|
move->m_flUpMove = ucmd->upmove; |
|
} |
|
|
|
IClientVehicle *pVehicle = player->GetVehicle(); |
|
if (pVehicle) |
|
{ |
|
pVehicle->SetupMove( player, ucmd, pHelper, move ); |
|
} |
|
|
|
// Copy constraint information |
|
if ( player->m_hConstraintEntity ) |
|
move->m_vecConstraintCenter = player->m_hConstraintEntity->GetAbsOrigin(); |
|
else |
|
move->m_vecConstraintCenter = player->m_vecConstraintCenter; |
|
|
|
move->m_flConstraintRadius = player->m_flConstraintRadius; |
|
move->m_flConstraintWidth = player->m_flConstraintWidth; |
|
move->m_flConstraintSpeedFactor = player->m_flConstraintSpeedFactor; |
|
|
|
#ifdef HL2_CLIENT_DLL |
|
// Convert to HL2 data. |
|
C_BaseHLPlayer *pHLPlayer = static_cast<C_BaseHLPlayer*>( player ); |
|
Assert( pHLPlayer ); |
|
|
|
CHLMoveData *pHLMove = static_cast<CHLMoveData*>( move ); |
|
Assert( pHLMove ); |
|
|
|
pHLMove->m_bIsSprinting = pHLPlayer->IsSprinting(); |
|
#endif |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Finish running prediction code |
|
// Input : &move - |
|
// *to - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::FinishMove( C_BasePlayer *player, CUserCmd *ucmd, CMoveData *move ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::FinishMove" ); |
|
|
|
player->m_RefEHandle = move->m_nPlayerHandle; |
|
|
|
player->m_vecVelocity = move->m_vecVelocity; |
|
|
|
player->m_vecNetworkOrigin = move->GetAbsOrigin(); |
|
|
|
player->m_Local.m_nOldButtons = move->m_nButtons; |
|
|
|
|
|
// NOTE: Don't copy this. the movement code modifies its local copy but is not expecting to be authoritative |
|
//player->m_flMaxspeed = move->m_flClientMaxSpeed; |
|
|
|
m_hLastGround = player->GetGroundEntity(); |
|
|
|
player->SetLocalOrigin( move->GetAbsOrigin() ); |
|
|
|
IClientVehicle *pVehicle = player->GetVehicle(); |
|
if (pVehicle) |
|
{ |
|
pVehicle->FinishMove( player, ucmd, move ); |
|
} |
|
|
|
// Sanity checks |
|
if ( player->m_hConstraintEntity ) |
|
Assert( move->m_vecConstraintCenter == player->m_hConstraintEntity->GetAbsOrigin() ); |
|
else |
|
Assert( move->m_vecConstraintCenter == player->m_vecConstraintCenter ); |
|
Assert( move->m_flConstraintRadius == player->m_flConstraintRadius ); |
|
Assert( move->m_flConstraintWidth == player->m_flConstraintWidth ); |
|
Assert( move->m_flConstraintSpeedFactor == player->m_flConstraintSpeedFactor ); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called before any movement processing |
|
// Input : *player - |
|
// *cmd - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::StartCommand( C_BasePlayer *player, CUserCmd *cmd ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::StartCommand" ); |
|
|
|
CPredictableId::ResetInstanceCounters(); |
|
|
|
player->m_pCurrentCommand = cmd; |
|
C_BaseEntity::SetPredictionRandomSeed( cmd ); |
|
C_BaseEntity::SetPredictionPlayer( player ); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called after any movement processing |
|
// Input : *player - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::FinishCommand( C_BasePlayer *player ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::FinishCommand" ); |
|
|
|
player->m_pCurrentCommand = NULL; |
|
C_BaseEntity::SetPredictionRandomSeed( NULL ); |
|
C_BaseEntity::SetPredictionPlayer( NULL ); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called before player thinks |
|
// Input : *player - |
|
// thinktime - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::RunPreThink( C_BasePlayer *player ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::RunPreThink" ); |
|
|
|
// Run think functions on the player |
|
if ( !player->PhysicsRunThink() ) |
|
return; |
|
|
|
// Called every frame to let game rules do any specific think logic for the player |
|
// FIXME: Do we need to set up a client side version of the gamerules??? |
|
// g_pGameRules->PlayerThink( player ); |
|
|
|
player->PreThink(); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Runs the PLAYER's thinking code if time. There is some play in the exact time the think |
|
// function will be called, because it is called before any movement is done |
|
// in a frame. Not used for pushmove objects, because they must be exact. |
|
// Returns false if the entity removed itself. |
|
// Input : *ent - |
|
// frametime - |
|
// clienttimebase - |
|
// Output : void CPlayerMove::RunThink |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::RunThink (C_BasePlayer *player, double frametime ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::RunThink" ); |
|
|
|
int thinktick = player->GetNextThinkTick(); |
|
|
|
if ( thinktick <= 0 || thinktick > player->m_nTickBase ) |
|
return; |
|
|
|
player->SetNextThink( TICK_NEVER_THINK ); |
|
|
|
// Think |
|
player->Think(); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called after player movement |
|
// Input : *player - |
|
// thinktime - |
|
// frametime - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::RunPostThink( C_BasePlayer *player ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::RunPostThink" ); |
|
|
|
// Run post-think |
|
player->PostThink(); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Predicts a single movement command for player |
|
// Input : *moveHelper - |
|
// *player - |
|
// *u - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::RunCommand( C_BasePlayer *player, CUserCmd *ucmd, IMoveHelper *moveHelper ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::RunCommand" ); |
|
#if defined( _DEBUG ) |
|
char sz[ 32 ]; |
|
Q_snprintf( sz, sizeof( sz ), "runcommand%04d", ucmd->command_number ); |
|
PREDICTION_TRACKVALUECHANGESCOPE( sz ); |
|
#endif |
|
StartCommand( player, ucmd ); |
|
|
|
// Set globals appropriately |
|
gpGlobals->curtime = player->m_nTickBase * TICK_INTERVAL; |
|
gpGlobals->frametime = m_bEnginePaused ? 0 : TICK_INTERVAL; |
|
|
|
g_pGameMovement->StartTrackPredictionErrors( player ); |
|
|
|
// TODO |
|
// TODO: Check for impulse predicted? |
|
|
|
// Do weapon selection |
|
if ( ucmd->weaponselect != 0 ) |
|
{ |
|
C_BaseCombatWeapon *weapon = dynamic_cast< C_BaseCombatWeapon * >( CBaseEntity::Instance( ucmd->weaponselect ) ); |
|
if ( weapon ) |
|
{ |
|
player->SelectItem( weapon->GetName(), ucmd->weaponsubtype ); |
|
} |
|
} |
|
|
|
// Latch in impulse. |
|
IClientVehicle *pVehicle = player->GetVehicle(); |
|
if ( ucmd->impulse ) |
|
{ |
|
// Discard impulse commands unless the vehicle allows them. |
|
// FIXME: UsingStandardWeapons seems like a bad filter for this. |
|
// The flashlight is an impulse command, for example. |
|
if ( !pVehicle || player->UsingStandardWeaponsInVehicle() ) |
|
{ |
|
player->m_nImpulse = ucmd->impulse; |
|
} |
|
} |
|
|
|
// Get button states |
|
player->UpdateButtonState( ucmd->buttons ); |
|
|
|
// TODO |
|
// CheckMovingGround( player, ucmd->frametime ); |
|
|
|
// TODO |
|
// g_pMoveData->m_vecOldAngles = player->pl.v_angle; |
|
|
|
// Copy from command to player unless game .dll has set angle using fixangle |
|
// if ( !player->pl.fixangle ) |
|
{ |
|
player->SetLocalViewAngles( ucmd->viewangles ); |
|
} |
|
|
|
// Call standard client pre-think |
|
RunPreThink( player ); |
|
|
|
// Call Think if one is set |
|
RunThink( player, TICK_INTERVAL ); |
|
|
|
// Setup input. |
|
{ |
|
|
|
SetupMove( player, ucmd, moveHelper, g_pMoveData ); |
|
} |
|
|
|
// RUN MOVEMENT |
|
if ( !pVehicle ) |
|
{ |
|
Assert( g_pGameMovement ); |
|
g_pGameMovement->ProcessMovement( player, g_pMoveData ); |
|
} |
|
else |
|
{ |
|
pVehicle->ProcessMovement( player, g_pMoveData ); |
|
} |
|
|
|
FinishMove( player, ucmd, g_pMoveData ); |
|
|
|
RunPostThink( player ); |
|
|
|
g_pGameMovement->FinishTrackPredictionErrors( player ); |
|
|
|
FinishCommand( player ); |
|
|
|
if ( gpGlobals->frametime > 0 ) |
|
{ |
|
player->m_nTickBase++; |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: In the forward direction, creates rays straight down and determines the |
|
// height of the 'floor' hit for each forward test. Then, if the samples show that the |
|
// player is about to enter an up/down slope, sets *idealpitch to look up or down that slope |
|
// as appropriate |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::SetIdealPitch ( C_BasePlayer *player, const Vector& origin, const QAngle& angles, const Vector& viewheight ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
Vector forward; |
|
Vector top, bottom; |
|
float floor_height[MAX_FORWARD]; |
|
int i, j; |
|
float step, dir; |
|
int steps; |
|
trace_t tr; |
|
|
|
if ( player->GetGroundEntity() == NULL ) |
|
return; |
|
|
|
// Don't do this on the 360.. |
|
if ( IsX360() ) |
|
return; |
|
|
|
AngleVectors( angles, &forward ); |
|
forward[2] = 0; |
|
|
|
// Now move forward by 36, 48, 60, etc. units from the eye position and drop lines straight down |
|
// 160 or so units to see what's below |
|
for (i=0 ; i<MAX_FORWARD ; i++) |
|
{ |
|
VectorMA( origin, (i+3)*12, forward, top ); |
|
|
|
top[2] += viewheight[ 2 ]; |
|
|
|
VectorCopy( top, bottom ); |
|
|
|
bottom[2] -= 160; |
|
|
|
UTIL_TraceLine( top, bottom, MASK_SOLID, NULL, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); |
|
|
|
// looking at a wall, leave ideal the way it was |
|
if ( tr.allsolid ) |
|
return; |
|
|
|
// near a dropoff/ledge |
|
if ( tr.fraction == 1 ) |
|
return; |
|
|
|
floor_height[i] = top[2] + tr.fraction*( bottom[2] - top[2] ); |
|
} |
|
|
|
dir = 0; |
|
steps = 0; |
|
for (j=1 ; j<i ; j++) |
|
{ |
|
step = floor_height[j] - floor_height[j-1]; |
|
if (step > -ON_EPSILON && step < ON_EPSILON) |
|
continue; |
|
|
|
if (dir && ( step-dir > ON_EPSILON || step-dir < -ON_EPSILON ) ) |
|
return; // mixed changes |
|
|
|
steps++; |
|
dir = step; |
|
} |
|
|
|
if (!dir) |
|
{ |
|
m_flIdealPitch = 0; |
|
return; |
|
} |
|
|
|
if (steps < 2) |
|
return; |
|
m_flIdealPitch = -dir * cl_idealpitchscale.GetFloat(); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Walk backward through predictables looking for ClientCreated entities |
|
// such as projectiles which were |
|
// 1) not actually ack'd by the server or |
|
// 2) were ack'd and made dormant and can now safely be removed |
|
// Input : last_command_packet - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::RemoveStalePredictedEntities( int sequence_number ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::RemoveStalePredictedEntities" ); |
|
|
|
int oldest_allowable_command = sequence_number; |
|
|
|
// Walk backward due to deletion from UtlVector |
|
int c = predictables->GetPredictableCount(); |
|
int i; |
|
for ( i = c - 1; i >= 0; i-- ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( i ); |
|
if ( !ent ) |
|
continue; |
|
|
|
// Don't do anything to truly predicted things (like player and weapons ) |
|
if ( ent->GetPredictable() ) |
|
continue; |
|
|
|
// What's left should be things like projectiles that are just waiting to be "linked" |
|
// to their server counterpart and deleted |
|
Assert( ent->IsClientCreated() ); |
|
if ( !ent->IsClientCreated() ) |
|
continue; |
|
|
|
// Snag the PredictionContext |
|
PredictionContext *ctx = ent->m_pPredictionContext; |
|
if ( !ctx ) |
|
{ |
|
continue; |
|
} |
|
|
|
// If it was ack'd then the server sent us the entity. |
|
// Leave it unless it wasn't made dormant this frame, in |
|
// which case it can be removed now |
|
if ( ent->m_PredictableID.GetAcknowledged() ) |
|
{ |
|
// Hasn't become dormant yet!!! |
|
if ( !ent->IsDormantPredictable() ) |
|
{ |
|
Assert( 0 ); |
|
continue; |
|
} |
|
|
|
// Still gets to live till next frame |
|
if ( ent->BecameDormantThisPacket() ) |
|
continue; |
|
|
|
C_BaseEntity *serverEntity = ctx->m_hServerEntity; |
|
if ( serverEntity ) |
|
{ |
|
// Notify that it's going to go away |
|
serverEntity->OnPredictedEntityRemove( true, ent ); |
|
} |
|
} |
|
else |
|
{ |
|
// Check context to see if it's too old? |
|
int command_entity_creation_happened = ctx->m_nCreationCommandNumber; |
|
// Give it more time to live...not time to kill it yet |
|
if ( command_entity_creation_happened > oldest_allowable_command ) |
|
continue; |
|
|
|
// If the client predicted the KILLME flag it's possible |
|
// that entity had such a short life that it actually |
|
// never was sent to us. In that case, just let it die a silent death |
|
if ( !ent->IsEFlagSet( EFL_KILLME ) ) |
|
{ |
|
if ( cl_showerror.GetInt() != 0 ) |
|
{ |
|
// It's bogus, server doesn't have a match, destroy it: |
|
Msg( "Removing unack'ed predicted entity: %s created %s(%i) id == %s : %p\n", |
|
ent->GetClassname(), |
|
ctx->m_pszCreationModule, |
|
ctx->m_nCreationLineNumber, |
|
ent->m_PredictableID.Describe(), |
|
ent ); |
|
} |
|
} |
|
|
|
// FIXME: Do we need an OnPredictedEntityRemove call with an "it's not valid" |
|
// flag of some kind |
|
} |
|
|
|
// This will remove it from predictables list and will also free the entity, etc. |
|
ent->Release(); |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::RestoreOriginalEntityState( void ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::RestoreOriginalEntityState" ); |
|
PREDICTION_TRACKVALUECHANGESCOPE( "restore" ); |
|
|
|
Assert( C_BaseEntity::IsAbsRecomputationsEnabled() ); |
|
|
|
// Transfer intermediate data from other predictables |
|
int pc = predictables->GetPredictableCount(); |
|
int p; |
|
for ( p = 0; p < pc; p++ ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( p ); |
|
if ( !ent ) |
|
continue; |
|
|
|
if ( ent->GetPredictable() ) |
|
{ |
|
ent->RestoreData( "RestoreOriginalEntityState", C_BaseEntity::SLOT_ORIGINALDATA, PC_EVERYTHING ); |
|
} |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : current_command - |
|
// curtime - |
|
// *cmd - |
|
// *tcmd - |
|
// *localPlayer - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::RunSimulation( int current_command, float curtime, CUserCmd *cmd, C_BasePlayer *localPlayer ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::RunSimulation" ); |
|
|
|
Assert( localPlayer ); |
|
C_CommandContext *ctx = localPlayer->GetCommandContext(); |
|
Assert( ctx ); |
|
|
|
ctx->needsprocessing = true; |
|
ctx->cmd = *cmd; |
|
ctx->command_number = current_command; |
|
|
|
IPredictionSystem::SuppressEvents( !IsFirstTimePredicted() ); |
|
|
|
int i; |
|
|
|
// Make sure simulation occurs at most once per entity per usercmd |
|
for ( i = 0; i < predictables->GetPredictableCount(); i++ ) |
|
{ |
|
C_BaseEntity *entity = predictables->GetPredictable( i ); |
|
if ( entity ) |
|
{ |
|
entity->m_nSimulationTick = -1; |
|
} |
|
} |
|
|
|
// Don't used cached numpredictables since entities can be created mid-prediction by the player |
|
for ( i = 0; i < predictables->GetPredictableCount(); i++ ) |
|
{ |
|
// Always reset |
|
gpGlobals->curtime = curtime; |
|
gpGlobals->frametime = m_bEnginePaused ? 0 : TICK_INTERVAL; |
|
|
|
C_BaseEntity *entity = predictables->GetPredictable( i ); |
|
|
|
if ( !entity ) |
|
continue; |
|
|
|
bool islocal = ( localPlayer == entity ) ? true : false; |
|
|
|
// Local player simulates first, if this assert fires then the predictables list isn't sorted |
|
// correctly (or we started predicting C_World???) |
|
if ( islocal ) |
|
{ |
|
Assert( i == 0 ); |
|
} |
|
|
|
// Player can't be this so cull other entities here |
|
if ( entity->GetFlags() & FL_STATICPROP ) |
|
{ |
|
continue; |
|
} |
|
|
|
// Player is not actually in the m_SimulatedByThisPlayer list, of course |
|
if ( entity->IsPlayerSimulated() ) |
|
{ |
|
continue; |
|
} |
|
|
|
if ( AddDataChangeEvent( entity, DATA_UPDATE_DATATABLE_CHANGED, &entity->m_DataChangeEventRef ) ) |
|
{ |
|
entity->OnPreDataChanged( DATA_UPDATE_DATATABLE_CHANGED ); |
|
} |
|
|
|
// Certain entities can be created locally and if so created, should be |
|
// simulated until a network update arrives |
|
if ( entity->IsClientCreated() ) |
|
{ |
|
// Only simulate these on new usercmds |
|
if ( !IsFirstTimePredicted() ) |
|
continue; |
|
|
|
entity->PhysicsSimulate(); |
|
} |
|
else |
|
{ |
|
entity->PhysicsSimulate(); |
|
} |
|
|
|
// Don't update last networked data here!!! |
|
entity->OnLatchInterpolatedVariables( LATCH_SIMULATION_VAR | LATCH_ANIMATION_VAR | INTERPOLATE_OMIT_UPDATE_LAST_NETWORKED ); |
|
} |
|
|
|
// Always reset after running command |
|
IPredictionSystem::SuppressEvents( false ); |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::Untouch( void ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
int numpredictables = predictables->GetPredictableCount(); |
|
|
|
// Loop through all entities again, checking their untouch if flagged to do so |
|
int i; |
|
for ( i = 0; i < numpredictables; i++ ) |
|
{ |
|
C_BaseEntity *entity = predictables->GetPredictable( i ); |
|
if ( !entity ) |
|
continue; |
|
|
|
if ( !entity->GetCheckUntouch() ) |
|
continue; |
|
|
|
entity->PhysicsCheckForEntityUntouch(); |
|
} |
|
#endif |
|
} |
|
|
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void InvalidateEFlagsRecursive( C_BaseEntity *pEnt, int nDirtyFlags, int nChildFlags = 0 ) |
|
{ |
|
pEnt->AddEFlags( nDirtyFlags ); |
|
nDirtyFlags |= nChildFlags; |
|
for (CBaseEntity *pChild = pEnt->FirstMoveChild(); pChild; pChild = pChild->NextMovePeer()) |
|
{ |
|
InvalidateEFlagsRecursive( pChild, nDirtyFlags ); |
|
} |
|
} |
|
#endif |
|
|
|
void CPrediction::StorePredictionResults( int predicted_frame ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::StorePredictionResults" ); |
|
PREDICTION_TRACKVALUECHANGESCOPE( "save" ); |
|
|
|
int i; |
|
int numpredictables = predictables->GetPredictableCount(); |
|
|
|
// Now save off all of the results |
|
for ( i = 0; i < numpredictables; i++ ) |
|
{ |
|
C_BaseEntity *entity = predictables->GetPredictable( i ); |
|
if ( !entity ) |
|
continue; |
|
|
|
// Certain entities can be created locally and if so created, should be |
|
// simulated until a network update arrives |
|
if ( !entity->GetPredictable() ) |
|
continue; |
|
|
|
// FIXME: The lack of this call inexplicably actually creates prediction errors |
|
InvalidateEFlagsRecursive( entity, EFL_DIRTY_ABSTRANSFORM | EFL_DIRTY_ABSVELOCITY | EFL_DIRTY_ABSANGVELOCITY ); |
|
|
|
entity->SaveData( "StorePredictionResults", predicted_frame, PC_EVERYTHING ); |
|
} |
|
#endif |
|
} |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : slots_to_remove - |
|
// previous_last_slot - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::ShiftIntermediateDataForward( int slots_to_remove, int number_of_commands_run ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::ShiftIntermediateDataForward" ); |
|
PREDICTION_TRACKVALUECHANGESCOPE( "shift" ); |
|
|
|
C_BasePlayer *current = C_BasePlayer::GetLocalPlayer(); |
|
// No local player object? |
|
if ( !current ) |
|
return; |
|
|
|
// Don't screw up memory of current player from history buffers if not filling in history buffers |
|
// during prediction!!! |
|
if ( !cl_predict->GetInt() ) |
|
return; |
|
|
|
int c = predictables->GetPredictableCount(); |
|
int i; |
|
for ( i = 0; i < c; i++ ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( i ); |
|
if ( !ent ) |
|
continue; |
|
|
|
if ( !ent->GetPredictable() ) |
|
continue; |
|
|
|
ent->ShiftIntermediateDataForward( slots_to_remove, number_of_commands_run ); |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : predicted_frame - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::RestoreEntityToPredictedFrame( int predicted_frame ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::RestoreEntityToPredictedFrame" ); |
|
PREDICTION_TRACKVALUECHANGESCOPE( "restoretopred" ); |
|
|
|
C_BasePlayer *current = C_BasePlayer::GetLocalPlayer(); |
|
// No local player object? |
|
if ( !current ) |
|
return; |
|
|
|
// Don't screw up memory of current player from history buffers if not filling in history buffers |
|
// during prediction!!! |
|
if ( !cl_predict->GetInt() ) |
|
return; |
|
|
|
int c = predictables->GetPredictableCount(); |
|
int i; |
|
for ( i = 0; i < c; i++ ) |
|
{ |
|
C_BaseEntity *ent = predictables->GetPredictable( i ); |
|
if ( !ent ) |
|
continue; |
|
|
|
if ( !ent->GetPredictable() ) |
|
continue; |
|
|
|
ent->RestoreData( "RestoreEntityToPredictedFrame", predicted_frame, PC_EVERYTHING ); |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Computes starting destination for intermediate prediction data results and |
|
// does any fixups required by network optimization |
|
// Input : received_new_world_update - |
|
// incoming_acknowledged - |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int CPrediction::ComputeFirstCommandToExecute( bool received_new_world_update, int incoming_acknowledged, int outgoing_command ) |
|
{ |
|
int destination_slot = 1; |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
int skipahead = 0; |
|
|
|
// If we didn't receive a new update ( or we received an update that didn't ack any new CUserCmds -- |
|
// so for the player it should be just like receiving no update ), just jump right up to the very |
|
// last command we created for this very frame since we probably wouldn't have had any errors without |
|
// being notified by the server of such a case. |
|
// NOTE: received_new_world_update only gets set to false if cl_pred_optimize >= 1 |
|
if ( !received_new_world_update || !m_nServerCommandsAcknowledged ) |
|
{ |
|
// this is where we would normally start |
|
int start = incoming_acknowledged + 1; |
|
// outgoing_command is where we really want to start |
|
skipahead = MAX( 0, ( outgoing_command - start ) ); |
|
// Don't start past the last predicted command, though, or we'll get prediction errors |
|
skipahead = MIN( skipahead, m_nCommandsPredicted ); |
|
|
|
// Always restore since otherwise we might start prediction using an "interpolated" value instead of a purely predicted value |
|
RestoreEntityToPredictedFrame( skipahead - 1 ); |
|
|
|
//Msg( "%i/%i no world, skip to %i restore from slot %i\n", |
|
// gpGlobals->framecount, |
|
// gpGlobals->tickcount, |
|
// skipahead, |
|
// skipahead - 1 ); |
|
} |
|
else |
|
{ |
|
#ifdef STAGING_ONLY |
|
int nPredictedLimit = cl_pred_optimize_prefer_server_data.GetBool() ? m_nCommandsPredicted - 1 : m_nCommandsPredicted; |
|
#else |
|
int nPredictedLimit = m_nCommandsPredicted; |
|
#endif // STAGING_ONLY |
|
// Otherwise, there is a second optimization, wherein if we did receive an update, but no |
|
// values differed (or were outside their epsilon) and the server actually acknowledged running |
|
// one or more commands, then we can revert the entity to the predicted state from last frame, |
|
// shift the # of commands worth of intermediate state off of front the intermediate state array, and |
|
// only predict the usercmd from the latest render frame. |
|
if ( cl_pred_optimize.GetInt() >= 2 && |
|
!m_bPreviousAckHadErrors && |
|
m_nCommandsPredicted > 0 && |
|
m_nServerCommandsAcknowledged <= nPredictedLimit ) |
|
{ |
|
// Copy all of the previously predicted data back into entity so we can skip repredicting it |
|
// This is the final slot that we previously predicted |
|
RestoreEntityToPredictedFrame( m_nCommandsPredicted - 1 ); |
|
|
|
// Shift intermediate state blocks down by # of commands ack'd |
|
ShiftIntermediateDataForward( m_nServerCommandsAcknowledged, m_nCommandsPredicted ); |
|
|
|
// Only predict new commands (note, this should be the same number that we could compute |
|
// above based on outgoing_command - incoming_acknowledged - 1 |
|
skipahead = ( m_nCommandsPredicted - m_nServerCommandsAcknowledged ); |
|
|
|
//Msg( "%i/%i optimize2, skip to %i restore from slot %i\n", |
|
// gpGlobals->framecount, |
|
// gpGlobals->tickcount, |
|
// skipahead, |
|
// m_nCommandsPredicted - 1 ); |
|
} |
|
else |
|
{ |
|
if ( m_bPreviousAckHadErrors ) |
|
{ |
|
C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer(); |
|
|
|
// If an entity gets a prediction error, then we want to clear out its interpolated variables |
|
// so we don't mix different samples at the same timestamps. We subtract 1 tick interval here because |
|
// if we don't, we'll have 3 interpolation entries with the same timestamp as this predicted |
|
// frame, so we won't be able to interpolate (which leads to jerky movement in the player when |
|
// ANY entity like your gun gets a prediction error). |
|
float flPrev = gpGlobals->curtime; |
|
gpGlobals->curtime = pLocalPlayer->GetTimeBase() - TICK_INTERVAL; |
|
|
|
for ( int i = 0; i < predictables->GetPredictableCount(); i++ ) |
|
{ |
|
C_BaseEntity *entity = predictables->GetPredictable( i ); |
|
if ( entity ) |
|
{ |
|
entity->ResetLatched(); |
|
} |
|
} |
|
|
|
gpGlobals->curtime = flPrev; |
|
} |
|
} |
|
} |
|
|
|
destination_slot += skipahead; |
|
|
|
// Always reset these values now that we handled them |
|
m_nCommandsPredicted = 0; |
|
m_bPreviousAckHadErrors = false; |
|
m_nServerCommandsAcknowledged = 0; |
|
#endif |
|
return destination_slot; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Actually does the prediction work, returns false if an error occurred |
|
//----------------------------------------------------------------------------- |
|
bool CPrediction::PerformPrediction( bool received_new_world_update, C_BasePlayer *localPlayer, |
|
int incoming_acknowledged, int outgoing_command ) |
|
{ |
|
MDLCACHE_CRITICAL_SECTION(); |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF( "CPrediction::PerformPrediction" ); |
|
|
|
// This makes sure , tahe we are allwoed to sample the world when it may not be ready to be sampled |
|
Assert( C_BaseEntity::IsAbsQueriesValid() ); |
|
Assert( C_BaseEntity::IsAbsRecomputationsEnabled() ); |
|
|
|
m_bInPrediction = true; |
|
|
|
// undo interpolation changes for entities we stand on |
|
C_BaseEntity *entity = localPlayer->GetGroundEntity(); |
|
|
|
while ( entity && entity->entindex() > 0) |
|
{ |
|
entity->MoveToLastReceivedPosition(); |
|
// undo changes for moveparents too |
|
entity = entity->GetMoveParent(); |
|
} |
|
|
|
// Start at command after last one server has processed and |
|
// go until we get to targettime or we run out of new commands |
|
int i = ComputeFirstCommandToExecute( received_new_world_update, incoming_acknowledged, outgoing_command ); |
|
|
|
//Msg( "%i/%i tickbase %i\n", |
|
// gpGlobals->framecount, |
|
// gpGlobals->tickcount, |
|
// localPlayer->m_nTickBase ); |
|
|
|
//for ( int k = 1; k < i; k++ ) |
|
//{ |
|
// Msg( "%i/%i Skip final tick %i into slot %i\n", |
|
// gpGlobals->framecount, gpGlobals->tickcount, |
|
// localPlayer->m_nTickBase - i + k + 1, |
|
// k - 1 ); |
|
//} |
|
|
|
Assert( i >= 1 ); |
|
while ( true ) |
|
{ |
|
// Incoming_acknowledged is the last usercmd the server acknowledged having acted upon |
|
int current_command = incoming_acknowledged + i; |
|
|
|
// We've caught up to the current command. |
|
if ( current_command > outgoing_command ) |
|
break; |
|
|
|
if ( i >= MULTIPLAYER_BACKUP ) |
|
break; |
|
|
|
CUserCmd *cmd = input->GetUserCmd( current_command ); |
|
|
|
if ( !cmd ) |
|
{ |
|
break; |
|
} |
|
|
|
|
|
// Is this the first time predicting this |
|
m_bFirstTimePredicted = !cmd->hasbeenpredicted; |
|
|
|
// Set globals appropriately |
|
float curtime = ( localPlayer->m_nTickBase ) * TICK_INTERVAL; |
|
|
|
RunSimulation( current_command, curtime, cmd, localPlayer ); |
|
|
|
gpGlobals->curtime = curtime; |
|
gpGlobals->frametime = m_bEnginePaused ? 0 : TICK_INTERVAL; |
|
|
|
// Call untouch on any entities no longer predicted to be touching |
|
Untouch(); |
|
|
|
// Store intermediate data into appropriate slot |
|
StorePredictionResults( i - 1 ); // Note that I starts at 1 |
|
|
|
m_nCommandsPredicted = i; |
|
|
|
if ( current_command == outgoing_command ) |
|
{ |
|
localPlayer->m_nFinalPredictedTick = localPlayer->m_nTickBase; |
|
} |
|
/* |
|
if ( 0 ) |
|
{ |
|
localPlayer->m_nFinalPredictedTick = localPlayer->m_nTickBase; |
|
Msg( "%i/%i Latch final tick %i start == %i into slot %i\n", |
|
gpGlobals->framecount, gpGlobals->tickcount, |
|
localPlayer->m_nFinalPredictedTick, |
|
localPlayer->m_nFinalPredictedTick - i, |
|
i - 1 ); |
|
} |
|
*/ |
|
|
|
/* |
|
Msg( "%i/%i Predicted command %i tickbase == %i first %s\n", |
|
gpGlobals->framecount, gpGlobals->tickcount, |
|
m_nCommandsPredicted, |
|
localPlayer->m_nTickBase, |
|
m_bFirstTimePredicted ? "yes" : "no" ); |
|
*/ |
|
|
|
// Mark that we issued any needed sounds, of not done already |
|
cmd->hasbeenpredicted = true; |
|
|
|
// Copy the state over. |
|
i++; |
|
} |
|
|
|
// Msg( "%i : predicted %i commands forward, %i ack'd last frame, had errors %s\n", |
|
// gpGlobals->tickcount, |
|
// m_nCommandsPredicted, |
|
// m_nServerCommandsAcknowledged, |
|
// m_bPreviousAckHadErrors ? "true" : "false" ); |
|
|
|
|
|
m_bInPrediction = false; |
|
|
|
|
|
// Somehow we looped past the end of the list (severe lag), don't predict at all |
|
if ( i > MULTIPLAYER_BACKUP ) |
|
{ |
|
return false; |
|
} |
|
#endif |
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : startframe - |
|
// validframe - |
|
// incoming_acknowledged - |
|
// outgoing_command - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::Update( int startframe, bool validframe, |
|
int incoming_acknowledged, int outgoing_command ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
VPROF_BUDGET( "CPrediction::Update", VPROF_BUDGETGROUP_PREDICTION ); |
|
|
|
m_bEnginePaused = engine->IsPaused(); |
|
|
|
bool received_new_world_update = true; |
|
|
|
// Still starting at same frame, so make sure we don't do extra prediction ,etc. |
|
if ( ( m_nPreviousStartFrame == startframe ) && |
|
cl_pred_optimize.GetBool() && |
|
cl_predict->GetInt() ) |
|
{ |
|
received_new_world_update = false; |
|
} |
|
|
|
m_nPreviousStartFrame = startframe; |
|
|
|
// Save off current timer values, etc. |
|
CGlobalVarsBase saveVars(true); |
|
saveVars = *gpGlobals; |
|
|
|
_Update( received_new_world_update, validframe, incoming_acknowledged, outgoing_command ); |
|
|
|
// Restore current timer values, etc. |
|
*gpGlobals = saveVars; |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Do the dirty deed of predicting the local player |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::_Update( bool received_new_world_update, bool validframe, |
|
int incoming_acknowledged, int outgoing_command ) |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer(); |
|
if ( !localPlayer ) |
|
return; |
|
|
|
// Always using current view angles no matter what |
|
// NOTE: ViewAngles are always interpreted as being *relative* to the player |
|
QAngle viewangles; |
|
engine->GetViewAngles( viewangles ); |
|
localPlayer->SetLocalAngles( viewangles ); |
|
|
|
if ( !validframe ) |
|
{ |
|
return; |
|
} |
|
|
|
// If we are not doing prediction, copy authoritative value into velocity and angle. |
|
if ( !cl_predict->GetInt() ) |
|
{ |
|
// When not predicting, we at least must make sure the player |
|
// view angles match the view angles... |
|
localPlayer->SetLocalViewAngles( viewangles ); |
|
return; |
|
} |
|
|
|
// This is cheesy, but if we have entities that are parented to attachments on other entities, then |
|
// it'll wind up needing to get a bone transform. |
|
{ |
|
C_BaseAnimating::InvalidateBoneCaches(); |
|
C_BaseAnimating::AutoAllowBoneAccess boneaccess( true, true ); |
|
|
|
// Remove any purely client predicted entities that were left "dangling" because the |
|
// server didn't acknowledge them or which can now safely be removed |
|
RemoveStalePredictedEntities( incoming_acknowledged ); |
|
|
|
// Restore objects back to "pristine" state from last network/world state update |
|
if ( received_new_world_update ) |
|
{ |
|
RestoreOriginalEntityState(); |
|
} |
|
|
|
if ( !PerformPrediction( received_new_world_update, localPlayer, incoming_acknowledged, outgoing_command ) ) |
|
return; |
|
} |
|
|
|
// Overwrite predicted angles with the actual view angles |
|
localPlayer->SetLocalAngles( viewangles ); |
|
|
|
// This allows us to sample the world when it may not be ready to be sampled |
|
Assert( C_BaseEntity::IsAbsQueriesValid() ); |
|
|
|
// FIXME: What about hierarchy here?!? |
|
SetIdealPitch( localPlayer, localPlayer->GetLocalOrigin(), localPlayer->GetLocalAngles(), localPlayer->m_vecViewOffset ); |
|
#endif |
|
} |
|
|
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CPrediction::IsFirstTimePredicted( void ) const |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
return m_bFirstTimePredicted; |
|
#else |
|
return false; |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : org - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::GetViewOrigin( Vector& org ) |
|
{ |
|
C_BasePlayer *player = C_BasePlayer::GetLocalPlayer(); |
|
if ( !player ) |
|
{ |
|
org.Init(); |
|
} |
|
else |
|
{ |
|
org = player->GetLocalOrigin(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : org - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::SetViewOrigin( Vector& org ) |
|
{ |
|
C_BasePlayer *player = C_BasePlayer::GetLocalPlayer(); |
|
if ( !player ) |
|
return; |
|
|
|
player->SetLocalOrigin( org ); |
|
player->m_vecNetworkOrigin = org; |
|
|
|
player->m_iv_vecOrigin.Reset(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : ang - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::GetViewAngles( QAngle& ang ) |
|
{ |
|
C_BasePlayer *player = C_BasePlayer::GetLocalPlayer(); |
|
if ( !player ) |
|
{ |
|
ang.Init(); |
|
} |
|
else |
|
{ |
|
ang = player->GetLocalAngles(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : ang - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::SetViewAngles( QAngle& ang ) |
|
{ |
|
C_BasePlayer *player = C_BasePlayer::GetLocalPlayer(); |
|
if ( !player ) |
|
return; |
|
|
|
player->SetViewAngles( ang ); |
|
player->m_iv_angRotation.Reset(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : ang - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::GetLocalViewAngles( QAngle& ang ) |
|
{ |
|
C_BasePlayer *player = C_BasePlayer::GetLocalPlayer(); |
|
if ( !player ) |
|
{ |
|
ang.Init(); |
|
} |
|
else |
|
{ |
|
ang = player->pl.v_angle; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : ang - |
|
//----------------------------------------------------------------------------- |
|
void CPrediction::SetLocalViewAngles( QAngle& ang ) |
|
{ |
|
C_BasePlayer *player = C_BasePlayer::GetLocalPlayer(); |
|
if ( !player ) |
|
return; |
|
|
|
player->SetLocalViewAngles( ang ); |
|
} |
|
|
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
//----------------------------------------------------------------------------- |
|
// Purpose: For determining that predicted creation entities are un-acked and should |
|
// be deleted |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int CPrediction::GetIncomingPacketNumber( void ) const |
|
{ |
|
return m_nIncomingPacketNumber; |
|
} |
|
#endif |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CPrediction::InPrediction( void ) const |
|
{ |
|
#if !defined( NO_ENTITY_PREDICTION ) |
|
return m_bInPrediction; |
|
#else |
|
return false; |
|
#endif |
|
}
|
|
|