Skip to content

NPC memory fixes and NPC VScript expansions #337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 308 additions & 3 deletions sp/src/game/server/ai_basenpc.cpp

Large diffs are not rendered by default.

44 changes: 42 additions & 2 deletions sp/src/game/server/ai_basenpc.h
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@ class CAI_BaseNPC : public CBaseCombatCharacter,
virtual void OnLooked( int iDistance );
virtual void OnListened();

virtual void OnSeeEntity( CBaseEntity *pEntity ) {}
virtual void OnSeeEntity( CBaseEntity *pEntity );

// If true, AI will try to see this entity regardless of distance.
virtual bool ShouldNotDistanceCull() { return false; }
Expand Down Expand Up @@ -1271,9 +1271,24 @@ class CAI_BaseNPC : public CBaseCombatCharacter,
void VScriptSetEnemy( HSCRIPT pEnemy );
Vector VScriptGetEnemyLKP();

HSCRIPT VScriptFindEnemyMemory( HSCRIPT pEnemy );
int VScriptNumEnemies();

HSCRIPT VScriptGetFirstEnemyMemory();
HSCRIPT VScriptGetNextEnemyMemory( HSCRIPT hMemory );

HSCRIPT VScriptFindEnemyMemory( HSCRIPT hEnemy );
bool VScriptUpdateEnemyMemory( HSCRIPT hEnemy, const Vector &position, HSCRIPT hInformer );
void VScriptClearEnemyMemory( HSCRIPT hEnemy );

void VScriptSetFreeKnowledgeDuration( float flDuration );
void VScriptSetEnemyDiscardTime( float flDuration );

int VScriptGetState();
int VScriptGetIdealState();
void VScriptSetIdealState( int nNPCState );

HSCRIPT VScriptGetTarget();
void VScriptSetTarget( HSCRIPT hTarget );

void VScriptWake( HSCRIPT hActivator ) { Wake( ToEnt(hActivator) ); }
void VScriptSleep() { Sleep(); }
Expand Down Expand Up @@ -1308,12 +1323,29 @@ class CAI_BaseNPC : public CBaseCombatCharacter,
void VScriptSetCondition( const char *szCondition ) { SetCondition( GetConditionID( szCondition ) ); }
void VScriptClearCondition( const char *szCondition ) { ClearCondition( GetConditionID( szCondition ) ); }

void VScriptSetCustomInterruptCondition( const char *szCondition ) { SetCustomInterruptCondition( GetConditionID( szCondition ) ); }
bool VScriptIsCustomInterruptConditionSet( const char *szCondition ) { return IsCustomInterruptConditionSet( GetConditionID( szCondition ) ); }
void VScriptClearCustomInterruptCondition( const char *szCondition ) { ClearCustomInterruptCondition( GetConditionID( szCondition ) ); }

void VScriptChainStartTask( const char *szTask, float flTaskData ) { ChainStartTask( AI_RemapFromGlobal( GetTaskID( szTask ) ), flTaskData ); }
void VScriptChainRunTask( const char *szTask, float flTaskData ) { ChainRunTask( AI_RemapFromGlobal( GetTaskID( szTask ) ), flTaskData ); }
void VScriptFailTask( const char *szFailReason ) { TaskFail( szFailReason ); }
void VScriptCompleteTask() { TaskComplete(); }
int VScriptGetTaskStatus() { return (int)GetTaskStatus(); }

HSCRIPT VScriptGetExpresser();

HSCRIPT VScriptGetCine();
int GetScriptState() { return m_scriptState; }

HSCRIPT VScriptGetSquad();

HSCRIPT VScriptGetBestSound( int validTypes );
HSCRIPT VScriptGetFirstHeardSound();
HSCRIPT VScriptGetNextHeardSound( HSCRIPT hSound );

HSCRIPT VScriptGetFirstSeenEntity( int nSeenType );
HSCRIPT VScriptGetNextSeenEntity( HSCRIPT hEnt, int nSeenType );
#endif

//-----------------------------------------------------
Expand Down Expand Up @@ -2394,6 +2426,14 @@ class CAI_BaseNPC : public CBaseCombatCharacter,
static ScriptHook_t g_Hook_GetActualShootPosition;
static ScriptHook_t g_Hook_OverrideMove;
static ScriptHook_t g_Hook_ShouldPlayFakeSequenceGesture;
static ScriptHook_t g_Hook_IsValidEnemy;
static ScriptHook_t g_Hook_CanBeAnEnemyOf;
static ScriptHook_t g_Hook_UpdateEnemyMemory;
static ScriptHook_t g_Hook_OnSeeEntity;
static ScriptHook_t g_Hook_OnListened;
static ScriptHook_t g_Hook_BuildScheduleTestBits;
static ScriptHook_t g_Hook_StartTask;
static ScriptHook_t g_Hook_RunTask;
#endif

private:
Expand Down
56 changes: 56 additions & 0 deletions sp/src/game/server/ai_basenpc_schedule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ extern ConVar ai_use_think_optimizations;

ConVar ai_simulate_task_overtime( "ai_simulate_task_overtime", "0" );

#ifdef MAPBASE
ConVar ai_enemy_memory_fixes( "ai_enemy_memory_fixes", "0", FCVAR_NONE, "Toggles Mapbase fixes for certain NPC AI not using enemy memory when it should." );
#endif

#define MAX_TASKS_RUN 10

struct TaskTimings
Expand Down Expand Up @@ -276,6 +280,14 @@ void CAI_BaseNPC::NextScheduledTask ( void )
void CAI_BaseNPC::BuildScheduleTestBits( void )
{
//NOTENOTE: Always defined in the leaf classes

#ifdef MAPBASE_VSCRIPT
if (m_ScriptScope.IsInitialized() && g_Hook_BuildScheduleTestBits.CanRunInScope( m_ScriptScope ))
{
ScriptVariant_t functionReturn;
g_Hook_BuildScheduleTestBits.Call( m_ScriptScope, &functionReturn, NULL );
}
#endif
}


Expand Down Expand Up @@ -730,6 +742,23 @@ void CAI_BaseNPC::MaintainSchedule ( void )
AI_PROFILE_SCOPE_BEGIN_( pszTaskName );
AI_PROFILE_SCOPE_BEGIN(CAI_BaseNPC_StartTask);

#ifdef MAPBASE_VSCRIPT
if (m_ScriptScope.IsInitialized() && g_Hook_StartTask.CanRunInScope( m_ScriptScope ))
{
// task, task_id, task_data
ScriptVariant_t functionReturn;
ScriptVariant_t args[] = { (bDebugTaskNames) ? pszTaskName : TaskName( pTask->iTask ), pTask->iTask, pTask->flTaskData };
if (g_Hook_StartTask.Call( m_ScriptScope, &functionReturn, args ))
{
// Returning false overrides original functionality
if (functionReturn.m_bool != false)
StartTask( pTask );
}
else
StartTask( pTask );
}
else
#endif
StartTask( pTask );

AI_PROFILE_SCOPE_END();
Expand Down Expand Up @@ -766,6 +795,23 @@ void CAI_BaseNPC::MaintainSchedule ( void )
int j;
for (j = 0; j < 8; j++)
{
#ifdef MAPBASE_VSCRIPT
if (m_ScriptScope.IsInitialized() && g_Hook_RunTask.CanRunInScope( m_ScriptScope ))
{
// task, task_id, task_data
ScriptVariant_t functionReturn;
ScriptVariant_t args[] = { (bDebugTaskNames) ? pszTaskName : TaskName( pTask->iTask ), pTask->iTask, pTask->flTaskData };
if (g_Hook_RunTask.Call( m_ScriptScope, &functionReturn, args ))
{
// Returning false overrides original functionality
if (functionReturn.m_bool != false)
RunTask( pTask );
}
else
RunTask( pTask );
}
else
#endif
RunTask( pTask );

if ( GetTaskInterrupt() == 0 || TaskIsComplete() || HasCondition(COND_TASK_FAILED) )
Expand Down Expand Up @@ -1971,7 +2017,17 @@ void CAI_BaseNPC::StartTask( const Task_t *pTask )
flMaxRange = m_flDistTooFar;
}

#ifdef MAPBASE
// By default, TASK_GET_PATH_TO_ENEMY_LKP_LOS acts identical to TASK_GET_PATH_TO_ENEMY_LOS.
// Considering the fact TASK_GET_PATH_TO_ENEMY_LKP doesn't use this code, this appears to be a mistake.
// In HL2, this task is used by Combine soldiers, metrocops, striders, and hunters.
// With this change, these NPCs will establish LOS according to enemy memory instead of where the enemy
// actually is. This may make the NPCs more consistent and fair, but their AI and levels built around it
// may have been designed around this bug, so this is currently being tied to a cvar.
Vector vecEnemy = ( task != TASK_GET_PATH_TO_ENEMY_LKP_LOS || !ai_enemy_memory_fixes.GetBool() ) ? GetEnemy()->GetAbsOrigin() : GetEnemyLKP();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to reread the comment a couple of times before I understood this. Just to be absolutely clear: There is no chance that TASK_GET_PATH_TO_ENEMY_LKP could use this code? Is it worth preserving the original case just to be sure?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is within a switch statement case that is used by TASK_GET_PATH_TO_ENEMY_LOS, TASK_GET_FLANK_RADIUS_PATH_TO_ENEMY_LOS, TASK_GET_FLANK_ARC_PATH_TO_ENEMY_LOS, and TASK_GET_PATH_TO_ENEMY_LKP_LOS. The case for TASK_GET_PATH_TO_ENEMY_LKP is farther above and does not fall back into this code.

#else
Vector vecEnemy = ( task != TASK_GET_PATH_TO_ENEMY_LKP ) ? GetEnemy()->GetAbsOrigin() : GetEnemyLKP();
#endif
Vector vecEnemyEye = vecEnemy + GetEnemy()->GetViewOffset();

Vector posLos;
Expand Down
7 changes: 7 additions & 0 deletions sp/src/game/server/ai_default.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,13 @@ class CAI_SystemHook : public CAutoGameSystem
g_AI_SensedObjectsManager.Init();
}

#ifdef MAPBASE_VSCRIPT
virtual void RegisterVScript()
{
g_pScriptVM->RegisterInstance( &g_AI_SensedObjectsManager, "SensedObjectsManager" );
}
#endif

void LevelShutdownPreEntity()
{
CBaseCombatCharacter::ResetVisibilityCache();
Expand Down
12 changes: 12 additions & 0 deletions sp/src/game/server/ai_memory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,18 @@ AI_EnemyInfo_t *CAI_Enemies::Find( CBaseEntity *pEntity, bool bTryDangerMemory )
}


//-----------------------------------------------------------------------------

#ifdef MAPBASE
unsigned char CAI_Enemies::FindIndex( CBaseEntity *pEntity )
{
if ( pEntity == AI_UNKNOWN_ENEMY )
pEntity = NULL;

return m_Map.Find( pEntity );
}
#endif

//-----------------------------------------------------------------------------

AI_EnemyInfo_t *CAI_Enemies::GetDangerMemory()
Expand Down
3 changes: 3 additions & 0 deletions sp/src/game/server/ai_memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class CAI_Enemies
AI_EnemyInfo_t *GetFirst( AIEnemiesIter_t *pIter );
AI_EnemyInfo_t *GetNext( AIEnemiesIter_t *pIter );
AI_EnemyInfo_t *Find( CBaseEntity *pEntity, bool bTryDangerMemory = false );
#ifdef MAPBASE
unsigned char FindIndex( CBaseEntity *pEntity );
#endif
AI_EnemyInfo_t *GetDangerMemory();

int NumEnemies() const { return m_Map.Count(); }
Expand Down
60 changes: 60 additions & 0 deletions sp/src/game/server/ai_senses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,43 @@ CBaseEntity *CAI_Senses::GetNextSeenEntity( AISightIter_t *pIter ) const
return NULL;
}

//-----------------------------------------------------------------------------

#ifdef MAPBASE
bool CAI_Senses::GetSeenEntityIndex( AISightIter_t *pIter, CBaseEntity *pSightEnt, seentype_t iSeenType ) const
{
COMPILE_TIME_ASSERT( sizeof( AISightIter_t ) == sizeof( AISightIterVal_t ) );

AISightIterVal_t *pIterVal = (AISightIterVal_t *)pIter;

// If we're searching for a specific type, start in that array
pIterVal->SeenArray = (char)iSeenType;
int iFirstArray = ( iSeenType == SEEN_ALL ) ? 0 : iSeenType;

for ( int i = iFirstArray; i < ARRAYSIZE( m_SeenArrays ); i++ )
{
for ( int j = pIterVal->iNext; j < m_SeenArrays[i]->Count(); j++ )
{
if ( (*m_SeenArrays[i])[j].Get() == pSightEnt )
{
pIterVal->array = i;
pIterVal->iNext = j+1;
return true;
}
}
pIterVal->iNext = 0;

// If we're searching for a specific type, don't move to the next array
if ( pIterVal->SeenArray != SEEN_ALL )
break;
}

(*pIter) = (AISightIter_t)(-1);
return false;
}
#endif


//-----------------------------------------------------------------------------

void CAI_Senses::BeginGather()
Expand Down Expand Up @@ -749,4 +786,27 @@ void CAI_SensedObjectsManager::AddEntity( CBaseEntity *pEntity )
m_SensedObjects.AddToTail( pEntity );
}

#ifdef MAPBASE
void CAI_SensedObjectsManager::RemoveEntity( CBaseEntity *pEntity )
{
int i = m_SensedObjects.Find( pEntity );
if (i == m_SensedObjects.InvalidIndex())
return;

pEntity->RemoveFlag( FL_OBJECT );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does removing this flag from the entity do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It prevents the entity from being sensed anymore. FL_OBJECT's purpose is to mark non-player, non-NPC entities that should be seen by NPC senses.

m_SensedObjects.FastRemove( i );
}
#endif

//-----------------------------------------------------------------------------

#ifdef MAPBASE_VSCRIPT
BEGIN_SCRIPTDESC_ROOT( CAI_SensedObjectsManager, SCRIPT_SINGLETON "Manager which handles sensed objects." )

DEFINE_SCRIPTFUNC_NAMED( ScriptAddEntity, "AddEntity", "Adds an entity to the sensed object list." )
DEFINE_SCRIPTFUNC_NAMED( ScriptRemoveEntity, "RemoveEntity", "Removes an entity from the sensed object list." )

END_SCRIPTDESC();
#endif

//=============================================================================
11 changes: 11 additions & 0 deletions sp/src/game/server/ai_senses.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class CAI_Senses : public CAI_Component

CBaseEntity * GetFirstSeenEntity( AISightIter_t *pIter, seentype_t iSeenType = SEEN_ALL ) const;
CBaseEntity * GetNextSeenEntity( AISightIter_t *pIter ) const;
#ifdef MAPBASE
bool GetSeenEntityIndex( AISightIter_t *pIter, CBaseEntity *pSightEnt, seentype_t iSeenType ) const;
#endif

CSound * GetFirstHeardSound( AISoundIter_t *pIter );
CSound * GetNextHeardSound( AISoundIter_t *pIter );
Expand Down Expand Up @@ -152,6 +155,14 @@ class CAI_SensedObjectsManager : public IEntityListener
CBaseEntity * GetNext( int *pIter );

virtual void AddEntity( CBaseEntity *pEntity );
#ifdef MAPBASE
virtual void RemoveEntity( CBaseEntity *pEntity );
#endif

#ifdef MAPBASE_VSCRIPT
void ScriptAddEntity( HSCRIPT hEnt ) { AddEntity( ToEnt( hEnt ) ); }
void ScriptRemoveEntity( HSCRIPT hEnt ) { RemoveEntity( ToEnt( hEnt ) ); }
#endif

private:
virtual void OnEntitySpawned( CBaseEntity *pEntity );
Expand Down
29 changes: 26 additions & 3 deletions sp/src/game/server/ai_squad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

CAI_SquadManager g_AI_SquadManager;

#ifdef MAPBASE
ConVar ai_squad_broadcast_elusion("ai_squad_broadcast_elusion", "0", FCVAR_NONE, "Tells the entire squad when an enemy is eluded");
#endif

//-----------------------------------------------------------------------------
// CAI_SquadManager
//
Expand Down Expand Up @@ -740,6 +744,25 @@ void CAI_Squad::UpdateEnemyMemory( CAI_BaseNPC *pUpdater, CBaseEntity *pEnemy, c

//------------------------------------------------------------------------------

#ifdef MAPBASE
void CAI_Squad::MarkEnemyAsEluded( CAI_BaseNPC *pUpdater, CBaseEntity *pEnemy )
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is gated by a convar, there isn't really any concern about it affecting normal gameplay. But I have to wonder what happens if one NPC in a squad can't see the enemy and another can. Isn't it possible an NPC could be marked as eluded while still being visible by another squad member?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's visible to another squad member, then the squad member will broadcast that to others in the squad and update the enemy's last known position. The eluded code only runs when the NPC is at the enemy's last known position and the enemy is nowhere to be found. I'm reasonably certain those two situations can't happen at once

{
if (!ai_squad_broadcast_elusion.GetBool())
return;

//Broadcast to all members of the squad
for ( int i = 0; i < m_SquadMembers.Count(); i++ )
{
if ( m_SquadMembers[i] != pUpdater )
{
m_SquadMembers[i]->GetEnemies()->MarkAsEluded( pEnemy );
}
}
}
#endif

//------------------------------------------------------------------------------

#ifdef PER_ENEMY_SQUADSLOTS

AISquadEnemyInfo_t *CAI_Squad::FindEnemyInfo( CBaseEntity *pEnemy )
Expand Down Expand Up @@ -883,14 +906,14 @@ void CAI_Squad::ScriptRemoveFromSquad( HSCRIPT hNPC ) { RemoveFromSquad( HScrip

bool CAI_Squad::ScriptIsSilentMember( HSCRIPT hNPC ) { return IsSilentMember( HScriptToClass<CAI_BaseNPC>( hNPC ) ); }

void CAI_Squad::ScriptSetSquadData( int iSlot, const char *data )
void CAI_Squad::ScriptSetSquadData( int iSlot, int data )
{
SetSquadData( iSlot, data );
}

const char *CAI_Squad::ScriptGetSquadData( int iSlot )
int CAI_Squad::ScriptGetSquadData( int iSlot )
{
const char *data;
int data;
GetSquadData( iSlot, &data );
return data;
}
Expand Down
10 changes: 8 additions & 2 deletions sp/src/game/server/ai_squad.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ class CAI_Squad

void SquadNewEnemy ( CBaseEntity *pEnemy );
void UpdateEnemyMemory( CAI_BaseNPC *pUpdater, CBaseEntity *pEnemy, const Vector &position );
#ifdef MAPBASE
// The idea behind this is that, if one squad member fails to locate the enemy, nobody in the squad knows where the enemy is
// Makes combat utilizing elusion a bit smoother
// (gated by ai_squad_broadcast_elusion cvar)
void MarkEnemyAsEluded( CAI_BaseNPC *pUpdater, CBaseEntity *pEnemy );
#endif

bool OccupyStrategySlotRange( CBaseEntity *pEnemy, int slotIDStart, int slotIDEnd, int *pSlot );
void VacateStrategySlot( CBaseEntity *pEnemy, int slot);
Expand Down Expand Up @@ -186,8 +192,8 @@ class CAI_Squad

bool ScriptIsSilentMember( HSCRIPT hNPC );

void ScriptSetSquadData( int iSlot, const char *data );
const char *ScriptGetSquadData( int iSlot );
void ScriptSetSquadData( int iSlot, int data );
int ScriptGetSquadData( int iSlot );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the intended purpose of this return type being changed from char* to int?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was one of the points in the PR description:

The VScript functions for squad data now use integers instead of strings, as even though squad data supposedly accepts any parameter, only integers are capable of save/restore. Since these functions were very obscure (and only partially functional), this is unlikely to break existing scripts.

#endif

private:
Expand Down
Loading
Loading