2242 lines
66 KiB
C++
2242 lines
66 KiB
C++
//========= Copyright 1996-2005, 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
|
|
|
|
#ifdef PORTAL2
|
|
#include "c_portal_player.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 )
|
|
|
|
extern ConVar cl_predictphysics;
|
|
|
|
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", FCVAR_RELEASE, "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)." );
|
|
|
|
static ConVar cl_pred_doresetlatch( "cl_pred_doresetlatch", "1", 0 );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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
|
|
|
|
extern IGameMovement *g_pGameMovement;
|
|
extern CMoveData *g_pMoveData;
|
|
|
|
void COM_Log( const char *pszFile, PRINTF_FORMAT_STRING const char *fmt, ...) FMTFUNCTION( 2, 3 ); // Log a debug message to specified file ( if pszFile == NULL uses c:\\hllog.txt )
|
|
void PhysicsSimulate( void );
|
|
|
|
#if defined( PORTAL )
|
|
ConVar cl_predicted_movement_uses_uninterpolated_physics( "cl_predicted_movement_uses_uninterpolated_physics", "1" );
|
|
extern void MoveUnpredictedPhysicsNearPlayerToNetworkedPosition( CBasePlayer *pPlayer );
|
|
#endif
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CPrediction::CPrediction( void ) : m_SavedVars( true )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
m_bInPrediction = false;
|
|
|
|
m_nPreviousStartFrame = -1;
|
|
m_nIncomingPacketNumber = 0;
|
|
|
|
m_bPlayerOriginTypedescriptionSearched = false;
|
|
m_bEnginePaused = false;
|
|
m_pPDumpPanel = NULL;
|
|
|
|
m_flLastServerWorldTimeStamp = -1.0f;
|
|
#endif
|
|
}
|
|
|
|
CPrediction::~CPrediction( void )
|
|
{
|
|
}
|
|
|
|
void CPrediction::Init( void )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
m_bOldCLPredictValue = cl_predict->GetBool();
|
|
m_pPDumpPanel = GetPDumpPanel();
|
|
#endif
|
|
}
|
|
|
|
void CPrediction::Shutdown( void )
|
|
{
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void CPrediction::CheckError( int nSlot, C_BasePlayer *player, int commands_acknowledged )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
Vector origin;
|
|
Vector delta;
|
|
float len;
|
|
static int pos[ MAX_SPLITSCREEN_PLAYERS ];
|
|
|
|
// Not in the game yet
|
|
if ( !engine->IsInGame() )
|
|
return;
|
|
|
|
// Not running prediction
|
|
if ( !cl_predict->GetInt() )
|
|
return;
|
|
|
|
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;
|
|
|
|
if ( !m_bPlayerOriginTypedescriptionSearched )
|
|
{
|
|
m_bPlayerOriginTypedescriptionSearched = true;
|
|
#ifndef PREDICT_ORIGIN_SPLIT
|
|
const typedescription_t *td = CPredictionCopy::FindFlatFieldByName( "m_vecNetworkOrigin", player->GetPredDescMap() );
|
|
if ( td )
|
|
{
|
|
m_PlayerOriginTypeDescription.AddToTail( td );
|
|
}
|
|
#else
|
|
const typedescription_t *td = CPredictionCopy::FindFlatFieldByName( "m_vecNetworkOrigin.x", player->GetPredDescMap() );
|
|
if ( td )
|
|
{
|
|
m_PlayerOriginTypeDescription.AddToTail( td );
|
|
}
|
|
td = CPredictionCopy::FindFlatFieldByName( "m_vecNetworkOrigin.y", player->GetPredDescMap() );
|
|
if ( td )
|
|
{
|
|
m_PlayerOriginTypeDescription.AddToTail( td );
|
|
}
|
|
td = CPredictionCopy::FindFlatFieldByName( "m_vecNetworkOrigin.z", player->GetPredDescMap() );
|
|
if ( td )
|
|
{
|
|
m_PlayerOriginTypeDescription.AddToTail( td );
|
|
}
|
|
|
|
if( m_PlayerOriginTypeDescription.Count() != 3 )
|
|
{
|
|
m_PlayerOriginTypeDescription.RemoveAll();
|
|
return;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if ( !m_PlayerOriginTypeDescription.Count() )
|
|
return;
|
|
|
|
Vector predicted_origin;
|
|
|
|
// Find the origin field in the database
|
|
// Splitting m_vecNetworkOrigin into component fields for prediction *
|
|
#ifndef PREDICT_ORIGIN_SPLIT
|
|
Q_memcpy( (Vector *)&predicted_origin, (Vector *)( (byte *)slot + m_PlayerOriginTypeDescription[ 0 ]->flatOffset[ TD_OFFSET_PACKED ] ), sizeof( Vector ) );
|
|
#else
|
|
for ( int i = 0; i < 3; ++i )
|
|
{
|
|
Q_memcpy( (float *)&predicted_origin[ i ], (float *)( (byte *)slot + m_PlayerOriginTypeDescription[ i ]->flatOffset[ TD_OFFSET_PACKED ] ), sizeof( float ) );
|
|
}
|
|
#endif
|
|
|
|
// 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 = 4 + nSlot * 10 + ( ++pos[ nSlot ] % 10 );
|
|
np.time_to_live = 3.0f;
|
|
|
|
engine->Con_NXPrintf( &np, "%d len(%6.3f) (%6.3f %6.3f %6.3f)", nSlot, len, delta.x, delta.y, delta.z );
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::ShutdownPredictables( void )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
|
|
int shutdown_count = 0;
|
|
int release_count = 0;
|
|
|
|
for ( int nSlot = 0; nSlot < MAX_SPLITSCREEN_PLAYERS; ++nSlot )
|
|
{
|
|
// Transfer intermediate data from other predictables
|
|
int c = GetPredictables( nSlot )->GetPredictableCount();
|
|
for ( int i = c - 1; i >= 0 ; i-- )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
// Shutdown predictables
|
|
if ( ent->GetPredictable() )
|
|
{
|
|
ent->ShutdownPredictable();
|
|
shutdown_count++;
|
|
}
|
|
// Otherwise, release client created entities
|
|
else
|
|
{
|
|
ent->Release();
|
|
release_count++;
|
|
}
|
|
}
|
|
|
|
// All gone now...
|
|
Assert( GetPredictables( nSlot )->GetPredictableCount() == 0 );
|
|
}
|
|
|
|
if ( ( release_count > 0 ) ||
|
|
( shutdown_count > 0 ) )
|
|
{
|
|
DevMsg( "Shutdown %i predictable entities and %i client-created entities\n",
|
|
shutdown_count,
|
|
release_count );
|
|
}
|
|
#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" );
|
|
}
|
|
|
|
FOR_EACH_VALID_SPLITSCREEN_PLAYER( nSlot )
|
|
{
|
|
Msg( "%d: Reinitialized %i predictable entities\n",
|
|
nSlot, GetPredictables( nSlot )->GetPredictableCount() );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::OnReceivedUncompressedPacket( void )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
m_nPreviousStartFrame = -1;
|
|
for ( int i = 0 ; i < MAX_SPLITSCREEN_PLAYERS; ++i )
|
|
{
|
|
Split_t &split = m_Split[ i ];
|
|
split.m_nCommandsPredicted = 0;
|
|
split.m_nServerCommandsAcknowledged = 0;
|
|
split.m_nLastCommandAcknowledged = 0;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : commands_acknowledged -
|
|
// current_world_update_packet -
|
|
// Output : void CPrediction::PreEntityPacketReceived
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::PreEntityPacketReceived ( int commands_acknowledged, int current_world_update_packet, int server_ticks_elapsed )
|
|
{
|
|
#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;
|
|
C_BaseEntity::s_nIncomingPacketCommandsAcknowledged = commands_acknowledged;
|
|
|
|
// 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;
|
|
}
|
|
|
|
FOR_EACH_VALID_SPLITSCREEN_PLAYER( nSlot )
|
|
{
|
|
C_BasePlayer *current = C_BasePlayer::GetLocalPlayer( nSlot );
|
|
// No local player object?
|
|
if ( !current )
|
|
continue;
|
|
|
|
// Transfer intermediate data from other predictables
|
|
int c = GetPredictables( nSlot )->GetPredictableCount();
|
|
|
|
int i;
|
|
for ( i = 0; i < c; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
if ( (commands_acknowledged != server_ticks_elapsed) && ent->PredictionIsPhysicallySimulated() )
|
|
{
|
|
ent->ShiftIntermediateData_TickAdjust( server_ticks_elapsed - commands_acknowledged, m_Split[nSlot].m_nCommandsPredicted );
|
|
m_Split[nSlot].m_bPerformedTickShift = true;
|
|
}
|
|
|
|
ent->PreEntityPacketReceived( commands_acknowledged );
|
|
ent->OnPostRestoreData();
|
|
}
|
|
}
|
|
#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" );
|
|
|
|
C_BaseEntity::s_nIncomingPacketCommandsAcknowledged = -1;
|
|
|
|
// Don't screw up memory of current player from history buffers if not filling in history buffers
|
|
// during prediction!!!
|
|
if ( !cl_predict->GetInt() )
|
|
return;
|
|
|
|
FOR_EACH_VALID_SPLITSCREEN_PLAYER( nSlot )
|
|
{
|
|
C_BasePlayer *current = C_BasePlayer::GetLocalPlayer( nSlot );
|
|
// No local player object?
|
|
if ( !current )
|
|
continue;
|
|
|
|
int c = GetPredictables( nSlot )->GetPredictableCount();
|
|
|
|
// Transfer intermediate data from other predictables
|
|
int i;
|
|
for ( i = 0; i < c; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
// Always mark as changed
|
|
if ( AddDataChangeEvent( ent, DATA_UPDATE_DATATABLE_CHANGED, &ent->m_DataChangeEventRef ) )
|
|
{
|
|
ent->OnPreDataChanged( DATA_UPDATE_DATATABLE_CHANGED );
|
|
}
|
|
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
|
|
}
|
|
|
|
void CPrediction::ShowPredictionListEntry( int listRow, int showlist, C_BaseEntity *ent, int &totalsize, int &totalsize_intermediate )
|
|
{
|
|
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() );
|
|
}
|
|
|
|
int oIndex = 0;
|
|
if ( ent->GetOwnerEntity() )
|
|
{
|
|
oIndex = ent->GetOwnerEntity()->entindex();
|
|
}
|
|
else if ( ent->IsPlayer() )
|
|
{
|
|
oIndex = ent->entindex();
|
|
}
|
|
else
|
|
{
|
|
C_BaseViewModel *pVM = ToBaseViewModel( ent );
|
|
if ( pVM && pVM->GetOwner() )
|
|
{
|
|
oIndex = pVM->GetOwner()->entindex();
|
|
}
|
|
}
|
|
|
|
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;
|
|
np.index = listRow;
|
|
if ( showlist >= 2 )
|
|
{
|
|
int size = GetClassMap().GetClassSize( ent->GetClassname() );
|
|
int intermediate_size = ent->GetIntermediateDataSize() * ( MULTIPLAYER_BACKUP + 1 );
|
|
|
|
engine->Con_NXPrintf( &np, "%15s %30s(%d) (%5i / %5i bytes): %15s",
|
|
sz,
|
|
ent->GetClassname(),
|
|
oIndex,
|
|
size,
|
|
intermediate_size,
|
|
ent->GetPredictable() ? "predicted" : "client created" );
|
|
|
|
totalsize += size;
|
|
totalsize_intermediate += intermediate_size;
|
|
}
|
|
else
|
|
{
|
|
engine->Con_NXPrintf( &np, "%15s %30s(%d): %15s",
|
|
sz,
|
|
ent->GetClassname(),
|
|
oIndex,
|
|
ent->GetPredictable() ? "predicted" : "client created" );
|
|
}
|
|
}
|
|
|
|
void CPrediction::FinishPredictionList( int listRow, int showlist, int totalsize, int totalsize_intermediate )
|
|
{
|
|
if ( !showlist )
|
|
return;
|
|
|
|
if ( showlist > 1 )
|
|
{
|
|
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;
|
|
np.index = listRow++;
|
|
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
|
|
while ( listRow < 20 )
|
|
{
|
|
engine->Con_NPrintf( listRow++, "" );
|
|
}
|
|
}
|
|
|
|
void CPrediction::CheckPredictConvar()
|
|
{
|
|
if ( cl_predict->GetBool() != m_bOldCLPredictValue )
|
|
{
|
|
if ( !m_bOldCLPredictValue )
|
|
{
|
|
ReinitPredictables();
|
|
}
|
|
|
|
for ( int i = 0 ; i < MAX_SPLITSCREEN_PLAYERS; ++i )
|
|
{
|
|
Split_t &split = m_Split[ i ];
|
|
split.m_nCommandsPredicted = 0;
|
|
split.m_nServerCommandsAcknowledged = 0;
|
|
split.m_nLastCommandAcknowledged = 0;
|
|
}
|
|
m_nPreviousStartFrame = -1;
|
|
}
|
|
|
|
m_bOldCLPredictValue = cl_predict->GetBool();
|
|
}
|
|
|
|
ConVar cl_prediction_error_timestamps( "cl_prediction_error_timestamps", "0" );
|
|
//-----------------------------------------------------------------------------
|
|
// 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
|
|
//Msg( "%i/%i ack %i commands/slot\n",
|
|
// gpGlobals->framecount,
|
|
// gpGlobals->tickcount,
|
|
// commands_acknowledged - 1 );
|
|
|
|
bool entityDumped = false;
|
|
bool bPredict = cl_predict->GetBool();
|
|
int showlist = cl_predictionlist.GetInt();
|
|
|
|
int listRow = 0;
|
|
|
|
FOR_EACH_VALID_SPLITSCREEN_PLAYER( nSlot )
|
|
{
|
|
Split_t &split = m_Split[ nSlot ];
|
|
split.m_nServerCommandsAcknowledged += commands_acknowledged;
|
|
split.m_bPreviousAckHadErrors = false;
|
|
split.m_bPreviousAckErrorTriggersFullLatchReset = false;
|
|
split.m_EntsWithPredictionErrorsInLastAck.RemoveAll();
|
|
|
|
if ( bPredict )
|
|
{
|
|
// Don't screw up memory of current player from history buffers if not filling in history buffers during prediction!!!
|
|
int totalsize = 0;
|
|
int totalsize_intermediate = 0;
|
|
|
|
// Build list of all predictables
|
|
int c = GetPredictables( nSlot )->GetPredictableCount();
|
|
bool *bHadErrors = (bool *)stackalloc( sizeof( bool ) * c );
|
|
// Transfer intermediate data from other predictables
|
|
int i;
|
|
for ( i = 0; i < c; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
bHadErrors[i] = ent->PostNetworkDataReceived( split.m_nServerCommandsAcknowledged );
|
|
if ( bHadErrors[i] )
|
|
{
|
|
split.m_bPreviousAckHadErrors = true;
|
|
split.m_bPreviousAckErrorTriggersFullLatchReset |= ent->PredictionErrorShouldResetLatchedForAllPredictables();
|
|
split.m_EntsWithPredictionErrorsInLastAck.AddToTail( ent );
|
|
}
|
|
|
|
if ( !showlist )
|
|
{
|
|
if ( error_check &&
|
|
!entityDumped &&
|
|
m_pPDumpPanel &&
|
|
ShouldDumpEntity( ent ) )
|
|
{
|
|
entityDumped = true;
|
|
m_pPDumpPanel->DumpEntity( ent, split.m_nServerCommandsAcknowledged );
|
|
}
|
|
continue;
|
|
}
|
|
|
|
ShowPredictionListEntry( listRow, showlist, ent, totalsize, totalsize_intermediate );
|
|
listRow++;
|
|
}
|
|
|
|
//Give entities with predicted fields that are not networked a chance to fix their current values for those fields.
|
|
//We do this in two passes. One pass to fix the fields, then another to save off the changes after they've all finished (to handle interdependancies, portals)
|
|
if( split.m_bPreviousAckHadErrors )
|
|
{
|
|
//give each predicted entity a chance to fix up its non-networked predicted fields
|
|
for ( i = 0; i < c; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
ent->HandlePredictionError( bHadErrors[i] );
|
|
}
|
|
|
|
//save off any changes
|
|
for ( i = 0; i < c; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
ent->SaveData( "PostNetworkDataReceived() Ack Errors", C_BaseEntity::SLOT_ORIGINALDATA, PC_EVERYTHING );
|
|
}
|
|
}
|
|
|
|
for ( i = c; --i >= 0; ) //go backwards to maintain ordering on shutdown
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
ent->CheckShutdownPredictable( "CPrediction::PostNetworkDataReceived" );
|
|
}
|
|
|
|
FinishPredictionList( listRow, showlist, totalsize, totalsize_intermediate );
|
|
|
|
if ( error_check )
|
|
{
|
|
C_BasePlayer *current = C_BasePlayer::GetLocalPlayer( nSlot );
|
|
if ( !current )
|
|
continue;
|
|
CheckError( nSlot, current, split.m_nServerCommandsAcknowledged );
|
|
}
|
|
}
|
|
|
|
// Can also look at regular entities
|
|
int dumpentindex = cl_predictionentitydump.GetInt();
|
|
if ( m_pPDumpPanel && error_check && !entityDumped && dumpentindex != -1 )
|
|
{
|
|
int last_entity = ClientEntityList().GetHighestEntityIndex();
|
|
if ( dumpentindex >= 0 && dumpentindex <= last_entity )
|
|
{
|
|
C_BaseEntity *ent = ClientEntityList().GetBaseEntity( dumpentindex );
|
|
if ( ent )
|
|
{
|
|
m_pPDumpPanel->DumpEntity( ent, split.m_nServerCommandsAcknowledged );
|
|
entityDumped = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( split.m_bPreviousAckHadErrors && cl_prediction_error_timestamps.GetBool() )
|
|
{
|
|
Warning( "Prediction errors occurred at %i %f\n", gpGlobals->tickcount, gpGlobals->curtime );
|
|
}
|
|
}
|
|
|
|
CheckPredictConvar();
|
|
|
|
if ( m_pPDumpPanel && error_check && !entityDumped )
|
|
{
|
|
m_pPDumpPanel->Clear();
|
|
}
|
|
#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_bGameCodeMovedPlayer = false;
|
|
if ( player->GetPreviouslyPredictedOrigin() != player->GetNetworkOrigin() )
|
|
{
|
|
move->m_bGameCodeMovedPlayer = true;
|
|
}
|
|
|
|
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
|
|
|
|
g_pGameMovement->SetupMovementBounds( move );
|
|
#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->SetAbsVelocity( move->m_vecVelocity );
|
|
|
|
player->m_vecNetworkOrigin = move->GetAbsOrigin();
|
|
player->SetPreviouslyPredictedOrigin( move->GetAbsOrigin() );
|
|
|
|
player->m_Local.m_nOldButtons = move->m_nButtons;
|
|
|
|
|
|
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" );
|
|
|
|
#if defined( USE_PREDICTABLEID )
|
|
CPredictableId::ResetInstanceCounters();
|
|
#endif
|
|
|
|
player->m_pCurrentCommand = cmd;
|
|
player->m_LastCmd = *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: Checks if the player is standing on a moving entity and adjusts velocity and
|
|
// basevelocity appropriately
|
|
// Input : *player -
|
|
// frametime -
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::CheckMovingGround( C_BasePlayer *player, double frametime )
|
|
{
|
|
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
|
|
|
|
#ifdef PORTAL2
|
|
assert_cast<CPortal_Player*>( player )->PreventCrouchJump( ucmd );
|
|
#endif
|
|
|
|
StartCommand( player, ucmd );
|
|
|
|
// Set globals appropriately
|
|
gpGlobals->curtime = player->m_nTickBase * TICK_INTERVAL;
|
|
gpGlobals->frametime = m_bEnginePaused ? 0 : TICK_INTERVAL;
|
|
|
|
// Add and subtract buttons we're forcing on the player
|
|
ucmd->buttons |= player->m_afButtonForced;
|
|
// ucmd->buttons &= ~player->m_afButtonDisabled; // MAY WANT TO DO THIS LATER!!!
|
|
|
|
g_pGameMovement->StartTrackPredictionErrors( player );
|
|
|
|
// TODO
|
|
// TODO: Check for impulse predicted?
|
|
|
|
// Do weapon selection
|
|
if ( ucmd->weaponselect != 0 )
|
|
{
|
|
C_BaseCombatWeapon *weapon = ToBaseCombatWeapon( 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, gpGlobals->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 );
|
|
}
|
|
|
|
|
|
{
|
|
VPROF_BUDGET( "CPrediction::ProcessMovement", "CPrediction::ProcessMovement" );
|
|
|
|
// RUN MOVEMENT
|
|
if ( !pVehicle )
|
|
{
|
|
Assert( g_pGameMovement );
|
|
g_pGameMovement->ProcessMovement( player, g_pMoveData );
|
|
}
|
|
else
|
|
{
|
|
pVehicle->ProcessMovement( player, g_pMoveData );
|
|
}
|
|
}
|
|
|
|
FinishMove( player, ucmd, g_pMoveData );
|
|
|
|
// Let server invoke any needed impact functions
|
|
VPROF_SCOPE_BEGIN( "moveHelper->ProcessImpacts(cl)" );
|
|
moveHelper->ProcessImpacts();
|
|
VPROF_SCOPE_END();
|
|
|
|
RunPostThink( player );
|
|
|
|
g_pGameMovement->FinishTrackPredictionErrors( player );
|
|
|
|
FinishCommand( player );
|
|
g_pGameMovement->Reset(); // fixes a crash: when loading highlights twice or after previously loaded map, there was a dirty player pointer in game movement
|
|
|
|
if( !m_bEnginePaused && 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 ( int nSlot, 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;
|
|
int step, dir, steps;
|
|
trace_t tr;
|
|
|
|
if ( player->GetGroundEntity() == NULL )
|
|
return;
|
|
|
|
// Don't do this on the 360..
|
|
if ( IsGameConsole() )
|
|
return;
|
|
|
|
AngleVectors( angles, &forward );
|
|
forward[2] = 0;
|
|
|
|
MDLCACHE_CRITICAL_SECTION();
|
|
|
|
// 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;
|
|
}
|
|
|
|
Split_t &split = m_Split[ nSlot ];
|
|
if (!dir)
|
|
{
|
|
split.m_flIdealPitch = 0;
|
|
return;
|
|
}
|
|
|
|
if (steps < 2)
|
|
return;
|
|
split.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 nSlot, int sequence_number )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION ) && defined( USE_PREDICTABLEID )
|
|
VPROF( "CPrediction::RemoveStalePredictedEntities" );
|
|
|
|
int oldest_allowable_command = sequence_number;
|
|
|
|
// Walk backward due to deletion from UtlVector
|
|
int c = GetPredictables( nSlot )->GetPredictableCount();
|
|
int i;
|
|
for ( i = c - 1; i >= 0; i-- )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->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( int nSlot )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
VPROF( "CPrediction::RestoreOriginalEntityState" );
|
|
PREDICTION_TRACKVALUECHANGESCOPE( "restore" );
|
|
|
|
Assert( C_BaseEntity::IsAbsRecomputationsEnabled() );
|
|
|
|
// Transfer intermediate data from other predictables
|
|
int c = GetPredictables( nSlot )->GetPredictableCount();
|
|
for ( int i = 0; i < c; ++i )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent || !ent->GetPredictable() )
|
|
continue;
|
|
|
|
ent->RestoreData( "RestoreOriginalEntityState", C_BaseEntity::SLOT_ORIGINALDATA, PC_EVERYTHING );
|
|
ent->OnPostRestoreData();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void CPrediction::ResetSimulationTick()
|
|
{
|
|
ASSERT_LOCAL_PLAYER_RESOLVABLE();
|
|
int nSlot = GET_ACTIVE_SPLITSCREEN_SLOT();
|
|
// Make sure simulation occurs at most once per entity per usercmd
|
|
for (int i = 0; i < GetPredictables( nSlot )->GetPredictableCount(); i++ )
|
|
{
|
|
C_BaseEntity *entity = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( entity && entity->GetSplitUserPlayerPredictionSlot() == nSlot )
|
|
{
|
|
entity->m_nSimulationTick = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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_LOCAL_PLAYER_RESOLVABLE();
|
|
int nSlot = GET_ACTIVE_SPLITSCREEN_SLOT();
|
|
|
|
Assert( localPlayer );
|
|
C_CommandContext *ctx = localPlayer->GetCommandContext();
|
|
Assert( ctx );
|
|
|
|
ctx->needsprocessing = true;
|
|
ctx->cmd = *cmd;
|
|
ctx->command_number = current_command;
|
|
|
|
IPredictionSystem::SuppressEvents( !IsFirstTimePredicted() );
|
|
|
|
ResetSimulationTick();
|
|
|
|
// Don't used cached numpredictables since entities can be created mid-prediction by the player
|
|
for ( int i = 0; i < GetPredictables( nSlot )->GetPredictableCount(); i++ )
|
|
{
|
|
// Always reset
|
|
gpGlobals->curtime = curtime;
|
|
gpGlobals->frametime = m_bEnginePaused ? 0 : TICK_INTERVAL;
|
|
|
|
C_BaseEntity *entity = GetPredictables( nSlot )->GetPredictable( i );
|
|
|
|
if ( !entity )
|
|
continue;
|
|
|
|
if ( entity->GetSplitUserPlayerPredictionSlot() != nSlot )
|
|
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 && !IsValidSplitScreenSlot( i ) )
|
|
continue;
|
|
|
|
// 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 );
|
|
}
|
|
|
|
// Only call the physics update for the entire physics system if we support predicted physics.
|
|
if ( cl_predictphysics.GetBool() )
|
|
{
|
|
PhysicsSimulate();
|
|
}
|
|
|
|
// Always reset after running command
|
|
IPredictionSystem::SuppressEvents( false );
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::Untouch( int nSlot )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
int numpredictables = GetPredictables( nSlot )->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 = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !entity )
|
|
continue;
|
|
|
|
if ( !entity->GetCheckUntouch() )
|
|
continue;
|
|
|
|
entity->PhysicsCheckForEntityUntouch();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void CPrediction::StorePredictionResults( int nSlot, int predicted_frame )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
VPROF( "CPrediction::StorePredictionResults" );
|
|
PREDICTION_TRACKVALUECHANGESCOPE( "save" );
|
|
|
|
int c = GetPredictables( nSlot )->GetPredictableCount();
|
|
|
|
// Now save off all of the results
|
|
for ( int i = 0; i < c; ++i )
|
|
{
|
|
C_BaseEntity *entity = GetPredictables( nSlot )->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 );
|
|
|
|
//if we're keeping first frame results, copy them now
|
|
if( m_Split[ nSlot ].m_bFirstTimePredicted && (entity->m_pIntermediateData_FirstPredicted[0] != NULL) )
|
|
{
|
|
entity->m_nIntermediateData_FirstPredictedShiftMarker = predicted_frame + 1;
|
|
memcpy( entity->m_pIntermediateData_FirstPredicted[predicted_frame + 1], entity->m_pIntermediateData[predicted_frame], entity->GetPredDescMap()->m_nPackedSize );
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : slots_to_remove -
|
|
// previous_last_slot -
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::ShiftIntermediateDataForward( int nSlot, int slots_to_remove, int number_of_commands_run )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
VPROF( "CPrediction::ShiftIntermediateDataForward" );
|
|
PREDICTION_TRACKVALUECHANGESCOPE( "shift" );
|
|
|
|
if ( !C_BasePlayer::HasAnyLocalPlayer() )
|
|
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 = GetPredictables( nSlot )->GetPredictableCount();
|
|
int i;
|
|
for ( i = 0; i < c; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
ent->ShiftIntermediateDataForward( slots_to_remove, number_of_commands_run );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : slots_to_remove -
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::ShiftFirstPredictedIntermediateDataForward( int nSlot, int slots_to_remove )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
if( (slots_to_remove == 0) || (slots_to_remove >= MULTIPLAYER_BACKUP) )
|
|
return;
|
|
|
|
VPROF( "CPrediction::ShiftFirstPredictedIntermediateDataForward" );
|
|
PREDICTION_TRACKVALUECHANGESCOPE( "shift" );
|
|
|
|
if ( !C_BasePlayer::HasAnyLocalPlayer() )
|
|
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 = GetPredictables( nSlot )->GetPredictableCount();
|
|
int i;
|
|
for ( i = 0; i < c; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
ent->ShiftFirstPredictedIntermediateDataForward( slots_to_remove );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : predicted_frame -
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::RestoreEntityToPredictedFrame( int nSlot, int predicted_frame )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
VPROF( "CPrediction::RestoreEntityToPredictedFrame" );
|
|
PREDICTION_TRACKVALUECHANGESCOPE( "restoretopred" );
|
|
|
|
if ( !C_BasePlayer::GetLocalPlayer( nSlot ) )
|
|
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 = GetPredictables( nSlot )->GetPredictableCount();
|
|
int i;
|
|
for ( i = 0; i < c; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
if ( !ent->GetPredictable() )
|
|
continue;
|
|
|
|
ent->RestoreData( "RestoreEntityToPredictedFrame", predicted_frame, PC_EVERYTHING );
|
|
ent->OnPostRestoreData();
|
|
}
|
|
#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( int nSlot, bool received_new_world_update, int incoming_acknowledged, int outgoing_command )
|
|
{
|
|
int destination_slot = 1;
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
int skipahead = 0;
|
|
|
|
Split_t &split = m_Split[ nSlot ];
|
|
|
|
// 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 || !split.m_nServerCommandsAcknowledged )
|
|
{
|
|
if( !split.m_bPerformedTickShift )
|
|
{
|
|
// 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, split.m_nCommandsPredicted );
|
|
|
|
// Always restore since otherwise we might start prediction using an "interpolated" value instead of a purely predicted value
|
|
RestoreEntityToPredictedFrame( nSlot, 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 &&
|
|
!split.m_bPreviousAckHadErrors &&
|
|
split.m_nCommandsPredicted > 0 &&
|
|
split.m_nServerCommandsAcknowledged <= split.m_nCommandsPredicted &&
|
|
!split.m_bPerformedTickShift )
|
|
{
|
|
// 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( nSlot, split.m_nCommandsPredicted - 1 );
|
|
|
|
// Shift intermediate state blocks down by # of commands ack'd
|
|
ShiftIntermediateDataForward( nSlot, split.m_nServerCommandsAcknowledged, split.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 = ( split.m_nCommandsPredicted - split.m_nServerCommandsAcknowledged );
|
|
|
|
//Msg( "%i/%i optimize2, skip to %i restore from slot %i\n",
|
|
// gpGlobals->framecount,
|
|
// gpGlobals->tickcount,
|
|
// skipahead,
|
|
// split.m_nCommandsPredicted - 1 );
|
|
}
|
|
else
|
|
{
|
|
if( split.m_bPreviousAckHadErrors )
|
|
{
|
|
int count = GetPredictables( nSlot )->GetPredictableCount();
|
|
for ( int i = 0; i < count; i++ )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent )
|
|
continue;
|
|
|
|
ent->m_bEverHadPredictionErrorsForThisCommand = true; //this bool will get pulled forward until a new command is predicted for the first time
|
|
}
|
|
}
|
|
|
|
if ( ( split.m_bPreviousAckHadErrors && cl_pred_doresetlatch.GetBool() ) ||
|
|
cl_pred_doresetlatch.GetInt() == 2 )
|
|
{
|
|
// Both players should have == time base, etc.
|
|
C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer( nSlot );
|
|
|
|
// 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;
|
|
|
|
if( split.m_bPreviousAckErrorTriggersFullLatchReset || (cl_pred_doresetlatch.GetInt() == 2) )
|
|
{
|
|
for ( int i = 0; i < GetPredictables( nSlot )->GetPredictableCount(); i++ )
|
|
{
|
|
C_BaseEntity *entity = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( entity )
|
|
{
|
|
entity->ResetLatched();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//individual latch resets
|
|
for ( int i = 0; i < split.m_EntsWithPredictionErrorsInLastAck.Count(); i++ )
|
|
{
|
|
C_BaseEntity *entity = split.m_EntsWithPredictionErrorsInLastAck[i];
|
|
if( entity )
|
|
{
|
|
//ensure it's still in our predictable list
|
|
for ( int j = 0; j < GetPredictables( nSlot )->GetPredictableCount(); j++ )
|
|
{
|
|
if( entity == GetPredictables( nSlot )->GetPredictable( j ) )
|
|
{
|
|
entity->ResetLatched();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
gpGlobals->curtime = flPrev;
|
|
}
|
|
}
|
|
}
|
|
|
|
ShiftFirstPredictedIntermediateDataForward( nSlot, split.m_nServerCommandsAcknowledged );
|
|
|
|
destination_slot += skipahead;
|
|
|
|
// Always reset these values now that we handled them
|
|
split.m_nCommandsPredicted = 0;
|
|
split.m_bPreviousAckHadErrors = false;
|
|
split.m_nServerCommandsAcknowledged = 0;
|
|
split.m_bPerformedTickShift = false;
|
|
#endif
|
|
return destination_slot;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Actually does the prediction work, returns false if an error occurred
|
|
//-----------------------------------------------------------------------------
|
|
bool CPrediction::PerformPrediction( int nSlot, C_BasePlayer *localPlayer, bool received_new_world_update, int incoming_acknowledged, int outgoing_command )
|
|
{
|
|
MDLCACHE_CRITICAL_SECTION();
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
VPROF( "CPrediction::PerformPrediction" );
|
|
|
|
Assert( localPlayer );
|
|
// This makes sure that we are allowed to sample the world when it may not be ready to be sampled
|
|
Assert( C_BaseEntity::IsAbsQueriesValid() );
|
|
Assert( C_BaseEntity::IsAbsRecomputationsEnabled() );
|
|
|
|
// 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( nSlot, received_new_world_update, incoming_acknowledged, outgoing_command );
|
|
|
|
Assert( i >= 1 );
|
|
|
|
// This is a hack to get the CTriggerAutoGameMovement auto duck triggers to correctly deal with prediction.
|
|
// Here we just untouch any triggers the player was touching (since we might have teleported the origin
|
|
// backward from it's previous position) and then re-touch any triggers it's currently in
|
|
#if !defined( PORTAL2 ) //portal2 coop likes trigger touch behavior consistent with serverside trigger touch behavior
|
|
localPlayer->SetCheckUntouch( true );
|
|
localPlayer->PhysicsCheckForEntityUntouch();
|
|
#endif
|
|
localPlayer->PhysicsTouchTriggers();
|
|
|
|
// undo interpolation changes for entities we stand on
|
|
C_BaseEntity *ground = localPlayer->GetGroundEntity();
|
|
|
|
while ( ground && ground->entindex() > 0 )
|
|
{
|
|
ground->MoveToLastReceivedPosition();
|
|
ground = ground->GetMoveParent();
|
|
}
|
|
|
|
#if defined( PORTAL )
|
|
if( cl_predicted_movement_uses_uninterpolated_physics.GetBool() )
|
|
{
|
|
//if we're going to treat physics objects as immovable solids on the client, they might as well be immovable solids in the place the server told us to put them
|
|
//otherwise interpolation effectively teleports it backwards without the need (or support) to push entities backwards with it.
|
|
//If you happen to be pushing a physics object, this will make movement traces freak out as we start in solid.
|
|
MoveUnpredictedPhysicsNearPlayerToNetworkedPosition( localPlayer );
|
|
}
|
|
#endif
|
|
|
|
Split_t &split = m_Split[ nSlot ];
|
|
|
|
physenv->DoneReferencingPreviousCommands( incoming_acknowledged - 1 );
|
|
|
|
bool bTooMany = outgoing_command - incoming_acknowledged >= MULTIPLAYER_BACKUP;
|
|
|
|
while ( !bTooMany )
|
|
{
|
|
// 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;
|
|
|
|
CUserCmd *cmd = input->GetUserCmd( nSlot, current_command );
|
|
if ( !cmd )
|
|
{
|
|
bTooMany = true;
|
|
break;
|
|
}
|
|
|
|
Assert( i < MULTIPLAYER_BACKUP );
|
|
|
|
// Is this the first time predicting this
|
|
split.m_bFirstTimePredicted = !cmd->hasbeenpredicted;
|
|
#if defined( KEEP_COMMAND_REPREDICTION_COUNT )
|
|
split.m_Debug_RepredictionCount = cmd->debug_RepredictionCount;
|
|
#endif
|
|
|
|
// Set globals appropriately
|
|
float curtime = ( localPlayer->m_nTickBase ) * TICK_INTERVAL;
|
|
|
|
if( physenv->IsPredicted() )
|
|
{
|
|
physenv->SetPredictionCommandNum( current_command );
|
|
|
|
if( (m_Split[ nSlot ].m_nCommandsPredicted == 0) && (i == 1) )
|
|
{
|
|
if( !m_Split[ nSlot ].m_bFirstTimePredicted )
|
|
{
|
|
for ( int j = 0; j < GetPredictables( nSlot )->GetPredictableCount(); j++ )
|
|
{
|
|
C_BaseEntity *entity = GetPredictables( nSlot )->GetPredictable( j );
|
|
|
|
if( entity->m_bEverHadPredictionErrorsForThisCommand )
|
|
{
|
|
// Always reset
|
|
gpGlobals->curtime = curtime;
|
|
gpGlobals->frametime = m_bEnginePaused ? 0 : TICK_INTERVAL;
|
|
|
|
const byte *predictedFrame = (const byte *)entity->GetFirstPredictedFrame( 0 );
|
|
entity->VPhysicsCompensateForPredictionErrors( predictedFrame );
|
|
}
|
|
}
|
|
}
|
|
//else - if lag is so low that we only predict every command once, do we need to keep around old data to base our corrections on?
|
|
}
|
|
}
|
|
|
|
if( !cmd->hasbeenpredicted )
|
|
{
|
|
int count = GetPredictables( nSlot )->GetPredictableCount();
|
|
for ( int i = 0; i < count; ++i )
|
|
{
|
|
C_BaseEntity *entity = GetPredictables( nSlot )->GetPredictable( i );
|
|
|
|
if( entity )
|
|
{
|
|
entity->m_bEverHadPredictionErrorsForThisCommand = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
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( nSlot );
|
|
|
|
// Store intermediate data into appropriate slot
|
|
StorePredictionResults( nSlot, i - 1 ); // Note that I starts at 1
|
|
|
|
split.m_nCommandsPredicted = i;
|
|
|
|
if ( current_command == outgoing_command )
|
|
{
|
|
localPlayer->m_nFinalPredictedTick = localPlayer->m_nTickBase;
|
|
}
|
|
|
|
// Mark that we issued any needed sounds, of not done already
|
|
cmd->hasbeenpredicted = true;
|
|
|
|
#if defined( KEEP_COMMAND_REPREDICTION_COUNT )
|
|
++cmd->debug_RepredictionCount;
|
|
#endif
|
|
|
|
// Copy the state over.
|
|
i++;
|
|
}
|
|
|
|
// Somehow we looped past the end of the list (severe lag), don't predict at all
|
|
return !bTooMany;
|
|
#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;
|
|
|
|
// HACK!
|
|
float flTimeStamp = engine->GetLastTimeStamp();
|
|
bool bTimeStampChanged = m_flLastServerWorldTimeStamp != flTimeStamp;
|
|
m_flLastServerWorldTimeStamp = flTimeStamp;
|
|
|
|
// 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() && bTimeStampChanged )
|
|
{
|
|
received_new_world_update = false;
|
|
}
|
|
|
|
m_nPreviousStartFrame = startframe;
|
|
|
|
// Save off current timer values, etc.
|
|
m_SavedVars = *gpGlobals;
|
|
|
|
FOR_EACH_VALID_SPLITSCREEN_PLAYER( nSlot )
|
|
{
|
|
ACTIVE_SPLITSCREEN_PLAYER_GUARD( nSlot );
|
|
_Update( nSlot, received_new_world_update, validframe, incoming_acknowledged, outgoing_command );
|
|
}
|
|
|
|
// Restore current timer values, etc.
|
|
*gpGlobals = m_SavedVars;
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Do the dirty deed of predicting the local player
|
|
//-----------------------------------------------------------------------------
|
|
void CPrediction::_Update( int nSlot, bool received_new_world_update, bool validframe,
|
|
int incoming_acknowledged, int outgoing_command )
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
|
|
QAngle viewangles;
|
|
|
|
C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer( nSlot );
|
|
if ( !localPlayer )
|
|
return;
|
|
|
|
m_Split[nSlot].m_nLastCommandAcknowledged = incoming_acknowledged;
|
|
|
|
// Always using current view angles no matter what
|
|
// NOTE: ViewAngles are always interpreted as being *relative* to the player
|
|
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::AutoAllowBoneAccess boneaccess( true, true );
|
|
|
|
// Invalidate bone cache on predictables
|
|
int c = GetPredictables( nSlot )->GetPredictableCount();
|
|
for ( int i = 0; i < c; ++i )
|
|
{
|
|
C_BaseEntity *ent = GetPredictables( nSlot )->GetPredictable( i );
|
|
if ( !ent ||
|
|
!ent->GetBaseAnimating() ||
|
|
!ent->GetPredictable() )
|
|
continue;
|
|
static_cast< C_BaseAnimating * >( ent )->InvalidateBoneCache();
|
|
}
|
|
|
|
// 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( nSlot, incoming_acknowledged );
|
|
// Restore objects back to "pristine" state from last network/world state update
|
|
if ( received_new_world_update )
|
|
{
|
|
RestoreOriginalEntityState( nSlot );
|
|
}
|
|
|
|
m_bInPrediction = true;
|
|
bool bValid = PerformPrediction( nSlot, localPlayer, received_new_world_update, incoming_acknowledged, outgoing_command );
|
|
m_bInPrediction = false;
|
|
if ( !bValid )
|
|
{
|
|
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() );
|
|
|
|
#ifndef CSTRIKE15 // this doesn't seem like it's applicable to the local cstrike player?
|
|
SetIdealPitch( nSlot, localPlayer, localPlayer->GetLocalOrigin(), localPlayer->GetLocalAngles(), localPlayer->m_vecViewOffset );
|
|
#endif
|
|
|
|
#endif
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CPrediction::IsFirstTimePredicted( void ) const
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
return m_Split[ GET_ACTIVE_SPLITSCREEN_SLOT() ].m_bFirstTimePredicted;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#if defined( KEEP_COMMAND_REPREDICTION_COUNT )
|
|
unsigned int CPrediction::GetRepredictionCount( void ) const
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
return m_Split[ GET_ACTIVE_SPLITSCREEN_SLOT() ].m_Debug_RepredictionCount;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: For verifying/fixing operations that don't save/load in a datatable very well
|
|
// Output : Returns the how many commands the server has processed and sent results for
|
|
//-----------------------------------------------------------------------------
|
|
int CPrediction::GetLastAcknowledgedCommandNumber( void ) const
|
|
{
|
|
#if !defined( NO_ENTITY_PREDICTION )
|
|
return m_Split[ GET_ACTIVE_SPLITSCREEN_SLOT() ].m_nLastCommandAcknowledged;
|
|
#else
|
|
return 0;
|
|
#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( gpGlobals->curtime );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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( gpGlobals->curtime );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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
|
|
}
|
|
|
|
float CPrediction::GetSavedTime() const
|
|
{
|
|
return m_SavedVars.curtime;
|
|
}
|
|
|
|
|
|
#if (PREDICTION_ERROR_CHECK_LEVEL > 0)
|
|
bool _Easy_DiffPrint_InternalConditions( C_BaseEntity *pEntity )
|
|
{
|
|
return (pEntity->GetPredictable() && prediction->InPrediction());
|
|
}
|
|
#endif
|