Kamay Xutax
fbc45f4a64
This check permits to fix interpolation problems on the local player that valve has been (fucking finally) caring about on counter-strike 2. To recall the original issue, the problem that Valve cared about is that interpolation had some problems with interpolating the local player because the screen would never in the first place match the tick "screen", because interpolation amount could never reach 0.0 or 1.0 Valve solution was to introduce bugs with lag compensating the local player and made the game worse, introducing a new way for cheaters to cheat even more on their games. I'm joking, but you can clearly see the outcome anyway. My solution is to simply set interpolation amount to 0.0 when a tick arrives. So when we shoot, we get the frame we shot with an interpolation amount at 0.0, perfectly aligned to user commands which is ideal for us. It might look a bit more unsmooth with lower fps but with high enough fps, the issue goes away anyway. It's not very noticeable which is very nice for us. No need to lag compensate the local player anymore !
1898 lines
53 KiB
C++
1898 lines
53 KiB
C++
//========= Copyright Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose:
|
|
//
|
|
// $NoKeywords: $
|
|
//=============================================================================//
|
|
#include "cbase.h"
|
|
#include "prediction.h"
|
|
#include "cdll_client_int.h"
|
|
#include "igamemovement.h"
|
|
#include "mathlib/mathlib.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)." );
|
|
|
|
#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 sz[ 32 ];
|
|
Q_snprintf( sz, sizeof( sz ), "postnetworkdata%d", commands_acknowledged );
|
|
PREDICTION_TRACKVALUECHANGESCOPE( sz );
|
|
#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_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->SetLocalVelocity(move->m_vecVelocity);
|
|
|
|
player->m_vecNetworkOrigin = move->GetAbsOrigin();
|
|
|
|
player->m_Local.m_nOldButtons = move->m_nButtons;
|
|
|
|
float pitch = move->m_vecAngles[PITCH];
|
|
|
|
if (pitch > 180.0f)
|
|
{
|
|
pitch -= 360.0f;
|
|
}
|
|
|
|
pitch = clamp(pitch, -90.f, 90.f);
|
|
|
|
move->m_vecAngles[PITCH] = pitch;
|
|
|
|
player->SetPoseParameter(player->LookupPoseParameter("body_pitch"), pitch);
|
|
|
|
player->SetLocalAngles(move->m_vecAngles);
|
|
|
|
// 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
|
|
}
|
|
|
|
void CPrediction::CheckMovingGround( CBasePlayer *player, double frametime )
|
|
{
|
|
VPROF( "CPrediction::CheckMovingGround()" );
|
|
|
|
CBaseEntity *groundentity;
|
|
|
|
if ( player->GetFlags() & FL_ONGROUND )
|
|
{
|
|
groundentity = player->GetGroundEntity();
|
|
if ( groundentity && ( groundentity->GetFlags() & FL_CONVEYOR) )
|
|
{
|
|
Vector vecNewVelocity;
|
|
groundentity->GetGroundVelocityToApply( vecNewVelocity );
|
|
|
|
if ( player->GetFlags() & FL_BASEVELOCITY )
|
|
{
|
|
vecNewVelocity += player->GetBaseVelocity();
|
|
}
|
|
player->SetBaseVelocity( vecNewVelocity );
|
|
player->AddFlag( FL_BASEVELOCITY );
|
|
}
|
|
}
|
|
|
|
if ( !( player->GetFlags() & FL_BASEVELOCITY ) )
|
|
{
|
|
// Apply momentum (add in half of the previous frame of velocity first)
|
|
player->ApplyAbsVelocityImpulse( (1.0 + ( frametime * 0.5 )) * player->GetBaseVelocity() );
|
|
player->SetBaseVelocity( vec3_origin );
|
|
}
|
|
|
|
player->RemoveFlag( FL_BASEVELOCITY );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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 );
|
|
|
|
g_pGameMovement->StartTrackPredictionErrors( player );
|
|
|
|
gpGlobals->frametime = m_bEnginePaused ? 0 : TICK_INTERVAL;
|
|
// Run post think after PreThink/Think function, this makes a room space for local interpolation.
|
|
gpGlobals->curtime = (player->m_nTickBase - 1) * TICK_INTERVAL;
|
|
|
|
RunPostThink( player );
|
|
|
|
gpGlobals->curtime = player->m_nTickBase * TICK_INTERVAL;
|
|
|
|
// 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 );
|
|
|
|
CheckMovingGround( player, gpGlobals->frametime );
|
|
|
|
g_pMoveData->m_vecOldAngles = player->pl.v_angle;
|
|
|
|
// TODO_ENHANCED: this will be implemented with trigger prediction update
|
|
// // Copy from command to player unless game .dll has set angle using fixangle
|
|
// if ( player->pl.fixangle == FIXANGLE_NONE )
|
|
// {
|
|
// player->pl.v_angle = ucmd->viewangles;
|
|
// }
|
|
// else if( player->pl.fixangle == FIXANGLE_RELATIVE )
|
|
// {
|
|
// player->pl.v_angle = ucmd->viewangles + player->pl.anglechange;
|
|
// }
|
|
|
|
// 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 );
|
|
|
|
moveHelper->ProcessImpacts();
|
|
|
|
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
|
|
{
|
|
// 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 <= m_nCommandsPredicted )
|
|
{
|
|
// 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
|
|
}
|