This article's documentation is for anything that uses the Source engine. Click here for more information.

NPC Lag Compensation: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(noted mem leak)
m (Nesciuse moved page NPC Lag Compensation/en to NPC Lag Compensation without leaving a redirect: Move en subpage to basepage)
 
(15 intermediate revisions by 10 users not shown)
Line 1: Line 1:
Without [[lag compensation]], when you shoot at a target, you have to take your ping time into account, and aim ahead of it accordingly. So if your ping is 100ms, you have to aim for where the target's head will be in 100ms, rather than where you see it right now.
{{LanguageBar}}
{{Source topicon}}[[Category:Programming]][[Category:AI Programming]][[Category:Modding]][[Category:Networking]][[Category:Tutorials]]
Without [[lag compensation]], when you shoot at a target, you have to take your ping time into account and aim ahead of it accordingly. So if your ping is 100ms, you have to aim for where the target's head will be in 100ms, rather than where you see it right now.


Clearly this is less than ideal, and so multiplayer Source games implement lag compensation, which when calculating whether a bullet hits or not, briefly adjusts the position of all players back by the shooter's ping, so that it calculates the bullet collision based upon exactly what the shooter saw when they fired. This effect is missing for NPCs, but can be duplicated from the player lag compensation code without too much trouble.
Clearly, this is less than ideal, so multiplayer Source games implement lag compensation. When calculating whether a bullet hits or not, it briefly adjusts the position of all players back by the shooter's ping, so that it calculates the bullet collision based upon exactly what the shooter saw when they fired. This effect is missing for NPCs but can be duplicated from the player lag compensation code without too much trouble.


This tutorial details all the changes required - while a lot of code is involved, 95% of it is duplicated from player lag compensation code.
This tutorial details all the changes required—while a lot of code is involved, 95% of it is duplicated from player lag compensation code.


{{bug|This code [[Talk:NPC Lag Compensation|has a memory leak]] somewhere.}}
==<code>ai_basenpc.h</code>==
Just before this line:


== ai_basenpc.h ==
<source lang=cpp>typedef CBitVec<MAX_CONDITIONS> CAI_ScheduleBits;</source>


Most of the coding will be done in player_lagcompensation.cpp, but first we will tackle a few little changes needed elsewhere. Open ai_basenpc.h, and at about line 450 you'll find the <code>CAI_Manager</code> class definition. In it, you'll see
Add the following (taken from <code>player_lagcompensation.cpp</code>, but edited):
 
<source lang=cpp>enum
{
MAX_AIS = 256
};</source>
 
This defines the length of the array used to represent "all AIs" - unfortunately it's private, so can't be accessed from outside the class. Remove this snippet, and just before the class declaration add instead
 
<source lang=cpp>#define MAX_AIS 256</source>
 
Now we can access this value from anywhere, and will do so later on. Also in this class, notice:
 
<source lang=cpp>void AddAI( CAI_BaseNPC *pAI );</source>
which should be at line 459. We're going NPCs to be able to track where in the AI manager's list they reside, so this function will be changed to return the index it adds them to. For now, just change the type to int
 
<source lang=cpp>int AddAI( CAI_BaseNPC *pAI );</source>
 
Now scroll down to line 2124, just before the closing bracer (<code>};</code>), and add


<source lang=cpp>
<source lang=cpp>
// used by lag compensation to be able to refer to & track specific NPCs, and detect changes in the AI list
#define MAX_LAYER_RECORDS (CBaseAnimatingOverlay::MAX_OVERLAYS)
void SetAIIndex(int i) { m_iAIIndex = i; }
int GetAIIndex() { return m_iAIIndex; }
private:
int m_iAIIndex;
</source>


This "AI Index" is what will track an NPC's position in the list.
struct LayerRecordNPC
{
int m_sequence;
float m_cycle;
float m_weight;
int m_order;


== ai_basenpc.cpp ==
LayerRecordNPC()
{
m_sequence = 0;
m_cycle = 0;
m_weight = 0;
m_order = 0;
}


Onto the includes at the top of ai_basenpc.cpp, add
LayerRecordNPC( const LayerRecordNPC& src )
 
{
<source lang=cpp>#include "ilagcompensationmanager.h"</source>
m_sequence = src.m_sequence;
m_cycle = src.m_cycle;
m_weight = src.m_weight;
m_order = src.m_order;
}
};


Find <code>void CAI_Manager::AddAI</code> at line 252, this is the function who's declaration we changed from void to int - change it to read as follows:
struct LagRecordNPC
 
<source lang=cpp>
int CAI_Manager::AddAI( CAI_BaseNPC *pAI )
{
{
m_AIs.AddToTail( pAI );
public:
return NumAIs()-1; // return the index it was added to
LagRecordNPC()
}
{
</source>
m_fFlags = 0;
m_vecOrigin.Init();
m_vecAngles.Init();
m_vecMins.Init();
m_vecMaxs.Init();
m_flSimulationTime = -1;
m_masterSequence = 0;
m_masterCycle = 0;
}


At line 11352, in the <code>CAI_BaseNPC</code> constructor, find the line
LagRecordNPC( const LagRecordNPC& src )
{
m_fFlags = src.m_fFlags;
m_vecOrigin = src.m_vecOrigin;
m_vecAngles = src.m_vecAngles;
m_vecMins = src.m_vecMins;
m_vecMaxs = src.m_vecMaxs;
m_flSimulationTime = src.m_flSimulationTime;
for( int layerIndex = 0; layerIndex < MAX_LAYER_RECORDS; ++layerIndex )
{
m_layerRecords[layerIndex] = src.m_layerRecords[layerIndex];
}
m_masterSequence = src.m_masterSequence;
m_masterCycle = src.m_masterCycle;
}


<source lang=cpp>g_AI_Manager.AddAI( this );</source>
// Did player die this frame
int m_fFlags;


and replace it with this
// Player position, orientation and bbox
Vector m_vecOrigin;
QAngle m_vecAngles;
Vector m_vecMins;
Vector m_vecMaxs;


<source lang=cpp>
float m_flSimulationTime;
SetAIIndex( g_AI_Manager.AddAI( this ) );
lagcompensation->RemoveNpcData( GetAIIndex() ); // make sure we're not inheriting anyone else's data
// Player animation details, so we can get the legs in the right spot.
</source>
LayerRecordNPC m_layerRecords[MAX_LAYER_RECORDS];
int m_masterSequence;
float m_masterCycle;
};</source>


This ensures that all NPCs are added to the lag compensation manager as soon as they're created.
We are going to attach an NPC's lag record directly onto the NPC to prevent confusion of records, so the NPC code needs to be able to access these.


Now in the destructor, at line 11378, we'll remove it from the NPC lag compensation, otherwise a crash occured when an NPC was killed and another created in the same game frame (eg headcrab coming off a zombie).
Now again in <code>ai_basenpc.h</code>, add the following to the {{ent|CAI_BaseNPC}} class definition:


<source lang=cpp>lagcompensation->RemoveNpcData( GetAIIndex() );</source>
<source lang=cpp>
public:
CUtlFixedLinkedList<LagRecordNPC>* GetLagTrack() { return m_LagTrack; }
LagRecordNPC* GetLagRestoreData() { if ( m_RestoreData != NULL ) return m_RestoreData; else return new LagRecordNPC(); }
LagRecordNPC* GetLagChangeData() { if ( m_ChangeData != NULL ) return m_ChangeData; else return new LagRecordNPC(); }
void SetLagRestoreData(LagRecordNPC* l) { if ( m_RestoreData != NULL ) delete m_RestoreData; m_RestoreData = l; }
void SetLagChangeData(LagRecordNPC* l) { if ( m_ChangeData != NULL ) delete m_ChangeData; m_ChangeData = l; }
void FlagForLagCompensation( bool tempValue ) { m_bFlaggedForLagCompensation = tempValue; }
bool IsLagFlagged() { return m_bFlaggedForLagCompensation; }


== ilagcompensationmanager.h ==
private:
 
CUtlFixedLinkedList<LagRecordNPC>* m_LagTrack;
This <code>RemoveNpcData</code> will be defined shortly, but first the interface <code>CAI_BaseNPC</code> uses to access it must be created - open ilagcompensationmanager.h, and add this at line 26
LagRecordNPC* m_RestoreData;
 
LagRecordNPC* m_ChangeData;
<source lang=cpp>virtual void RemoveNpcData(int index) = 0;</source>
bool m_bFlaggedForLagCompensation;</source>


== player.cpp / player.h ==
This "lag record" is what stores an NPC's historical position and animation information.
==<code>ai_basenpc.cpp</code>==
At the bottom of the <code>CAI_BaseNPC</code> constructor, add this line:
<source lang=cpp>m_LagTrack = new CUtlFixedLinkedList< LagRecordNPC >();</source>


To decide what should be lag compensated, the player class has a function, <code>WantsLagCompensationOnEntity</code> - which returns true or false. Unfortunately, its designed to only accept a player as its "entity" parameter - but we want it to lag compensate NPCs too, so we'll change it from taking only a CBasePlayer pointer to taking a CBaseEntity ... which could be a player, or an NPC.
In the beginning of the <code>CAI_BaseNPC</code> destructor, add this line:
<source lang=cpp>m_LagTrack->Purge();
delete m_LagTrack;</source>


Find where the function <code>WantsLagCompensationOnEntity</code> is defined, and change the first parameter from <code>const CBasePlayer *pPlayer</code> to <code>const CBaseEntity *pEntity</code>
==<code>player.cpp</code>/<code>player.h</code>==
To decide what should be lag compensated, the player class has a function, <code>WantsLagCompensationOnEntity</code>, which returns true or false. Unfortunately, it's designed to only accept a player as its "entity" parameter… but we want it to lag compensate NPCs too. To accomplish this, we'll change it from taking only a <code>CBasePlayer</code> pointer to taking a <code>CBaseEntity</code>, which could be a player or an NPC.


Now in player.cpp, find this function at around line 682, and change the CBasePlayer parameter to CBaseEntity, as before. Replace all references to pPlayer (''only in this function!'') to pEntity (assuming thats what you renamed the parameter), and then remove the line
Find where the function <code>WantsLagCompensationOnEntity</code> is defined and change the first parameter from <code>const CBasePlayer *pPlayer</code> to <code>const CBaseEntity *pEntity</code>.


Now in <code>player.cpp</code>, find this function at around line 682, and change the <code>CBasePlayer</code> parameter to <code>CBaseEntity</code>, as before. Replace all references to <code>pPlayer</code> (''only in this function!'') to <code>pEntity</code> (assuming that's what you renamed the parameter), then remove the line:
<source lang=cpp>float maxDistance = 1.5 * pPlayer->MaxSpeed() * sv_maxunlag.GetFloat();</source>
<source lang=cpp>float maxDistance = 1.5 * pPlayer->MaxSpeed() * sv_maxunlag.GetFloat();</source>


Replace it with the following:
Replace it with the following:
<source lang=cpp>
<source lang=cpp>
float maxspeed;
float maxspeed;
Line 101: Line 137:
</source>
</source>


That should do exactly the same for players, and something similar for NPCs.
That should do exactly the same for players and something similar for NPCs.
 
== hl2mp_player.cpp / hl2mp_player.h ==
 
The same function exists, more or less identically, for hl2mp_player.h & hl2mp_player.cpp, so we'll have to fix that too. Open them both up. First in the header, find where the function <code>WantsLagCompensationOnEntity</code> is defined (as with player.h), and change the first parameter from <code>const CBasePlayer *pPlayer</code> to <code>const CBaseEntity *pEntity</code>.


Now in hl2mp_player.cpp, find <code>WantsLagCompensationOnEntity</code>. Except for a check at the start, this actually does exactly the same as the player.cpp version, so just chop everything except the first <code>if</code>, and tell it to call the player.cpp version by using <code>BaseClass</code>:
==<code>hl2mp_player.cpp</code>/<code>hl2mp_player.h</code>==
The same function exists, more or less identically, for <code>hl2mp_player.h</code> & <code>hl2mp_player.cpp</code>, so we'll have to fix that too. Open them both up. First in the header, find where the function <code>WantsLagCompensationOnEntity</code> is defined (as with <code>player.h</code>), and change the first parameter from <code>const CBasePlayer *pPlayer</code> to <code>const CBaseEntity *pEntity</code>.


Now in <code>hl2mp_player.cpp</code>, find <code>WantsLagCompensationOnEntity</code>. Except for a check at the start, this actually does exactly the same as the <code>player.cpp</code> version, so just chop everything except the first <code>if</code> and tell it to call the <code>player.cpp</code> version by using <code>BaseClass</code>:
<source lang=cpp>
<source lang=cpp>
bool CHL2MP_Player::WantsLagCompensationOnEntity( const CBaseEntity *pEntity, const CUserCmd *pCmd, const CBitVec<MAX_EDICTS> *pEntityTransmitBits ) const
bool CHL2MP_Player::WantsLagCompensationOnEntity( const CBaseEntity *pEntity, const CUserCmd *pCmd, const CBitVec<MAX_EDICTS> *pEntityTransmitBits ) const
Line 121: Line 155:
</source>
</source>


== player_lagcompensation.cpp ==
==<code>player_lagcompensation.cpp</code>==
 
Right, that's all the bits and pieces done, we can now actually do some lag compensation. Essentially, we're going to duplicate everything this class does for players, and make it work for NPCs. We'll go through all the functions in the order that they're written, adjusting and adding as necessary.
Right, thats all the bits and pieces done, we can now actually do some lag compensation. Essentially, we're going to duplicate everything this class does for players, and make it work for NPCs. We'll go through all the functions in the order that they're written, adjusting and adding as necessary.
 
To start with, add this to the include list


To start with, add this to the include list:
<source lang=cpp>#include "ai_basenpc.h"</source>
<source lang=cpp>#include "ai_basenpc.h"</source>


Line 173: Line 205:
</source>
</source>


Now comes the CLagCompensationManager class definition.
The <code>private:</code> section of the <code>CLagCompensationManager</code> class definition should start with the function:
To the constructor, add
 
<source lang=cpp>m_bNeedsAIUpdate = true;</source>
 
Under the declarations of StartLagCompensation and FinishLagCompensation, add
 
<source lang=cpp>
void RemoveNpcData(int index) // clear specific NPC's history
{
CUtlFixedLinkedList< LagRecord > *track = &m_EntityTrack[index];
track->Purge();
}
</source>
 
The <code>private:</code> section should immediately follow this, starting with the function
 
<source lang=cpp>void BacktrackPlayer( CBasePlayer *player, float flTargetTime );</source>
<source lang=cpp>void BacktrackPlayer( CBasePlayer *player, float flTargetTime );</source>


Just under this, add
Just under this, add:
 
<source lang=cpp>void BacktrackEntity( CAI_BaseNPC *entity, float flTargetTime );</source>
<source lang=cpp>void BacktrackEntity( CAI_BaseNPC *entity, float flTargetTime );</source>
 
Next up is <code>FrameUpdatePostEntityThink</code>. This function is dominated by a large <code>for</code> loop, which we're going to duplicate and make work for NPCs instead. Add this onto the end of the function:
Next is the ClearHistory function, which loops through all players and purges their records. Have it do the same for NPCs:
 
<source lang=cpp>
ClearHistory()
{
for ( int i=0; i<MAX_PLAYERS; i++ ) // all players
m_PlayerTrack[i].Purge();
for ( int j=0; j<MAX_AIS; j++ ) // and all npcs
m_EntityTrack[j].Purge();
}
</source>
 
Now declare a function, <code>UpdateAIIndexes();</code>, and create <code>Entity</code> versions of all the <code>Player</code> variables except for m_pCurrentPlayer. Also add a bool, <code>m_bNeedsAIUpdate</code>:
 
<source lang=cpp>
void UpdateAIIndexes();
 
// keep a list of lag records for each player
CUtlFixedLinkedList< LagRecord > m_PlayerTrack[ MAX_PLAYERS ];
CUtlFixedLinkedList< LagRecord > m_EntityTrack[ MAX_AIS ];
 
// Scratchpad for determining what needs to be restored
CBitVec<MAX_PLAYERS> m_RestorePlayer;
CBitVec<MAX_AIS> m_RestoreEntity;
bool m_bNeedToRestore;
LagRecord m_RestoreData[ MAX_PLAYERS ]; // player data before we moved him back
LagRecord m_ChangeData[ MAX_PLAYERS ]; // player data where we moved him back
 
LagRecord m_EntityRestoreData[ MAX_AIS ];
LagRecord m_EntityChangeData[ MAX_AIS ];
 
CBasePlayer *m_pCurrentPlayer; // The player we are doing lag compensation for
 
bool m_bNeedsAIUpdate;
};
</source>
 
Next up is <code>FrameUpdatePostEntityThink</code>.
Onto the very start, add
 
<source lang=cpp>
if ( m_bNeedsAIUpdate )
UpdateAIIndexes(); // only bother if we haven't had one yet
else // setting this true here ensures that the update happens at the start of the next frame
m_bNeedsAIUpdate = true;
</source>
 
Basically we want UpdateAIIndexes to be called every frame, but it has to be called ''before'' any lag compensation occurs. If no lag compensation occurs, it will be called here instead. This function is dominated by a large for loop, which we're going to duplicate and make work for NPCs instead. Add this onto the end of the function:
 
<source lang=cpp>
<source lang=cpp>
// Iterate all active NPCs
// Iterate all active NPCs
Line 254: Line 219:
{
{
CAI_BaseNPC *pNPC = ppAIs[i];
CAI_BaseNPC *pNPC = ppAIs[i];
CUtlFixedLinkedList< LagRecord > *track = &m_EntityTrack[i];
if ( !pNPC )
if ( !pNPC )
{
track->RemoveAll();
continue;
continue;
}
 
CUtlFixedLinkedList< LagRecordNPC > *track = pNPC->GetLagTrack();


Assert( track->Count() < 1000 ); // insanity check
Assert( track->Count() < 1000 ); // insanity check
Line 268: Line 230:
while ( track->IsValidIndex( tailIndex ) )
while ( track->IsValidIndex( tailIndex ) )
{
{
LagRecord &tail = track->Element( tailIndex );
LagRecordNPC &tail = track->Element( tailIndex );


// if tail is within limits, stop
// if tail is within limits, stop
Line 282: Line 244:
if ( track->Count() > 0 )
if ( track->Count() > 0 )
{
{
LagRecord &head = track->Element( track->Head() );
LagRecordNPC &head = track->Element( track->Head() );


// check if entity changed simulation time since last time updated
// check if entity changed simulation time since last time updated
Line 293: Line 255:


// add new record to track
// add new record to track
LagRecord &record = track->Element( track->AddToHead() );
LagRecordNPC &record = track->Element( track->AddToHead() );


record.m_fFlags = 0;
record.m_fFlags = 0;
Line 324: Line 286:
</source>
</source>


The next function is a new one, created because of a problem with NPCs. Positions in the Player List are fixed: if a player is in Position 5, they will always be in Position 5. With the AI index, its different. When the AI in position 4 dies, the one from the end of the list is moved into position 4 to replace it. This function is designed to account for that, and swap the movement records when an NPC's index changes.
At the top of StartLagCompensation, just under <code>m_RestorePlayer.ClearAll();</code>, add:
 
<source lang=cpp>int nAIs = g_AI_Manager.NumAIs();
<source lang=cpp>
CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
void CLagCompensationManager::UpdateAIIndexes()
for (int i=0; i<nAIs; i++)
{
ppAIs[ i ]->FlagForLagCompensation(false);</source>
CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
int nAIs = g_AI_Manager.NumAIs();
 
for ( int i = 0; i < nAIs; i++ )
{
CAI_BaseNPC *pNPC = ppAIs[i];
if ( pNPC && pNPC->GetAIIndex() != i ) // index of NPC has changed
{// move their data to their new index, probably wanting to delete the old track record
int oldIndex = pNPC->GetAIIndex();
int newIndex = i;
 
//Msg("Lag compensation record adjusting, moving from index %i to %i\n",oldIndex,newIndex);
 
CUtlFixedLinkedList< LagRecord > *track = &m_EntityTrack[ oldIndex ];
CUtlFixedLinkedList< LagRecord > *oldTrack = &m_EntityTrack[ newIndex ];
 
m_EntityTrack[oldIndex] = *oldTrack;
m_EntityTrack[newIndex] = *track;
if ( oldTrack->Count() > 0 ) // there's data in the auld yin, probably from someone newly dead,
{// but if not we'll swap them round so that the old one can then fix their AI index
//Msg("Index %i already contains data!\n",newIndex);
for ( int j=0; j<nAIs; j++ )
{
CAI_BaseNPC *pConflictingNPC = ppAIs[j];
if ( pConflictingNPC && pConflictingNPC->GetAIIndex() == newIndex )
{// found the conflicting NPC, swap them into the old index
pConflictingNPC->SetAIIndex(oldIndex); // presumably they'll fix themselves further down the loop
Warning("Lag compensation adjusting entity index, swapping with an existing entity! (%i & %i)\n",oldIndex,newIndex);
break;
}
}
}
 
pNPC->SetAIIndex(newIndex);
}
}
}
</source>
 
Onto the start of StartLagCompensation, add
 
<source lang=cpp>
// sort out any changes to the AI indexing
if ( m_bNeedsAIUpdate ) // to be called once per frame... must happen BEFORE lag compensation -
{// if that happens, that is. if not its called at the end of the frame
m_bNeedsAIUpdate = false;
UpdateAIIndexes();
}
 
m_RestoreEntity.ClearAll();
</source>
 
Under the two Q_memsets in this function, add two more
 
<source lang=cpp>
Q_memset( m_EntityRestoreData, 0, sizeof( m_EntityRestoreData ) );
Q_memset( m_EntityChangeData, 0, sizeof( m_EntityChangeData ) );
</source>
 
This function ends with a for loop, commented with


This function ends with a <code>for</code> loop, commented with:
<source lang=cpp>// Iterate all active players</source>
<source lang=cpp>// Iterate all active players</source>
Remove it, and replace it with two:
Remove it and replace it with two of:


<source lang=cpp>
<source lang=cpp>
Line 412: Line 315:


// also iterate all monsters
// also iterate all monsters
CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
int nAIs = g_AI_Manager.NumAIs();
for ( int i = 0; i < nAIs; i++ )
for ( int i = 0; i < nAIs; i++ )
{
{
Line 427: Line 327:
</source>
</source>


We only want to do one thing to <code>BacktrackPlayer</code>, and that check for entities also where we check for players that we might be bumping into. Find
We only want to do one thing to <code>BacktrackPlayer</code>, and that check for entities also where we check for players that we might be bumping into. Find:


<source lang=cpp>
<source lang=cpp>
Line 435: Line 335:
</source>
</source>


After the closing bracer of that if (that is, the <code>}</code> matching its <code>{</code>), add this else statement
After the closing bracer of that if (that is, the <code>}</code> matching its <code>{</code>), add this <code>else</code> statement:


<source lang=cpp>
<source lang=cpp>
Line 454: Line 354:
// If we haven't backtracked this player, do it now
// If we haven't backtracked this player, do it now
// this deliberately ignores WantsLagCompensationOnEntity.
// this deliberately ignores WantsLagCompensationOnEntity.
if ( pNPC && !m_RestoreEntity.Get( pNPC->GetAIIndex() ) )
if ( pNPC && !pNPC->IsLagFlagged() )
{
{
// prevent recursion - save a copy of m_RestoreEntity,
// prevent recursion - save a copy of m_RestoreEntity,
Line 460: Line 360:


// Temp turn this flag on
// Temp turn this flag on
m_RestoreEntity.Set( pNPC->GetAIIndex() );
pNPC->FlagForLagCompensation(true);


BacktrackEntity( pHitEntity, flTargetTime );
BacktrackEntity( pHitEntity, flTargetTime );


// Remove the temp flag
// Remove the temp flag
m_RestoreEntity.Clear( pNPC->GetAIIndex() );
pNPC->FlagForLagCompensation(false);
}
}
}
}
Line 482: Line 382:


// get track history of this entity
// get track history of this entity
int index = pEntity->GetAIIndex();
CUtlFixedLinkedList< LagRecordNPC > *track = pEntity->GetLagTrack();
CUtlFixedLinkedList< LagRecord > *track = &m_EntityTrack[ index ];


// check if we have at leat one entry
// check if we have at leat one entry
Line 491: Line 390:
int curr = track->Head();
int curr = track->Head();


LagRecord *prevRecord = NULL;
LagRecordNPC *prevRecord = NULL;
LagRecord *record = NULL;
LagRecordNPC *record = NULL;


Vector prevOrg = pEntity->GetLocalOrigin();
Vector prevOrg = pEntity->GetLocalOrigin();
Line 621: Line 520:
// If we haven't backtracked this player, do it now
// If we haven't backtracked this player, do it now
// this deliberately ignores WantsLagCompensationOnEntity.
// this deliberately ignores WantsLagCompensationOnEntity.
if ( pNPC && !m_RestoreEntity.Get( pNPC->GetAIIndex() ) )
if ( pNPC && !pNPC->IsLagFlagged() )
{
{
// prevent recursion - save a copy of m_RestoreEntity,
// prevent recursion - save a copy of m_RestoreEntity,
Line 627: Line 526:


// Temp turn this flag on
// Temp turn this flag on
m_RestoreEntity.Set( pNPC->GetAIIndex() );
pNPC->FlagForLagCompensation(true);


BacktrackEntity( pHitEntity, flTargetTime );
BacktrackEntity( pHitEntity, flTargetTime );


// Remove the temp flag
// Remove the temp flag
m_RestoreEntity.Clear( pNPC->GetAIIndex() );
pNPC->FlagForLagCompensation(false);
}
}
}
}
Line 664: Line 563:
// See if this represents a change for the entity
// See if this represents a change for the entity
int flags = 0;
int flags = 0;
LagRecord *restore = &m_EntityRestoreData[ index ];
LagRecordNPC *restore = new LagRecordNPC();//pEntity->GetLagRestoreData();
LagRecord *change  = &m_EntityChangeData[ index ];
LagRecordNPC *change  = new LagRecordNPC();//pEntity->GetLagChangeData();


QAngle angdiff = pEntity->GetLocalAngles() - ang;
QAngle angdiff = pEntity->GetLocalAngles() - ang;
Line 759: Line 658:
if( (frac > 0.0f)  &&  interpolationAllowed )
if( (frac > 0.0f)  &&  interpolationAllowed )
{
{
LayerRecord &recordsLayerRecord = record->m_layerRecords[layerIndex];
LayerRecordNPC &recordsLayerRecord = record->m_layerRecords[layerIndex];
LayerRecord &prevRecordsLayerRecord = prevRecord->m_layerRecords[layerIndex];
LayerRecordNPC &prevRecordsLayerRecord = prevRecord->m_layerRecords[layerIndex];
if( (recordsLayerRecord.m_order == prevRecordsLayerRecord.m_order)
if( (recordsLayerRecord.m_order == prevRecordsLayerRecord.m_order)
&& (recordsLayerRecord.m_sequence == prevRecordsLayerRecord.m_sequence)
&& (recordsLayerRecord.m_sequence == prevRecordsLayerRecord.m_sequence)
Line 805: Line 704:
NDebugOverlay::EntityBounds( pEntity, 255, 0, 0, 32, 10 ); */
NDebugOverlay::EntityBounds( pEntity, 255, 0, 0, 32, 10 ); */


m_RestoreEntity.Set( index ); //remember that we changed this entity
pEntity->FlagForLagCompensation(true); //remember that we changed this entity
 
m_bNeedToRestore = true;  // we changed at least one player / entity
m_bNeedToRestore = true;  // we changed at least one player / entity
restore->m_fFlags = flags; // we need to restore these flags
restore->m_fFlags = flags; // we need to restore these flags
change->m_fFlags = flags; // we have changed these flags
change->m_fFlags = flags; // we have changed these flags
pEntity->SetLagRestoreData(restore);
pEntity->SetLagChangeData(change);


if( sv_showlagcompensation.GetInt() == 1 )
if( sv_showlagcompensation.GetInt() == 1 )
Line 818: Line 721:
</div>
</div>


And lastly, add this onto the end of FinishLagCompensation:
Lastly, add this onto the end of <code>FinishLagCompensation</code>:


<div style="max-height:40em;overflow:auto;">
<div style="max-height:40em;overflow:auto;">
Line 830: Line 733:
CAI_BaseNPC *pNPC = ppAIs[i];
CAI_BaseNPC *pNPC = ppAIs[i];
if ( !m_RestoreEntity.Get( i ) )
if ( !pNPC->IsLagFlagged() )
{
{
// entity wasn't changed by lag compensation
// entity wasn't changed by lag compensation
Line 836: Line 739:
}
}


LagRecord *restore = &m_EntityRestoreData[ i ];
LagRecordNPC *restore = pNPC->GetLagRestoreData();
LagRecord *change  = &m_EntityChangeData[ i ];
LagRecordNPC *change  = pNPC->GetLagChangeData();


bool restoreSimulationTime = false;
bool restoreSimulationTime = false;
Line 908: Line 811:
</div>
</div>


Congratulations, assuming this compiles, your NPCs should all be lag compensated. To verify that its working, run a map and in the console first type sv_cheats 1, then sv_showlagcompensation 1 - when you shoot at an NPC, blue rectangles representing each bone should be drawn around it.
Congratulations! Assuming this compiles, your NPCs should all be lag-compensated. To verify that it's working, run a map. In the console, first type {{ent|sv_cheats|1}}, then <code>sv_showlagcompensation 1</code>. When you shoot at an NPC, blue rectangles representing each bone should be drawn around it.
 
[[Category:Networking]]
[[Category:AI Programming]]

Latest revision as of 08:27, 12 July 2024

English (en)中文 (zh)Translate (Translate)

Without lag compensation, when you shoot at a target, you have to take your ping time into account and aim ahead of it accordingly. So if your ping is 100ms, you have to aim for where the target's head will be in 100ms, rather than where you see it right now.

Clearly, this is less than ideal, so multiplayer Source games implement lag compensation. When calculating whether a bullet hits or not, it briefly adjusts the position of all players back by the shooter's ping, so that it calculates the bullet collision based upon exactly what the shooter saw when they fired. This effect is missing for NPCs but can be duplicated from the player lag compensation code without too much trouble.

This tutorial details all the changes required—while a lot of code is involved, 95% of it is duplicated from player lag compensation code.

ai_basenpc.h

Just before this line:

typedef CBitVec<MAX_CONDITIONS> CAI_ScheduleBits;

Add the following (taken from player_lagcompensation.cpp, but edited):

#define MAX_LAYER_RECORDS (CBaseAnimatingOverlay::MAX_OVERLAYS)

struct LayerRecordNPC
{
	int m_sequence;
	float m_cycle;
	float m_weight;
	int m_order;

	LayerRecordNPC()
	{
		m_sequence = 0;
		m_cycle = 0;
		m_weight = 0;
		m_order = 0;
	}

	LayerRecordNPC( const LayerRecordNPC& src )
	{
		m_sequence = src.m_sequence;
		m_cycle = src.m_cycle;
		m_weight = src.m_weight;
		m_order = src.m_order;
	}
};

struct LagRecordNPC
{
public:
	LagRecordNPC()
	{
		m_fFlags = 0;
		m_vecOrigin.Init();
		m_vecAngles.Init();
		m_vecMins.Init();
		m_vecMaxs.Init();
		m_flSimulationTime = -1;
		m_masterSequence = 0;
		m_masterCycle = 0;
	}

	LagRecordNPC( const LagRecordNPC& src )
	{
		m_fFlags = src.m_fFlags;
		m_vecOrigin = src.m_vecOrigin;
		m_vecAngles = src.m_vecAngles;
		m_vecMins = src.m_vecMins;
		m_vecMaxs = src.m_vecMaxs;
		m_flSimulationTime = src.m_flSimulationTime;
		for( int layerIndex = 0; layerIndex < MAX_LAYER_RECORDS; ++layerIndex )
		{
			m_layerRecords[layerIndex] = src.m_layerRecords[layerIndex];
		}
		m_masterSequence = src.m_masterSequence;
		m_masterCycle = src.m_masterCycle;
	}

	// Did player die this frame
	int						m_fFlags;

	// Player position, orientation and bbox
	Vector					m_vecOrigin;
	QAngle					m_vecAngles;
	Vector					m_vecMins;
	Vector					m_vecMaxs;

	float					m_flSimulationTime;	
	
	// Player animation details, so we can get the legs in the right spot.
	LayerRecordNPC			m_layerRecords[MAX_LAYER_RECORDS];
	int						m_masterSequence;
	float					m_masterCycle;
};

We are going to attach an NPC's lag record directly onto the NPC to prevent confusion of records, so the NPC code needs to be able to access these.

Now again in ai_basenpc.h, add the following to the CAI_BaseNPC class definition:

public:
	CUtlFixedLinkedList<LagRecordNPC>* GetLagTrack() { return m_LagTrack; }
	LagRecordNPC*	GetLagRestoreData() { if ( m_RestoreData != NULL ) return m_RestoreData; else return new LagRecordNPC(); }
	LagRecordNPC*	GetLagChangeData() { if ( m_ChangeData != NULL ) return m_ChangeData; else return new LagRecordNPC(); }
	void		SetLagRestoreData(LagRecordNPC* l) { if ( m_RestoreData != NULL ) delete m_RestoreData; m_RestoreData = l; }
	void		SetLagChangeData(LagRecordNPC* l) { if ( m_ChangeData != NULL ) delete m_ChangeData; m_ChangeData = l; }
	void		FlagForLagCompensation( bool tempValue ) { m_bFlaggedForLagCompensation = tempValue; }
	bool		IsLagFlagged() { return m_bFlaggedForLagCompensation; }

private:
	CUtlFixedLinkedList<LagRecordNPC>* m_LagTrack;
	LagRecordNPC*	m_RestoreData;
	LagRecordNPC*	m_ChangeData;
	bool		m_bFlaggedForLagCompensation;

This "lag record" is what stores an NPC's historical position and animation information.

ai_basenpc.cpp

At the bottom of the CAI_BaseNPC constructor, add this line:

m_LagTrack = new CUtlFixedLinkedList< LagRecordNPC >();

In the beginning of the CAI_BaseNPC destructor, add this line:

m_LagTrack->Purge();
delete m_LagTrack;

player.cpp/player.h

To decide what should be lag compensated, the player class has a function, WantsLagCompensationOnEntity, which returns true or false. Unfortunately, it's designed to only accept a player as its "entity" parameter… but we want it to lag compensate NPCs too. To accomplish this, we'll change it from taking only a CBasePlayer pointer to taking a CBaseEntity, which could be a player or an NPC.

Find where the function WantsLagCompensationOnEntity is defined and change the first parameter from const CBasePlayer *pPlayer to const CBaseEntity *pEntity.

Now in player.cpp, find this function at around line 682, and change the CBasePlayer parameter to CBaseEntity, as before. Replace all references to pPlayer (only in this function!) to pEntity (assuming that's what you renamed the parameter), then remove the line:

float maxDistance = 1.5 * pPlayer->MaxSpeed() * sv_maxunlag.GetFloat();

Replace it with the following:

float maxspeed;
CBasePlayer *pPlayer = ToBasePlayer((CBaseEntity*)pEntity);
if ( pPlayer )
	maxspeed = pPlayer->MaxSpeed();
else
	maxspeed = 600;
float maxDistance = 1.5 * maxspeed * sv_maxunlag.GetFloat();

That should do exactly the same for players and something similar for NPCs.

hl2mp_player.cpp/hl2mp_player.h

The same function exists, more or less identically, for hl2mp_player.h & hl2mp_player.cpp, so we'll have to fix that too. Open them both up. First in the header, find where the function WantsLagCompensationOnEntity is defined (as with player.h), and change the first parameter from const CBasePlayer *pPlayer to const CBaseEntity *pEntity.

Now in hl2mp_player.cpp, find WantsLagCompensationOnEntity. Except for a check at the start, this actually does exactly the same as the player.cpp version, so just chop everything except the first if and tell it to call the player.cpp version by using BaseClass:

bool CHL2MP_Player::WantsLagCompensationOnEntity( const CBaseEntity *pEntity, const CUserCmd *pCmd, const CBitVec<MAX_EDICTS> *pEntityTransmitBits ) const
{
	// No need to lag compensate at all if we're not attacking in this command and
	// we haven't attacked recently.
	if ( !( pCmd->buttons & IN_ATTACK ) && (pCmd->command_number - m_iLastWeaponFireUsercmd > 5) )
		return false;

	return BaseClass::WantsLagCompensationOnEntity(pEntity,pCmd,pEntityTransmitBits);
}

player_lagcompensation.cpp

Right, that's all the bits and pieces done, we can now actually do some lag compensation. Essentially, we're going to duplicate everything this class does for players, and make it work for NPCs. We'll go through all the functions in the order that they're written, adjusting and adding as necessary.

To start with, add this to the include list:

#include "ai_basenpc.h"

The first big function here is RestorePlayerTo. We're going to duplicate this, and make the duplicate work for NPCs instead. Paste this after the end of that function, just before the class definition that follows:

static void RestoreEntityTo( CAI_BaseNPC *pEntity, const Vector &vWantedPos )
{
	// Try to move to the wanted position from our current position.
	trace_t tr;
	VPROF_BUDGET( "RestoreEntityTo", "CLagCompensationManager" );
	UTIL_TraceEntity( pEntity, vWantedPos, vWantedPos, MASK_NPCSOLID, pEntity, COLLISION_GROUP_NPC, &tr );
	if ( tr.startsolid || tr.allsolid )
	{
		if ( sv_unlag_debug.GetBool() )
		{
			DevMsg( "RestorepEntityTo() could not restore entity position for %s ( %.1f %.1f %.1f )\n",
					pEntity->GetClassname(), vWantedPos.x, vWantedPos.y, vWantedPos.z );
		}

		UTIL_TraceEntity( pEntity, pEntity->GetLocalOrigin(), vWantedPos, MASK_NPCSOLID, pEntity, COLLISION_GROUP_NPC, &tr );
		if ( tr.startsolid || tr.allsolid )
		{
			// In this case, the guy got stuck back wherever we lag compensated him to. Nasty.

			if ( sv_unlag_debug.GetBool() )
				DevMsg( " restore failed entirely\n" );
		}
		else
		{
			// We can get to a valid place, but not all the way back to where we were.
			Vector vPos;
			VectorLerp( pEntity->GetLocalOrigin(), vWantedPos, tr.fraction * g_flFractionScale, vPos );
			UTIL_SetOrigin( pEntity, vPos, true );

			if ( sv_unlag_debug.GetBool() )
				DevMsg( " restore got most of the way\n" );
		}
	}
	else
	{
		// Cool, the entity can go back to whence he came.
		UTIL_SetOrigin( pEntity, tr.endpos, true );
	}
}

The private: section of the CLagCompensationManager class definition should start with the function:

void BacktrackPlayer( CBasePlayer *player, float flTargetTime );

Just under this, add:

void BacktrackEntity( CAI_BaseNPC *entity, float flTargetTime );

Next up is FrameUpdatePostEntityThink. This function is dominated by a large for loop, which we're going to duplicate and make work for NPCs instead. Add this onto the end of the function:

// Iterate all active NPCs
CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
int nAIs = g_AI_Manager.NumAIs();

for ( int i = 0; i < nAIs; i++ )
{
	CAI_BaseNPC *pNPC = ppAIs[i];
	if ( !pNPC )
		continue;

	CUtlFixedLinkedList< LagRecordNPC > *track = pNPC->GetLagTrack();

	Assert( track->Count() < 1000 ); // insanity check

	// remove tail records that are too old
	int tailIndex = track->Tail();
	while ( track->IsValidIndex( tailIndex ) )
	{
		LagRecordNPC &tail = track->Element( tailIndex );

		// if tail is within limits, stop
		if ( tail.m_flSimulationTime >= flDeadtime )
			break;
		
		// remove tail, get new tail
		track->Remove( tailIndex );
		tailIndex = track->Tail();
	}

	// check if head has same simulation time
	if ( track->Count() > 0 )
	{
		LagRecordNPC &head = track->Element( track->Head() );

		// check if entity changed simulation time since last time updated
		if ( &head && head.m_flSimulationTime >= pNPC->GetSimulationTime() )
			continue; // don't add new entry for same or older time

		// Simulation Time is set when an entity moves or rotates ...
		// this error occurs when whatever entity it is that breaks it moves or rotates then, presumably?
	}

	// add new record to track
	LagRecordNPC &record = track->Element( track->AddToHead() );

	record.m_fFlags = 0;
	if ( pNPC->IsAlive() )
	{
		record.m_fFlags |= LC_ALIVE;
	}

	record.m_flSimulationTime	= pNPC->GetSimulationTime();
	record.m_vecAngles			= pNPC->GetLocalAngles();
	record.m_vecOrigin			= pNPC->GetLocalOrigin();
	record.m_vecMaxs			= pNPC->WorldAlignMaxs();
	record.m_vecMins			= pNPC->WorldAlignMins();

	int layerCount = pNPC->GetNumAnimOverlays();
	for( int layerIndex = 0; layerIndex < layerCount; ++layerIndex )
	{
		CAnimationLayer *currentLayer = pNPC->GetAnimOverlay(layerIndex);
		if( currentLayer )
		{
			record.m_layerRecords[layerIndex].m_cycle = currentLayer->m_flCycle;
			record.m_layerRecords[layerIndex].m_order = currentLayer->m_nOrder;
			record.m_layerRecords[layerIndex].m_sequence = currentLayer->m_nSequence;
			record.m_layerRecords[layerIndex].m_weight = currentLayer->m_flWeight;
		}
	}
	record.m_masterSequence = pNPC->GetSequence();
	record.m_masterCycle = pNPC->GetCycle();
}

At the top of StartLagCompensation, just under m_RestorePlayer.ClearAll();, add:

int nAIs = g_AI_Manager.NumAIs();
CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
for (int i=0; i<nAIs; i++)
	ppAIs[ i ]->FlagForLagCompensation(false);

This function ends with a for loop, commented with:

// Iterate all active players

Remove it and replace it with two of:

// Iterate all active players
const CBitVec<MAX_EDICTS> *pEntityTransmitBits = engine->GetEntityTransmitBitsForClient( player->entindex() - 1 );
for ( int i = 1; i <= gpGlobals->maxClients; i++ )
{
	CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
	if ( !pPlayer || player == pPlayer )
		continue;

	// Custom checks for if things should lag compensate (based on things like what team the player is on).
	if ( !player->WantsLagCompensationOnEntity( pPlayer, cmd, pEntityTransmitBits ) )
		continue;

	// Move other player back in time
	BacktrackPlayer( pPlayer, TICKS_TO_TIME( targettick ) );
}

// also iterate all monsters
for ( int i = 0; i < nAIs; i++ )
{
	CAI_BaseNPC *pNPC = ppAIs[i];
	// Custom checks for if things should lag compensate
	if ( !pNPC || !player->WantsLagCompensationOnEntity( pNPC, cmd, pEntityTransmitBits ) )
		continue;
	
	// Move NPC back in time
	BacktrackEntity( pNPC, TICKS_TO_TIME( targettick ) );
}

We only want to do one thing to BacktrackPlayer, and that check for entities also where we check for players that we might be bumping into. Find:

// don't lag compensate the current player
if ( pHitPlayer && ( pHitPlayer != m_pCurrentPlayer ) )	
{

After the closing bracer of that if (that is, the } matching its {), add this else statement:

else
{
	CAI_BaseNPC *pHitEntity = dynamic_cast<CAI_BaseNPC *>( tr.m_pEnt );
	if ( pHitEntity )
	{
		CAI_BaseNPC *pNPC = NULL;
		CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
		int nAIs = g_AI_Manager.NumAIs();
		for ( int i = 0; i < nAIs; i++ ) // we'll have to find this entity's index though :(
		{
			pNPC = ppAIs[i];
			if ( pNPC == pHitEntity )
				break;
		}
		// If we haven't backtracked this player, do it now
		// this deliberately ignores WantsLagCompensationOnEntity.
		if ( pNPC && !pNPC->IsLagFlagged() )
		{
			// prevent recursion - save a copy of m_RestoreEntity,
			// pretend that this player is off-limits

			// Temp turn this flag on
			pNPC->FlagForLagCompensation(true);

			BacktrackEntity( pHitEntity, flTargetTime );

			// Remove the temp flag
			pNPC->FlagForLagCompensation(false);
		}
	}
}

Two more big blocks to go. Firstly, you guessed it, we're gonna make an NPC version of BacktrackPlayer, BacktrackEntity. It's big!

void CLagCompensationManager::BacktrackEntity( CAI_BaseNPC *pEntity, float flTargetTime )
{
	Vector org, mins, maxs;
	QAngle ang;

	VPROF_BUDGET( "BacktrackEntity", "CLagCompensationManager" );

	// get track history of this entity
	CUtlFixedLinkedList< LagRecordNPC > *track = pEntity->GetLagTrack();

	// check if we have at leat one entry
	if ( track->Count() <= 0 )
		return;

	int curr = track->Head();

	LagRecordNPC *prevRecord = NULL;
	LagRecordNPC *record = NULL;

	Vector prevOrg = pEntity->GetLocalOrigin();
	
	// Walk context looking for any invalidating event
	while( track->IsValidIndex(curr) )
	{
		// remember last record
		prevRecord = record;

		// get next record
		record = &track->Element( curr );

		if ( !(record->m_fFlags & LC_ALIVE) )
		{
			// entity must be alive, lost track
			return;
		}

		Vector delta = record->m_vecOrigin - prevOrg;
		if ( delta.LengthSqr() > LAG_COMPENSATION_TELEPORTED_DISTANCE_SQR )
		{
			// lost track, moved too far (may have teleported)
			return; 
		}

		// did we find a context smaller than target time ?
		if ( record->m_flSimulationTime <= flTargetTime )
			break; // hurra, stop

		prevOrg = record->m_vecOrigin;

		// go one step back in time
		curr = track->Next( curr );
	}

	Assert( record );

	if ( !record )
	{
		if ( sv_unlag_debug.GetBool() )
		{
			DevMsg( "No valid positions in history for BacktrackEntity ( %s )\n", pEntity->GetClassname() );
		}

		return; // that should never happen
	}

	float frac = 0.0f;
 	if ( prevRecord && 
		 (record->m_flSimulationTime < flTargetTime) &&
		 (record->m_flSimulationTime < prevRecord->m_flSimulationTime) )
	{
		// we didn't find the exact time but have a valid previous record
		// so interpolate between these two records;

		Assert( prevRecord->m_flSimulationTime > record->m_flSimulationTime );
		Assert( flTargetTime < prevRecord->m_flSimulationTime );

		// calc fraction between both records
		frac = ( flTargetTime - record->m_flSimulationTime ) / 
			( prevRecord->m_flSimulationTime - record->m_flSimulationTime );

		Assert( frac > 0 && frac < 1 ); // should never extrapolate

		ang  = Lerp( frac, record->m_vecAngles, prevRecord->m_vecAngles );
		org  = Lerp( frac, record->m_vecOrigin, prevRecord->m_vecOrigin  );
		mins = Lerp( frac, record->m_vecMins, prevRecord->m_vecMins  );
		maxs = Lerp( frac, record->m_vecMaxs, prevRecord->m_vecMaxs );
	}
	else
 	{
		// we found the exact record or no other record to interpolate with
		// just copy these values since they are the best we have
		ang  = record->m_vecAngles;
		org  = record->m_vecOrigin;
		mins = record->m_vecMins;
		maxs = record->m_vecMaxs;
	}

	// See if this is still a valid position for us to teleport to
	if ( sv_unlag_fixstuck.GetBool() )
	{
		// Try to move to the wanted position from our current position.
 		trace_t tr;
		UTIL_TraceEntity( pEntity, org, org, MASK_NPCSOLID, &tr );
		if ( tr.startsolid || tr.allsolid )
		{
			if ( sv_unlag_debug.GetBool() )
				DevMsg( "WARNING: BackupEntity trying to back entity into a bad position - %s\n", pEntity->GetClassname() );

			CBasePlayer *pHitPlayer = dynamic_cast<CBasePlayer *>( tr.m_pEnt );

			// don't lag compensate the current player
			if ( pHitPlayer && ( pHitPlayer != m_pCurrentPlayer ) )	
			{
				// If we haven't backtracked this player, do it now
				// this deliberately ignores WantsLagCompensationOnEntity.
				if ( !m_RestorePlayer.Get( pHitPlayer->entindex() - 1 ) )
				{
					// prevent recursion - save a copy of m_RestorePlayer,
					// pretend that this player is off-limits
					int pl_index = pEntity->entindex() - 1;

					// Temp turn this flag on
					m_RestorePlayer.Set( pl_index );

					BacktrackPlayer( pHitPlayer, flTargetTime );

					// Remove the temp flag
					m_RestorePlayer.Clear( pl_index );
				}				
			}
			else
			{
				CAI_BaseNPC *pHitEntity = dynamic_cast<CAI_BaseNPC *>( tr.m_pEnt );
				if ( pHitEntity )
				{
					CAI_BaseNPC *pNPC = NULL;
					CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
					int nAIs = g_AI_Manager.NumAIs();
					for ( int i = 0; i < nAIs; i++ ) // we'll have to find this entity's index though :(
					{
						pNPC = ppAIs[i];
						if ( pNPC == pHitEntity )
							break;
					}
					// If we haven't backtracked this player, do it now
					// this deliberately ignores WantsLagCompensationOnEntity.
					if ( pNPC && !pNPC->IsLagFlagged() )
					{
						// prevent recursion - save a copy of m_RestoreEntity,
						// pretend that this player is off-limits

						// Temp turn this flag on
						pNPC->FlagForLagCompensation(true);

						BacktrackEntity( pHitEntity, flTargetTime );

						// Remove the temp flag
						pNPC->FlagForLagCompensation(false);
					}
				}
			}

			// now trace us back as far as we can go
			UTIL_TraceEntity( pEntity, pEntity->GetLocalOrigin(), org, MASK_NPCSOLID, &tr );

			if ( tr.startsolid || tr.allsolid )
			{
				// Our starting position is bogus

				if ( sv_unlag_debug.GetBool() )
					DevMsg( "Backtrack failed completely, bad starting position\n" );
			}
			else
			{
				// We can get to a valid place, but not all the way to the target
				Vector vPos;
				VectorLerp( pEntity->GetLocalOrigin(), org, tr.fraction * g_flFractionScale, vPos );
				
				// This is as close as we're going to get
				org = vPos;

				if ( sv_unlag_debug.GetBool() )
					DevMsg( "Backtrack got most of the way\n" );
			}
		}
	}
	
	// See if this represents a change for the entity
	int flags = 0;
	LagRecordNPC *restore = new LagRecordNPC();//pEntity->GetLagRestoreData();
	LagRecordNPC *change  = new LagRecordNPC();//pEntity->GetLagChangeData();

	QAngle angdiff = pEntity->GetLocalAngles() - ang;
	Vector orgdiff = pEntity->GetLocalOrigin() - org;

	// Always remember the pristine simulation time in case we need to restore it.
	restore->m_flSimulationTime = pEntity->GetSimulationTime();

	if ( angdiff.LengthSqr() > LAG_COMPENSATION_EPS_SQR )
	{
		flags |= LC_ANGLES_CHANGED;
		restore->m_vecAngles = pEntity->GetLocalAngles();
		pEntity->SetLocalAngles( ang );
		change->m_vecAngles = ang;
	}

	// Use absolute equality here
	if ( ( mins != pEntity->WorldAlignMins() ) ||
		 ( maxs != pEntity->WorldAlignMaxs() ) )
	{
		flags |= LC_SIZE_CHANGED;
		restore->m_vecMins = pEntity->WorldAlignMins() ;
		restore->m_vecMaxs = pEntity->WorldAlignMaxs();
		pEntity->SetSize( mins, maxs );
		change->m_vecMins = mins;
		change->m_vecMaxs = maxs;
	}

	// Note, do origin at end since it causes a relink into the k/d tree
	if ( orgdiff.LengthSqr() > LAG_COMPENSATION_EPS_SQR )
	{
		flags |= LC_ORIGIN_CHANGED;
		restore->m_vecOrigin = pEntity->GetLocalOrigin();
		pEntity->SetLocalOrigin( org );
		change->m_vecOrigin = org;
	}

	// Sorry for the loss of the optimization for the case of people
	// standing still, but you breathe even on the server.
	// This is quicker than actually comparing all bazillion floats.
	flags |= LC_ANIMATION_CHANGED;
	restore->m_masterSequence = pEntity->GetSequence();
	restore->m_masterCycle = pEntity->GetCycle();

	bool interpolationAllowed = false;
	if( prevRecord && (record->m_masterSequence == prevRecord->m_masterSequence) )
	{
		// If the master state changes, all layers will be invalid too, so don't interp (ya know, interp barely ever happens anyway)
		interpolationAllowed = true;
	}
	
	////////////////////////
	// First do the master settings
 	bool interpolatedMasters = false;
	if( frac > 0.0f && interpolationAllowed )
 	{
  		interpolatedMasters = true;
		pEntity->SetSequence( Lerp( frac, record->m_masterSequence, prevRecord->m_masterSequence ) );
		pEntity->SetCycle( Lerp( frac, record->m_masterCycle, prevRecord->m_masterCycle ) );

		if( record->m_masterCycle > prevRecord->m_masterCycle )
		{
			// the older record is higher in frame than the newer, it must have wrapped around from 1 back to 0
			// add one to the newer so it is lerping from .9 to 1.1 instead of .9 to .1, for example.
			float newCycle = Lerp( frac, record->m_masterCycle, prevRecord->m_masterCycle + 1 );
			pEntity->SetCycle(newCycle < 1 ? newCycle : newCycle - 1 );// and make sure .9 to 1.2 does not end up 1.05
		}
		else
		{
			pEntity->SetCycle( Lerp( frac, record->m_masterCycle, prevRecord->m_masterCycle ) );
		}
	}
 	if( !interpolatedMasters )
	{
		pEntity->SetSequence(record->m_masterSequence);
		pEntity->SetCycle(record->m_masterCycle);
	}

	////////////////////////
	// Now do all the layers
	int layerCount = pEntity->GetNumAnimOverlays();
	for( int layerIndex = 0; layerIndex < layerCount; ++layerIndex )
	{
		CAnimationLayer *currentLayer = pEntity->GetAnimOverlay(layerIndex);
		if( currentLayer )
		{
			restore->m_layerRecords[layerIndex].m_cycle = currentLayer->m_flCycle;
			restore->m_layerRecords[layerIndex].m_order = currentLayer->m_nOrder;
			restore->m_layerRecords[layerIndex].m_sequence = currentLayer->m_nSequence;
			restore->m_layerRecords[layerIndex].m_weight = currentLayer->m_flWeight;

			bool interpolated = false;
			if( (frac > 0.0f)  &&  interpolationAllowed )
			{
				LayerRecordNPC &recordsLayerRecord = record->m_layerRecords[layerIndex];
				LayerRecordNPC &prevRecordsLayerRecord = prevRecord->m_layerRecords[layerIndex];
				if( (recordsLayerRecord.m_order == prevRecordsLayerRecord.m_order)
					&& (recordsLayerRecord.m_sequence == prevRecordsLayerRecord.m_sequence)
					)
				{
					// We can't interpolate across a sequence or order change
					interpolated = true;
					if( recordsLayerRecord.m_cycle > prevRecordsLayerRecord.m_cycle )
					{
						// the older record is higher in frame than the newer, it must have wrapped around from 1 back to 0
						// add one to the newer so it is lerping from .9 to 1.1 instead of .9 to .1, for example.
						float newCycle = Lerp( frac, recordsLayerRecord.m_cycle, prevRecordsLayerRecord.m_cycle + 1 );
						currentLayer->m_flCycle = newCycle < 1 ? newCycle : newCycle - 1;// and make sure .9 to 1.2 does not end up 1.05
					}
					else
					{
						currentLayer->m_flCycle = Lerp( frac, recordsLayerRecord.m_cycle, prevRecordsLayerRecord.m_cycle  );
					}
					currentLayer->m_nOrder = recordsLayerRecord.m_order;
					currentLayer->m_nSequence = recordsLayerRecord.m_sequence;
					currentLayer->m_flWeight = Lerp( frac, recordsLayerRecord.m_weight, prevRecordsLayerRecord.m_weight  );
				}
			}
			if( !interpolated )
			{
				//Either no interp, or interp failed.  Just use record.
				currentLayer->m_flCycle = record->m_layerRecords[layerIndex].m_cycle;
				currentLayer->m_nOrder = record->m_layerRecords[layerIndex].m_order;
				currentLayer->m_nSequence = record->m_layerRecords[layerIndex].m_sequence;
				currentLayer->m_flWeight = record->m_layerRecords[layerIndex].m_weight;
			}
		}
	}
	
	if ( !flags )
		return; // we didn't change anything

	if ( sv_lagflushbonecache.GetBool() )
		pEntity->InvalidateBoneCache();

	/*char text[256]; Q_snprintf( text, sizeof(text), "time %.2f", flTargetTime );
	pEntity->DrawServerHitboxes( 10 );
	NDebugOverlay::Text( org, text, false, 10 );
	NDebugOverlay::EntityBounds( pEntity, 255, 0, 0, 32, 10 ); */

	pEntity->FlagForLagCompensation(true); //remember that we changed this entity

	m_bNeedToRestore = true;  // we changed at least one player / entity
	restore->m_fFlags = flags; // we need to restore these flags
	change->m_fFlags = flags; // we have changed these flags

	pEntity->SetLagRestoreData(restore);
	pEntity->SetLagChangeData(change);

	if( sv_showlagcompensation.GetInt() == 1 )
	{
		pEntity->DrawServerHitboxes(4, true);
	}
}

Lastly, add this onto the end of FinishLagCompensation:

// also iterate all monsters
CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs();
int nAIs = g_AI_Manager.NumAIs();

for ( int i = 0; i < nAIs; i++ )
{
	CAI_BaseNPC *pNPC = ppAIs[i];
	
	if ( !pNPC->IsLagFlagged() )
	{
		// entity wasn't changed by lag compensation
		continue;
	}

	LagRecordNPC *restore = pNPC->GetLagRestoreData();
	LagRecordNPC *change  = pNPC->GetLagChangeData();

	bool restoreSimulationTime = false;

	if ( restore->m_fFlags & LC_SIZE_CHANGED )
	{
		restoreSimulationTime = true;

		// see if simulation made any changes, if no, then do the restore, otherwise,
		//  leave new values in
		if ( pNPC->WorldAlignMins() == change->m_vecMins && 
			pNPC->WorldAlignMaxs() == change->m_vecMaxs )
		{
			// Restore it
			pNPC->SetSize( restore->m_vecMins, restore->m_vecMaxs );
		}
	}

	if ( restore->m_fFlags & LC_ANGLES_CHANGED )
	{ 		  
		restoreSimulationTime = true;

		if ( pNPC->GetLocalAngles() == change->m_vecAngles )
		{
			pNPC->SetLocalAngles( restore->m_vecAngles );
		}
	}

	if ( restore->m_fFlags & LC_ORIGIN_CHANGED )
	{
		restoreSimulationTime = true;

		// Okay, let's see if we can do something reasonable with the change
		Vector delta = pNPC->GetLocalOrigin() - change->m_vecOrigin;
		
		// If it moved really far, just leave the player in the new spot!!!
		if ( delta.LengthSqr() < LAG_COMPENSATION_TELEPORTED_DISTANCE_SQR )
		{
			RestoreEntityTo( pNPC, restore->m_vecOrigin + delta );
		}
	}

	if( restore->m_fFlags & LC_ANIMATION_CHANGED )
	{
		restoreSimulationTime = true;

		pNPC->SetSequence(restore->m_masterSequence);
		pNPC->SetCycle(restore->m_masterCycle);

		int layerCount = pNPC->GetNumAnimOverlays();
		for( int layerIndex = 0; layerIndex < layerCount; ++layerIndex )
		{
			CAnimationLayer *currentLayer = pNPC->GetAnimOverlay(layerIndex);
			if( currentLayer )
			{
				currentLayer->m_flCycle = restore->m_layerRecords[layerIndex].m_cycle;
				currentLayer->m_nOrder = restore->m_layerRecords[layerIndex].m_order;
				currentLayer->m_nSequence = restore->m_layerRecords[layerIndex].m_sequence;
				currentLayer->m_flWeight = restore->m_layerRecords[layerIndex].m_weight;
			}
		}
	}

	if ( restoreSimulationTime )
	{
		pNPC->SetSimulationTime( restore->m_flSimulationTime );
	}
}

Congratulations! Assuming this compiles, your NPCs should all be lag-compensated. To verify that it's working, run a map. In the console, first type sv_cheats 1, then sv_showlagcompensation 1. When you shoot at an NPC, blue rectangles representing each bone should be drawn around it.