diff options
Diffstat (limited to 'game/server/NextBot')
46 files changed, 19118 insertions, 0 deletions
diff --git a/game/server/NextBot/Behavior/BehaviorBackUp.h b/game/server/NextBot/Behavior/BehaviorBackUp.h new file mode 100644 index 0000000..e3a65f0 --- /dev/null +++ b/game/server/NextBot/Behavior/BehaviorBackUp.h @@ -0,0 +1,118 @@ +// BehaviorBackUp.h +// Back up for a short duration +// Author: Michael Booth, March 2007 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _BEHAVIOR_BACK_UP_H_ +#define _BEHAVIOR_BACK_UP_H_ + + +//---------------------------------------------------------------------------------------------- +/** + * Move backwards for a short duration away from a given position. + * Useful to dislodge ourselves if we get stuck while following our path. + */ +template < typename Actor > +class BehaviorBackUp : public Action< Actor > +{ +public: + BehaviorBackUp( const Vector &avoidPos ); + + virtual ActionResult< Actor > OnStart( Actor *me, Action< Actor > *priorAction ); + virtual ActionResult< Actor > Update( Actor *me, float interval ); + + virtual EventDesiredResult< Actor > OnStuck( Actor *me ); + + virtual const char *GetName( void ) const { return "BehaviorBackUp"; } + +private: + CountdownTimer m_giveUpTimer; + CountdownTimer m_backupTimer; + CountdownTimer m_jumpTimer; + Vector m_way; + Vector m_avoidPos; +}; + + +//---------------------------------------------------------------------------------------------- +template < typename Actor > +inline BehaviorBackUp< Actor >::BehaviorBackUp( const Vector &avoidPos ) +{ + m_avoidPos = avoidPos; +} + + +//---------------------------------------------------------------------------------------------- +template < typename Actor > +inline ActionResult< Actor > BehaviorBackUp< Actor >::OnStart( Actor *me, Action< Actor > *priorAction ) +{ + ILocomotion *mover = me->GetLocomotionInterface(); + + // don't back off if we're on a ladder + if ( mover && mover->IsUsingLadder() ) + { + return Done(); + } + + float backupTime = RandomFloat( 0.3f, 0.5f ); + + m_backupTimer.Start( backupTime ); + m_jumpTimer.Start( 1.5f * backupTime ); + m_giveUpTimer.Start( 2.5f * backupTime ); + + m_way = me->GetPosition() - m_avoidPos; + m_way.NormalizeInPlace(); + + return Continue(); +} + + +//---------------------------------------------------------------------------------------------- +template < typename Actor > +inline ActionResult< Actor > BehaviorBackUp< Actor >::Update( Actor *me, float interval ) +{ + if ( m_giveUpTimer.IsElapsed() ) + { + return Done(); + } + +// if ( m_jumpTimer.HasStarted() && m_jumpTimer.IsElapsed() ) +// { +// me->GetLocomotionInterface()->Jump(); +// m_jumpTimer.Invalidate(); +// } + + ILocomotion *mover = me->GetLocomotionInterface(); + if ( mover ) + { + Vector goal; + + if ( m_backupTimer.IsElapsed() ) + { + // move towards bad spot + goal = m_avoidPos; // me->GetPosition() - 100.0f * m_way; + } + else + { + // move away from bad spot + goal = me->GetPosition() + 100.0f * m_way; + } + + mover->Approach( goal ); + } + + return Continue(); +} + + +//---------------------------------------------------------------------------------------------- +template < typename Actor > +inline EventDesiredResult< Actor > BehaviorBackUp< Actor >::OnStuck( Actor *me ) +{ + return TryToSustain( RESULT_IMPORTANT, "Stuck while trying to back up" ); +} + + + +#endif // _BEHAVIOR_BACK_UP_H_ + diff --git a/game/server/NextBot/Behavior/BehaviorMoveTo.h b/game/server/NextBot/Behavior/BehaviorMoveTo.h new file mode 100644 index 0000000..f3b608f --- /dev/null +++ b/game/server/NextBot/Behavior/BehaviorMoveTo.h @@ -0,0 +1,126 @@ +// BehaviorMoveTo.h +// Move to a potentially far away position +// Author: Michael Booth, June 2007 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _BEHAVIOR_MOVE_TO_H_ +#define _BEHAVIOR_MOVE_TO_H_ + + +//---------------------------------------------------------------------------------------------- +/** + * Move to a potentially far away position, using path planning. + */ +template < typename Actor, typename PathCost > +class BehaviorMoveTo : public Action< Actor > +{ +public: + BehaviorMoveTo( const Vector &goal, Action< Actor > *successAction = NULL, Action< Actor > *failAction = NULL ); + + virtual ActionResult< Actor > OnStart( Actor *me, Action< Actor > *priorAction ); + virtual ActionResult< Actor > Update( Actor *me, float interval ); + + virtual EventDesiredResult< Actor > OnMoveToSuccess( Actor *me, const Path *path ); + virtual EventDesiredResult< Actor > OnMoveToFailure( Actor *me, const Path *path, MoveToFailureType reason ); + + virtual bool ComputePath( Actor *me, const Vector &goal, PathFollower *path ); + + virtual const char *GetName( void ) const { return "BehaviorMoveTo"; } + +private: + Vector m_goal; + PathFollower m_path; + Action< Actor > *m_successAction; + Action< Actor > *m_failAction; +}; + + +//---------------------------------------------------------------------------------------------- +template < typename Actor, typename PathCost > +inline BehaviorMoveTo< Actor, PathCost >::BehaviorMoveTo( const Vector &goal, Action< Actor > *successAction, Action< Actor > *failAction ) +{ + m_goal = goal; + m_path.Invalidate(); + m_successAction = successAction; + m_failAction = failAction; +} + + +//---------------------------------------------------------------------------------------------- +template < typename Actor, typename PathCost > +inline bool BehaviorMoveTo< Actor, PathCost >::ComputePath( Actor *me, const Vector &goal, PathFollower *path ) +{ + PathCost cost( me ); + return path->Compute( me, goal, cost ); +} + + +//---------------------------------------------------------------------------------------------- +template < typename Actor, typename PathCost > +inline ActionResult< Actor > BehaviorMoveTo< Actor, PathCost >::OnStart( Actor *me, Action< Actor > *priorAction ) +{ + if ( !this->ComputePath( me, m_goal, &m_path ) ) + { + if ( m_failAction ) + { + return this->ChangeTo( m_failAction, "No path to goal" ); + } + + return this->Done( "No path to goal" ); + } + + return this->Continue(); +} + + +//---------------------------------------------------------------------------------------------- +template < typename Actor, typename PathCost > +inline ActionResult< Actor > BehaviorMoveTo< Actor, PathCost >::Update( Actor *me, float interval ) +{ + // if path became invalid during last tick for any reason, we're done + if ( !m_path.IsValid() ) + { + if ( m_failAction ) + { + return this->ChangeTo( m_failAction, "Path is invalid" ); + } + + return this->Done( "Path is invalid" ); + } + + // move along path - success/fail event handlers will exit behavior when goal is reached + m_path.Update( me ); + + return this->Continue(); +} + + +//---------------------------------------------------------------------------------------------- +template < typename Actor, typename PathCost > +inline EventDesiredResult< Actor > BehaviorMoveTo< Actor, PathCost >::OnMoveToSuccess( Actor *me, const Path *path ) +{ + if ( m_successAction ) + { + return this->TryChangeTo( m_successAction, RESULT_CRITICAL, "OnMoveToSuccess" ); + } + + return this->TryDone( RESULT_CRITICAL, "OnMoveToSuccess" ); +} + + +//---------------------------------------------------------------------------------------------- +template < typename Actor, typename PathCost > +inline EventDesiredResult< Actor > BehaviorMoveTo< Actor, PathCost >::OnMoveToFailure( Actor *me, const Path *path, MoveToFailureType reason ) +{ + if ( m_failAction ) + { + return this->TryChangeTo( m_failAction, RESULT_CRITICAL, "OnMoveToFailure" ); + } + + return this->TryDone( RESULT_CRITICAL, "OnMoveToFailure" ); +} + + + +#endif // _BEHAVIOR_MOVE_TO_H_ + diff --git a/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.cpp b/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.cpp new file mode 100644 index 0000000..5ac954a --- /dev/null +++ b/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.cpp @@ -0,0 +1,75 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// NextBot paths that go through this entity must fulfill the given prerequisites to pass +// Michael Booth, August 2009 + +#include "cbase.h" +#include "func_nav_prerequisite.h" +#include "ndebugoverlay.h" +#include "modelentities.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +LINK_ENTITY_TO_CLASS( func_nav_prerequisite, CFuncNavPrerequisite ); + +BEGIN_DATADESC( CFuncNavPrerequisite ) + DEFINE_KEYFIELD( m_task, FIELD_INTEGER, "Task" ), + DEFINE_KEYFIELD( m_taskEntityName, FIELD_STRING, "Entity" ), + DEFINE_KEYFIELD( m_taskValue, FIELD_FLOAT, "Value" ), + DEFINE_KEYFIELD( m_isDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +END_DATADESC() + +IMPLEMENT_AUTO_LIST( IFuncNavPrerequisiteAutoList ); + + +//----------------------------------------------------------------------------- +CFuncNavPrerequisite::CFuncNavPrerequisite() +{ + m_task = TASK_NONE; + m_hTaskEntity = NULL; +} + + +//----------------------------------------------------------------------------- +void CFuncNavPrerequisite::Spawn( void ) +{ + AddSpawnFlags( SF_TRIGGER_ALLOW_CLIENTS ); + + BaseClass::Spawn(); + InitTrigger(); +} + + +//----------------------------------------------------------------------------- +bool CFuncNavPrerequisite::IsTask( TaskType task ) const +{ + return task == m_task ? true : false; +} + + +//----------------------------------------------------------------------------- +CBaseEntity *CFuncNavPrerequisite::GetTaskEntity( void ) +{ + if ( m_hTaskEntity == NULL ) + { + m_hTaskEntity = gEntList.FindEntityByName( NULL, m_taskEntityName ); + } + return m_hTaskEntity; +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavPrerequisite::InputEnable( inputdata_t &inputdata ) +{ + m_isDisabled = false; +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavPrerequisite::InputDisable( inputdata_t &inputdata ) +{ + m_isDisabled = true; +} diff --git a/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.h b/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.h new file mode 100644 index 0000000..bcc23f6 --- /dev/null +++ b/game/server/NextBot/NavMeshEntities/func_nav_prerequisite.h @@ -0,0 +1,56 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// NextBot paths that go through this entity must fulfill the given prerequisites to pass +// Michael Booth, August 2009 + +#ifndef FUNC_NAV_PREREQUISITE_H +#define FUNC_NAV_PREREQUISITE_H + +#include "triggers.h" + +/** + * NextBot paths that pass through this entity must fulfill the given prerequisites to pass + */ +DECLARE_AUTO_LIST( IFuncNavPrerequisiteAutoList ); + +class CFuncNavPrerequisite : public CBaseTrigger, public IFuncNavPrerequisiteAutoList +{ + DECLARE_CLASS( CFuncNavPrerequisite, CBaseTrigger ); + +public: + CFuncNavPrerequisite(); + + DECLARE_DATADESC(); + + virtual void Spawn( void ); + + enum TaskType + { + TASK_NONE = 0, + TASK_DESTROY_ENTITY = 1, + TASK_MOVE_TO_ENTITY = 2, + TASK_WAIT = 3, + }; + + bool IsTask( TaskType type ) const; + CBaseEntity *GetTaskEntity( void ); + float GetTaskValue( void ) const; + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + bool IsEnabled( void ) const { return !m_isDisabled; } + +protected: + int m_task; + string_t m_taskEntityName; + float m_taskValue; + bool m_isDisabled; + EHANDLE m_hTaskEntity; +}; + +inline float CFuncNavPrerequisite::GetTaskValue( void ) const +{ + return m_taskValue; +} + + +#endif // FUNC_NAV_PREREQUISITE_H diff --git a/game/server/NextBot/NextBot.cpp b/game/server/NextBot/NextBot.cpp new file mode 100644 index 0000000..0165a03 --- /dev/null +++ b/game/server/NextBot/NextBot.cpp @@ -0,0 +1,523 @@ +// NextBotCombatCharacter.cpp +// Next generation bot system +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "team.h" +#include "CRagdollMagnet.h" + +#include "NextBot.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotBodyInterface.h" + +#ifdef TERROR +#include "TerrorGamerules.h" +#endif + +#include "vprof.h" +#include "datacache/imdlcache.h" +#include "EntityFlame.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +ConVar NextBotStop( "nb_stop", "0", FCVAR_CHEAT | FCVAR_REPLICATED, "Stop all NextBots" ); + + +//-------------------------------------------------------------------------------------------------------- +class CSendBotCommand +{ +public: + CSendBotCommand( const char *command ) + { + m_command = command; + } + + bool operator() ( INextBot *bot ) + { + bot->OnCommandString( m_command ); + return true; + } + + const char *m_command; +}; + + +CON_COMMAND_F( nb_command, "Sends a command string to all bots", FCVAR_CHEAT ) +{ + if ( args.ArgC() <= 1 ) + { + Msg( "Missing command string" ); + return; + } + + CSendBotCommand sendCmd( args.ArgS() ); + TheNextBots().ForEachBot( sendCmd ); +} + + + +//----------------------------------------------------------------------------------------------------- +BEGIN_DATADESC( NextBotCombatCharacter ) + + DEFINE_THINKFUNC( DoThink ), + +END_DATADESC() + + +//----------------------------------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST( NextBotCombatCharacter, DT_NextBot ) +END_SEND_TABLE() + + +//----------------------------------------------------------------------------------------------------- +NextBotDestroyer::NextBotDestroyer( int team ) +{ + m_team = team; +} + + +//----------------------------------------------------------------------------------------------------- +bool NextBotDestroyer::operator() ( INextBot *bot ) +{ + if ( m_team == TEAM_ANY || bot->GetEntity()->GetTeamNumber() == m_team ) + { + // players need to be kicked, not deleted + if ( bot->GetEntity()->IsPlayer() ) + { + CBasePlayer *player = dynamic_cast< CBasePlayer * >( bot->GetEntity() ); + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) ); + } + else + { + UTIL_Remove( bot->GetEntity() ); + } + } + return true; +} + + +//----------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nb_delete_all, "Delete all non-player NextBot entities.", FCVAR_CHEAT ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CTeam *team = NULL; + + if ( args.ArgC() == 2 ) + { + const char *teamName = args[1]; + + for( int i=0; i < g_Teams.Count(); ++i ) + { + if ( FStrEq( teamName, g_Teams[i]->GetName() ) ) + { + // delete all bots on this team + team = g_Teams[i]; + break; + } + } + + if ( team == NULL ) + { + Msg( "Invalid team '%s'\n", teamName ); + return; + } + } + + // delete all bots on all teams + NextBotDestroyer destroyer( team ? team->GetTeamNumber() : TEAM_ANY ); + TheNextBots().ForEachBot( destroyer ); +} + + +//----------------------------------------------------------------------------------------------------- +class NextBotApproacher +{ +public: + NextBotApproacher( void ) + { + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player ) + { + Vector forward; + player->EyeVectors( &forward ); + + trace_t result; + unsigned int mask = MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE | CONTENTS_GRATE | CONTENTS_WINDOW; + UTIL_TraceLine( player->EyePosition(), player->EyePosition() + 999999.9f * forward, mask, player, COLLISION_GROUP_NONE, &result ); + if ( result.DidHit() ) + { + NDebugOverlay::Cross3D( result.endpos, 5, 0, 255, 0, true, 10.0f ); + m_isGoalValid = true; + m_goal = result.endpos; + } + else + { + m_isGoalValid = false; + } + } + } + + bool operator() ( INextBot *bot ) + { + if ( TheNextBots().IsDebugFilterMatch( bot ) ) + { + bot->OnCommandApproach( m_goal ); + } + return true; + } + + bool m_isGoalValid; + Vector m_goal; +}; + +CON_COMMAND_F( nb_move_to_cursor, "Tell all NextBots to move to the cursor position", FCVAR_CHEAT ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + NextBotApproacher approach; + TheNextBots().ForEachBot( approach ); +} + + +//---------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------- +bool IgnoreActorsTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask ) +{ + CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); + return ( entity->MyCombatCharacterPointer() == NULL ); // includes all bots, npcs, players, and TF2 buildings +} + + +//---------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------- +bool VisionTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask ) +{ + // Honor BlockLOS also to allow seeing through partially-broken doors + CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); + return ( entity->MyCombatCharacterPointer() == NULL && entity->BlocksLOS() ); +} + + +//---------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------- + +NextBotCombatCharacter::NextBotCombatCharacter( void ) + +{ + m_lastAttacker = NULL; + m_didModelChange = false; +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::Spawn( void ) +{ + BaseClass::Spawn(); + + // reset bot components + Reset(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + + SetMoveType( MOVETYPE_CUSTOM ); + + SetCollisionGroup( COLLISION_GROUP_PLAYER ); + + m_iMaxHealth = m_iHealth; + m_takedamage = DAMAGE_YES; + + MDLCACHE_CRITICAL_SECTION(); + InitBoneControllers( ); + + // set up think callback + SetThink( &NextBotCombatCharacter::DoThink ); + SetNextThink( gpGlobals->curtime ); + + m_lastAttacker = NULL; +} + + +bool NextBotCombatCharacter::IsAreaTraversable( const CNavArea *area ) const +{ + if ( !area ) + return false; + ILocomotion *mover = GetLocomotionInterface(); + if ( mover && !mover->IsAreaTraversable( area ) ) + return false; + return BaseClass::IsAreaTraversable( area ); +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::DoThink( void ) +{ + VPROF_BUDGET( "NextBotCombatCharacter::DoThink", "NextBot" ); + + SetNextThink( gpGlobals->curtime ); + + if ( BeginUpdate() ) + { + // emit model change event + if ( m_didModelChange ) + { + m_didModelChange = false; + + OnModelChanged(); + + // propagate model change into NextBot event responders + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnModelChanged(); + } + } + + UpdateLastKnownArea(); + + // update bot components + if ( !NextBotStop.GetBool() && (GetFlags() & FL_FROZEN) == 0 ) + { + Update(); + } + + EndUpdate(); + } +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::Touch( CBaseEntity *other ) +{ + if ( ShouldTouch( other ) ) + { + // propagate touch into NextBot event responders + trace_t result; + result = GetTouchTrace(); + + // OnContact refers to *physical* contact, not triggers or other non-physical entities + if ( result.DidHit() || other->MyCombatCharacterPointer() != NULL ) + { + OnContact( other, &result ); + } + } + + BaseClass::Touch( other ); +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::SetModel( const char *szModelName ) +{ + // actually change the model + BaseClass::SetModel( szModelName ); + + // need to do a lazy-check because precache system also invokes this + m_didModelChange = true; +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bool bCalledByLevelDesigner ) +{ + BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner ); + + // propagate event to components + OnIgnite(); +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::Ignite( float flFlameLifetime, CBaseEntity *pAttacker ) +{ + if ( IsOnFire() ) + return; + + // BaseClass::Ignite stuff, plus SetAttacker on the flame, so our attacker gets credit + CEntityFlame *pFlame = CEntityFlame::Create( this ); + if ( pFlame ) + { + pFlame->SetLifetime( flFlameLifetime ); + AddFlag( FL_ONFIRE ); + + SetEffectEntity( pFlame ); + } + m_OnIgnite.FireOutput( this, this ); + + // propagate event to components + OnIgnite(); +} + + +//---------------------------------------------------------------------------------------------------------- +int NextBotCombatCharacter::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + // track our last attacker + if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() ) + { + m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer(); + } + + // propagate event to components + OnInjured( info ); + + return CBaseCombatCharacter::OnTakeDamage_Alive( info ); +} + + +//---------------------------------------------------------------------------------------------------------- +int NextBotCombatCharacter::OnTakeDamage_Dying( const CTakeDamageInfo &info ) +{ + // track our last attacker + if ( info.GetAttacker()->MyCombatCharacterPointer() ) + { + m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer(); + } + + // propagate event to components + OnInjured( info ); + + return CBaseCombatCharacter::OnTakeDamage_Dying( info ); +} + +//---------------------------------------------------------------------------------------------------------- +/** + * Can't use CBaseCombatCharacter's Event_Killed because it will immediately ragdoll us + */ +static int g_DeathStartEvent = 0; +void NextBotCombatCharacter::Event_Killed( const CTakeDamageInfo &info ) +{ + // track our last attacker + if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() ) + { + m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer(); + } + + // propagate event to my components + OnKilled( info ); + + // Advance life state to dying + m_lifeState = LIFE_DYING; + +#ifdef TERROR + /* + * TODO: Make this game-generic + */ + // Create the death event just like players do. + TerrorGameRules()->DeathNoticeForEntity( this, info ); + + // Infected specific event + TerrorGameRules()->DeathNoticeForInfected( this, info ); +#endif + + if ( GetOwnerEntity() != NULL ) + { + GetOwnerEntity()->DeathNotice( this ); + } + + // inform the other bots + TheNextBots().OnKilled( this, info ); +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::PerformCustomPhysics( Vector *pNewPosition, Vector *pNewVelocity, QAngle *pNewAngles, QAngle *pNewAngVelocity ) +{ + ILocomotion *mover = GetLocomotionInterface(); + if ( mover ) + { + // hack to keep ground entity from being NULL'd when Z velocity is positive + SetGroundEntity( mover->GetGround() ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +bool NextBotCombatCharacter::BecomeRagdoll( const CTakeDamageInfo &info, const Vector &forceVector ) +{ + // See if there's a ragdoll magnet that should influence our force. + Vector adjustedForceVector = forceVector; + CRagdollMagnet *magnet = CRagdollMagnet::FindBestMagnet( this ); + if ( magnet ) + { + adjustedForceVector += magnet->GetForceVector( this ); + } + + // clear the deceased's sound channels.(may have been firing or reloading when killed) + EmitSound( "BaseCombatCharacter.StopWeaponSounds" ); + + return BaseClass::BecomeRagdoll( info, adjustedForceVector ); +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::HandleAnimEvent( animevent_t *event ) +{ + // propagate event to components + OnAnimationEvent( event ); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Propagate event into NextBot event responders + */ +void NextBotCombatCharacter::OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ) +{ + INextBotEventResponder::OnNavAreaChanged( enteredArea, leftArea ); + + BaseClass::OnNavAreaChanged( enteredArea, leftArea ); +} + + +//---------------------------------------------------------------------------------------------------------- +Vector NextBotCombatCharacter::EyePosition( void ) +{ + if ( GetBodyInterface() ) + { + return GetBodyInterface()->GetEyePosition(); + } + + return BaseClass::EyePosition(); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Return true if this object can be +used by the bot + */ +bool NextBotCombatCharacter::IsUseableEntity( CBaseEntity *entity, unsigned int requiredCaps ) +{ + if ( entity ) + { + int caps = entity->ObjectCaps(); + if ( caps & (FCAP_IMPULSE_USE|FCAP_CONTINUOUS_USE|FCAP_ONOFF_USE|FCAP_DIRECTIONAL_USE) ) + { + if ( (caps & requiredCaps) == requiredCaps ) + { + return true; + } + } + } + + return false; +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotCombatCharacter::UseEntity( CBaseEntity *entity, USE_TYPE useType ) +{ + if ( IsUseableEntity( entity ) ) + { + variant_t emptyVariant; + entity->AcceptInput( "Use", this, this, emptyVariant, useType ); + } +} diff --git a/game/server/NextBot/NextBot.h b/game/server/NextBot/NextBot.h new file mode 100644 index 0000000..ae89cab --- /dev/null +++ b/game/server/NextBot/NextBot.h @@ -0,0 +1,104 @@ +// NextBotCombatCharacter.h +// Next generation bot system +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_H_ +#define _NEXT_BOT_H_ + +#include "NextBotInterface.h" +#include "NextBotManager.h" + +#ifdef TERROR +#include "player_lagcompensation.h" +#endif + +class NextBotCombatCharacter; +struct animevent_t; + +extern ConVar NextBotStop; + + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- +/** + * A Next Bot derived from CBaseCombatCharacter + */ +class NextBotCombatCharacter : public CBaseCombatCharacter, public INextBot +{ +public: + DECLARE_CLASS( NextBotCombatCharacter, CBaseCombatCharacter ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + NextBotCombatCharacter( void ); + virtual ~NextBotCombatCharacter() { } + + virtual void Spawn( void ); + + virtual Vector EyePosition( void ); + + virtual INextBot *MyNextBotPointer( void ) { return this; } + + // Event hooks into NextBot system --------------------------------------- + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual int OnTakeDamage_Dying( const CTakeDamageInfo &info ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void HandleAnimEvent( animevent_t *event ); + virtual void OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ); // invoked (by UpdateLastKnownArea) when we enter a new nav area (or it is reset to NULL) + virtual void Touch( CBaseEntity *other ); + virtual void SetModel( const char *szModelName ); + virtual void Ignite( float flFlameLifetime, bool bNPCOnly = true, float flSize = 0.0f, bool bCalledByLevelDesigner = false ); + virtual void Ignite( float flFlameLifetime, CBaseEntity *pAttacker ); + //------------------------------------------------------------------------ + + virtual bool IsUseableEntity( CBaseEntity *entity, unsigned int requiredCaps = 0 ); + void UseEntity( CBaseEntity *entity, USE_TYPE useType = USE_TOGGLE ); + + // Implement this if you use MOVETYPE_CUSTOM + virtual void PerformCustomPhysics( Vector *pNewPosition, Vector *pNewVelocity, QAngle *pNewAngles, QAngle *pNewAngVelocity ); + + virtual bool BecomeRagdoll( const CTakeDamageInfo &info, const Vector &forceVector ); + + // hook to INextBot update + void DoThink( void ); + + // expose to public + int GetLastHitGroup( void ) const; // where on our body were we injured last + + virtual bool IsAreaTraversable( const CNavArea *area ) const; // return true if we can use the given area + + virtual CBaseCombatCharacter *GetLastAttacker( void ) const; // return the character who last attacked me + + // begin INextBot public interface ---------------------------------------------------------------- + virtual NextBotCombatCharacter *GetEntity( void ) const { return const_cast< NextBotCombatCharacter * >( this ); } + virtual NextBotCombatCharacter *GetNextBotCombatCharacter( void ) const { return const_cast< NextBotCombatCharacter * >( this ); } + + +private: + EHANDLE m_lastAttacker; + + bool m_didModelChange; +}; + + +inline CBaseCombatCharacter *NextBotCombatCharacter::GetLastAttacker( void ) const +{ + return ( m_lastAttacker.Get() == NULL ) ? NULL : m_lastAttacker->MyCombatCharacterPointer(); +} + +inline int NextBotCombatCharacter::GetLastHitGroup( void ) const +{ + return LastHitGroup(); +} + +//----------------------------------------------------------------------------------------------------- +class NextBotDestroyer +{ +public: + NextBotDestroyer( int team ); + bool operator() ( INextBot *bot ); + int m_team; // the team to delete bots from, or TEAM_ANY for any team +}; + +#endif // _NEXT_BOT_H_ diff --git a/game/server/NextBot/NextBotAttentionInterface.cpp b/game/server/NextBot/NextBotAttentionInterface.cpp new file mode 100644 index 0000000..a774551 --- /dev/null +++ b/game/server/NextBot/NextBotAttentionInterface.cpp @@ -0,0 +1,162 @@ +// NextBotAttentionInterface.cpp +// Manage what this bot pays attention to +// Author: Michael Booth, April 2007 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "NextBot.h" +#include "NextBotAttentionInterface.h" +#include "NextBotBodyInterface.h" + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//------------------------------------------------------------------------------------------ +/** + * Reset to initial state + */ +void IAttention::Reset( void ) +{ + m_body = GetBot()->GetBodyInterface(); + + m_attentionSet.RemoveAll(); +} + + +//------------------------------------------------------------------------------------------ +/** + * Update internal state + */ +void IAttention::Update( void ) +{ +} + + +//------------------------------------------------------------------------------------------ +void IAttention::AttendTo( const CBaseCombatCharacter *who, const char *reason ) +{ + if ( !IsAwareOf( who ) ) + { + PointOfInterest p; + p.m_type = PointOfInterest::WHO; + p.m_who = who; + p.m_duration.Start(); + + m_attentionSet.AddToTail( p ); + } +} + + +//------------------------------------------------------------------------------------------ +void IAttention::AttendTo( const CBaseEntity *what, const char *reason ) +{ + if ( !IsAwareOf( what ) ) + { + PointOfInterest p; + p.m_type = PointOfInterest::WHAT; + p.m_what = what; + p.m_duration.Start(); + + m_attentionSet.AddToTail( p ); + } +} + + +//------------------------------------------------------------------------------------------ +void IAttention::AttendTo( const Vector &where, IAttention::SignificanceLevel significance, const char *reason ) +{ + PointOfInterest p; + p.m_type = PointOfInterest::WHERE; + p.m_where = where; + p.m_duration.Start(); + + m_attentionSet.AddToTail( p ); +} + + +//------------------------------------------------------------------------------------------ +void IAttention::Disregard( const CBaseCombatCharacter *who, const char *reason ) +{ + FOR_EACH_VEC( m_attentionSet, it ) + { + if ( m_attentionSet[ it ].m_type == PointOfInterest::WHO ) + { + CBaseCombatCharacter *myWho = m_attentionSet[ it ].m_who; + + if ( !myWho || myWho->entindex() == who->entindex() ) + { + m_attentionSet.Remove( it ); + return; + } + } + } +} + + +//------------------------------------------------------------------------------------------ +void IAttention::Disregard( const CBaseEntity *what, const char *reason ) +{ + FOR_EACH_VEC( m_attentionSet, it ) + { + if ( m_attentionSet[ it ].m_type == PointOfInterest::WHAT ) + { + CBaseCombatCharacter *myWhat = m_attentionSet[ it ].m_what; + + if ( !myWhat || myWhat->entindex() == what->entindex() ) + { + m_attentionSet.Remove( it ); + return; + } + } + } +} + + +//------------------------------------------------------------------------------------------ +/** + * Return true if given actor is in our attending set + */ +bool IAttention::IsAwareOf( const CBaseCombatCharacter *who ) const +{ + FOR_EACH_VEC( m_attentionSet, it ) + { + if ( m_attentionSet[ it ].m_type == PointOfInterest::WHO ) + { + CBaseCombatCharacter *myWho = m_attentionSet[ it ].m_who; + + if ( myWho && myWho->entindex() == who->entindex() ) + { + return true; + } + } + } + + return false; +} + + +//------------------------------------------------------------------------------------------ +/** + * Return true if given object is in our attending set + */ +bool IAttention::IsAwareOf( const CBaseEntity *what ) const +{ + FOR_EACH_VEC( m_attentionSet, it ) + { + if ( m_attentionSet[ it ].m_type == PointOfInterest::WHAT ) + { + CBaseEntity *myWhat = m_attentionSet[ it ].m_what; + + if ( myWhat && myWhat->entindex() == what->entindex() ) + { + return true; + } + } + } + + return false; +}
\ No newline at end of file diff --git a/game/server/NextBot/NextBotAttentionInterface.h b/game/server/NextBot/NextBotAttentionInterface.h new file mode 100644 index 0000000..26bec43 --- /dev/null +++ b/game/server/NextBot/NextBotAttentionInterface.h @@ -0,0 +1,81 @@ +// NextBotAttentionInterface.h +// Manage what this bot pays attention to +// Author: Michael Booth, April 2007 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_ATTENTION_INTERFACE_H_ +#define _NEXT_BOT_ATTENTION_INTERFACE_H_ + +#include "NextBotComponentInterface.h" + +class INextBot; +class IBody; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for managing what a bot pays attention to. + * Vision determines what see see and notice -> Attention determines which of those things we look at -> Low level head/aiming simulation actually moves our head/eyes + */ +class IAttention : public INextBotComponent +{ +public: + IAttention( INextBot *bot ) : INextBotComponent( bot ) { } + virtual ~IAttention() { } + + virtual void Reset( void ) { } // reset to initial state + virtual void Update( void ) { } // update internal state + + enum SignificanceLevel + { + BORING, // background noise + INTERESTING, // notably interesting + COMPELLING, // very hard to pay attention to anything else + IRRESISTIBLE, // can't look away + }; + + // override these to control the significance of entities in a context-specific way + virtual int CompareSignificance( const CBaseEntity *a, const CBaseEntity *b ) const; // returns <0 if a < b, 0 if a==b, or >0 if a>b + + // bring things to our attention + virtual void AttendTo( CBaseEntity *what, const char *reason = NULL ); + virtual void AttendTo( const Vector &where, SignificanceLevel significance, const char *reason = NULL ); + + // remove things from our attention + virtual void Disregard( CBaseEntity *what, const char *reason = NULL ); + + virtual bool IsAwareOf( CBaseEntity *what ) const; // return true if given object is in our attending set + virtual float GetAwareDuration( CBaseEntity *what ) const; // return how long we've been aware of this entity + + // INextBotEventResponder ------------------------------------------------------------------ + virtual void OnInjured( const CTakeDamageInfo &info ); // when bot is damaged by something + virtual void OnContact( CBaseEntity *other, CGameTrace *result = NULL ); // invoked when bot touches 'other' + virtual void OnSight( CBaseEntity *subject ); // when subject initially enters bot's visual awareness + virtual void OnLostSight( CBaseEntity *subject ); // when subject leaves enters bot's visual awareness + virtual void OnSound( CBaseEntity *source, const CSoundParameters ¶ms ); // when an entity emits a sound + + +private: + IBody *m_body; // to access head aiming + + struct PointOfInterest + { + enum { ENTITY, POSITION } m_type; + CHandle< CBaseEntity > m_entity; + Vector m_position; + + IntervalTimer m_duration; // how long has this PoI been in our attention set + }; + + CUtlVector< PointOfInterest > m_attentionSet; // the set of things we are attending to + + +}; + +inline int IAttention::CompareSignificance( const CBaseEntity *a, const CBaseEntity *b ) const +{ + return 0; +} + +#endif // _NEXT_BOT_ATTENTION_INTERFACE_H_ + diff --git a/game/server/NextBot/NextBotBehavior.h b/game/server/NextBot/NextBotBehavior.h new file mode 100644 index 0000000..2f92a84 --- /dev/null +++ b/game/server/NextBot/NextBotBehavior.h @@ -0,0 +1,1936 @@ +// NextBotBehaviorEngine.h +// Behavioral system constructed from Actions +// Author: Michael Booth, April 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _BEHAVIOR_ENGINE_H_ +#define _BEHAVIOR_ENGINE_H_ + +#include "fmtstr.h" +#include "NextBotEventResponderInterface.h" +#include "NextBotContextualQueryInterface.h" +#include "NextBotDebug.h" +#include "tier0/vprof.h" + + +//#define DEBUG_BEHAVIOR_MEMORY +extern ConVar NextBotDebugHistory; + +/** + * Notes: + * + * By using return results to cause transitions, we ensure the atomic-ness + * of these transitions. For instance, it is not possible to change to a + * new Action and continue execution of code in the current Action. + * + * Creation and deletion of Actions during transitions allows passing of + * type-safe arguments between Actions via constructors. + * + * Events are propagated to each Action in the hierarchy. If an + * action is suspended for another action, it STILL RECEIVES EVENTS + * that are not handled by the events "above it" in the suspend stack. + * In other words, the active Action gets the first response, and if it + * returns CONTINUE, the Action buried beneath it can process it, + * and so on deeper into the stack of suspended Actions. + * + * About events: + * It is not possible to have event handlers instantaneously change + * state upon return due to out-of-order and recurrence issues, not + * to mention deleting the state out from under itself. Therefore, + * events return DESIRED results, and the highest priority result + * is executed at the next Update(). + * + * About buried Actions causing SUSPEND_FOR results: + * If a buried Action reacts to an event by returning a SUSPEND_FOR, + * the new interrupting Action is put at the TOP of the stack, burying + * whatever Action was there. + * + */ + + +// forward declaration +template < typename Actor > class Action; + +/** + * The possible consequences of an Action + */ +enum ActionResultType +{ + CONTINUE, // continue executing this action next frame - nothing has changed + CHANGE_TO, // change actions next frame + SUSPEND_FOR, // put the current action on hold for the new action + DONE, // this action has finished, resume suspended action + SUSTAIN, // for use with event handlers - a way to say "It's important to keep doing what I'm doing" +}; + + +//---------------------------------------------------------------------------------------------- +/** + * Actions and Event processors return results derived from this class. + * Do not assemble this yourself - use the Continue(), ChangeTo(), Done(), and SuspendFor() + * methods within Action. + */ +template < typename Actor > +struct IActionResult +{ + IActionResult( ActionResultType type = CONTINUE, Action< Actor > *action = NULL, const char *reason = NULL ) + { + m_type = type; + m_action = action; + m_reason = reason; + } + + bool IsDone( void ) const + { + return ( m_type == DONE ); + } + + bool IsContinue( void ) const + { + return ( m_type == CONTINUE ); + } + + bool IsRequestingChange( void ) const + { + return ( m_type == CHANGE_TO || m_type == SUSPEND_FOR || m_type == DONE ); + } + + const char *GetTypeName( void ) const + { + switch ( m_type ) + { + case CHANGE_TO: return "CHANGE_TO"; + case SUSPEND_FOR: return "SUSPEND_FOR"; + case DONE: return "DONE"; + case SUSTAIN: return "SUSTAIN"; + + default: + case CONTINUE: return "CONTINUE"; + } + } + + ActionResultType m_type; + Action< Actor > *m_action; + const char *m_reason; +}; + + +//---------------------------------------------------------------------------------------------- +/** + * When an Action is executed it returns this result. + * Do not assemble this yourself - use the Continue(), ChangeTo(), Done(), and SuspendFor() + * methods within Action. + */ +template < typename Actor > +struct ActionResult : public IActionResult< Actor > +{ + // this is derived from IActionResult to ensure that ActionResult and EventDesiredResult cannot be silently converted + ActionResult( ActionResultType type = CONTINUE, Action< Actor > *action = NULL, const char *reason = NULL ) : IActionResult< Actor >( type, action, reason ) { } +}; + + +//---------------------------------------------------------------------------------------------- +/** + * When an event is processed, it returns this DESIRED result, + * which may or MAY NOT happen, depending on other event results + * that occur simultaneously. + * Do not assemble this yourself - use the TryContinue(), TryChangeTo(), TryDone(), TrySustain(), + * and TrySuspendFor() methods within Action. + */ +enum EventResultPriorityType +{ + RESULT_NONE, // no result + RESULT_TRY, // use this result, or toss it out, either is ok + RESULT_IMPORTANT, // try extra-hard to use this result + RESULT_CRITICAL // this result must be used - emit an error if it can't be +}; + +template < typename Actor > +struct EventDesiredResult : public IActionResult< Actor > +{ + EventDesiredResult( ActionResultType type = CONTINUE, Action< Actor > *action = NULL, EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) : IActionResult< Actor >( type, action, reason ) + { + m_priority = priority; + } + + EventResultPriorityType m_priority; +}; + + +//------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------- +/** + * A Behavior is the root of an Action hierarchy as well as its container/manager. + * Instantiate a Behavior with the root Action of your behavioral system, and + * call Behavior::Update() to drive it. + */ +template < typename Actor > +class Behavior : public INextBotEventResponder, public IContextualQuery +{ +public: + DECLARE_CLASS( Behavior, INextBotEventResponder ); + + Behavior( Action< Actor > *initialAction, const char *name = "" ) : m_name( "%s", name ) + { + m_action = initialAction; + m_me = NULL; + } + + virtual ~Behavior() + { + if ( m_me && m_action ) + { + // allow all currently active Actions to end + m_action->InvokeOnEnd( m_me, this, NULL ); + m_me = NULL; + } + + // dig down to the bottom of the action stack and delete + // that, so we don't leak action memory since action + // destructors intentionally don't delete actions + // "buried" underneath them. + Action< Actor > *bottomAction; + for( bottomAction = m_action; bottomAction && bottomAction->m_buriedUnderMe; bottomAction = bottomAction->m_buriedUnderMe ) + ; + + if ( bottomAction ) + { + delete bottomAction; + } + + // delete any dead Actions + m_deadActionVector.PurgeAndDeleteElements(); + } + + /** + * Reset this Behavior with the given Action. If this Behavior + * was already running, this will delete all current Actions and + * restart the Behavior with the new one. + */ + void Reset( Action< Actor > *action ) + { + if ( m_me && m_action ) + { + // allow all currently active Actions to end + m_action->InvokeOnEnd( m_me, this, NULL ); + m_me = NULL; + } + + // find "bottom" action (see comment in destructor) + Action< Actor > *bottomAction; + for( bottomAction = m_action; bottomAction && bottomAction->m_buriedUnderMe; bottomAction = bottomAction->m_buriedUnderMe ) + ; + + if ( bottomAction ) + { + delete bottomAction; + } + + // delete any dead Actions + m_deadActionVector.PurgeAndDeleteElements(); + + m_action = action; + } + + /** + * Return true if this Behavior contains no actions + */ + bool IsEmpty( void ) const + { + return m_action == NULL; + } + + /** + * Execute this Behavior + */ + void Update( Actor *me, float interval ) + { + if ( me == NULL || IsEmpty() ) + { + return; + } + + m_me = me; + + m_action = m_action->ApplyResult( me, this, m_action->InvokeUpdate( me, this, interval ) ); + + if ( m_action && me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + CFmtStr msg; + me->DisplayDebugText( msg.sprintf( "%s: %s", GetName(), m_action->DebugString() ) ); + } + + // delete any dead Actions + m_deadActionVector.PurgeAndDeleteElements(); + } + + /** + * If this Behavior has not been Update'd in a long time, + * call Resume() to let the system know its internal state may + * be out of date. + */ + void Resume( Actor *me ) + { + if ( me == NULL || IsEmpty() ) + { + return; + } + + m_action = m_action->ApplyResult( me, this, m_action->OnResume( me, NULL ) ); + + if ( m_action && me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + CFmtStr msg; + me->DisplayDebugText( msg.sprintf( "%s: %s", GetName(), m_action->DebugString() ) ); + } + } + + /** + * Use this method to destroy Actions used by this Behavior. + * We cannot delete Actions in-line since Action updates can potentially + * invoke event responders which will then use potentially deleted + * Action pointers, causing memory corruption. + * Instead, we will collect the dead Actions and delete them at the + * end of Update(). + */ + void DestroyAction( Action< Actor > *dead ) + { + m_deadActionVector.AddToTail( dead ); + } + + const char *GetName( void ) const + { + return m_name; + } + + // INextBotEventResponder propagation ---------------------------------------------------------------------- + virtual INextBotEventResponder *FirstContainedResponder( void ) const + { + return m_action; + } + + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const + { + return NULL; + } + + // IContextualQuery propagation ---------------------------------------------------------------------------- + virtual QueryResultType ShouldPickUp( const INextBot *me, CBaseEntity *item ) const // if the desired item was available right now, should we pick it up? + { + QueryResultType result = ANSWER_UNDEFINED; + + if ( m_action ) + { + // find innermost child action + Action< Actor > *action; + for( action = m_action; action->m_child; action = action->m_child ) + ; + + // work our way through our containers + while( action && result == ANSWER_UNDEFINED ) + { + Action< Actor > *containingAction = action->m_parent; + + // work our way up the stack + while( action && result == ANSWER_UNDEFINED ) + { + result = action->ShouldPickUp( me, item ); + action = action->GetActionBuriedUnderMe(); + } + + action = containingAction; + } + } + + return result; + } + + virtual QueryResultType ShouldHurry( const INextBot *me ) const // are we in a hurry? + { + QueryResultType result = ANSWER_UNDEFINED; + + if ( m_action ) + { + // find innermost child action + Action< Actor > *action; + for( action = m_action; action->m_child; action = action->m_child ) + ; + + // work our way through our containers + while( action && result == ANSWER_UNDEFINED ) + { + Action< Actor > *containingAction = action->m_parent; + + // work our way up the stack + while( action && result == ANSWER_UNDEFINED ) + { + result = action->ShouldHurry( me ); + action = action->GetActionBuriedUnderMe(); + } + + action = containingAction; + } + } + + return result; + } + + virtual QueryResultType ShouldRetreat( const INextBot *me ) const // is it time to retreat? + { + QueryResultType result = ANSWER_UNDEFINED; + + if ( m_action ) + { + // find innermost child action + Action< Actor > *action; + for( action = m_action; action->m_child; action = action->m_child ) + ; + + // work our way through our containers + while( action && result == ANSWER_UNDEFINED ) + { + Action< Actor > *containingAction = action->m_parent; + + // work our way up the stack + while( action && result == ANSWER_UNDEFINED ) + { + result = action->ShouldRetreat( me ); + action = action->GetActionBuriedUnderMe(); + } + + action = containingAction; + } + } + + return result; + } + + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const // should we attack "them"? + { + QueryResultType result = ANSWER_UNDEFINED; + + if ( m_action ) + { + // find innermost child action + Action< Actor > *action; + for( action = m_action; action->m_child; action = action->m_child ) + ; + + // work our way through our containers + while( action && result == ANSWER_UNDEFINED ) + { + Action< Actor > *containingAction = action->m_parent; + + // work our way up the stack + while( action && result == ANSWER_UNDEFINED ) + { + result = action->ShouldAttack( me, them ); + action = action->GetActionBuriedUnderMe(); + } + + action = containingAction; + } + } + + return result; + } + + virtual QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const // return true if we should wait for 'blocker' that is across our path somewhere up ahead. + { + QueryResultType result = ANSWER_UNDEFINED; + + if ( m_action ) + { + // find innermost child action + Action< Actor > *action; + for( action = m_action; action->m_child; action = action->m_child ) + ; + + // work our way through our containers + while( action && result == ANSWER_UNDEFINED ) + { + Action< Actor > *containingAction = action->m_parent; + + // work our way up the stack + while( action && result == ANSWER_UNDEFINED ) + { + result = action->IsHindrance( me, blocker ); + action = action->GetActionBuriedUnderMe(); + } + + action = containingAction; + } + } + + return result; + } + + + virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const // given a subject, return the world space position we should aim at + { + Vector result = vec3_origin; + + if ( m_action ) + { + // find innermost child action + Action< Actor > *action; + for( action = m_action; action->m_child; action = action->m_child ) + ; + + // work our way through our containers + while( action && result == vec3_origin ) + { + Action< Actor > *containingAction = action->m_parent; + + // work our way up the stack + while( action && result == vec3_origin ) + { + result = action->SelectTargetPoint( me, subject ); + action = action->GetActionBuriedUnderMe(); + } + + action = containingAction; + } + } + + return result; + } + + + /** + * Allow bot to approve of positions game movement tries to put him into. + * This is most useful for bots derived from CBasePlayer that go through + * the player movement system. + */ + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const + { + QueryResultType result = ANSWER_UNDEFINED; + + if ( m_action ) + { + // find innermost child action + Action< Actor > *action; + for( action = m_action; action->m_child; action = action->m_child ) + ; + + // work our way through our containers + while( action && result == ANSWER_UNDEFINED ) + { + Action< Actor > *containingAction = action->m_parent; + + // work our way up the stack + while( action && result == ANSWER_UNDEFINED ) + { + result = action->IsPositionAllowed( me, pos ); + action = action->GetActionBuriedUnderMe(); + } + + action = containingAction; + } + } + + return result; + } + + + + virtual const CKnownEntity *SelectMoreDangerousThreat( const INextBot *me, const CBaseCombatCharacter *subject, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const // return the more dangerous of the two threats, or NULL if we have no opinion + { + const CKnownEntity *result = NULL; + + if ( m_action ) + { + // find innermost child action + Action< Actor > *action; + for( action = m_action; action->m_child; action = action->m_child ) + ; + + // work our way through our containers + while( action && result == NULL ) + { + Action< Actor > *containingAction = action->m_parent; + + // work our way up the stack + while( action && result == NULL ) + { + result = action->SelectMoreDangerousThreat( me, subject, threat1, threat2 ); + action = action->GetActionBuriedUnderMe(); + } + + action = containingAction; + } + } + + return result; + } + + +private: + Action< Actor > *m_action; + + #define MAX_NAME_LENGTH 32 + CFmtStrN< MAX_NAME_LENGTH > m_name; + + Actor *m_me; + + CUtlVector< Action< Actor > * > m_deadActionVector; // completed Actions pending deletion +}; + + +//---------------------------------------------------------------------------------------------- +/** + * Something an Actor does. + * Actions can contain Actions, representing the precise context of the Actor's behavior. + * A system of Actions is contained within a Behavior, which acts as the manager + * of the Action system. + */ +template < typename Actor > +class Action : public INextBotEventResponder, public IContextualQuery +{ +public: + DECLARE_CLASS( Action, INextBotEventResponder ); + + Action( void ); + virtual ~Action(); + + virtual const char *GetName( void ) const = 0; // return name of this action + virtual bool IsNamed( const char *name ) const; // return true if given name matches the name of this Action + virtual const char *GetFullName( void ) const; // return a temporary string showing the full lineage of this one action + Actor *GetActor( void ) const; // return the Actor performing this Action (valid just before OnStart() is invoked) + + //----------------------------------------------------------------------------------------- + /** + * Try to start the Action. Result is immediately processed, + * which can cause an immediate transition, another OnStart(), etc. + * An Action can count on each OnStart() being followed (eventually) with an OnEnd(). + */ + virtual ActionResult< Actor > OnStart( Actor *me, Action< Actor > *priorAction ) { return Continue(); } + + /** + * Do the work of the Action. It is possible for Update to not be + * called between a given OnStart/OnEnd pair due to immediate transitions. + */ + virtual ActionResult< Actor > Update( Actor *me, float interval ) { return Continue(); } + + // Invoked when an Action is ended for any reason + virtual void OnEnd( Actor *me, Action< Actor > *nextAction ) { } + + /* + * When an Action is suspended by a new action. + * Note that only CONTINUE and DONE are valid results. All other results will + * be considered as a CONTINUE. + */ + virtual ActionResult< Actor > OnSuspend( Actor *me, Action< Actor > *interruptingAction ) { return Continue(); } + + // When an Action is resumed after being suspended + virtual ActionResult< Actor > OnResume( Actor *me, Action< Actor > *interruptingAction ) { return Continue(); } + + /** + * To cause a state change, use these methods to create an ActionResult to + * return from OnStart, Update, or OnResume. + */ + ActionResult< Actor > Continue( void ) const; + ActionResult< Actor > ChangeTo( Action< Actor > *action, const char *reason = NULL ) const; + ActionResult< Actor > SuspendFor( Action< Actor > *action, const char *reason = NULL ) const; + ActionResult< Actor > Done( const char *reason = NULL ) const; + + // create and return an Action to start as sub-action within this Action when it starts + virtual Action< Actor > *InitialContainedAction( Actor *me ) { return NULL; } + + //----------------------------------------------------------------------------------------- + /** + * Override the event handler methods below to respond to events that occur during this Action + * NOTE: These are identical to the events in INextBotEventResponder with the addition + * of an actor argument and a return result. Their translators are located in the private area + * below. + */ + virtual EventDesiredResult< Actor > OnLeaveGround( Actor *me, CBaseEntity *ground ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnLandOnGround( Actor *me, CBaseEntity *ground ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnContact( Actor *me, CBaseEntity *other, CGameTrace *result = NULL ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnMoveToSuccess( Actor *me, const Path *path ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnMoveToFailure( Actor *me, const Path *path, MoveToFailureType reason ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnStuck( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnUnStuck( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnPostureChanged( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnAnimationActivityComplete( Actor *me, int activity ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnAnimationActivityInterrupted( Actor *me, int activity ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnAnimationEvent( Actor *me, animevent_t *event ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnIgnite( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnInjured( Actor *me, const CTakeDamageInfo &info ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnKilled( Actor *me, const CTakeDamageInfo &info ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnOtherKilled( Actor *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnSight( Actor *me, CBaseEntity *subject ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnLostSight( Actor *me, CBaseEntity *subject ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnSound( Actor *me, CBaseEntity *source, const Vector &pos, KeyValues *keys ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnSpokeConcept( Actor *me, CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnWeaponFired( Actor *me, CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnNavAreaChanged( Actor *me, CNavArea *newArea, CNavArea *oldArea ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnModelChanged( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnPickUp( Actor *me, CBaseEntity *item, CBaseCombatCharacter *giver ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnDrop( Actor *me, CBaseEntity *item ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnActorEmoted( Actor *me, CBaseCombatCharacter *emoter, int emote ) { return TryContinue(); } + + virtual EventDesiredResult< Actor > OnCommandAttack( Actor *me, CBaseEntity *victim ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCommandApproach( Actor *me, const Vector &pos, float range ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCommandApproach( Actor *me, CBaseEntity *goal ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCommandRetreat( Actor *me, CBaseEntity *threat, float range ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCommandPause( Actor *me, float duration ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCommandResume( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCommandString( Actor *me, const char *command ) { return TryContinue(); } + + virtual EventDesiredResult< Actor > OnShoved( Actor *me, CBaseEntity *pusher ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnBlinded( Actor *me, CBaseEntity *blinder ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnTerritoryContested( Actor *me, int territoryID ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnTerritoryCaptured( Actor *me, int territoryID ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnTerritoryLost( Actor *me, int territoryID ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnWin( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnLose( Actor *me ) { return TryContinue(); } + +#ifdef DOTA_SERVER_DLL + virtual EventDesiredResult< Actor > OnCommandMoveTo( Actor *me, const Vector &pos ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCommandMoveToAggressive( Actor *me, const Vector &pos ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCommandAttack( Actor *me, CBaseEntity *victim, bool bDeny ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCastAbilityNoTarget( Actor *me, CDOTABaseAbility *ability ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCastAbilityOnPosition( Actor *me, CDOTABaseAbility *ability, const Vector &pos ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCastAbilityOnTarget( Actor *me, CDOTABaseAbility *ability, CBaseEntity *target ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnDropItem( Actor *me, const Vector &pos, CBaseEntity *item ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnPickupItem( Actor *me, CBaseEntity *item ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnPickupRune( Actor *me, CBaseEntity *item ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnStop( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnFriendThreatened( Actor *me, CBaseEntity *friendly, CBaseEntity *threat ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnCancelAttack( Actor *me, CBaseEntity *pTarget ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnDominated( Actor *me ) { return TryContinue(); } + virtual EventDesiredResult< Actor > OnWarped( Actor *me, Vector vStartPos ) { return TryContinue(); } +#endif + + /** + * Event handlers must return one of these. + */ + EventDesiredResult< Actor > TryContinue( EventResultPriorityType priority = RESULT_TRY ) const; + EventDesiredResult< Actor > TryChangeTo( Action< Actor > *action, EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) const; + EventDesiredResult< Actor > TrySuspendFor( Action< Actor > *action, EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) const; + EventDesiredResult< Actor > TryDone( EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) const; + EventDesiredResult< Actor > TryToSustain( EventResultPriorityType priority = RESULT_TRY, const char *reason = NULL ) const; + + + //----------------------------------------------------------------------------------------- + Action< Actor > *GetActiveChildAction( void ) const; + Action< Actor > *GetParentAction( void ) const; // the Action that I'm running inside of + + bool IsSuspended( void ) const; // return true if we are currently suspended for another Action + + const char *DebugString( void ) const; // return a temporary string describing the current action stack for debugging + + /** + * Sometimes we want to pass through other NextBots. OnContact() will always + * be invoked, but collision resolution can be skipped if this + * method returns false. + */ + virtual bool IsAbleToBlockMovementOf( const INextBot *botInMotion ) const { return true; } + + // INextBotEventResponder propagation ---------------------------------------------------------------------- + virtual INextBotEventResponder *FirstContainedResponder( void ) const; + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const; + + +private: + + /** + * These macros are used below to translate INextBotEventResponder event methods + * into Action event handler methods + */ + #define PROCESS_EVENT( METHOD ) \ + { \ + if ( !m_isStarted ) \ + return; \ + \ + Action< Actor > *_action = this; \ + EventDesiredResult< Actor > _result; \ + \ + while( _action ) \ + { \ + if ( m_actor && (m_actor->IsDebugging(NEXTBOT_EVENTS) || NextBotDebugHistory.GetBool())) \ + { \ + m_actor->DebugConColorMsg( NEXTBOT_EVENTS, Color( 100, 100, 100, 255 ), "%3.2f: %s:%s: %s received EVENT %s\n", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName(), _action->GetFullName(), #METHOD ); \ + } \ + _result = _action->METHOD( m_actor ); \ + if ( !_result.IsContinue() ) \ + break; \ + _action = _action->GetActionBuriedUnderMe(); \ + } \ + \ + if ( _action ) \ + { \ + if ( m_actor && _result.IsRequestingChange() && (m_actor->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) ) \ + { \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName() ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "%s ", _action->GetFullName() ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "reponded to EVENT %s with ", #METHOD ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "%s %s ", _result.GetTypeName(), _result.m_action ? _result.m_action->GetName() : "" ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), "%s\n", _result.m_reason ? _result.m_reason : "" ); \ + } \ + \ + _action->StorePendingEventResult( _result, #METHOD ); \ + } \ + \ + INextBotEventResponder::METHOD(); \ + } + + + #define PROCESS_EVENT_WITH_1_ARG( METHOD, ARG1 ) \ + { \ + if ( !m_isStarted ) \ + return; \ + \ + Action< Actor > *_action = this; \ + EventDesiredResult< Actor > _result; \ + \ + while( _action ) \ + { \ + if ( m_actor && (m_actor->IsDebugging(NEXTBOT_EVENTS) || NextBotDebugHistory.GetBool()) ) \ + { \ + m_actor->DebugConColorMsg( NEXTBOT_EVENTS, Color( 100, 100, 100, 255 ), "%3.2f: %s:%s: %s received EVENT %s\n", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName(), _action->GetFullName(), #METHOD ); \ + } \ + _result = _action->METHOD( m_actor, ARG1 ); \ + if ( !_result.IsContinue() ) \ + break; \ + _action = _action->GetActionBuriedUnderMe(); \ + } \ + \ + if ( _action ) \ + { \ + if ( m_actor && (m_actor->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) && _result.IsRequestingChange() && _action ) \ + { \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName() ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "%s ", _action->GetFullName() ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "reponded to EVENT %s with ", #METHOD ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "%s %s ", _result.GetTypeName(), _result.m_action ? _result.m_action->GetName() : "" ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), "%s\n", _result.m_reason ? _result.m_reason : "" ); \ + } \ + \ + _action->StorePendingEventResult( _result, #METHOD ); \ + } \ + \ + INextBotEventResponder::METHOD( ARG1 ); \ + } + + + #define PROCESS_EVENT_WITH_2_ARGS( METHOD, ARG1, ARG2 ) \ + { \ + if ( !m_isStarted ) \ + return; \ + \ + Action< Actor > *_action = this; \ + EventDesiredResult< Actor > _result; \ + \ + while( _action ) \ + { \ + if ( m_actor && (m_actor->IsDebugging(NEXTBOT_EVENTS) || NextBotDebugHistory.GetBool()) ) \ + { \ + m_actor->DebugConColorMsg( NEXTBOT_EVENTS, Color( 100, 100, 100, 255 ), "%3.2f: %s:%s: %s received EVENT %s\n", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName(), _action->GetFullName(), #METHOD ); \ + } \ + _result = _action->METHOD( m_actor, ARG1, ARG2 ); \ + if ( !_result.IsContinue() ) \ + break; \ + _action = _action->GetActionBuriedUnderMe(); \ + } \ + \ + if ( _action ) \ + { \ + if ( m_actor && (m_actor->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) && _result.IsRequestingChange() && _action ) \ + { \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName() ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "%s ", _action->GetFullName() ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "reponded to EVENT %s with ", #METHOD ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "%s %s ", _result.GetTypeName(), _result.m_action ? _result.m_action->GetName() : "" ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), "%s\n", _result.m_reason ? _result.m_reason : "" ); \ + } \ + \ + _action->StorePendingEventResult( _result, #METHOD ); \ + } \ + \ + INextBotEventResponder::METHOD( ARG1, ARG2 ); \ + } + + + #define PROCESS_EVENT_WITH_3_ARGS( METHOD, ARG1, ARG2, ARG3 ) \ + { \ + if ( !m_isStarted ) \ + return; \ + \ + Action< Actor > *_action = this; \ + EventDesiredResult< Actor > _result; \ + \ + while( _action ) \ + { \ + if ( m_actor && (m_actor->IsDebugging(NEXTBOT_EVENTS) || NextBotDebugHistory.GetBool()) ) \ + { \ + m_actor->DebugConColorMsg( NEXTBOT_EVENTS, Color( 100, 100, 100, 255 ), "%3.2f: %s:%s: %s received EVENT %s\n", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName(), _action->GetFullName(), #METHOD ); \ + } \ + _result = _action->METHOD( m_actor, ARG1, ARG2, ARG3 ); \ + if ( !_result.IsContinue() ) \ + break; \ + _action = _action->GetActionBuriedUnderMe(); \ + } \ + \ + if ( _action ) \ + { \ + if ( m_actor && (m_actor->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) && _result.IsRequestingChange() && _action ) \ + { \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, m_actor->GetDebugIdentifier(), m_behavior->GetName() ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "%s ", _action->GetFullName() ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 0, 255 ), "reponded to EVENT %s with ", #METHOD ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "%s %s ", _result.GetTypeName(), _result.m_action ? _result.m_action->GetName() : "" ); \ + m_actor->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), "%s\n", _result.m_reason ? _result.m_reason : "" ); \ + } \ + \ + _action->StorePendingEventResult( _result, #METHOD ); \ + } \ + \ + INextBotEventResponder::METHOD( ARG1, ARG2, ARG3 ); \ + } + + + /** + * Translate incoming events into Action events + * DO NOT OVERRIDE THESE METHODS + */ + virtual void OnLeaveGround( CBaseEntity *ground ) { PROCESS_EVENT_WITH_1_ARG( OnLeaveGround, ground ); } + virtual void OnLandOnGround( CBaseEntity *ground ) { PROCESS_EVENT_WITH_1_ARG( OnLandOnGround, ground ); } + virtual void OnContact( CBaseEntity *other, CGameTrace *result ) { PROCESS_EVENT_WITH_2_ARGS( OnContact, other, result ); } + virtual void OnMoveToSuccess( const Path *path ) { PROCESS_EVENT_WITH_1_ARG( OnMoveToSuccess, path ); } + virtual void OnMoveToFailure( const Path *path, MoveToFailureType reason ) { PROCESS_EVENT_WITH_2_ARGS( OnMoveToFailure, path, reason ); } + virtual void OnStuck( void ) { PROCESS_EVENT( OnStuck ); } + virtual void OnUnStuck( void ) { PROCESS_EVENT( OnUnStuck ); } + virtual void OnPostureChanged( void ) { PROCESS_EVENT( OnPostureChanged ); } + virtual void OnAnimationActivityComplete( int activity ) { PROCESS_EVENT_WITH_1_ARG( OnAnimationActivityComplete, activity ); } + virtual void OnAnimationActivityInterrupted( int activity ) { PROCESS_EVENT_WITH_1_ARG( OnAnimationActivityInterrupted, activity ); } + virtual void OnAnimationEvent( animevent_t *event ) { PROCESS_EVENT_WITH_1_ARG( OnAnimationEvent, event ); } + virtual void OnIgnite( void ) { PROCESS_EVENT( OnIgnite ); } + virtual void OnInjured( const CTakeDamageInfo &info ) { PROCESS_EVENT_WITH_1_ARG( OnInjured, info ); } + virtual void OnKilled( const CTakeDamageInfo &info ) { PROCESS_EVENT_WITH_1_ARG( OnKilled, info ); } + virtual void OnOtherKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) { PROCESS_EVENT_WITH_2_ARGS( OnOtherKilled, victim, info ); } + virtual void OnSight( CBaseEntity *subject ) { PROCESS_EVENT_WITH_1_ARG( OnSight, subject ); } + virtual void OnLostSight( CBaseEntity *subject ) { PROCESS_EVENT_WITH_1_ARG( OnLostSight, subject ); } + virtual void OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys ) { PROCESS_EVENT_WITH_3_ARGS( OnSound, source, pos, keys ); } + virtual void OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ) { PROCESS_EVENT_WITH_3_ARGS( OnSpokeConcept, who, concept, response ); } + virtual void OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ) { PROCESS_EVENT_WITH_2_ARGS( OnWeaponFired, whoFired, weapon ); } + virtual void OnNavAreaChanged( CNavArea *newArea, CNavArea *oldArea ) { PROCESS_EVENT_WITH_2_ARGS( OnNavAreaChanged, newArea, oldArea ); } + virtual void OnModelChanged( void ) { PROCESS_EVENT( OnModelChanged ); } + virtual void OnPickUp( CBaseEntity *item, CBaseCombatCharacter *giver ) { PROCESS_EVENT_WITH_2_ARGS( OnPickUp, item, giver ); } + virtual void OnDrop( CBaseEntity *item ) { PROCESS_EVENT_WITH_1_ARG( OnDrop, item ); } + virtual void OnActorEmoted( CBaseCombatCharacter *emoter, int emote ) { PROCESS_EVENT_WITH_2_ARGS( OnActorEmoted, emoter, emote ); } + + virtual void OnCommandAttack( CBaseEntity *victim ) { PROCESS_EVENT_WITH_1_ARG( OnCommandAttack, victim ); } + virtual void OnCommandApproach( const Vector &pos, float range ) { PROCESS_EVENT_WITH_2_ARGS( OnCommandApproach, pos, range ); } + virtual void OnCommandApproach( CBaseEntity *goal ) { PROCESS_EVENT_WITH_1_ARG( OnCommandApproach, goal ); } + virtual void OnCommandRetreat( CBaseEntity *threat, float range ) { PROCESS_EVENT_WITH_2_ARGS( OnCommandRetreat, threat, range ); } + virtual void OnCommandPause( float duration ) { PROCESS_EVENT_WITH_1_ARG( OnCommandPause, duration ); } + virtual void OnCommandResume( void ) { PROCESS_EVENT( OnCommandResume ); } + virtual void OnCommandString( const char *command ) { PROCESS_EVENT_WITH_1_ARG( OnCommandString, command ); } + + virtual void OnShoved( CBaseEntity *pusher ) { PROCESS_EVENT_WITH_1_ARG( OnShoved, pusher ); } + virtual void OnBlinded( CBaseEntity *blinder ) { PROCESS_EVENT_WITH_1_ARG( OnBlinded, blinder ); } + virtual void OnTerritoryContested( int territoryID ) { PROCESS_EVENT_WITH_1_ARG( OnTerritoryContested, territoryID ); } + virtual void OnTerritoryCaptured( int territoryID ) { PROCESS_EVENT_WITH_1_ARG( OnTerritoryCaptured, territoryID ); } + virtual void OnTerritoryLost( int territoryID ) { PROCESS_EVENT_WITH_1_ARG( OnTerritoryLost, territoryID ); } + virtual void OnWin( void ) { PROCESS_EVENT( OnWin ); } + virtual void OnLose( void ) { PROCESS_EVENT( OnLose ); } + +#ifdef DOTA_SERVER_DLL + virtual void OnCommandMoveTo( const Vector &pos ) { PROCESS_EVENT_WITH_1_ARG( OnCommandMoveTo, pos ); } + virtual void OnCommandMoveToAggressive( const Vector &pos ) { PROCESS_EVENT_WITH_1_ARG( OnCommandMoveToAggressive, pos ); } + virtual void OnCommandAttack( CBaseEntity *victim, bool bDeny ) { PROCESS_EVENT_WITH_2_ARGS( OnCommandAttack, victim, bDeny ); } + virtual void OnCastAbilityNoTarget( CDOTABaseAbility *ability ) { PROCESS_EVENT_WITH_1_ARG( OnCastAbilityNoTarget, ability ); } + virtual void OnCastAbilityOnPosition( CDOTABaseAbility *ability, const Vector &pos ) { PROCESS_EVENT_WITH_2_ARGS( OnCastAbilityOnPosition, ability, pos ); } + virtual void OnCastAbilityOnTarget( CDOTABaseAbility *ability, CBaseEntity *target ) { PROCESS_EVENT_WITH_2_ARGS( OnCastAbilityOnTarget, ability, target ); } + virtual void OnDropItem( const Vector &pos, CBaseEntity *item ) { PROCESS_EVENT_WITH_2_ARGS( OnDropItem, pos, item ); } + virtual void OnPickupItem( CBaseEntity *item ) { PROCESS_EVENT_WITH_1_ARG( OnPickupItem, item ); } + virtual void OnPickupRune( CBaseEntity *item ) { PROCESS_EVENT_WITH_1_ARG( OnPickupRune, item ); } + virtual void OnStop() { PROCESS_EVENT( OnStop ); } + virtual void OnFriendThreatened( CBaseEntity *friendly, CBaseEntity *threat ) { PROCESS_EVENT_WITH_2_ARGS( OnFriendThreatened, friendly, threat ); } + virtual void OnCancelAttack( CBaseEntity *pTarget ) { PROCESS_EVENT_WITH_1_ARG( OnCancelAttack, pTarget ); } + virtual void OnDominated() { PROCESS_EVENT( OnDominated ); } + virtual void OnWarped( Vector vStartPos ) { PROCESS_EVENT_WITH_1_ARG( OnWarped, vStartPos ); } +#endif + + friend class Behavior< Actor>; // the containing Behavior class + Behavior< Actor > *m_behavior; // the Behavior this Action is part of + + Action< Actor > *m_parent; // the Action that contains us + Action< Actor > *m_child; // the ACTIVE Action we contain, top of the stack. Use m_buriedUnderMe, m_coveringMe on the child to traverse to other suspended children + + Action< Actor > *m_buriedUnderMe; // the Action just "under" us in the stack that we will resume to when we finish + Action< Actor > *m_coveringMe; // the Action just "above" us in the stack that will resume to us when it finishes + + Actor *m_actor; // only valid after OnStart() + mutable EventDesiredResult< Actor > m_eventResult; // set by event handlers + bool m_isStarted; // Action doesn't start until OnStart() is invoked + bool m_isSuspended; // are we suspended for another Action + + Action< Actor > *GetActionBuriedUnderMe( void ) const // return Action just "under" us that we will resume to when we finish + { + return m_buriedUnderMe; + } + + Action< Actor > *GetActionCoveringMe( void ) const // return Action just "above" us that will resume to us when it finishes + { + return m_coveringMe; + } + + /** + * If any Action buried underneath me has either exited + * or is changing to a different Action, we're "out of scope" + */ + bool IsOutOfScope( void ) const + { + for( Action< Actor > *under = GetActionBuriedUnderMe(); under; under = under->GetActionBuriedUnderMe() ) + { + if ( under->m_eventResult.m_type == CHANGE_TO || + under->m_eventResult.m_type == DONE ) + { + return true; + } + } + return false; + } + + /** + * Process any pending events with the stack. This is called + * by the active Action on the top of the stack, and walks + * through any buried Actions checking for pending event results. + */ + ActionResult< Actor > ProcessPendingEvents( void ) const + { + // if an event has requested a change, honor it + if ( m_eventResult.IsRequestingChange() ) + { + ActionResult< Actor > result( m_eventResult.m_type, m_eventResult.m_action, m_eventResult.m_reason ); + + // clear event result in case this change is a suspend and we later resume this action + m_eventResult = TryContinue( RESULT_NONE ); + + return result; + } + + // check for pending event changes buried in the stack + Action< Actor > *under = GetActionBuriedUnderMe(); + while( under ) + { + if ( under->m_eventResult.m_type == SUSPEND_FOR ) + { + // process this pending event in-place and push new Action on the top of the stack + ActionResult< Actor > result( under->m_eventResult.m_type, under->m_eventResult.m_action, under->m_eventResult.m_reason ); + + // clear event result in case this change is a suspend and we later resume this action + under->m_eventResult = TryContinue( RESULT_NONE ); + + return result; + } + + under = under->GetActionBuriedUnderMe(); + } + + return Continue(); + } + + // given the result of this Action's work, apply the result to potentially cause a state transition + Action< Actor > * ApplyResult( Actor *me, Behavior< Actor > *behavior, ActionResult< Actor > result ); + + /** + * The methods below do the bookkeeping of each event, propagate the activity through the hierarchy, + * and invoke the virtual event for each. + */ + ActionResult< Actor > InvokeOnStart( Actor *me, Behavior< Actor > *behavior, Action< Actor > *priorAction, Action< Actor > *buriedUnderMeAction ); + ActionResult< Actor > InvokeUpdate( Actor *me, Behavior< Actor > *behavior, float interval ); + void InvokeOnEnd( Actor *me, Behavior< Actor > *behavior, Action< Actor > *nextAction ); + Action< Actor > * InvokeOnSuspend( Actor *me, Behavior< Actor > *behavior, Action< Actor > *interruptingAction ); + ActionResult< Actor > InvokeOnResume( Actor *me, Behavior< Actor > *behavior, Action< Actor > *interruptingAction ); + + /** + * Store the given event result, attending to priorities + */ + void StorePendingEventResult( const EventDesiredResult< Actor > &result, const char *eventName ) + { + if ( result.IsContinue() ) + { + return; + } + + if ( result.m_priority >= m_eventResult.m_priority ) + { + if ( m_eventResult.m_priority == RESULT_CRITICAL ) + { + if ( developer.GetBool() ) + { + DevMsg( "%3.2f: WARNING: %s::%s() RESULT_CRITICAL collision\n", gpGlobals->curtime, GetName(), eventName ); + } + } + + // new result as important or more so - destroy the replaced action + if ( m_eventResult.m_action ) + { + delete m_eventResult.m_action; + } + + // We keep the most recently processed event because this allows code to check history/state to + // do custom event collision handling. If we keep the first event at this priority and discard + // subsequent events (original behavior) there is no way to predict future collision resolutions (MSB). + m_eventResult = result; + } + else + { + // new result is lower priority than previously stored result - discard it + if ( result.m_action ) + { + // destroy the unused action + delete result.m_action; + } + } + } + + char *BuildDecoratedName( char *name, const Action< Actor > *action ) const; // recursive name outMsg for DebugString() + + void PrintStateToConsole( void ) const; +}; + + +//------------------------------------------------------------------------------------------- +template < typename Actor > +Action< Actor >::Action( void ) +{ + m_parent = NULL; + m_child = NULL; + m_buriedUnderMe = NULL; + m_coveringMe = NULL; + m_actor = NULL; + m_behavior = NULL; + + m_isStarted = false; + m_isSuspended = false; + + m_eventResult = TryContinue( RESULT_NONE ); + +#ifdef DEBUG_BEHAVIOR_MEMORY + ConColorMsg( Color( 255, 0, 255, 255 ), "%3.2f: NEW %0X\n", gpGlobals->curtime, this ); +#endif +} + + +//------------------------------------------------------------------------------------------- +template < typename Actor > +Action< Actor >::~Action() +{ +#ifdef DEBUG_BEHAVIOR_MEMORY + ConColorMsg( Color( 255, 0, 255, 255 ), "%3.2f: DELETE %0X\n", gpGlobals->curtime, this ); +#endif + + if ( m_parent ) + { + // if I'm my parent's active child, update parent's pointer + if ( m_parent->m_child == this ) + { + m_parent->m_child = m_buriedUnderMe; + } + } + + // delete all my children. + // our m_child pointer always points to the topmost + // child in the stack, so work our way back thru the + // 'buried' children and delete them. + Action< Actor > *child, *next = NULL; + for( child = m_child; child; child = next ) + { + next = child->m_buriedUnderMe; + delete child; + } + + if ( m_buriedUnderMe ) + { + // we're going away, so my buried sibling is now on top + m_buriedUnderMe->m_coveringMe = NULL; + } + + // delete any actions stacked on top of me + if ( m_coveringMe ) + { + // recursion will march down the chain + delete m_coveringMe; + } + + // delete any pending event result + if ( m_eventResult.m_action ) + { + delete m_eventResult.m_action; + } +} + + +template < typename Actor > +bool Action< Actor >::IsNamed( const char *name ) const +{ + return FStrEq( GetName(), name ); +} + + +template < typename Actor > +Actor *Action< Actor >::GetActor( void ) const +{ + return m_actor; +} + +template < typename Actor > +ActionResult< Actor > Action< Actor >::Continue( void ) const +{ + return ActionResult< Actor >( CONTINUE, NULL, NULL ); +} + +template < typename Actor > +ActionResult< Actor > Action< Actor >::ChangeTo( Action< Actor > *action, const char *reason ) const +{ + return ActionResult< Actor >( CHANGE_TO, action, reason ); +} + +template < typename Actor > +ActionResult< Actor > Action< Actor >::SuspendFor( Action< Actor > *action, const char *reason ) const +{ + // clear any pending transitions requested by events, or this SuspendFor will + // immediately be out of scope + m_eventResult = TryContinue( RESULT_NONE ); + + return ActionResult< Actor >( SUSPEND_FOR, action, reason ); +} + +template < typename Actor > +ActionResult< Actor > Action< Actor >::Done( const char *reason ) const +{ + return ActionResult< Actor >( DONE, NULL, reason ); +} + + +//------------------------------------------------------------------------------------------- +template < typename Actor > +EventDesiredResult< Actor > Action< Actor >::TryContinue( EventResultPriorityType priority ) const +{ + return EventDesiredResult< Actor >( CONTINUE, NULL, priority ); +} + +template < typename Actor > +EventDesiredResult< Actor > Action< Actor >::TryChangeTo( Action< Actor > *action, EventResultPriorityType priority, const char *reason ) const +{ + return EventDesiredResult< Actor >( CHANGE_TO, action, priority, reason ); +} + +template < typename Actor > +EventDesiredResult< Actor > Action< Actor >::TrySuspendFor( Action< Actor > *action, EventResultPriorityType priority, const char *reason ) const +{ + return EventDesiredResult< Actor >( SUSPEND_FOR, action, priority, reason ); +} + +template < typename Actor > +EventDesiredResult< Actor > Action< Actor >::TryDone( EventResultPriorityType priority, const char *reason /*= NULL*/ ) const +{ + return EventDesiredResult< Actor >( DONE, NULL, priority, reason ); +} + +template < typename Actor > +EventDesiredResult< Actor > Action< Actor >::TryToSustain( EventResultPriorityType priority, const char *reason /*= NULL*/ ) const +{ + return EventDesiredResult< Actor >( SUSTAIN, NULL, priority, reason ); +} + + +//------------------------------------------------------------------------------------------- +template < typename Actor > +Action< Actor > *Action< Actor >::GetActiveChildAction( void ) const +{ + return m_child; +} + + +//------------------------------------------------------------------------------------------- +// the Action that I'm running inside of +template < typename Actor > +Action< Actor > *Action< Actor >::GetParentAction( void ) const +{ + return m_parent; +} + + +//------------------------------------------------------------------------------------------- +/** + * Return true if we are currently suspended for another Action + */ +template < typename Actor > +bool Action< Actor >::IsSuspended( void ) const +{ + return m_isSuspended; +} + + +//------------------------------------------------------------------------------------------- +/** + * Start this Action. + * The act of calling InvokeOnStart is the edge case that 'enters' a state. + */ +template < typename Actor > +ActionResult< Actor > Action< Actor >::InvokeOnStart( Actor *me, Behavior< Actor > *behavior, Action< Actor > *priorAction, Action< Actor > *buriedUnderMeAction ) +{ + // debug display + if ( (me->IsDebugging(NEXTBOT_BEHAVIOR) || NextBotDebugHistory.GetBool()) ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), " STARTING " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" ); + } + + // these value must be valid before invoking OnStart, in case an OnSuspend happens + m_isStarted = true; + m_actor = me; + m_behavior = behavior; + + // maintain parent/child relationship during transitions + if ( priorAction ) + { + m_parent = priorAction->m_parent; + } + + if ( m_parent ) + { + // child pointer of an Action always points to the ACTIVE child + // parent pointers are set when child Actions are instantiated + m_parent->m_child = this; + } + + // maintain stack pointers + m_buriedUnderMe = buriedUnderMeAction; + if ( buriedUnderMeAction ) + { + buriedUnderMeAction->m_coveringMe = this; + } + + // we are always on top of the stack. if our priorAction was buried, it cleared + // everything covering it when it ended (which happens before we start) + m_coveringMe = NULL; + + // start the optional child action + m_child = InitialContainedAction( me ); + if ( m_child ) + { + // define initial parent/child relationship + m_child->m_parent = this; + + m_child = m_child->ApplyResult( me, behavior, ChangeTo( m_child, "Starting child Action" ) ); + } + + // start ourselves + ActionResult< Actor > result = OnStart( me, priorAction ); + + return result; +} + + +//------------------------------------------------------------------------------------------- +template < typename Actor > +ActionResult< Actor > Action< Actor >::InvokeUpdate( Actor *me, Behavior< Actor > *behavior, float interval ) +{ + // an explicit "out of scope" check is needed here to prevent any + // pending events causing an out of scope action to linger + if ( IsOutOfScope() ) + { + // exit self to make this Action active and allow result to take effect on its next Update + return Done( "Out of scope" ); + } + + if ( !m_isStarted ) + { + // this Action has not yet begun - start it + return ChangeTo( this, "Starting Action" ); + } + + // honor any pending event results + ActionResult< Actor > eventResult = ProcessPendingEvents(); + if ( !eventResult.IsContinue() ) + { + return eventResult; + } + + // update our child action first, since it has the most specific behavior + if ( m_child ) + { + m_child = m_child->ApplyResult( me, behavior, m_child->InvokeUpdate( me, behavior, interval ) ); + } + + // update ourselves + ActionResult< Actor > result; + { + VPROF_BUDGET( GetName(), "NextBot" ); + + result = Update( me, interval ); + } + + return result; +} + + +//------------------------------------------------------------------------------------------- +/** + * This method calls the virtual OnEnd() method for the Action, its children, and Actions + * stacked on top of it. + * It does NOT delete resources, or disturb pointer relationships, because this Action + * needs to remain valid for a short while as an argument to OnStart(), OnSuspend(), etc for + * the next Action. + * The destructor for the Action frees memory for this Action, its children, etc. + */ +template < typename Actor > +void Action< Actor >::InvokeOnEnd( Actor *me, Behavior< Actor > *behavior, Action< Actor > *nextAction ) +{ + if ( !m_isStarted ) + { + // we are not started (or never were) + return; + } + + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), " ENDING " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" ); + } + + // we are no longer started + m_isStarted = false; + + // tell child Action(s) to leave (but don't disturb the list itself) + Action< Actor > *child, *next = NULL; + for( child = m_child; child; child = next ) + { + next = child->m_buriedUnderMe; + child->InvokeOnEnd( me, behavior, nextAction ); + } + + // leave ourself + OnEnd( me, nextAction ); + + // leave any Actions stacked on top of me + if ( m_coveringMe ) + { + m_coveringMe->InvokeOnEnd( me, behavior, nextAction ); + } +} + + +//------------------------------------------------------------------------------------------- +/** + * Just invoke OnSuspend - when the interrupting Action is started it will + * update our buried/covered pointers. + * OnSuspend may cause this Action to exit. + */ +template < typename Actor > +Action< Actor > * Action< Actor >::InvokeOnSuspend( Actor *me, Behavior< Actor > *behavior, Action< Actor > *interruptingAction ) +{ + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 255, 255 ), " SUSPENDING " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" ); + } + + // suspend child Action + if ( m_child ) + { + m_child = m_child->InvokeOnSuspend( me, behavior, interruptingAction ); + } + + // suspend ourselves + m_isSuspended = true; + ActionResult< Actor > result = OnSuspend( me, interruptingAction ); + + if ( result.IsDone() ) + { + // we want to be replaced instead of suspended + InvokeOnEnd( me, behavior, NULL ); + + Action< Actor > * buried = GetActionBuriedUnderMe(); + + behavior->DestroyAction( this ); + + // new Action on top of the stack + return buried; + } + + // we are still on top of the stack at this moment + return this; +} + + +//------------------------------------------------------------------------------------------- +template < typename Actor > +ActionResult< Actor > Action< Actor >::InvokeOnResume( Actor *me, Behavior< Actor > *behavior, Action< Actor > *interruptingAction ) +{ + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 255, 255 ), " RESUMING " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" ); + } + + if ( !m_isSuspended ) + { + // we were never suspended + return Continue(); + } + + if ( m_eventResult.IsRequestingChange() ) + { + // this Action is not actually being Resumed, because a change + // is already pending from a prior event + return Continue(); + } + + // resume ourselves + m_isSuspended = false; + m_coveringMe = NULL; + + if ( m_parent ) + { + // we are once again our parent's active child + m_parent->m_child = this; + } + + // resume child Action + if ( m_child ) + { + m_child = m_child->ApplyResult( me, behavior, m_child->InvokeOnResume( me, behavior, interruptingAction ) ); + } + + // actually resume ourselves + ActionResult< Actor > result = OnResume( me, interruptingAction ); + + return result; +} + + +//------------------------------------------------------------------------------------------- +/** + * Given the result of this Action's work, apply the result to potentially create a new Action + */ +template < typename Actor > +Action< Actor > *Action< Actor >::ApplyResult( Actor *me, Behavior< Actor > *behavior, ActionResult< Actor > result ) +{ + Action< Actor > *newAction = result.m_action; + + switch( result.m_type ) + { + //----------------------------------------------------------------------------------------------------- + // transition to new Action + case CHANGE_TO: + { + if ( newAction == NULL ) + { + DevMsg( "Error: Attempted CHANGE_TO to a NULL Action\n" ); + AssertMsg( false, "Action: Attempted CHANGE_TO to a NULL Action" ); + return this; + } + + // debug display + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() ); + + if ( this == newAction ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), "START " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), newAction->GetName() ); + } + else + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), this->GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 0, 255 ), " CHANGE_TO " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), newAction->GetName() ); + } + + if ( result.m_reason ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 150, 255, 150, 255 ), " (%s)\n", result.m_reason ); + } + else + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" ); + } + } + + // we are done + this->InvokeOnEnd( me, behavior, newAction ); + + // start the new Action + ActionResult< Actor > startResult = newAction->InvokeOnStart( me, behavior, this, this->m_buriedUnderMe ); + + // discard ended action + if ( this != newAction ) + { + behavior->DestroyAction( this ); + } + + // debug display + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + newAction->PrintStateToConsole(); + } + + // apply result of starting the Action + return newAction->ApplyResult( me, behavior, startResult ); + } + + //----------------------------------------------------------------------------------------------------- + // temporarily suspend ourselves for the newAction, covering it on the stack + case SUSPEND_FOR: + { + // interrupting Action always goes on the TOP of the stack - find it + Action< Actor > *topAction = this; + while ( topAction->m_coveringMe ) + { + topAction = topAction->m_coveringMe; + } + + // debug display + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() ); + + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), this->GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 255, 255 ), " caused " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), topAction->GetName() ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 0, 255, 255 ), " to SUSPEND_FOR " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), newAction->GetName() ); + + if ( result.m_reason ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 150, 255, 150, 255 ), " (%s)\n", result.m_reason ); + } + else + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" ); + } + } + + // suspend the Action we just covered up + topAction = topAction->InvokeOnSuspend( me, behavior, newAction ); + + // begin the interrupting Action. + ActionResult< Actor > startResult = newAction->InvokeOnStart( me, behavior, topAction, topAction ); + + // debug display + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + newAction->PrintStateToConsole(); + } + + return newAction->ApplyResult( me, behavior, startResult ); + } + + //----------------------------------------------------------------------------------------------------- + case DONE: + { + // resume buried action + Action< Actor > *resumedAction = this->m_buriedUnderMe; + + // we are finished + this->InvokeOnEnd( me, behavior, resumedAction ); + + // debug display + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) || NextBotDebugHistory.GetBool() ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 150, 255 ), "%3.2f: %s:%s: ", gpGlobals->curtime, me->GetDebugIdentifier(), behavior->GetName() ); + + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), this->GetName() ); + + if ( resumedAction ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), " DONE, RESUME " ); + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), resumedAction->GetName() ); + } + else + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 0, 255, 0, 255 ), " DONE." ); + } + + if ( result.m_reason ) + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 150, 255, 150, 255 ), " (%s)\n", result.m_reason ); + } + else + { + me->DebugConColorMsg( NEXTBOT_BEHAVIOR, Color( 255, 255, 255, 255 ), "\n" ); + } + } + + if ( resumedAction == NULL ) + { + // all Actions complete + behavior->DestroyAction( this ); + return NULL; + } + + // resume uncovered action + ActionResult< Actor > resumeResult = resumedAction->InvokeOnResume( me, behavior, this ); + + // debug display + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + resumedAction->PrintStateToConsole(); + } + + // discard ended action + behavior->DestroyAction( this ); + + // apply result of OnResume() + return resumedAction->ApplyResult( me, behavior, resumeResult ); + } + + case CONTINUE: + case SUSTAIN: + default: + { + // no change, continue the current action next frame + return this; + } + } +} + + +//------------------------------------------------------------------------------------------- +/** + * Propagate events to sub actions + */ +template < typename Actor > +INextBotEventResponder *Action< Actor >::FirstContainedResponder( void ) const +{ + return GetActiveChildAction(); +} + +template < typename Actor > +INextBotEventResponder *Action< Actor >::NextContainedResponder( INextBotEventResponder *current ) const +{ + return NULL; +} + + +//------------------------------------------------------------------------------------------- +/** + * Return a temporary string describing the current action stack for debugging + */ +template < typename Actor > +const char *Action< Actor >::DebugString( void ) const +{ + static char str[ 256 ]; + + str[0] = '\000'; + + // find root + const Action< Actor > *root = this; + while ( root->m_parent ) + { + root = root->m_parent; + } + + return BuildDecoratedName( str, root ); +} + + +//------------------------------------------------------------------------------------------- +template < typename Actor > +char *Action< Actor >::BuildDecoratedName( char *name, const Action< Actor > *action ) const +{ + const int fudge = 256; + + // add the name of the given action + Q_strcat( name, action->GetName(), fudge ); + + // add any contained actions + const Action< Actor > *child = action->GetActiveChildAction(); + if ( child ) + { + Q_strcat( name, "( ", fudge ); + BuildDecoratedName( name, child ); + Q_strcat( name, " )", fudge ); + } + + // append buried actions + const Action< Actor > *buried = action->GetActionBuriedUnderMe(); + if ( buried ) + { + Q_strcat( name, "<<", fudge ); + BuildDecoratedName( name, buried ); + } + + return name; +} + + +//------------------------------------------------------------------------------------------- +/** + * Return a temporary string showing the full lineage of this one action + */ +template < typename Actor > +const char *Action< Actor >::GetFullName( void ) const +{ + const int fudge = 256; + static char str[ fudge ]; + + str[0] = '\000'; + + const int maxStack = 64; + const char *nameStack[ maxStack ]; + int stackIndex = 0; + + for( const Action< Actor > *action = this; + stackIndex < maxStack && action; + action = action->m_parent ) + { + nameStack[ stackIndex++ ] = action->GetName(); + } + + // assemble name + for( int i = stackIndex-1; i > 0; --i ) + { + Q_strcat( str, nameStack[ i ], fudge ); + Q_strcat( str, "/", fudge ); + } + + Q_strcat( str, nameStack[ 0 ], fudge ); + + /* + for( int i = 0; i < stackIndex-1; ++i ) + { + Q_strcat( str, " )", fudge ); + } + */ + + return str; +} + + +//------------------------------------------------------------------------------------------- +template < typename Actor > +void Action< Actor >::PrintStateToConsole( void ) const +{ + // emit the Behavior name + //ConColorMsg( Color( 255, 255, 255, 255 ), "%s: ", m_behavior->GetName() ); + + // build the state string + const char *msg = DebugString(); + + const int colorCount = 6; + Color colorTable[ colorCount ]; + colorTable[ 0 ].SetColor( 255, 150, 150, 255 ); + colorTable[ 1 ].SetColor( 150, 255, 150, 255 ); + colorTable[ 2 ].SetColor( 150, 150, 255, 255 ); + colorTable[ 3 ].SetColor( 255, 255, 150, 255 ); + colorTable[ 4 ].SetColor( 50, 255, 255, 255 ); + colorTable[ 5 ].SetColor( 255, 150, 255, 255 ); + + // output the color-coded state string + const int maxBufferSize = 256; + char buffer[ maxBufferSize ]; + + int colorIndex = 0; + int buriedLevel = 0; + + char *outMsg = buffer; + for( const char *c = msg; *c != '\000'; ++c ) + { + *outMsg = *c; + ++outMsg; + + if ( *c == '(' ) + { + *outMsg = '\000'; + + Color color = colorTable[ colorIndex ]; + + if ( buriedLevel ) + { + // draw buried labels darkly + color.SetColor( color.r() * 0.5, color.g() * 0.5, color.b() * 0.5, 255 ); + ++buriedLevel; + } + + //ConColorMsg( color, "%s", buffer ); + DevMsg( "%s", buffer ); + + colorIndex = ( colorIndex + 1 ) % colorCount; + + outMsg = buffer; + } + else if ( *c == ')' ) + { + // emit the closing paren with next batch + --outMsg; + *outMsg = '\000'; + + Color color = colorTable[ colorIndex ]; + + if ( buriedLevel ) + { + // draw buried labels darkly + color.SetColor( color.r() * 0.5, color.g() * 0.5, color.b() * 0.5, 255 ); + + --buriedLevel; + } + + //ConColorMsg( color, "%s", buffer ); + DevMsg( "%s", buffer ); + + --colorIndex; + if ( colorIndex < 0 ) + colorIndex = colorCount-1; + + outMsg = buffer; + + *outMsg = ')'; + ++outMsg; + } + else if ( *c == '<' && buriedLevel == 0 ) + { + // caught a "<<" stack push + ++c; + + *outMsg = '<'; + ++outMsg; + *outMsg = '\000'; + + // output active substring at full brightness + //ConColorMsg( colorTable[ colorIndex ], "%s", buffer ); + DevMsg( "%s", buffer ); + + outMsg = buffer; + + // from here until end of Action, use dim colors + buriedLevel = 1; + } + + } + + *outMsg = '\000'; + //ConColorMsg( colorTable[ colorIndex ], "%s", buffer ); + DevMsg( "%s", buffer ); + + //ConColorMsg( colorTable[ colorIndex ], "\n\n" ); + DevMsg( "\n\n" ); +} + + + + + +#endif // _BEHAVIOR_ENGINE_H_ + + + + + diff --git a/game/server/NextBot/NextBotBodyInterface.cpp b/game/server/NextBot/NextBotBodyInterface.cpp new file mode 100644 index 0000000..143ed4c --- /dev/null +++ b/game/server/NextBot/NextBotBodyInterface.cpp @@ -0,0 +1,55 @@ +// NextBotBodyInterface.cpp +// Control and information about the bot's body state (posture, animation state, etc) +// Author: Michael Booth, April 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "NextBot.h" +#include "NextBotBodyInterface.h" + + +void IBody::AimHeadTowards( const Vector &lookAtPos, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason ) +{ + if ( replyWhenAimed ) + { + replyWhenAimed->OnFail( GetBot(), INextBotReply::FAILED ); + } +} + +void IBody::AimHeadTowards( CBaseEntity *subject, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason ) +{ + if ( replyWhenAimed ) + { + replyWhenAimed->OnFail( GetBot(), INextBotReply::FAILED ); + } +} + +bool IBody::SetPosition( const Vector &pos ) +{ + GetBot()->GetEntity()->SetAbsOrigin( pos ); + return true; +} + +const Vector &IBody::GetEyePosition( void ) const +{ + static Vector eye; + + eye = GetBot()->GetEntity()->WorldSpaceCenter(); + + return eye; +} + +const Vector &IBody::GetViewVector( void ) const +{ + static Vector view; + + AngleVectors( GetBot()->GetEntity()->EyeAngles(), &view ); + + return view; +} + +bool IBody::IsHeadAimingOnTarget( void ) const +{ + return false; +} diff --git a/game/server/NextBot/NextBotBodyInterface.h b/game/server/NextBot/NextBotBodyInterface.h new file mode 100644 index 0000000..8d8a59c --- /dev/null +++ b/game/server/NextBot/NextBotBodyInterface.h @@ -0,0 +1,325 @@ +// NextBotBodyInterface.h +// Control and information about the bot's body state (posture, animation state, etc) +// Author: Michael Booth, April 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_BODY_INTERFACE_H_ +#define _NEXT_BOT_BODY_INTERFACE_H_ + +#include "animation.h" +#include "NextBotComponentInterface.h" + +class INextBot; +struct animevent_t; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for control and information about the bot's body state (posture, animation state, etc) + */ +class IBody : public INextBotComponent +{ +public: + IBody( INextBot *bot ) : INextBotComponent( bot ) { } + virtual ~IBody() { } + + virtual void Reset( void ) { INextBotComponent::Reset(); } // reset to initial state + virtual void Update( void ) { } // update internal state + + /** + * Move the bot to a new position. + * If the body is not currently movable or if it + * is in a motion-controlled animation activity + * the position will not be changed and false will be returned. + */ + virtual bool SetPosition( const Vector &pos ); + + virtual const Vector &GetEyePosition( void ) const; // return the eye position of the bot in world coordinates + virtual const Vector &GetViewVector( void ) const; // return the view unit direction vector in world coordinates + + enum LookAtPriorityType + { + BORING, + INTERESTING, // last known enemy location, dangerous sound location + IMPORTANT, // a danger + CRITICAL, // an active threat to our safety + MANDATORY // nothing can interrupt this look at - two simultaneous look ats with this priority is an error + }; + virtual void AimHeadTowards( const Vector &lookAtPos, + LookAtPriorityType priority = BORING, + float duration = 0.0f, + INextBotReply *replyWhenAimed = NULL, + const char *reason = NULL ); // aim the bot's head towards the given goal + virtual void AimHeadTowards( CBaseEntity *subject, + LookAtPriorityType priority = BORING, + float duration = 0.0f, + INextBotReply *replyWhenAimed = NULL, + const char *reason = NULL ); // continually aim the bot's head towards the given subject + + virtual bool IsHeadAimingOnTarget( void ) const; // return true if the bot's head has achieved its most recent lookat target + virtual bool IsHeadSteady( void ) const; // return true if head is not rapidly turning to look somewhere else + virtual float GetHeadSteadyDuration( void ) const; // return the duration that the bot's head has not been rotating + virtual float GetHeadAimSubjectLeadTime( void ) const; // return how far into the future we should predict our moving subject's position to aim at when tracking subject look-ats + virtual float GetHeadAimTrackingInterval( void ) const; // return how often we should sample our target's position and velocity to update our aim tracking, to allow realistic slop in tracking + virtual void ClearPendingAimReply( void ) { } // clear out currently pending replyWhenAimed callback + + virtual float GetMaxHeadAngularVelocity( void ) const; // return max turn rate of head in degrees/second + + enum ActivityType + { + MOTION_CONTROLLED_XY = 0x0001, // XY position and orientation of the bot is driven by the animation. + MOTION_CONTROLLED_Z = 0x0002, // Z position of the bot is driven by the animation. + ACTIVITY_UNINTERRUPTIBLE= 0x0004, // activity can't be changed until animation finishes + ACTIVITY_TRANSITORY = 0x0008, // a short animation that takes over from the underlying animation momentarily, resuming it upon completion + ENTINDEX_PLAYBACK_RATE = 0x0010, // played back at different rates based on entindex + }; + + /** + * Begin an animation activity, return false if we cant do that right now. + */ + virtual bool StartActivity( Activity act, unsigned int flags = 0 ); + virtual int SelectAnimationSequence( Activity act ) const; // given an Activity, select and return a specific animation sequence within it + + virtual Activity GetActivity( void ) const; // return currently animating activity + virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one + virtual bool HasActivityType( unsigned int flags ) const; // return true if currently animating activity has any of the given flags + + enum PostureType + { + STAND, + CROUCH, + SIT, + CRAWL, + LIE + }; + virtual void SetDesiredPosture( PostureType posture ) { } // request a posture change + virtual PostureType GetDesiredPosture( void ) const; // get posture body is trying to assume + virtual bool IsDesiredPosture( PostureType posture ) const; // return true if body is trying to assume this posture + virtual bool IsInDesiredPosture( void ) const; // return true if body's actual posture matches its desired posture + + virtual PostureType GetActualPosture( void ) const; // return body's current actual posture + virtual bool IsActualPosture( PostureType posture ) const; // return true if body is actually in the given posture + + virtual bool IsPostureMobile( void ) const; // return true if body's current posture allows it to move around the world + virtual bool IsPostureChanging( void ) const; // return true if body's posture is in the process of changing to new posture + + + /** + * "Arousal" is the level of excitedness/arousal/anxiety of the body. + * Is changes instantaneously to avoid complex interactions with posture transitions. + */ + enum ArousalType + { + NEUTRAL, + ALERT, + INTENSE + }; + virtual void SetArousal( ArousalType arousal ) { } // arousal level change + virtual ArousalType GetArousal( void ) const; // get arousal level + virtual bool IsArousal( ArousalType arousal ) const; // return true if body is at this arousal level + + + virtual float GetHullWidth( void ) const; // width of bot's collision hull in XY plane + virtual float GetHullHeight( void ) const; // height of bot's current collision hull based on posture + virtual float GetStandHullHeight( void ) const; // height of bot's collision hull when standing + virtual float GetCrouchHullHeight( void ) const; // height of bot's collision hull when crouched + virtual const Vector &GetHullMins( void ) const; // return current collision hull minimums based on actual body posture + virtual const Vector &GetHullMaxs( void ) const; // return current collision hull maximums based on actual body posture + + virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) + virtual unsigned int GetCollisionGroup( void ) const; +}; + + +inline bool IBody::IsHeadSteady( void ) const +{ + return true; +} + +inline float IBody::GetHeadSteadyDuration( void ) const +{ + return 0.0f; +} + +inline float IBody::GetHeadAimSubjectLeadTime( void ) const +{ + return 0.0f; +} + +inline float IBody::GetHeadAimTrackingInterval( void ) const +{ + return 0.0f; +} + +inline float IBody::GetMaxHeadAngularVelocity( void ) const +{ + return 1000.0f; +} + +inline bool IBody::StartActivity( Activity act, unsigned int flags ) +{ + return false; +} + +inline int IBody::SelectAnimationSequence( Activity act ) const +{ + return 0; +} + +inline Activity IBody::GetActivity( void ) const +{ + return ACT_INVALID; +} + +inline bool IBody::IsActivity( Activity act ) const +{ + return false; +} + +inline bool IBody::HasActivityType( unsigned int flags ) const +{ + return false; +} + +inline IBody::PostureType IBody::GetDesiredPosture( void ) const +{ + return IBody::STAND; +} + +inline bool IBody::IsDesiredPosture( PostureType posture ) const +{ + return true; +} + +inline bool IBody::IsInDesiredPosture( void ) const +{ + return true; +} + +inline IBody::PostureType IBody::GetActualPosture( void ) const +{ + return IBody::STAND; +} + +inline bool IBody::IsActualPosture( PostureType posture ) const +{ + return true; +} + +inline bool IBody::IsPostureMobile( void ) const +{ + return true; +} + +inline bool IBody::IsPostureChanging( void ) const +{ + return false; +} + +inline IBody::ArousalType IBody::GetArousal( void ) const +{ + return IBody::NEUTRAL; +} + +inline bool IBody::IsArousal( ArousalType arousal ) const +{ + return true; +} + +//--------------------------------------------------------------------------------------------------------------------------- +/** + * Width of bot's collision hull in XY plane + */ +inline float IBody::GetHullWidth( void ) const +{ + return 26.0f; +} + + +//--------------------------------------------------------------------------------------------------------------------------- +/** + * Height of bot's current collision hull based on posture + */ +inline float IBody::GetHullHeight( void ) const +{ + switch( GetActualPosture() ) + { + case LIE: + return 16.0f; + + case SIT: + case CROUCH: + return GetCrouchHullHeight(); + + case STAND: + default: + return GetStandHullHeight(); + } +} + + +//--------------------------------------------------------------------------------------------------------------------------- +/** + * Height of bot's collision hull when standing + */ +inline float IBody::GetStandHullHeight( void ) const +{ + return 68.0f; +} + + +//--------------------------------------------------------------------------------------------------------------------------- +/** + * Height of bot's collision hull when crouched + */ +inline float IBody::GetCrouchHullHeight( void ) const +{ + return 32.0f; +} + + +//--------------------------------------------------------------------------------------------------------------------------- +/** + * Return current collision hull minimums based on actual body posture + */ +inline const Vector &IBody::GetHullMins( void ) const +{ + static Vector hullMins; + + hullMins.x = -GetHullWidth()/2.0f; + hullMins.y = hullMins.x; + hullMins.z = 0.0f; + + return hullMins; +} + + +//--------------------------------------------------------------------------------------------------------------------------- +/** + * Return current collision hull maximums based on actual body posture + */ +inline const Vector &IBody::GetHullMaxs( void ) const +{ + static Vector hullMaxs; + + hullMaxs.x = GetHullWidth()/2.0f; + hullMaxs.y = hullMaxs.x; + hullMaxs.z = GetHullHeight(); + + return hullMaxs; +} + + +inline unsigned int IBody::GetSolidMask( void ) const +{ + return MASK_NPCSOLID; +} + +inline unsigned int IBody::GetCollisionGroup( void ) const +{ + return COLLISION_GROUP_NONE; +} + + + +#endif // _NEXT_BOT_BODY_INTERFACE_H_ diff --git a/game/server/NextBot/NextBotComponentInterface.cpp b/game/server/NextBot/NextBotComponentInterface.cpp new file mode 100644 index 0000000..0c4c172 --- /dev/null +++ b/game/server/NextBot/NextBotComponentInterface.cpp @@ -0,0 +1,24 @@ +// NextBotComponentInterface.cpp +// Implentation of system methods for NextBot component interface +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "NextBotInterface.h" +#include "NextBotComponentInterface.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +INextBotComponent::INextBotComponent( INextBot *bot ) +{ + m_curInterval = TICK_INTERVAL; + m_lastUpdateTime = 0; + m_bot = bot; + + // register this component with the bot + bot->RegisterComponent( this ); +} + + diff --git a/game/server/NextBot/NextBotComponentInterface.h b/game/server/NextBot/NextBotComponentInterface.h new file mode 100644 index 0000000..344d708 --- /dev/null +++ b/game/server/NextBot/NextBotComponentInterface.h @@ -0,0 +1,98 @@ +// NextBotComponentInterface.h +// Interface for all components +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_COMPONENT_INTERFACE_H_ +#define _NEXT_BOT_COMPONENT_INTERFACE_H_ + +#include "NextBotEventResponderInterface.h" + +class INextBot; +class Path; +class CGameTrace; +class CTakeDamageInfo; + + +//-------------------------------------------------------------------------------------------------------------------------- +/** + * Various processes can invoke a "reply" (ie: callback) via instances of this interface + */ +class INextBotReply +{ +public: + virtual void OnSuccess( INextBot *bot ) { } // invoked when process completed successfully + + enum FailureReason + { + DENIED, + INTERRUPTED, + FAILED + }; + virtual void OnFail( INextBot *bot, FailureReason reason ) { } // invoked when process failed +}; + + +//-------------------------------------------------------------------------------------------------------------------------- +/** + * Next Bot component interface + */ +class INextBotComponent : public INextBotEventResponder +{ +public: + INextBotComponent( INextBot *bot ); + virtual ~INextBotComponent() { } + + virtual void Reset( void ) { m_lastUpdateTime = 0; m_curInterval = TICK_INTERVAL; } // reset to initial state + virtual void Update( void ) = 0; // update internal state + virtual void Upkeep( void ) { } // lightweight update guaranteed to occur every server tick + + inline bool ComputeUpdateInterval(); // return false is no time has elapsed (interval is zero) + inline float GetUpdateInterval(); + + virtual INextBot *GetBot( void ) const { return m_bot; } + +private: + float m_lastUpdateTime; + float m_curInterval; + + friend class INextBot; + + INextBot *m_bot; + INextBotComponent *m_nextComponent; // simple linked list of components in the bot +}; + + +inline bool INextBotComponent::ComputeUpdateInterval() +{ + if ( m_lastUpdateTime ) + { + float interval = gpGlobals->curtime - m_lastUpdateTime; + + const float minInterval = 0.0001f; + if ( interval > minInterval ) + { + m_curInterval = interval; + m_lastUpdateTime = gpGlobals->curtime; + return true; + } + + return false; + } + + // First update - assume a reasonable interval. + // We need the very first update to do work, for cases + // where the bot was just created and we need to propagate + // an event to it immediately. + m_curInterval = 0.033f; + m_lastUpdateTime = gpGlobals->curtime - m_curInterval; + + return true; +} + +inline float INextBotComponent::GetUpdateInterval() +{ + return m_curInterval; +} + +#endif // _NEXT_BOT_COMPONENT_INTERFACE_H_ diff --git a/game/server/NextBot/NextBotContextualQueryInterface.h b/game/server/NextBot/NextBotContextualQueryInterface.h new file mode 100644 index 0000000..d117bca --- /dev/null +++ b/game/server/NextBot/NextBotContextualQueryInterface.h @@ -0,0 +1,102 @@ +// NextBotContextualQueryInterface.h +// Queries within the context of the bot's current behavior state +// Author: Michael Booth, June 2007 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_CONTEXTUAL_QUERY_H_ +#define _NEXT_BOT_CONTEXTUAL_QUERY_H_ + +class INextBot; +class CBaseEntity; +class CBaseCombatCharacter; +class Path; +class CKnownEntity; + +/** + * Since behaviors can have several concurrent actions active, we ask + * the topmost child action first, and if it defers, its parent, and so + * on, until we get a definitive answer. + */ +enum QueryResultType +{ + ANSWER_NO, + ANSWER_YES, + ANSWER_UNDEFINED +}; + +// Can pass this into IContextualQuery::IsHindrance to see if any hindrance is ever possible +#define IS_ANY_HINDRANCE_POSSIBLE ( (CBaseEntity*)0xFFFFFFFF ) + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for queries that are dependent on the bot's current behavior state + */ +class IContextualQuery +{ +public: + virtual ~IContextualQuery() { } + + virtual QueryResultType ShouldPickUp( const INextBot *me, CBaseEntity *item ) const; // if the desired item was available right now, should we pick it up? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + virtual QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const; // return true if we should wait for 'blocker' that is across our path somewhere up ahead. + + virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const; // given a subject, return the world space position we should aim at + + /** + * Allow bot to approve of positions game movement tries to put him into. + * This is most useful for bots derived from CBasePlayer that go through + * the player movement system. + */ + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; + + virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me, + const CBaseCombatCharacter *subject, + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats to 'subject', or NULL if we have no opinion +}; + +inline QueryResultType IContextualQuery::ShouldPickUp( const INextBot *me, CBaseEntity *item ) const +{ + return ANSWER_UNDEFINED; +} + +inline QueryResultType IContextualQuery::ShouldHurry( const INextBot *me ) const +{ + return ANSWER_UNDEFINED; +} + +inline QueryResultType IContextualQuery::ShouldRetreat( const INextBot *me ) const +{ + return ANSWER_UNDEFINED; +} + +inline QueryResultType IContextualQuery::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + return ANSWER_UNDEFINED; +} + +inline QueryResultType IContextualQuery::IsHindrance( const INextBot *me, CBaseEntity *blocker ) const +{ + return ANSWER_UNDEFINED; +} + +inline Vector IContextualQuery::SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const +{ + return vec3_origin; +} + +inline QueryResultType IContextualQuery::IsPositionAllowed( const INextBot *me, const Vector &pos ) const +{ + return ANSWER_UNDEFINED; +} + +inline const CKnownEntity *IContextualQuery::SelectMoreDangerousThreat( const INextBot *me, const CBaseCombatCharacter *subject, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const +{ + return NULL; +} + + +#endif // _NEXT_BOT_CONTEXTUAL_QUERY_H_ diff --git a/game/server/NextBot/NextBotDebug.h b/game/server/NextBot/NextBotDebug.h new file mode 100644 index 0000000..88e0ba2 --- /dev/null +++ b/game/server/NextBot/NextBotDebug.h @@ -0,0 +1,23 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef NEXTBOT_DEBUG_H +#define NEXTBOT_DEBUG_H +//------------------------------------------------------------------------------ +// Debug flags for nextbot + +enum NextBotDebugType +{ + NEXTBOT_DEBUG_NONE = 0, + NEXTBOT_BEHAVIOR = 0x0001, + NEXTBOT_LOOK_AT = 0x0002, + NEXTBOT_PATH = 0x0004, + NEXTBOT_ANIMATION = 0x0008, + NEXTBOT_LOCOMOTION = 0x0010, + NEXTBOT_VISION = 0x0020, + NEXTBOT_HEARING = 0x0040, + NEXTBOT_EVENTS = 0x0080, + NEXTBOT_ERRORS = 0x0100, // when things go wrong, like being stuck + + NEXTBOT_DEBUG_ALL = 0xFFFF +}; + +#endif diff --git a/game/server/NextBot/NextBotEventResponderInterface.h b/game/server/NextBot/NextBotEventResponderInterface.h new file mode 100644 index 0000000..9e7ba56 --- /dev/null +++ b/game/server/NextBot/NextBotEventResponderInterface.h @@ -0,0 +1,550 @@ +// NextBotEventResponderInterface.h +// Interface for propagating and responding to events +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_EVENT_RESPONDER_INTERFACE_H_ +#define _NEXT_BOT_EVENT_RESPONDER_INTERFACE_H_ + +class Path; +class CTakeDamageInfo; +class CBaseEntity; +class CDOTABaseAbility; + +struct CSoundParameters; +struct animevent_t; + +#include "ai_speech.h" + + +//-------------------------------------------------------------------------------------------------------------------------- +enum MoveToFailureType +{ + FAIL_NO_PATH_EXISTS, + FAIL_STUCK, + FAIL_FELL_OFF, +}; + +//-------------------------------------------------------------------------------------------------------------------------- +/** + * Events propagated to/between components. + * To add an event, add its signature here and implement its propagation + * to derived classes via FirstContainedResponder() and NextContainedResponder(). + * NOTE: Also add a translator to the Action class in NextBotBehavior.h. + */ +class INextBotEventResponder +{ +public: + DECLARE_CLASS_NOBASE( INextBotEventResponder ); + + virtual ~INextBotEventResponder() { } + + // these methods are used by derived classes to define how events propagate + virtual INextBotEventResponder *FirstContainedResponder( void ) const { return NULL; } + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; } + + // + // Events. All events must be 'extended' by calling the derived class explicitly to ensure propagation. + // Each event must implement its propagation in this interface class. + // + virtual void OnLeaveGround( CBaseEntity *ground ); // invoked when bot leaves ground for any reason + virtual void OnLandOnGround( CBaseEntity *ground ); // invoked when bot lands on the ground after being in the air + + virtual void OnContact( CBaseEntity *other, CGameTrace *result = NULL ); // invoked when bot touches 'other' + + virtual void OnMoveToSuccess( const Path *path ); // invoked when a bot reaches the end of the given Path + virtual void OnMoveToFailure( const Path *path, MoveToFailureType reason ); // invoked when a bot fails to reach the end of the given Path + virtual void OnStuck( void ); // invoked when bot becomes stuck while trying to move + virtual void OnUnStuck( void ); // invoked when a previously stuck bot becomes un-stuck and can again move + + virtual void OnPostureChanged( void ); // when bot has assumed new posture (query IBody for posture) + + virtual void OnAnimationActivityComplete( int activity ); // when animation activity has finished playing + virtual void OnAnimationActivityInterrupted( int activity );// when animation activity was replaced by another animation + virtual void OnAnimationEvent( animevent_t *event ); // when a QC-file animation event is triggered by the current animation sequence + + virtual void OnIgnite( void ); // when bot starts to burn + virtual void OnInjured( const CTakeDamageInfo &info ); // when bot is damaged by something + virtual void OnKilled( const CTakeDamageInfo &info ); // when the bot's health reaches zero + virtual void OnOtherKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); // when someone else dies + + virtual void OnSight( CBaseEntity *subject ); // when subject initially enters bot's visual awareness + virtual void OnLostSight( CBaseEntity *subject ); // when subject leaves enters bot's visual awareness + + virtual void OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys ); // when an entity emits a sound. "pos" is world coordinates of sound. "keys" are from sound's GameData + virtual void OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ); // when an Actor speaks a concept + virtual void OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ); // when someone fires a weapon + + virtual void OnNavAreaChanged( CNavArea *newArea, CNavArea *oldArea ); // when bot enters a new navigation area + + virtual void OnModelChanged( void ); // when the entity's model has been changed + + virtual void OnPickUp( CBaseEntity *item, CBaseCombatCharacter *giver ); // when something is added to our inventory + virtual void OnDrop( CBaseEntity *item ); // when something is removed from our inventory + virtual void OnActorEmoted( CBaseCombatCharacter *emoter, int emote ); // when "emoter" does an "emote" (ie: manual voice command, etc) + + virtual void OnCommandAttack( CBaseEntity *victim ); // attack the given entity + virtual void OnCommandApproach( const Vector &pos, float range = 0.0f ); // move to within range of the given position + virtual void OnCommandApproach( CBaseEntity *goal ); // follow the given leader + virtual void OnCommandRetreat( CBaseEntity *threat, float range = 0.0f ); // retreat from the threat at least range units away (0 == infinite) + virtual void OnCommandPause( float duration = 0.0f ); // pause for the given duration (0 == forever) + virtual void OnCommandResume( void ); // resume after a pause + + virtual void OnCommandString( const char *command ); // for debugging: respond to an arbitrary string representing a generalized command + + virtual void OnShoved( CBaseEntity *pusher ); // 'pusher' has shoved me + virtual void OnBlinded( CBaseEntity *blinder ); // 'blinder' has blinded me with a flash of light + + virtual void OnTerritoryContested( int territoryID ); // territory has been invaded and is changing ownership + virtual void OnTerritoryCaptured( int territoryID ); // we have captured enemy territory + virtual void OnTerritoryLost( int territoryID ); // we have lost territory to the enemy + + virtual void OnWin( void ); + virtual void OnLose( void ); + +#ifdef DOTA_SERVER_DLL + virtual void OnCommandMoveTo( const Vector &pos ); + virtual void OnCommandMoveToAggressive( const Vector &pos ); + virtual void OnCommandAttack( CBaseEntity *victim, bool bDeny ); + virtual void OnCastAbilityNoTarget( CDOTABaseAbility *ability ); + virtual void OnCastAbilityOnPosition( CDOTABaseAbility *ability, const Vector &pos ); + virtual void OnCastAbilityOnTarget( CDOTABaseAbility *ability, CBaseEntity *target ); + virtual void OnDropItem( const Vector &pos, CBaseEntity *item ); + virtual void OnPickupItem( CBaseEntity *item ); + virtual void OnPickupRune( CBaseEntity *item ); + virtual void OnStop(); + virtual void OnFriendThreatened( CBaseEntity *friendly, CBaseEntity *threat ); + virtual void OnCancelAttack( CBaseEntity *pTarget ); + virtual void OnDominated(); + virtual void OnWarped( Vector vStartPos ); +#endif +}; + + +inline void INextBotEventResponder::OnLeaveGround( CBaseEntity *ground ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnLeaveGround( ground ); + } +} + +inline void INextBotEventResponder::OnLandOnGround( CBaseEntity *ground ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnLandOnGround( ground ); + } +} + +inline void INextBotEventResponder::OnContact( CBaseEntity *other, CGameTrace *result ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnContact( other, result ); + } +} + +inline void INextBotEventResponder::OnMoveToSuccess( const Path *path ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnMoveToSuccess( path ); + } +} + +inline void INextBotEventResponder::OnMoveToFailure( const Path *path, MoveToFailureType reason ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnMoveToFailure( path, reason ); + } +} + +inline void INextBotEventResponder::OnStuck( void ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnStuck(); + } +} + +inline void INextBotEventResponder::OnUnStuck( void ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnUnStuck(); + } +} + +inline void INextBotEventResponder::OnPostureChanged( void ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnPostureChanged(); + } +} + +inline void INextBotEventResponder::OnAnimationActivityComplete( int activity ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnAnimationActivityComplete( activity ); + } +} + +inline void INextBotEventResponder::OnAnimationActivityInterrupted( int activity ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnAnimationActivityInterrupted( activity ); + } +} + +inline void INextBotEventResponder::OnAnimationEvent( animevent_t *event ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnAnimationEvent( event ); + } +} + +inline void INextBotEventResponder::OnIgnite( void ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnIgnite(); + } +} + +inline void INextBotEventResponder::OnInjured( const CTakeDamageInfo &info ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnInjured( info ); + } +} + +inline void INextBotEventResponder::OnKilled( const CTakeDamageInfo &info ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnKilled( info ); + } +} + +inline void INextBotEventResponder::OnOtherKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnOtherKilled( victim, info ); + } +} + +inline void INextBotEventResponder::OnSight( CBaseEntity *subject ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnSight( subject ); + } +} + +inline void INextBotEventResponder::OnLostSight( CBaseEntity *subject ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnLostSight( subject ); + } +} + +inline void INextBotEventResponder::OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnSound( source, pos, keys ); + } +} + +inline void INextBotEventResponder::OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnSpokeConcept( who, concept, response ); + } +} + +inline void INextBotEventResponder::OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnWeaponFired( whoFired, weapon ); + } +} + +inline void INextBotEventResponder::OnNavAreaChanged( CNavArea *newArea, CNavArea *oldArea ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnNavAreaChanged( newArea, oldArea ); + } +} + +inline void INextBotEventResponder::OnModelChanged( void ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnModelChanged(); + } +} + +inline void INextBotEventResponder::OnPickUp( CBaseEntity *item, CBaseCombatCharacter *giver ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnPickUp( item, giver ); + } +} + +inline void INextBotEventResponder::OnDrop( CBaseEntity *item ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnDrop( item ); + } +} + +inline void INextBotEventResponder::OnActorEmoted( CBaseCombatCharacter *emoter, int emote ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnActorEmoted( emoter, emote ); + } +} + +inline void INextBotEventResponder::OnShoved( CBaseEntity *pusher ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnShoved( pusher ); + } +} + +inline void INextBotEventResponder::OnBlinded( CBaseEntity *blinder ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnBlinded( blinder ); + } +} + +inline void INextBotEventResponder::OnCommandAttack( CBaseEntity *victim ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandAttack( victim ); + } +} + +inline void INextBotEventResponder::OnCommandApproach( const Vector &pos, float range ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandApproach( pos, range ); + } +} + +inline void INextBotEventResponder::OnCommandApproach( CBaseEntity *goal ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandApproach( goal ); + } +} + +inline void INextBotEventResponder::OnCommandRetreat( CBaseEntity *threat, float range ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandRetreat( threat, range ); + } +} + +inline void INextBotEventResponder::OnCommandPause( float duration ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandPause( duration ); + } +} + +inline void INextBotEventResponder::OnCommandResume( void ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandResume(); + } +} + +inline void INextBotEventResponder::OnCommandString( const char *command ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandString( command ); + } +} + +inline void INextBotEventResponder::OnTerritoryContested( int territoryID ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnTerritoryContested( territoryID ); + } +} + +inline void INextBotEventResponder::OnTerritoryCaptured( int territoryID ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnTerritoryCaptured( territoryID ); + } +} + +inline void INextBotEventResponder::OnTerritoryLost( int territoryID ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnTerritoryLost( territoryID ); + } +} + +inline void INextBotEventResponder::OnWin( void ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnWin(); + } +} + +inline void INextBotEventResponder::OnLose( void ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnLose(); + } +} + +#ifdef DOTA_SERVER_DLL +inline void INextBotEventResponder::OnCommandMoveTo( const Vector &pos ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandMoveTo( pos ); + } +} + +inline void INextBotEventResponder::OnCommandMoveToAggressive( const Vector &pos ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandMoveToAggressive( pos ); + } +} + +inline void INextBotEventResponder::OnCommandAttack( CBaseEntity *victim, bool bDeny ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCommandAttack( victim, bDeny ); + } +} + +inline void INextBotEventResponder::OnCastAbilityNoTarget( CDOTABaseAbility *ability ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCastAbilityNoTarget( ability ); + } +} + +inline void INextBotEventResponder::OnCastAbilityOnPosition( CDOTABaseAbility *ability, const Vector &pos ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCastAbilityOnPosition( ability, pos ); + } +} + +inline void INextBotEventResponder::OnCastAbilityOnTarget( CDOTABaseAbility *ability, CBaseEntity *target ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCastAbilityOnTarget( ability, target ); + } +} + +inline void INextBotEventResponder::OnDropItem( const Vector &pos, CBaseEntity *item ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnDropItem( pos, item ); + } +} + +inline void INextBotEventResponder::OnPickupItem( CBaseEntity *item ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnPickupItem( item ); + } +} + +inline void INextBotEventResponder::OnPickupRune( CBaseEntity *item ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnPickupRune( item ); + } +} + +inline void INextBotEventResponder::OnStop() +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnStop(); + } +} + +inline void INextBotEventResponder::OnFriendThreatened( CBaseEntity *friendly, CBaseEntity *threat ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnFriendThreatened( friendly, threat ); + } +} + +inline void INextBotEventResponder::OnCancelAttack( CBaseEntity *pTarget ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnCancelAttack( pTarget ); + } +} + +inline void INextBotEventResponder::OnDominated() +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnDominated(); + } +} + +inline void INextBotEventResponder::OnWarped( Vector vStartPos ) +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + sub->OnWarped( vStartPos ); + } +} +#endif + +#endif // _NEXT_BOT_EVENT_RESPONDER_INTERFACE_H_ diff --git a/game/server/NextBot/NextBotGroundLocomotion.cpp b/game/server/NextBot/NextBotGroundLocomotion.cpp new file mode 100644 index 0000000..27f6a52 --- /dev/null +++ b/game/server/NextBot/NextBotGroundLocomotion.cpp @@ -0,0 +1,1442 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// NextBotGroundLocomotion.cpp +// Basic ground-based movement for NextBotCombatCharacters +// Author: Michael Booth, February 2009 +// Note: This is a refactoring of ZombieBotLocomotion from L4D + +#include "cbase.h" + +#include "func_break.h" +#include "func_breakablesurf.h" +#include "activitylist.h" +#include "BasePropDoor.h" + +#include "nav.h" +#include "NextBot.h" +#include "NextBotGroundLocomotion.h" +#include "NextBotUtil.h" +#include "functorutils.h" +#include "SharedFunctorUtils.h" + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#pragma warning( disable : 4355 ) // warning 'this' used in base member initializer list - we're using it safely + + +//---------------------------------------------------------------------------------------------------------- +NextBotGroundLocomotion::NextBotGroundLocomotion( INextBot *bot ) : ILocomotion( bot ) +{ + m_nextBot = NULL; + m_ladder = NULL; + m_desiredLean.x = 0.0f; + m_desiredLean.y = 0.0f; + m_desiredLean.z = 0.0f; + + m_bRecomputePostureOnCollision = false; + m_ignorePhysicsPropTimer.Invalidate(); +} + + +//---------------------------------------------------------------------------------------------------------- +NextBotGroundLocomotion::~NextBotGroundLocomotion() +{ +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Reset locomotor to initial state + */ +void NextBotGroundLocomotion::Reset( void ) +{ + BaseClass::Reset(); + m_bRecomputePostureOnCollision = false; + m_ignorePhysicsPropTimer.Invalidate(); + + m_nextBot = static_cast< NextBotCombatCharacter * >( GetBot()->GetEntity() ); + + m_desiredSpeed = 0.0f; + m_velocity = vec3_origin; + m_acceleration = vec3_origin; + + m_desiredLean.x = 0.0f; + m_desiredLean.y = 0.0f; + m_desiredLean.z = 0.0f; + + m_ladder = NULL; + + m_isJumping = false; + m_isJumpingAcrossGap = false; + m_ground = NULL; + m_groundNormal = Vector( 0, 0, 1.0f ); + m_isClimbingUpToLedge = false; + m_isUsingFullFeetTrace = false; + + m_moveVector = Vector( 1, 0, 0 ); + + m_priorPos = m_nextBot->GetPosition(); + m_lastValidPos = m_nextBot->GetPosition(); + + m_inhibitObstacleAvoidanceTimer.Invalidate(); + + m_accumApproachVectors = vec3_origin; + m_accumApproachWeights = 0.0f; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Move the bot along a ladder + */ +bool NextBotGroundLocomotion::TraverseLadder( void ) +{ + // not climbing a ladder right now + return false; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Update internal state + */ +void NextBotGroundLocomotion::Update( void ) +{ + VPROF_BUDGET( "NextBotGroundLocomotion::Update", "NextBot" ); + + BaseClass::Update(); + + const float deltaT = GetUpdateInterval(); + + // apply accumulated position changes + ApplyAccumulatedApproach(); + + // need to do this first thing, because ground constraints, etc, can change it + Vector origPos = GetFeet(); + + IBody *body = GetBot()->GetBodyInterface(); + + if ( TraverseLadder() ) + { + // bot is climbing a ladder + return; + } + + if ( !body->IsPostureMobile() ) + { + // sitting/lying on the ground - no slip + m_acceleration.x = 0.0f; + m_acceleration.y = 0.0f; + m_velocity.x = 0.0f; + m_velocity.y = 0.0f; + } + + bool wasOnGround = IsOnGround(); + + if ( !body->HasActivityType( IBody::MOTION_CONTROLLED_Z ) ) + { + // fall if in the air + if ( !IsOnGround() ) + { + // no ground below us - fall + m_acceleration.z -= GetGravity(); + } + + if ( !IsClimbingOrJumping() || m_velocity.z <= 0.0f ) + { + // keep us on the ground + UpdateGroundConstraint(); + } + } + + Vector newPos = GetFeet(); + + // + // Update position physics + // + Vector right( m_moveVector.y, -m_moveVector.x, 0.0f ); + + if ( IsOnGround() ) // || m_isClimbingUpToLedge ) + { + if ( IsAttemptingToMove() ) + { + float forwardSpeed = DotProduct( m_velocity, m_moveVector ); + Vector forwardVelocity = forwardSpeed * m_moveVector; + Vector sideVelocity = DotProduct( m_velocity, right ) * right; + + Vector frictionAccel = vec3_origin; + + // only apply friction along forward direction if we are sliding backwards + if ( forwardSpeed < 0.0f ) + { + frictionAccel = -GetFrictionForward() * forwardVelocity; + } + + // always apply lateral friction to counteract sideslip + frictionAccel += -GetFrictionSideways() * sideVelocity; + + m_acceleration.x += frictionAccel.x; + m_acceleration.y += frictionAccel.y; + } + else + { + // come to a stop if we haven't been told to move + m_acceleration = vec3_origin; + m_velocity = vec3_origin; + } + } + + // compute new position, taking into account MOTION_CONTROLLED animations in progress + if ( body->HasActivityType( IBody::MOTION_CONTROLLED_XY ) ) + { + m_acceleration.x = 0.0f; + m_acceleration.y = 0.0f; + m_velocity.x = GetBot()->GetEntity()->GetAbsVelocity().x; + m_velocity.y = GetBot()->GetEntity()->GetAbsVelocity().y; + } + else + { + // euler integration + m_velocity.x += m_acceleration.x * deltaT; + m_velocity.y += m_acceleration.y * deltaT; + + // euler integration + newPos.x += m_velocity.x * deltaT; + newPos.y += m_velocity.y * deltaT; + } + + if ( body->HasActivityType( IBody::MOTION_CONTROLLED_Z ) ) + { + m_acceleration.z = 0.0f; + m_velocity.z = GetBot()->GetEntity()->GetAbsVelocity().z; + } + else + { + // euler integration + m_velocity.z += m_acceleration.z * deltaT; + + // euler integration + newPos.z += m_velocity.z * deltaT; + } + + // move bot to new position, resolving collisions along the way + UpdatePosition( newPos ); + + + // set actual velocity based on position change after collision resolution step + Vector adjustedVelocity = ( GetFeet() - origPos ) / deltaT; + + if ( !body->HasActivityType( IBody::MOTION_CONTROLLED_XY ) ) + { + m_velocity.x = adjustedVelocity.x; + m_velocity.y = adjustedVelocity.y; + } + + if ( !body->HasActivityType( IBody::MOTION_CONTROLLED_Z ) ) + { + m_velocity.z = adjustedVelocity.z; + } + + + // collision resolution may create very high instantaneous velocities, limit it + Vector2D groundVel = m_velocity.AsVector2D(); + m_actualSpeed = groundVel.NormalizeInPlace(); + + if ( IsOnGround() ) + { + if ( m_actualSpeed > GetRunSpeed() ) + { + m_actualSpeed = GetRunSpeed(); + m_velocity.x = m_actualSpeed * groundVel.x; + m_velocity.y = m_actualSpeed * groundVel.y; + } + + // remove downward velocity when landing on the ground + if ( !wasOnGround ) + { + m_velocity.z = 0.0f; + m_acceleration.z = 0.0f; + } + } + else + { + // we're falling. if our velocity has become zero for any reason, shove it forward + const float epsilon = 1.0f; + if ( m_velocity.IsLengthLessThan( epsilon ) ) + { + m_velocity = GetRunSpeed() * GetGroundMotionVector(); + } + } + + // update entity velocity to that of locomotor + m_nextBot->SetAbsVelocity( m_velocity ); + + +#ifdef LEANING + // lean sideways proportional to lateral acceleration + QAngle lean = GetDesiredLean(); + + float sideAccel = DotProduct( right, m_acceleration ); + float slide = sideAccel / GetMaxAcceleration(); + + // max lean depends on how fast we're actually moving + float maxLeanAngle = NextBotLeanMaxAngle.GetFloat() * m_actualSpeed / GetRunSpeed(); + + // actual lean angle is proportional to lateral acceleration (sliding) + float desiredSideLean = -maxLeanAngle * slide; + + lean.y += ( desiredSideLean - lean.y ) * NextBotLeanRate.GetFloat() * deltaT; + + SetDesiredLean( lean ); +#endif // _DEBUG + + + // reset acceleration accumulation + m_acceleration = vec3_origin; + + // debug display + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + // track position over time + if ( IsOnGround() ) + { + NDebugOverlay::Cross3D( GetFeet(), 1.0f, 0, 255, 0, true, 15.0f ); + } + else + { + NDebugOverlay::Cross3D( GetFeet(), 1.0f, 0, 255, 255, true, 15.0f ); + } + } +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Move directly towards given position. + * We need to do this in-air as well to land jumps. + */ +void NextBotGroundLocomotion::Approach( const Vector &rawPos, float goalWeight ) +{ + BaseClass::Approach( rawPos ); + + m_accumApproachVectors += ( rawPos - GetFeet() ) * goalWeight; + m_accumApproachWeights += goalWeight; + m_bRecomputePostureOnCollision = true; +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotGroundLocomotion::ApplyAccumulatedApproach( void ) +{ + VPROF_BUDGET( "NextBotGroundLocomotion::ApplyAccumulatedApproach", "NextBot" ); + + Vector rawPos = GetFeet(); + + const float deltaT = GetUpdateInterval(); + + if ( deltaT <= 0.0f ) + return; + + if ( m_accumApproachWeights > 0.0f ) + { + Vector approachDelta = m_accumApproachVectors / m_accumApproachWeights; + + // limit total movement to our max speed + float maxMove = GetRunSpeed() * deltaT; + + float desiredMove = approachDelta.NormalizeInPlace(); + if ( desiredMove > maxMove ) + { + desiredMove = maxMove; + } + + rawPos += desiredMove * approachDelta; + + m_accumApproachVectors = vec3_origin; + m_accumApproachWeights = 0.0f; + } + + // can only move in 2D - geometry moves us up and down + Vector pos( rawPos.x, rawPos.y, GetFeet().z ); + + if ( !GetBot()->GetBodyInterface()->IsPostureMobile() ) + { + // body is not in a movable state right now + return; + } + + Vector currentPos = m_nextBot->GetPosition(); + + // compute unit vector to goal position + m_moveVector = pos - currentPos; + m_moveVector.z = 0.0f; + float change = m_moveVector.NormalizeInPlace(); + + const float epsilon = 0.001f; + if ( change < epsilon ) + { + // no motion + m_forwardLean = 0.0f; + m_sideLean = 0.0f; + return; + } + +/* + // lean forward/backward based on acceleration + float desiredLean = m_acceleration / NextBotLeanForwardAccel.GetFloat(); + + QAngle lean = GetDesiredLean(); + + lean.x = NextBotLeanMaxAngle.GetFloat() * clamp( desiredLean, -1.0f, 1.0f ); + + SetDesiredLean( lean ); +*/ + + Vector newPos; + + // if we just started a jump, don't snap to the ground - let us get in the air first + if ( DidJustJump() || !IsOnGround() ) + { + if ( false && m_isClimbingUpToLedge ) // causes bots to hang in air stuck against edges + { + // drive towards the approach position in XY to help reach ledge + m_moveVector = m_ledgeJumpGoalPos - currentPos; + m_moveVector.z = 0.0f; + m_moveVector.NormalizeInPlace(); + + m_acceleration += GetMaxAcceleration() * m_moveVector; + } + } + else if ( IsOnGround() ) + { + // on the ground - move towards the approach position + m_isClimbingUpToLedge = false; + + // snap forward movement vector along floor + const Vector &groundNormal = GetGroundNormal(); + + Vector left( -m_moveVector.y, m_moveVector.x, 0.0f ); + m_moveVector = CrossProduct( left, groundNormal ); + m_moveVector.NormalizeInPlace(); + + // limit maximum forward speed from self-acceleration + float forwardSpeed = DotProduct( m_velocity, m_moveVector ); + + float maxSpeed = MIN( m_desiredSpeed, GetSpeedLimit() ); + + if ( forwardSpeed < maxSpeed ) + { + float ratio = ( forwardSpeed <= 0.0f ) ? 0.0f : ( forwardSpeed / maxSpeed ); + float governor = 1.0f - ( ratio * ratio * ratio * ratio ); + + // accelerate towards goal + m_acceleration += governor * GetMaxAcceleration() * m_moveVector; + } + } +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Move the bot to the precise given position immediately, + */ +void NextBotGroundLocomotion::DriveTo( const Vector &pos ) +{ + BaseClass::DriveTo( pos ); + m_bRecomputePostureOnCollision = true; + UpdatePosition( pos ); +} + + +//-------------------------------------------------------------------------------------------- +/* + * Trace filter solely for use with DetectCollision() below. + */ +class GroundLocomotionCollisionTraceFilter : public CTraceFilterSimple +{ +public: + GroundLocomotionCollisionTraceFilter( INextBot *me, const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup ) + { + m_me = me; + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) ) + { + CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); + + // don't collide with ourself + if ( entity && m_me->IsSelf( entity ) ) + return false; + + return m_me->GetLocomotionInterface()->ShouldCollideWith( entity ); + } + + return false; + } + + INextBot *m_me; +}; + + +//---------------------------------------------------------------------------------------------------------- +/** + * Check for collisions during move and attempt to resolve them + */ +bool NextBotGroundLocomotion::DetectCollision( trace_t *pTrace, int &recursionLimit, const Vector &from, const Vector &to, const Vector &vecMins, const Vector &vecMaxs ) +{ + IBody *body = GetBot()->GetBodyInterface(); + + CBaseEntity *ignore = m_ignorePhysicsPropTimer.IsElapsed() ? NULL : m_ignorePhysicsProp; + GroundLocomotionCollisionTraceFilter filter( GetBot(), ignore, body->GetCollisionGroup() ); + + TraceHull( from, to, vecMins, vecMaxs, body->GetSolidMask(), &filter, pTrace ); + + if ( !pTrace->DidHit() ) + return false; + + // + // A collision occurred - resolve it + // + + // bust through "flimsy" breakables and keep on going + if ( pTrace->DidHitNonWorldEntity() && pTrace->m_pEnt != NULL ) + { + CBaseEntity *other = pTrace->m_pEnt; + + if ( !other->MyCombatCharacterPointer() && IsEntityTraversable( other, IMMEDIATELY ) /*&& IsFlimsy( other )*/ ) + { + if ( recursionLimit <= 0 ) + return true; + + --recursionLimit; + + // break the weak breakable we collided with + CTakeDamageInfo damageInfo( GetBot()->GetEntity(), GetBot()->GetEntity(), 100.0f, DMG_CRUSH ); + CalculateExplosiveDamageForce( &damageInfo, GetMotionVector(), pTrace->endpos ); + other->TakeDamage( damageInfo ); + + // retry trace now that the breakable is out of the way + return DetectCollision( pTrace, recursionLimit, from, to, vecMins, vecMaxs ); + } + } + + /// @todo Only invoke OnContact() and Touch() once per collision pair + // inform other components of collision + if ( GetBot()->ShouldTouch( pTrace->m_pEnt ) ) + { + GetBot()->OnContact( pTrace->m_pEnt, pTrace ); + } + + INextBot *them = dynamic_cast< INextBot * >( pTrace->m_pEnt ); + if ( them && them->ShouldTouch( m_nextBot ) ) + { + /// @todo construct mirror of trace + them->OnContact( m_nextBot ); + } + else + { + pTrace->m_pEnt->Touch( GetBot()->GetEntity() ); + } + + return true; +} + + +//---------------------------------------------------------------------------------------------------------- +Vector NextBotGroundLocomotion::ResolveCollision( const Vector &from, const Vector &to, int recursionLimit ) +{ + VPROF_BUDGET( "NextBotGroundLocomotion::ResolveCollision", "NextBotExpensive" ); + + IBody *body = GetBot()->GetBodyInterface(); + if ( body == NULL || recursionLimit < 0 ) + { + Assert( !m_bRecomputePostureOnCollision ); + return to; + } + + // Only bother to recompute posture if we're currently standing or crouching + if ( m_bRecomputePostureOnCollision ) + { + if ( !body->IsActualPosture( IBody::STAND ) && !body->IsActualPosture( IBody::CROUCH ) ) + { + m_bRecomputePostureOnCollision = false; + } + } + + // get bounding limits, ignoring step-upable height + bool bPerformCrouchTest = false; + Vector mins; + Vector maxs; + if ( m_isUsingFullFeetTrace ) + { + mins = body->GetHullMins(); + } + else + { + mins = body->GetHullMins() + Vector( 0, 0, GetStepHeight() ); + } + if ( !m_bRecomputePostureOnCollision ) + { + maxs = body->GetHullMaxs(); + if ( mins.z >= maxs.z ) + { + // if mins.z is greater than maxs.z, the engine will Assert + // in UTIL_TraceHull, and it won't work as advertised. + mins.z = maxs.z - 2.0f; + } + } + else + { + const float halfSize = body->GetHullWidth() / 2.0f; + maxs.Init( halfSize, halfSize, body->GetStandHullHeight() ); + bPerformCrouchTest = true; + } + + trace_t trace; + Vector desiredGoal = to; + Vector resolvedGoal; + IBody::PostureType nPosture = IBody::STAND; + while( true ) + { + bool bCollided = DetectCollision( &trace, recursionLimit, from, desiredGoal, mins, maxs ); + if ( !bCollided ) + { + resolvedGoal = desiredGoal; + break; + } + + // If we hit really close to our target, then stop + if ( !trace.startsolid && desiredGoal.DistToSqr( trace.endpos ) < 1.0f ) + { + resolvedGoal = trace.endpos; + break; + } + + // Check for crouch test, if it's necessary + // Don't bother about checking for crouch if we hit an actor + // Also don't bother checking for crouch if we hit a plane that pushes us upwards + if ( bPerformCrouchTest ) + { + // Don't do this work twice + bPerformCrouchTest = false; + + nPosture = body->GetDesiredPosture(); + + if ( !trace.m_pEnt->MyNextBotPointer() && !trace.m_pEnt->IsPlayer() ) + { + // Here, our standing trace hit the world or something non-breakable + // If we're not currently crouching, then see if we could travel + // the entire distance if we were crouched + if ( nPosture != IBody::CROUCH ) + { + trace_t crouchTrace; + NextBotTraversableTraceFilter crouchFilter( GetBot(), ILocomotion::IMMEDIATELY ); + Vector vecCrouchMax( maxs.x, maxs.y, body->GetCrouchHullHeight() ); + TraceHull( from, desiredGoal, mins, vecCrouchMax, body->GetSolidMask(), &crouchFilter, &crouchTrace ); + if ( crouchTrace.fraction >= 1.0f && !crouchTrace.startsolid ) + { + nPosture = IBody::CROUCH; + } + } + } + else if ( nPosture == IBody::CROUCH ) + { + // Here, our standing trace hit an actor + + // NOTE: This test occurs almost never, based on my tests + // Converts from crouch to stand in the case where the player + // is currently crouching, *and* his first trace (with the standing hull) + // hits an actor *and* if he didn't hit that actor, he could have + // moved standing the entire way to his desired endpoint + trace_t standTrace; + NextBotTraversableTraceFilter standFilter( GetBot(), ILocomotion::IMMEDIATELY ); + TraceHull( from, desiredGoal, mins, maxs, body->GetSolidMask(), &standFilter, &standTrace ); + if ( standTrace.fraction >= 1.0f && !standTrace.startsolid ) + { + nPosture = IBody::STAND; + } + } + + // Our first trace was based on the standing hull. + // If we need be crouched, the trace was bogus; we need to do another + if ( nPosture == IBody::CROUCH ) + { + maxs.z = body->GetCrouchHullHeight(); + continue; + } + } + + if ( trace.startsolid ) + { + // stuck inside solid; don't move + + if ( trace.m_pEnt && !trace.m_pEnt->IsWorld() ) + { + // only ignore physics props that are not doors + if ( dynamic_cast< CPhysicsProp * >( trace.m_pEnt ) != NULL && dynamic_cast< CBasePropDoor * >( trace.m_pEnt ) == NULL ) + { + IPhysicsObject *physics = trace.m_pEnt->VPhysicsGetObject(); + if ( physics && physics->IsMoveable() ) + { + // we've intersected a (likely moving) physics prop - ignore it for awhile so we can move out of it + m_ignorePhysicsProp = trace.m_pEnt; + m_ignorePhysicsPropTimer.Start( 1.0f ); + } + } + } + + // return to last known non-interpenetrating position + resolvedGoal = m_lastValidPos; + + break; + } + + if ( --recursionLimit <= 0 ) + { + // reached recursion limit, no more adjusting allowed + resolvedGoal = trace.endpos; + break; + } + + // never slide downwards/concave to avoid getting stuck in the ground + if ( trace.plane.normal.z < 0.0f ) + { + trace.plane.normal.z = 0.0f; + trace.plane.normal.NormalizeInPlace(); + } + + // slide off of surface we hit + Vector fullMove = desiredGoal - from; + Vector leftToMove = fullMove * ( 1.0f - trace.fraction ); + + // obey climbing slope limit + if ( !body->HasActivityType( IBody::MOTION_CONTROLLED_Z ) && + trace.plane.normal.z < GetTraversableSlopeLimit() && + fullMove.z > 0.0f ) + { + fullMove.z = 0.0f; + trace.plane.normal.z = 0.0f; + trace.plane.normal.NormalizeInPlace(); + } + + float blocked = DotProduct( trace.plane.normal, leftToMove ); + + Vector unconstrained = fullMove - blocked * trace.plane.normal; + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::Line( trace.endpos, + trace.endpos + 20.0f * trace.plane.normal, + 255, 0, 150, true, 15.0f ); + } + + // check for collisions along remainder of move + // But don't bother if we're not going to deflect much + Vector remainingMove = from + unconstrained; + if ( remainingMove.DistToSqr( trace.endpos ) < 1.0f ) + { + resolvedGoal = trace.endpos; + break; + } + + desiredGoal = remainingMove; + } + + if ( !trace.startsolid ) + { + m_lastValidPos = resolvedGoal; + } + + if ( m_bRecomputePostureOnCollision ) + { + m_bRecomputePostureOnCollision = false; + + if ( !body->IsActualPosture( nPosture ) ) + { + body->SetDesiredPosture( nPosture ); + } + } + + return resolvedGoal; +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Collect the closest actors + */ +class ClosestActorsScan +{ +public: + ClosestActorsScan( const Vector &spot, int team, float maxRange = 0.0f, CBaseCombatCharacter *ignore = NULL ) + { + m_spot = spot; + m_team = team; + m_close = NULL; + + if ( maxRange > 0.0f ) + { + m_closeRangeSq = maxRange * maxRange; + } + else + { + m_closeRangeSq = 999999999.9f; + } + + m_ignore = ignore; + } + + bool operator() ( CBaseCombatCharacter *actor ) + { + if (actor == m_ignore) + return true; + + if (actor->IsAlive() && (m_team == TEAM_ANY || actor->GetTeamNumber() == m_team)) + { + Vector to = actor->WorldSpaceCenter() - m_spot; + float rangeSq = to.LengthSqr(); + if (rangeSq < m_closeRangeSq) + { + m_closeRangeSq = rangeSq; + m_close = actor; + } + } + return true; + } + + CBaseCombatCharacter *GetActor( void ) const + { + return m_close; + } + + bool IsCloserThan( float range ) + { + return (m_closeRangeSq < (range * range)); + } + + bool IsFartherThan( float range ) + { + return (m_closeRangeSq > (range * range)); + } + + Vector m_spot; + int m_team; + CBaseCombatCharacter *m_close; + float m_closeRangeSq; + CBaseCombatCharacter *m_ignore; +}; + + +#ifdef SKIPME +//---------------------------------------------------------------------------------------------------------- +/** + * Push away zombies that are interpenetrating + */ +Vector NextBotGroundLocomotion::ResolveZombieCollisions( const Vector &pos ) +{ + Vector adjustedNewPos = pos; + + Infected *me = m_nextBot->MyInfectedPointer(); + const float hullWidth = me->GetBodyInterface()->GetHullWidth(); + + // only avoid if we're actually trying to move somewhere, and are enraged + if ( me != NULL && !IsUsingLadder() && !IsClimbingOrJumping() && IsOnGround() && m_nextBot->IsAlive() && IsAttemptingToMove() /*&& GetBot()->GetBodyInterface()->IsArousal( IBody::INTENSE )*/ ) + { + VPROF_BUDGET( "NextBotGroundLocomotion::ResolveZombieCollisions", "NextBot" ); + + const CUtlVector< CHandle< Infected > > &neighbors = me->GetNeighbors(); + Vector avoid = vec3_origin; + float avoidWeight = 0.0f; + + FOR_EACH_VEC( neighbors, it ) + { + Infected *them = neighbors[ it ]; + + if ( them ) + { + Vector toThem = them->GetAbsOrigin() - me->GetAbsOrigin(); + toThem.z = 0.0f; + + float range = toThem.NormalizeInPlace(); + + if ( range < hullWidth ) + { + // these two infected are in contact + me->Touch( them ); + + // move out of contact + float penetration = hullWidth - range; + + float weight = 1.0f + ( 2.0f * penetration/hullWidth ); + avoid += -weight * toThem; + avoidWeight += weight; + } + } + } + + if ( avoidWeight > 0.0f ) + { + adjustedNewPos += 3.0f * ( avoid / avoidWeight ); + } + } + + return adjustedNewPos; +} +#endif // _DEBUG + + +//---------------------------------------------------------------------------------------------------------- +/** + * Move to newPos, resolving any collisions along the way + */ +void NextBotGroundLocomotion::UpdatePosition( const Vector &newPos ) +{ + VPROF_BUDGET( "NextBotGroundLocomotion::UpdatePosition", "NextBot" ); + + if ( NextBotStop.GetBool() || (m_nextBot->GetFlags() & FL_FROZEN) != 0 || newPos == m_nextBot->GetPosition() ) + { + return; + } + + // avoid very nearby Actors to simulate "mushy" collisions between actors in contact with each other + //Vector adjustedNewPos = ResolveZombieCollisions( newPos ); + Vector adjustedNewPos = newPos; + + // check for collisions during move and resolve them + const int recursionLimit = 3; + Vector safePos = ResolveCollision( m_nextBot->GetPosition(), adjustedNewPos, recursionLimit ); + + // set the bot's position + if ( GetBot()->GetIntentionInterface()->IsPositionAllowed( GetBot(), safePos ) != ANSWER_NO ) + { + m_nextBot->SetPosition( safePos ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Prevent bot from sliding through floor, and snap to the ground if we're very near it + */ +void NextBotGroundLocomotion::UpdateGroundConstraint( void ) +{ + VPROF_BUDGET( "NextBotGroundLocomotion::UpdateGroundConstraint", "NextBotExpensive" ); + + // if we're up on the upward arc of our jump, don't interfere by snapping to ground + // don't do ground constraint if we're climbing a ladder + if ( DidJustJump() || IsAscendingOrDescendingLadder() ) + { + m_isUsingFullFeetTrace = false; + return; + } + + IBody *body = GetBot()->GetBodyInterface(); + if ( body == NULL ) + { + return; + } + + float halfWidth = body->GetHullWidth()/2.0f; + + // since we only care about ground collisions, keep hull short to avoid issues with low ceilings + /// @TODO: We need to also check actual hull height to avoid interpenetrating the world + float hullHeight = GetStepHeight(); + + // always need tolerance even when jumping/falling to make sure we detect ground penetration + // must be at least step height to avoid 'falling' down stairs + const float stickToGroundTolerance = GetStepHeight() + 0.01f; + + trace_t ground; + NextBotTraceFilterIgnoreActors filter( m_nextBot, body->GetCollisionGroup() ); + + TraceHull( m_nextBot->GetPosition() + Vector( 0, 0, GetStepHeight() + 0.001f ), + m_nextBot->GetPosition() + Vector( 0, 0, -stickToGroundTolerance ), + Vector( -halfWidth, -halfWidth, 0 ), + Vector( halfWidth, halfWidth, hullHeight ), + body->GetSolidMask(), &filter, &ground ); + + if ( ground.startsolid ) + { + // we're inside the ground - bad news + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) && !( gpGlobals->framecount % 60 ) ) + { + DevMsg( "%3.2f: Inside ground, ( %.0f, %.0f, %.0f )\n", gpGlobals->curtime, m_nextBot->GetPosition().x, m_nextBot->GetPosition().y, m_nextBot->GetPosition().z ); + } + return; + } + + if ( ground.fraction < 1.0f ) + { + // there is ground below us + m_groundNormal = ground.plane.normal; + + m_isUsingFullFeetTrace = false; + + // zero velocity normal to the ground + float normalVel = DotProduct( m_groundNormal, m_velocity ); + m_velocity -= normalVel * m_groundNormal; + + // check slope limit + if ( ground.plane.normal.z < GetTraversableSlopeLimit() ) + { + // too steep to stand here + + // too steep to be ground - treat it like a wall hit + if ( ( m_velocity.x * ground.plane.normal.x + m_velocity.y * ground.plane.normal.y ) <= 0.0f ) + { + GetBot()->OnContact( ground.m_pEnt, &ground ); + } + + // we're contacting some kind of ground + // zero accelerations normal to the ground + + float normalAccel = DotProduct( m_groundNormal, m_acceleration ); + m_acceleration -= normalAccel * m_groundNormal; + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + DevMsg( "%3.2f: NextBotGroundLocomotion - Too steep to stand here\n", gpGlobals->curtime ); + NDebugOverlay::Line( GetFeet(), GetFeet() + 20.0f * ground.plane.normal, 255, 150, 0, true, 5.0f ); + } + + // clear out upward velocity so we don't walk up lightpoles + m_velocity.z = MIN( 0, m_velocity.z ); + m_acceleration.z = MIN( 0, m_acceleration.z ); + + return; + } + + // inform other components of collision if we didn't land on the 'world' + if ( ground.m_pEnt && !ground.m_pEnt->IsWorld() ) + { + GetBot()->OnContact( ground.m_pEnt, &ground ); + } + + // snap us to the ground + m_nextBot->SetPosition( ground.endpos ); + + if ( !IsOnGround() ) + { + // just landed + m_nextBot->SetGroundEntity( ground.m_pEnt ); + m_ground = ground.m_pEnt; + + // landing stops any jump in progress + m_isJumping = false; + m_isJumpingAcrossGap = false; + + GetBot()->OnLandOnGround( ground.m_pEnt ); + } + } + else + { + // not on the ground + if ( IsOnGround() ) + { + GetBot()->OnLeaveGround( m_nextBot->GetGroundEntity() ); + if ( !IsClimbingUpToLedge() && !IsJumpingAcrossGap() ) + { + m_isUsingFullFeetTrace = true; // We're in the air and there's space below us, so use the full trace + m_acceleration.z -= GetGravity(); // start our gravity now + } + } + } +} + + +//---------------------------------------------------------------------------------------------------------- +/* +void NextBotGroundLocomotion::StandUp( void ) +{ + // make sure there is room to stand + trace_t result; + const float halfSize = GetHullWidth()/3.0f; + Vector standHullMin( -halfSize, -halfSize, GetStepHeight() + 0.1f ); + Vector standHullMax( halfSize, halfSize, GetStandHullHeight() ); + + TraceHull( GetFeet(), GetFeet(), standHullMin, standHullMax, MASK_NPCSOLID, m_nextBot, MASK_DEFAULTPLAYERSOLID, &result ); + + if ( result.fraction >= 1.0f && !result.startsolid ) + { + m_isCrouching = false; + } +} +*/ + + +//---------------------------------------------------------------------------------------------------------- +/** + * Initiate a climb to an adjacent high ledge + */ +bool NextBotGroundLocomotion::ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle ) +{ + return false; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Initiate a jump across an empty volume of space to far side + */ +void NextBotGroundLocomotion::JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward ) +{ + // can only jump if we're on the ground + if ( !IsOnGround() ) + { + return; + } + + IBody *body = GetBot()->GetBodyInterface(); + if ( !body->StartActivity( ACT_JUMP ) ) + { + // body can't jump right now + return; + } + + + // scale impulse to land on target + Vector toGoal = landingGoal - GetFeet(); + + // equation doesn't work if we're jumping upwards + float height = toGoal.z; + toGoal.z = 0.0f; + + float range = toGoal.NormalizeInPlace(); + + // jump out at 45 degree angle + const float cos45 = 0.7071f; + + // avoid division by zero + if ( height > 0.9f * range ) + { + height = 0.9f * range; + } + + // ballistic equation to find initial velocity assuming 45 degree inclination and landing at give range and height + float launchVel = ( range / cos45 ) / sqrt( ( 2.0f * ( range - height ) ) / GetGravity() ); + + Vector up( 0, 0, 1 ); + Vector ahead = up + toGoal; + ahead.NormalizeInPlace(); + + //m_velocity = cos45 * launchVel * ahead; + m_velocity = launchVel * ahead; + m_acceleration = vec3_origin; + + m_isJumping = true; + m_isJumpingAcrossGap = true; + m_isClimbingUpToLedge = false; + + GetBot()->OnLeaveGround( m_nextBot->GetGroundEntity() ); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Initiate a simple undirected jump in the air + */ +void NextBotGroundLocomotion::Jump( void ) +{ + // can only jump if we're on the ground + if ( !IsOnGround() ) + { + return; + } + + IBody *body = GetBot()->GetBodyInterface(); + if ( !body->StartActivity( ACT_JUMP ) ) + { + // body can't jump right now + return; + } + + // jump straight up + m_velocity.z = sqrt( 2.0f * GetGravity() * GetMaxJumpHeight() ); + + m_isJumping = true; + m_isClimbingUpToLedge = false; + + GetBot()->OnLeaveGround( m_nextBot->GetGroundEntity() ); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Set movement speed to running + */ +void NextBotGroundLocomotion::Run( void ) +{ + m_desiredSpeed = GetRunSpeed(); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Set movement speed to walking + */ +void NextBotGroundLocomotion::Walk( void ) +{ + m_desiredSpeed = GetWalkSpeed(); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Set movement speed to stopeed + */ +void NextBotGroundLocomotion::Stop( void ) +{ + m_desiredSpeed = 0.0f; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Return true if standing on something + */ +bool NextBotGroundLocomotion::IsOnGround( void ) const +{ + return (m_nextBot->GetGroundEntity() != NULL); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Invoked when bot leaves ground for any reason + */ +void NextBotGroundLocomotion::OnLeaveGround( CBaseEntity *ground ) +{ + m_nextBot->SetGroundEntity( NULL ); + m_ground = NULL; + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + DevMsg( "%3.2f: NextBotGroundLocomotion::OnLeaveGround\n", gpGlobals->curtime ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Invoked when bot lands on the ground after being in the air + */ +void NextBotGroundLocomotion::OnLandOnGround( CBaseEntity *ground ) +{ + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + DevMsg( "%3.2f: NextBotGroundLocomotion::GetBot()->OnLandOnGround\n", gpGlobals->curtime ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Get maximum speed bot can reach, regardless of desired speed + */ +float NextBotGroundLocomotion::GetSpeedLimit( void ) const +{ + // if we're crouched, move at reduced speed + if ( !GetBot()->GetBodyInterface()->IsActualPosture( IBody::STAND ) ) + { + return 0.75f * GetRunSpeed(); + } + + // no limit + return 99999999.9f; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Climb the given ladder to the top and dismount + */ +void NextBotGroundLocomotion::ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ) +{ + // if we're already climbing this ladder, don't restart + if ( m_ladder == ladder && m_isGoingUpLadder ) + { + return; + } + + m_ladder = ladder; + m_ladderDismountGoal = dismountGoal; + m_isGoingUpLadder = true; + + IBody *body = GetBot()->GetBodyInterface(); + if ( body ) + { + // line them up to climb in XY + Vector mountSpot = m_ladder->m_bottom + m_ladder->GetNormal() * (0.75f * body->GetHullWidth()); + mountSpot.z = GetBot()->GetPosition().z; + + UpdatePosition( mountSpot ); + + body->StartActivity( ACT_CLIMB_UP, IBody::MOTION_CONTROLLED_Z ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Descend the given ladder to the bottom and dismount + */ +void NextBotGroundLocomotion::DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ) +{ + // if we're already descending this ladder, don't restart + if ( m_ladder == ladder && !m_isGoingUpLadder ) + { + return; + } + + m_ladder = ladder; + m_ladderDismountGoal = dismountGoal; + m_isGoingUpLadder = false; + + IBody *body = GetBot()->GetBodyInterface(); + if ( body ) + { + // line them up to climb in XY + Vector mountSpot = m_ladder->m_top + m_ladder->GetNormal() * (0.75f * body->GetHullWidth()); + mountSpot.z = GetBot()->GetPosition().z; + + UpdatePosition( mountSpot ); + + float ladderYaw = UTIL_VecToYaw( -m_ladder->GetNormal() ); + + QAngle angles = m_nextBot->GetLocalAngles(); + angles.y = ladderYaw; + + m_nextBot->SetLocalAngles( angles ); + + body->StartActivity( ACT_CLIMB_DOWN, IBody::MOTION_CONTROLLED_Z ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +bool NextBotGroundLocomotion::IsUsingLadder( void ) const +{ + return ( m_ladder != NULL ); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * We are actually on the ladder right now, either climbing up or down + */ +bool NextBotGroundLocomotion::IsAscendingOrDescendingLadder( void ) const +{ + return IsUsingLadder(); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Return position of "feet" - point below centroid of bot at feet level + */ +const Vector &NextBotGroundLocomotion::GetFeet( void ) const +{ + return m_nextBot->GetPosition(); +} + + +//---------------------------------------------------------------------------------------------------------- +const Vector & NextBotGroundLocomotion::GetAcceleration( void ) const +{ + return m_acceleration; +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotGroundLocomotion::SetAcceleration( const Vector &accel ) +{ + m_acceleration = accel; +} + + +//---------------------------------------------------------------------------------------------------------- +void NextBotGroundLocomotion::SetVelocity( const Vector &vel ) +{ + m_velocity = vel; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Return current world space velocity + */ +const Vector &NextBotGroundLocomotion::GetVelocity( void ) const +{ + return m_velocity; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Invoked when an bot reaches its MoveTo goal + */ +void NextBotGroundLocomotion::OnMoveToSuccess( const Path *path ) +{ + // stop + m_velocity = vec3_origin; + m_acceleration = vec3_origin; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Invoked when an bot fails to reach a MoveTo goal + */ +void NextBotGroundLocomotion::OnMoveToFailure( const Path *path, MoveToFailureType reason ) +{ + // stop + m_velocity = vec3_origin; + m_acceleration = vec3_origin; +} + + +//---------------------------------------------------------------------------------------------------------- +bool NextBotGroundLocomotion::DidJustJump( void ) const +{ + return IsClimbingOrJumping() && (m_nextBot->GetAbsVelocity().z > 0.0f); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Rotate body to face towards "target" + */ +void NextBotGroundLocomotion::FaceTowards( const Vector &target ) +{ + const float deltaT = GetUpdateInterval(); + + QAngle angles = m_nextBot->GetLocalAngles(); + + float desiredYaw = UTIL_VecToYaw( target - GetFeet() ); + + float angleDiff = UTIL_AngleDiff( desiredYaw, angles.y ); + + float deltaYaw = GetMaxYawRate() * deltaT; + + if (angleDiff < -deltaYaw) + { + angles.y -= deltaYaw; + } + else if (angleDiff > deltaYaw) + { + angles.y += deltaYaw; + } + else + { + angles.y += angleDiff; + } + + m_nextBot->SetLocalAngles( angles ); +} + + + diff --git a/game/server/NextBot/NextBotGroundLocomotion.h b/game/server/NextBot/NextBotGroundLocomotion.h new file mode 100644 index 0000000..ecd221a --- /dev/null +++ b/game/server/NextBot/NextBotGroundLocomotion.h @@ -0,0 +1,274 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// NextBotGroundLocomotion.h +// Basic ground-based movement for NextBotCombatCharacters +// Author: Michael Booth, February 2009 +// Note: This is a refactoring of ZombieBotLocomotion from L4D + +#ifndef NEXT_BOT_GROUND_LOCOMOTION_H +#define NEXT_BOT_GROUND_LOCOMOTION_H + +#include "NextBotLocomotionInterface.h" +#include "nav_mesh.h" + + +class NextBotCombatCharacter; + +//---------------------------------------------------------------------------------------------------------------- +/** + * Basic ground-based movement for NextBotCombatCharacters. + * This locomotor resolves collisions and assumes a ground-based bot under the influence of gravity. + */ +class NextBotGroundLocomotion : public ILocomotion +{ +public: + DECLARE_CLASS( NextBotGroundLocomotion, ILocomotion ); + + NextBotGroundLocomotion( INextBot *bot ); + virtual ~NextBotGroundLocomotion(); + + virtual void Reset( void ); // reset locomotor to initial state + virtual void Update( void ); // update internal state + + virtual void Approach( const Vector &pos, float goalWeight = 1.0f ); // move directly towards the given position + virtual void DriveTo( const Vector &pos ); // Move the bot to the precise given position immediately, + + virtual bool ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle ); // initiate a jump to an adjacent high ledge, return false if climb can't start + virtual void JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward ); // initiate a jump across an empty volume of space to far side + virtual void Jump( void ); // initiate a simple undirected jump in the air + virtual bool IsClimbingOrJumping( void ) const; // is jumping in any form + virtual bool IsClimbingUpToLedge( void ) const; // is climbing up to a high ledge + virtual bool IsJumpingAcrossGap( void ) const; // is jumping across a gap to the far side + + virtual void Run( void ); // set desired movement speed to running + virtual void Walk( void ); // set desired movement speed to walking + virtual void Stop( void ); // set desired movement speed to stopped + virtual bool IsRunning( void ) const; + virtual void SetDesiredSpeed( float speed ); // set desired speed for locomotor movement + virtual float GetDesiredSpeed( void ) const; // returns the current desired speed + + virtual float GetSpeedLimit( void ) const; // get maximum speed bot can reach, regardless of desired speed + + virtual bool IsOnGround( void ) const; // return true if standing on something + virtual void OnLeaveGround( CBaseEntity *ground ); // invoked when bot leaves ground for any reason + virtual void OnLandOnGround( CBaseEntity *ground ); // invoked when bot lands on the ground after being in the air + virtual CBaseEntity *GetGround( void ) const; // return the current ground entity or NULL if not on the ground + virtual const Vector &GetGroundNormal( void ) const;// surface normal of the ground we are in contact with + + virtual void ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ); // climb the given ladder to the top and dismount + virtual void DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ); // descend the given ladder to the bottom and dismount + virtual bool IsUsingLadder( void ) const; + virtual bool IsAscendingOrDescendingLadder( void ) const; // we are actually on the ladder right now, either climbing up or down + + virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target" + + virtual void SetDesiredLean( const QAngle &lean ); + virtual const QAngle &GetDesiredLean( void ) const; + + virtual const Vector &GetFeet( void ) const; // return position of "feet" - the driving point where the bot contacts the ground + + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + virtual float GetDeathDropHeight( void ) const; // distance at which we will die if we fall + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetWalkSpeed( void ) const; // get maximum walking speed + + virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor + virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor + + virtual const Vector &GetAcceleration( void ) const; // return current world space acceleration + virtual void SetAcceleration( const Vector &accel ); // set world space acceleration + + virtual const Vector &GetVelocity( void ) const; // return current world space velocity + virtual void SetVelocity( const Vector &vel ); // set world space velocity + + virtual void OnMoveToSuccess( const Path *path ); // invoked when an bot reaches its MoveTo goal + virtual void OnMoveToFailure( const Path *path, MoveToFailureType reason ); // invoked when an bot fails to reach a MoveTo goal + +private: + void UpdatePosition( const Vector &newPos ); // move to newPos, resolving any collisions along the way + void UpdateGroundConstraint( void ); // keep ground solid + Vector ResolveCollisionV0( Vector from, Vector to, int recursionLimit ); + + Vector ResolveZombieCollisions( const Vector &pos ); // push away zombies that are interpenetrating + Vector ResolveCollision( const Vector &from, const Vector &to, int recursionLimit ); // check for collisions along move + bool DetectCollision( trace_t *pTrace, int &nDestructionAllowed, const Vector &from, const Vector &to, const Vector &vecMins, const Vector &vecMaxs ); + void ApplyAccumulatedApproach( void ); + bool DidJustJump( void ) const; // return true if we just started a jump + bool TraverseLadder( void ); // return true if we are climbing a ladder + + virtual float GetGravity( void ) const; // return gravity force acting on bot + virtual float GetFrictionForward( void ) const; // return magnitude of forward friction + virtual float GetFrictionSideways( void ) const; // return magnitude of lateral friction + virtual float GetMaxYawRate( void ) const; // return max rate of yaw rotation + + +private: + NextBotCombatCharacter *m_nextBot; + + Vector m_priorPos; // last update's position + Vector m_lastValidPos; // last valid position (not interpenetrating) + + Vector m_acceleration; + Vector m_velocity; + + float m_desiredSpeed; // speed bot wants to be moving + float m_actualSpeed; // actual speed bot is moving + + float m_maxRunSpeed; + + float m_forwardLean; + float m_sideLean; + QAngle m_desiredLean; + + bool m_isJumping; // if true, we have jumped and have not yet hit the ground + bool m_isJumpingAcrossGap; // if true, we have jumped across a gap and have not yet hit the ground + EHANDLE m_ground; // have to manage this ourselves, since MOVETYPE_CUSTOM always NULLs out GetGroundEntity() + Vector m_groundNormal; // surface normal of the ground we are in contact with + bool m_isClimbingUpToLedge; // true if we are jumping up to an adjacent ledge + Vector m_ledgeJumpGoalPos; + bool m_isUsingFullFeetTrace; // true if we're in the air and tracing the lowest StepHeight in ResolveCollision + + const CNavLadder *m_ladder; // ladder we are currently climbing/descending + const CNavArea *m_ladderDismountGoal; // the area we enter when finished with our ladder move + bool m_isGoingUpLadder; // if false, we're going down + + CountdownTimer m_inhibitObstacleAvoidanceTimer; // when active, turn off path following feelers + + CountdownTimer m_wiggleTimer; // for wiggling + NavRelativeDirType m_wiggleDirection; + + mutable Vector m_eyePos; // for use with GetEyes(), etc. + + Vector m_moveVector; // the direction of our motion in XY plane + float m_moveYaw; // global yaw of movement direction + + Vector m_accumApproachVectors; // weighted sum of Approach() calls since last update + float m_accumApproachWeights; + bool m_bRecomputePostureOnCollision; + + CountdownTimer m_ignorePhysicsPropTimer; // if active, don't collide with physics props (because we got stuck in one) + EHANDLE m_ignorePhysicsProp; // which prop to ignore +}; + + +inline float NextBotGroundLocomotion::GetGravity( void ) const +{ + return 1000.0f; +} + +inline float NextBotGroundLocomotion::GetFrictionForward( void ) const +{ + return 0.0f; +} + +inline float NextBotGroundLocomotion::GetFrictionSideways( void ) const +{ + return 3.0f; +} + +inline float NextBotGroundLocomotion::GetMaxYawRate( void ) const +{ + return 250.0f; +} + +inline CBaseEntity *NextBotGroundLocomotion::GetGround( void ) const +{ + return m_ground; +} + + +inline const Vector &NextBotGroundLocomotion::GetGroundNormal( void ) const +{ + return m_groundNormal; +} + + +inline void NextBotGroundLocomotion::SetDesiredLean( const QAngle &lean ) +{ + m_desiredLean = lean; +} + + +inline const QAngle &NextBotGroundLocomotion::GetDesiredLean( void ) const +{ + return m_desiredLean; +} + + +inline void NextBotGroundLocomotion::SetDesiredSpeed( float speed ) +{ + m_desiredSpeed = speed; +} + + +inline float NextBotGroundLocomotion::GetDesiredSpeed( void ) const +{ + return m_desiredSpeed; +} + + +inline bool NextBotGroundLocomotion::IsClimbingOrJumping( void ) const +{ + return m_isJumping; +} + +inline bool NextBotGroundLocomotion::IsClimbingUpToLedge( void ) const +{ + return m_isClimbingUpToLedge; +} + +inline bool NextBotGroundLocomotion::IsJumpingAcrossGap( void ) const +{ + return m_isJumpingAcrossGap; +} + +inline bool NextBotGroundLocomotion::IsRunning( void ) const +{ + /// @todo Rethink interface to distinguish actual state vs desired state (do we want to be running, or are we actually at running speed right now) + return m_actualSpeed > 0.9f * GetRunSpeed(); +} + + +inline float NextBotGroundLocomotion::GetStepHeight( void ) const +{ + return 18.0f; +} + + +inline float NextBotGroundLocomotion::GetMaxJumpHeight( void ) const +{ + return 180.0f; // 120.0f; // 84.0f; // 58.0f; +} + + +inline float NextBotGroundLocomotion::GetDeathDropHeight( void ) const +{ + return 200.0f; +} + + +inline float NextBotGroundLocomotion::GetRunSpeed( void ) const +{ + return 150.0f; +} + + +inline float NextBotGroundLocomotion::GetWalkSpeed( void ) const +{ + return 75.0f; +} + +inline float NextBotGroundLocomotion::GetMaxAcceleration( void ) const +{ + return 500.0f; +} + +inline float NextBotGroundLocomotion::GetMaxDeceleration( void ) const +{ + return 500.0f; +} + + +#endif // NEXT_BOT_GROUND_LOCOMOTION_H + diff --git a/game/server/NextBot/NextBotHearingInterface.h b/game/server/NextBot/NextBotHearingInterface.h new file mode 100644 index 0000000..76a802d --- /dev/null +++ b/game/server/NextBot/NextBotHearingInterface.h @@ -0,0 +1,34 @@ +// NextBotHearingInterface.h +// Interface for auditory queries of a bot +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_HEARING_INTERFACE_H_ +#define _NEXT_BOT_HEARING_INTERFACE_H_ + +#include "NextBotComponentInterface.h" + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for hearing sounds + */ +class IHearing : public INextBotComponent +{ +public: + IHearing( INextBot *bot ) : INextBotComponent( bot ) { } + virtual ~IHearing() { } + + virtual void Reset( void ); // reset to initial state + virtual void Update( void ); // update internal state + + virtual float GetTimeSinceHeard( int team ) const; // return time since we heard any member of the given team + + virtual CBaseEntity *GetClosestRecognized( int team = TEAM_ANY ) const; // return the closest recognized entity + virtual int GetRecognizedCount( int team, float rangeLimit = -1.0f ) const; // return the number of actors on the given team visible to us closer than rangeLimit + + virtual float GetMaxHearingRange( void ) const; // return maximum distance we can hear + virtual float GetMinRecognizeTime( void ) const; // return HEARING reaction time +}; + + +#endif // _NEXT_BOT_HEARING_INTERFACE_H_ diff --git a/game/server/NextBot/NextBotIntentionInterface.cpp b/game/server/NextBot/NextBotIntentionInterface.cpp new file mode 100644 index 0000000..b775896 --- /dev/null +++ b/game/server/NextBot/NextBotIntentionInterface.cpp @@ -0,0 +1,91 @@ +// NextBotIntentionInterface.cpp +// Interface for intentional thinking +// Author: Michael Booth, November 2007 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "NextBotInterface.h" +#include "NextBotIntentionInterface.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//------------------------------------------------------------------------------------------------------------------------ +/** + * Given a subject, return the world space position we should aim at + */ +Vector IIntention::SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub ); + if ( query ) + { + // return the response of the first responder that gives a definitive answer + Vector result = query->SelectTargetPoint( me, subject ); + if ( result != vec3_origin ) + { + return result; + } + } + } + + // no answer, use a reasonable position + Vector threatMins, threatMaxs; + subject->CollisionProp()->WorldSpaceAABB( &threatMins, &threatMaxs ); + Vector targetPoint = subject->GetAbsOrigin(); + targetPoint.z += 0.7f * ( threatMaxs.z - threatMins.z ); + + return targetPoint; +} + + +//------------------------------------------------------------------------------------------------------------------------ +/** + * Given two threats, decide which one is more dangerous + */ +const CKnownEntity *IIntention::SelectMoreDangerousThreat( const INextBot *me, const CBaseCombatCharacter *subject, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const +{ + if ( !threat1 || threat1->IsObsolete() ) + { + if ( threat2 && !threat2->IsObsolete() ) + return threat2; + + return NULL; + } + else if ( !threat2 || threat2->IsObsolete() ) + { + return threat1; + } + + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub ); + if ( query ) + { + // return the response of the first responder that gives a definitive answer + const CKnownEntity *result = query->SelectMoreDangerousThreat( me, subject, threat1, threat2 ); + if ( result ) + { + return result; + } + } + } + + // no specific decision was made - return closest threat as most dangerous + float range1 = ( subject->GetAbsOrigin() - threat1->GetLastKnownPosition() ).LengthSqr(); + float range2 = ( subject->GetAbsOrigin() - threat2->GetLastKnownPosition() ).LengthSqr(); + + if ( range1 < range2 ) + { + return threat1; + } + + return threat2; +} + + + + diff --git a/game/server/NextBot/NextBotIntentionInterface.h b/game/server/NextBot/NextBotIntentionInterface.h new file mode 100644 index 0000000..8ff337b --- /dev/null +++ b/game/server/NextBot/NextBotIntentionInterface.h @@ -0,0 +1,211 @@ +// NextBotIntentionInterface.h +// Interface for intentional thinking +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_INTENTION_INTERFACE_H_ +#define _NEXT_BOT_INTENTION_INTERFACE_H_ + +#include "NextBotComponentInterface.h" +#include "NextBotContextualQueryInterface.h" + +class INextBot; + +// +// Insert this macro in your INextBot-derived class declaration to +// create a IIntention-derived class that handles the bookkeeping +// of instantiating a Behavior with an initial Action and updating it. +// +#define DECLARE_INTENTION_INTERFACE( Actor ) \ + \ + class Actor##Intention : public IIntention \ + { \ + public: \ + Actor##Intention( Actor *me ); \ + virtual ~Actor##Intention(); \ + virtual void Reset( void ); \ + virtual void Update( void ); \ + virtual INextBotEventResponder *FirstContainedResponder( void ) const { return m_behavior; } \ + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; } \ + private: \ + Behavior< Actor > *m_behavior; \ + }; \ + \ + public: virtual IIntention *GetIntentionInterface( void ) const { return m_intention; } \ + private: Actor##Intention *m_intention; \ + public: + + +// +// Use this macro to create the implementation code for the IIntention-derived class +// declared above. Since this requires InitialAction, it must occur after +// that Action has been declared, so it can be new'd here. +// +#define IMPLEMENT_INTENTION_INTERFACE( Actor, InitialAction ) \ + Actor::Actor##Intention::Actor##Intention( Actor *me ) : IIntention( me ) { m_behavior = new Behavior< Actor >( new InitialAction ); } \ + Actor::Actor##Intention::~Actor##Intention() { delete m_behavior; } \ + void Actor::Actor##Intention::Reset( void ) { delete m_behavior; m_behavior = new Behavior< Actor >( new InitialAction ); } \ + void Actor::Actor##Intention::Update( void ) { m_behavior->Update( static_cast< Actor * >( GetBot() ), GetUpdateInterval() ); } + + +// +// Use this macro in the constructor of your bot to allocate the IIntention-derived class +// +#define ALLOCATE_INTENTION_INTERFACE( Actor ) { m_intention = new Actor##Intention( this ); } + +// +// Use this macro in the destructor of your bot to deallocate the IIntention-derived class +// +#define DEALLOCATE_INTENTION_INTERFACE { if ( m_intention ) delete m_intention; } + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for intentional thinking. + * The assumption is that this is a container for one or more concurrent Behaviors. + * The "primary" Behavior is the FirstContainedResponder, and so on. + * IContextualQuery requests are prioritized in contained responder order, such that the first responder + * that returns a definitive answer is accepted. WITHIN a given responder (ie: a Behavior), the deepest child + * Behavior in the active stack is asked first, then its parent, and so on, allowing the most specific active + * Behavior to override the query responses of its more general parent Behaviors. + */ +class IIntention : public INextBotComponent, public IContextualQuery +{ +public: + IIntention( INextBot *bot ) : INextBotComponent( bot ) { } + virtual ~IIntention() { } + + virtual void Reset( void ) { INextBotComponent::Reset(); } // reset to initial state + virtual void Update( void ) { } // update internal state + + // IContextualQuery propagation -------------------------------- + virtual QueryResultType ShouldPickUp( const INextBot *me, CBaseEntity *item ) const; // if the desired item was available right now, should we pick it up? + virtual QueryResultType ShouldHurry( const INextBot *me ) const; // are we in a hurry? + virtual QueryResultType ShouldRetreat( const INextBot *me ) const; // is it time to retreat? + virtual QueryResultType ShouldAttack( const INextBot *me, const CKnownEntity *them ) const; // should we attack "them"? + virtual QueryResultType IsHindrance( const INextBot *me, CBaseEntity *blocker ) const; // return true if we should wait for 'blocker' that is across our path somewhere up ahead. + virtual Vector SelectTargetPoint( const INextBot *me, const CBaseCombatCharacter *subject ) const; // given a subject, return the world space position we should aim at + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; // is the a place we can be? + virtual const CKnownEntity * SelectMoreDangerousThreat( const INextBot *me, + const CBaseCombatCharacter *subject, // the subject of the danger + const CKnownEntity *threat1, + const CKnownEntity *threat2 ) const; // return the more dangerous of the two threats, or NULL if we have no opinion + // NOTE: As further queries are added, update the Behavior class to propagate them +}; + + +inline QueryResultType IIntention::ShouldPickUp( const INextBot *me, CBaseEntity *item ) const +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub ); + if ( query ) + { + // return the response of the first responder that gives a definitive answer + QueryResultType result = query->ShouldPickUp( me, item ); + if ( result != ANSWER_UNDEFINED ) + { + return result; + } + } + } + return ANSWER_UNDEFINED; +} + + +inline QueryResultType IIntention::ShouldHurry( const INextBot *me ) const +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub ); + if ( query ) + { + // return the response of the first responder that gives a definitive answer + QueryResultType result = query->ShouldHurry( me ); + if ( result != ANSWER_UNDEFINED ) + { + return result; + } + } + } + return ANSWER_UNDEFINED; +} + + +inline QueryResultType IIntention::ShouldRetreat( const INextBot *me ) const +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub ); + if ( query ) + { + // return the response of the first responder that gives a definitive answer + QueryResultType result = query->ShouldRetreat( me ); + if ( result != ANSWER_UNDEFINED ) + { + return result; + } + } + } + return ANSWER_UNDEFINED; +} + + +inline QueryResultType IIntention::ShouldAttack( const INextBot *me, const CKnownEntity *them ) const +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub ); + if ( query ) + { + // return the response of the first responder that gives a definitive answer + QueryResultType result = query->ShouldAttack( me, them ); + if ( result != ANSWER_UNDEFINED ) + { + return result; + } + } + } + return ANSWER_UNDEFINED; +} + + +inline QueryResultType IIntention::IsHindrance( const INextBot *me, CBaseEntity *blocker ) const +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub ); + if ( query ) + { + // return the response of the first responder that gives a definitive answer + QueryResultType result = query->IsHindrance( me, blocker ); + if ( result != ANSWER_UNDEFINED ) + { + return result; + } + } + } + return ANSWER_UNDEFINED; +} + + +inline QueryResultType IIntention::IsPositionAllowed( const INextBot *me, const Vector &pos ) const +{ + for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) + { + const IContextualQuery *query = dynamic_cast< const IContextualQuery * >( sub ); + if ( query ) + { + // return the response of the first responder that gives a definitive answer + QueryResultType result = query->IsPositionAllowed( me, pos ); + if ( result != ANSWER_UNDEFINED ) + { + return result; + } + } + } + return ANSWER_UNDEFINED; +} + + +#endif // _NEXT_BOT_INTENTION_INTERFACE_H_ diff --git a/game/server/NextBot/NextBotInterface.cpp b/game/server/NextBot/NextBotInterface.cpp new file mode 100644 index 0000000..f802e10 --- /dev/null +++ b/game/server/NextBot/NextBotInterface.cpp @@ -0,0 +1,537 @@ +// NextBotInterface.cpp +// Implentation of system methods for NextBot interface +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "props.h" +#include "fmtstr.h" +#include "team.h" + +#include "NextBotInterface.h" +#include "NextBotBodyInterface.h" +#include "NextBotManager.h" + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// development only, off by default for 360 +ConVar NextBotDebugHistory( "nb_debug_history", IsX360() ? "0" : "1", FCVAR_CHEAT, "If true, each bot keeps a history of debug output in memory" ); + +//---------------------------------------------------------------------------------------------------------------- +INextBot::INextBot( void ) : m_debugHistory( MAX_NEXTBOT_DEBUG_HISTORY, 0 ) // CUtlVector: grow to max length, alloc 0 initially +{ + m_tickLastUpdate = -999; + m_id = -1; + m_componentList = NULL; + m_debugDisplayLine = 0; + + m_immobileTimer.Invalidate(); + m_immobileCheckTimer.Invalidate(); + m_immobileAnchor = vec3_origin; + + m_currentPath = NULL; + + // register with the manager + m_id = TheNextBots().Register( this ); +} + + +//---------------------------------------------------------------------------------------------------------------- +INextBot::~INextBot() +{ + ResetDebugHistory(); + + // tell the manager we're gone + TheNextBots().UnRegister( this ); + + // delete Intention first, since destruction of Actions may access other components + if ( m_baseIntention ) + delete m_baseIntention; + + if ( m_baseLocomotion ) + delete m_baseLocomotion; + + if ( m_baseBody ) + delete m_baseBody; + + if ( m_baseVision ) + delete m_baseVision; +} + + +//---------------------------------------------------------------------------------------------------------------- +void INextBot::Reset( void ) +{ + m_tickLastUpdate = -999; + m_debugType = 0; + m_debugDisplayLine = 0; + + m_immobileTimer.Invalidate(); + m_immobileCheckTimer.Invalidate(); + m_immobileAnchor = vec3_origin; + + for( INextBotComponent *comp = m_componentList; comp; comp = comp->m_nextComponent ) + { + comp->Reset(); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +void INextBot::ResetDebugHistory( void ) +{ + for ( int i=0; i<m_debugHistory.Count(); ++i ) + { + delete m_debugHistory[i]; + } + + m_debugHistory.RemoveAll(); +} + + +//---------------------------------------------------------------------------------------------------------------- +bool INextBot::BeginUpdate() +{ + if ( TheNextBots().ShouldUpdate( this ) ) + { + TheNextBots().NotifyBeginUpdate( this ); + return true; + } + return false; +} + +//---------------------------------------------------------------------------------------------------------------- +void INextBot::EndUpdate( void ) +{ + TheNextBots().NotifyEndUpdate( this ); +} + +//---------------------------------------------------------------------------------------------------------------- +void INextBot::Update( void ) +{ + VPROF_BUDGET( "INextBot::Update", "NextBot" ); + + m_debugDisplayLine = 0; + + if ( IsDebugging( NEXTBOT_DEBUG_ALL ) ) + { + CFmtStr msg; + DisplayDebugText( msg.sprintf( "#%d", GetEntity()->entindex() ) ); + } + + UpdateImmobileStatus(); + + // update all components + for( INextBotComponent *comp = m_componentList; comp; comp = comp->m_nextComponent ) + { + if ( comp->ComputeUpdateInterval() ) + { + comp->Update(); + } + } +} + + +//---------------------------------------------------------------------------------------------------------------- +void INextBot::Upkeep( void ) +{ + VPROF_BUDGET( "INextBot::Upkeep", "NextBot" ); + + // do upkeep for all components + for( INextBotComponent *comp = m_componentList; comp; comp = comp->m_nextComponent ) + { + comp->Upkeep(); + } +} + + +//---------------------------------------------------------------------------------------------------------------- +bool INextBot::SetPosition( const Vector &pos ) +{ + IBody *body = GetBodyInterface(); + if (body) + { + return body->SetPosition( pos ); + } + + // fall back to setting raw entity position + GetEntity()->SetAbsOrigin( pos ); + return true; +} + + +//---------------------------------------------------------------------------------------------------------------- +const Vector &INextBot::GetPosition( void ) const +{ + return const_cast< INextBot * >( this )->GetEntity()->GetAbsOrigin(); +} + + +//---------------------------------------------------------------------------------------------------------------- +/** + * Return true if given actor is our enemy + */ +bool INextBot::IsEnemy( const CBaseEntity *them ) const +{ + if ( them == NULL ) + return false; + + // this is not strictly correct, as spectators are not enemies + return const_cast< INextBot * >( this )->GetEntity()->GetTeamNumber() != them->GetTeamNumber(); +} + + +//---------------------------------------------------------------------------------------------------------------- +/** + * Return true if given actor is our friend + */ +bool INextBot::IsFriend( const CBaseEntity *them ) const +{ + if ( them == NULL ) + return false; + + return const_cast< INextBot * >( this )->GetEntity()->GetTeamNumber() == them->GetTeamNumber(); +} + + +//---------------------------------------------------------------------------------------------------------------- +/** + * Return true if 'them' is actually me + */ +bool INextBot::IsSelf( const CBaseEntity *them ) const +{ + if ( them == NULL ) + return false; + + return const_cast< INextBot * >( this )->GetEntity()->entindex() == them->entindex(); +} + + +//---------------------------------------------------------------------------------------------------------------- +/** + * Components call this to register themselves with the bot that contains them + */ +void INextBot::RegisterComponent( INextBotComponent *comp ) +{ + // add to head of singly linked list + comp->m_nextComponent = m_componentList; + m_componentList = comp; +} + + +//---------------------------------------------------------------------------------------------------------------- +bool INextBot::IsRangeLessThan( CBaseEntity *subject, float range ) const +{ + Vector botPos; + CBaseEntity *bot = const_cast< INextBot * >( this )->GetEntity(); + if ( !bot || !subject ) + return 0.0f; + + bot->CollisionProp()->CalcNearestPoint( subject->WorldSpaceCenter(), &botPos ); + float computedRange = subject->CollisionProp()->CalcDistanceFromPoint( botPos ); + return computedRange < range; +} + + +//---------------------------------------------------------------------------------------------------------------- +bool INextBot::IsRangeLessThan( const Vector &pos, float range ) const +{ + Vector to = pos - GetPosition(); + return to.IsLengthLessThan( range ); +} + + +//---------------------------------------------------------------------------------------------------------------- +bool INextBot::IsRangeGreaterThan( CBaseEntity *subject, float range ) const +{ + Vector botPos; + CBaseEntity *bot = const_cast< INextBot * >( this )->GetEntity(); + if ( !bot || !subject ) + return true; + + bot->CollisionProp()->CalcNearestPoint( subject->WorldSpaceCenter(), &botPos ); + float computedRange = subject->CollisionProp()->CalcDistanceFromPoint( botPos ); + return computedRange > range; +} + + +//---------------------------------------------------------------------------------------------------------------- +bool INextBot::IsRangeGreaterThan( const Vector &pos, float range ) const +{ + Vector to = pos - GetPosition(); + return to.IsLengthGreaterThan( range ); +} + + +//---------------------------------------------------------------------------------------------------------------- +float INextBot::GetRangeTo( CBaseEntity *subject ) const +{ + Vector botPos; + CBaseEntity *bot = const_cast< INextBot * >( this )->GetEntity(); + if ( !bot || !subject ) + return 0.0f; + + bot->CollisionProp()->CalcNearestPoint( subject->WorldSpaceCenter(), &botPos ); + float computedRange = subject->CollisionProp()->CalcDistanceFromPoint( botPos ); + return computedRange; +} + + +//---------------------------------------------------------------------------------------------------------------- +float INextBot::GetRangeTo( const Vector &pos ) const +{ + Vector to = pos - GetPosition(); + return to.Length(); +} + + +//---------------------------------------------------------------------------------------------------------------- +float INextBot::GetRangeSquaredTo( CBaseEntity *subject ) const +{ + Vector botPos; + CBaseEntity *bot = const_cast< INextBot * >( this )->GetEntity(); + if ( !bot || !subject ) + return 0.0f; + + bot->CollisionProp()->CalcNearestPoint( subject->WorldSpaceCenter(), &botPos ); + float computedRange = subject->CollisionProp()->CalcDistanceFromPoint( botPos ); + return computedRange * computedRange; +} + + +//---------------------------------------------------------------------------------------------------------------- +float INextBot::GetRangeSquaredTo( const Vector &pos ) const +{ + Vector to = pos - GetPosition(); + return to.LengthSqr(); +} + + +//---------------------------------------------------------------------------------------------------------------- +bool INextBot::IsDebugging( unsigned int type ) const +{ + if ( TheNextBots().IsDebugging( type ) ) + { + return TheNextBots().IsDebugFilterMatch( this ); + } + + return false; +} + + +//---------------------------------------------------------------------------------------------------------------- +/** + * Return the name of this bot for debugging purposes + */ +const char *INextBot::GetDebugIdentifier( void ) const +{ + const int nameSize = 256; + static char name[ nameSize ]; + + Q_snprintf( name, nameSize, "%s(#%d)", const_cast< INextBot * >( this )->GetEntity()->GetClassname(), const_cast< INextBot * >( this )->GetEntity()->entindex() ); + + return name; +} + + +//---------------------------------------------------------------------------------------------------------------- +/** + * Return true if we match the given debug symbol + */ +bool INextBot::IsDebugFilterMatch( const char *name ) const +{ + // compare debug identifier + if ( !Q_strnicmp( name, GetDebugIdentifier(), Q_strlen( name ) ) ) + { + return true; + } + + // compare team name + CTeam *team = GetEntity()->GetTeam(); + if ( team && !Q_strnicmp( name, team->GetName(), Q_strlen( name ) ) ) + { + return true; + } + + + return false; +} + + +//---------------------------------------------------------------------------------------------------------------- +/** + * There are some things we never want to climb on + */ +bool INextBot::IsAbleToClimbOnto( const CBaseEntity *object ) const +{ + if ( object == NULL || !const_cast<CBaseEntity *>(object)->IsAIWalkable() ) + { + return false; + } + + // never climb onto doors + if ( FClassnameIs( const_cast< CBaseEntity * >( object ), "prop_door*" ) || FClassnameIs( const_cast< CBaseEntity * >( object ), "func_door*" ) ) + { + return false; + } + + // ok to climb on this object + return true; +} + + +//---------------------------------------------------------------------------------------------------------------- +/** + * Can we break this object + */ +bool INextBot::IsAbleToBreak( const CBaseEntity *object ) const +{ + if ( object && object->m_takedamage == DAMAGE_YES ) + { + if ( FClassnameIs( const_cast< CBaseEntity * >( object ), "func_breakable" ) && + object->GetHealth() ) + { + return true; + } + + if ( FClassnameIs( const_cast< CBaseEntity * >( object ), "func_breakable_surf" ) ) + { + return true; + } + + if ( dynamic_cast< const CBreakableProp * >( object ) != NULL ) + { + return true; + } + } + + return false; +} + + +//---------------------------------------------------------------------------------------------------------- +void INextBot::DisplayDebugText( const char *text ) const +{ + const_cast< INextBot * >( this )->GetEntity()->EntityText( m_debugDisplayLine++, text, 0.1 ); +} + + +//-------------------------------------------------------------------------------------------------------- +void INextBot::DebugConColorMsg( NextBotDebugType debugType, const Color &color, const char *fmt, ... ) +{ + bool isDataFormatted = false; + + va_list argptr; + char data[ MAX_NEXTBOT_DEBUG_LINE_LENGTH ]; + + if ( developer.GetBool() && IsDebugging( debugType ) ) + { + va_start(argptr, fmt); + Q_vsnprintf(data, sizeof( data ), fmt, argptr); + va_end(argptr); + isDataFormatted = true; + + ConColorMsg( color, "%s", data ); + } + + if ( !NextBotDebugHistory.GetBool() ) + { + if ( m_debugHistory.Count() ) + { + ResetDebugHistory(); + } + return; + } + + // Don't bother with event data - it's spammy enough to overshadow everything else. + if ( debugType == NEXTBOT_EVENTS ) + return; + + if ( !isDataFormatted ) + { + va_start(argptr, fmt); + Q_vsnprintf(data, sizeof( data ), fmt, argptr); + va_end(argptr); + isDataFormatted = true; + } + + int lastLine = m_debugHistory.Count() - 1; + if ( lastLine >= 0 ) + { + NextBotDebugLineType *line = m_debugHistory[lastLine]; + if ( line->debugType == debugType && V_strstr( line->data, "\n" ) == NULL ) + { + // append onto previous line + V_strncat( line->data, data, MAX_NEXTBOT_DEBUG_LINE_LENGTH ); + return; + } + } + + // Prune out an old line if needed, keeping a pointer to re-use the memory + NextBotDebugLineType *line = NULL; + if ( m_debugHistory.Count() == MAX_NEXTBOT_DEBUG_HISTORY ) + { + line = m_debugHistory[0]; + m_debugHistory.Remove( 0 ); + } + + // Add to debug history + if ( !line ) + { + line = new NextBotDebugLineType; + } + line->debugType = debugType; + V_strncpy( line->data, data, MAX_NEXTBOT_DEBUG_LINE_LENGTH ); + m_debugHistory.AddToTail( line ); +} + + +//-------------------------------------------------------------------------------------------------------- +// build a vector of debug history of the given types +void INextBot::GetDebugHistory( unsigned int type, CUtlVector< const NextBotDebugLineType * > *lines ) const +{ + if ( !lines ) + return; + + lines->RemoveAll(); + + for ( int i=0; i<m_debugHistory.Count(); ++i ) + { + NextBotDebugLineType *line = m_debugHistory[i]; + if ( line->debugType & type ) + { + lines->AddToTail( line ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +void INextBot::UpdateImmobileStatus( void ) +{ + if ( m_immobileCheckTimer.IsElapsed() ) + { + m_immobileCheckTimer.Start( 1.0f ); + + // if we haven't moved farther than this in 1 second, we're immobile + if ( ( GetEntity()->GetAbsOrigin() - m_immobileAnchor ).IsLengthGreaterThan( GetImmobileSpeedThreshold() ) ) + { + // moved far enough, not immobile + m_immobileAnchor = GetEntity()->GetAbsOrigin(); + m_immobileTimer.Invalidate(); + } + else + { + // haven't escaped our anchor - we are immobile + if ( !m_immobileTimer.HasStarted() ) + { + m_immobileTimer.Start(); + } + } + } +} + diff --git a/game/server/NextBot/NextBotInterface.h b/game/server/NextBot/NextBotInterface.h new file mode 100644 index 0000000..a2fd67e --- /dev/null +++ b/game/server/NextBot/NextBotInterface.h @@ -0,0 +1,302 @@ +// NextBotInterface.h +// Interface for NextBot +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_INTERFACE_H_ +#define _NEXT_BOT_INTERFACE_H_ + +#include "NextBot/NextBotKnownEntity.h" +#include "NextBotComponentInterface.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotBodyInterface.h" +#include "NextBotIntentionInterface.h" +#include "NextBotVisionInterface.h" +#include "NextBotDebug.h" + +class CBaseCombatCharacter; +class PathFollower; + +//---------------------------------------------------------------------------------------------------------------- +/** + * A general purpose filter interface for various bot systems + */ +class INextBotFilter +{ +public: + virtual bool IsSelected( const CBaseEntity *candidate ) const = 0; // return true if this entity passes the filter +}; + + +//---------------------------------------------------------------------------------------------------------------- +class INextBot : public INextBotEventResponder +{ +public: + INextBot( void ); + virtual ~INextBot(); + + int GetBotId() const; + + bool BeginUpdate(); + void EndUpdate(); + + virtual void Reset( void ); // (EXTEND) reset to initial state + virtual void Update( void ); // (EXTEND) update internal state + virtual void Upkeep( void ); // (EXTEND) lightweight update guaranteed to occur every server tick + + void FlagForUpdate( bool b = true ); + bool IsFlaggedForUpdate(); + int GetTickLastUpdate() const; + void SetTickLastUpdate( int ); + + virtual bool IsRemovedOnReset( void ) const { return true; } // remove this bot when the NextBot manager calls Reset + + virtual CBaseCombatCharacter *GetEntity( void ) const = 0; + virtual class NextBotCombatCharacter *GetNextBotCombatCharacter( void ) const { return NULL; } + +#ifdef TERROR + virtual class SurvivorBot *MySurvivorBotPointer() const { return NULL; } +#endif + + // interfaces are never NULL - return base no-op interfaces at a minimum + virtual ILocomotion * GetLocomotionInterface( void ) const; + virtual IBody * GetBodyInterface( void ) const; + virtual IIntention * GetIntentionInterface( void ) const; + virtual IVision * GetVisionInterface( void ) const; + + /** + * Attempt to change the bot's position. Return true if successful. + */ + virtual bool SetPosition( const Vector &pos ); + virtual const Vector &GetPosition( void ) const; // get the global position of the bot + + /** + * Friend/enemy/neutral queries + */ + virtual bool IsEnemy( const CBaseEntity *them ) const; // return true if given entity is our enemy + virtual bool IsFriend( const CBaseEntity *them ) const; // return true if given entity is our friend + virtual bool IsSelf( const CBaseEntity *them ) const; // return true if 'them' is actually me + + /** + * Can we climb onto this entity? + */ + virtual bool IsAbleToClimbOnto( const CBaseEntity *object ) const; + + /** + * Can we break this entity? + */ + virtual bool IsAbleToBreak( const CBaseEntity *object ) const; + + /** + * Sometimes we want to pass through other NextBots. OnContact() will always + * be invoked, but collision resolution can be skipped if this + * method returns false. + */ + virtual bool IsAbleToBlockMovementOf( const INextBot *botInMotion ) const { return true; } + + /** + * Should we ever care about noticing physical contact with this entity? + */ + virtual bool ShouldTouch( const CBaseEntity *object ) const { return true; } + + /** + * This immobile system is used to track the global state of "am I actually moving or not". + * The OnStuck() event is only emitted when following a path, and paths can be recomputed, etc. + */ + virtual bool IsImmobile( void ) const; // return true if we haven't moved in awhile + virtual float GetImmobileDuration( void ) const; // how long have we been immobile + virtual void ClearImmobileStatus( void ); + virtual float GetImmobileSpeedThreshold( void ) const; // return units/second below which this actor is considered "immobile" + + /** + * Get the last PathFollower we followed. This method gives other interfaces a + * single accessor to the most recent Path being followed by the myriad of + * different PathFollowers used in the various behaviors the bot may be doing. + */ + virtual const PathFollower *GetCurrentPath( void ) const; + virtual void SetCurrentPath( const PathFollower *path ); + virtual void NotifyPathDestruction( const PathFollower *path ); // this PathFollower is going away, which may or may not be ours + + // between distance utility methods + virtual bool IsRangeLessThan( CBaseEntity *subject, float range ) const; + virtual bool IsRangeLessThan( const Vector &pos, float range ) const; + virtual bool IsRangeGreaterThan( CBaseEntity *subject, float range ) const; + virtual bool IsRangeGreaterThan( const Vector &pos, float range ) const; + virtual float GetRangeTo( CBaseEntity *subject ) const; + virtual float GetRangeTo( const Vector &pos ) const; + virtual float GetRangeSquaredTo( CBaseEntity *subject ) const; + virtual float GetRangeSquaredTo( const Vector &pos ) const; + + // event propagation + virtual INextBotEventResponder *FirstContainedResponder( void ) const; + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const; + + virtual bool IsDebugging( unsigned int type ) const; // return true if this bot is debugging any of the given types + virtual const char *GetDebugIdentifier( void ) const; // return the name of this bot for debugging purposes + virtual bool IsDebugFilterMatch( const char *name ) const; // return true if we match the given debug symbol + virtual void DisplayDebugText( const char *text ) const; // show a line of text on the bot in the world + void DebugConColorMsg( NextBotDebugType debugType, const Color &color, PRINTF_FORMAT_STRING const char *fmt, ... ); + + enum { + MAX_NEXTBOT_DEBUG_HISTORY = 100, + MAX_NEXTBOT_DEBUG_LINE_LENGTH = 256, + }; + struct NextBotDebugLineType + { + NextBotDebugType debugType; + char data[ MAX_NEXTBOT_DEBUG_LINE_LENGTH ]; + }; + void GetDebugHistory( unsigned int type, CUtlVector< const NextBotDebugLineType * > *lines ) const; // build a vector of debug history of the given types + //------------------------------------------------------------------------------ + + +private: + friend class INextBotComponent; + void RegisterComponent( INextBotComponent *comp ); // components call this to register themselves with the bot that contains them + INextBotComponent *m_componentList; // the first component + + const PathFollower *m_currentPath; // the path we most recently followed + + int m_id; + bool m_bFlaggedForUpdate; + int m_tickLastUpdate; + + unsigned int m_debugType; + mutable int m_debugDisplayLine; + + Vector m_immobileAnchor; + CountdownTimer m_immobileCheckTimer; + IntervalTimer m_immobileTimer; + void UpdateImmobileStatus( void ); + + mutable ILocomotion *m_baseLocomotion; + mutable IBody *m_baseBody; + mutable IIntention *m_baseIntention; + mutable IVision *m_baseVision; + //mutable IAttention *m_baseAttention; + + // Debugging info + void ResetDebugHistory( void ); + CUtlVector< NextBotDebugLineType * > m_debugHistory; +}; + + +inline const PathFollower *INextBot::GetCurrentPath( void ) const +{ + return m_currentPath; +} + +inline void INextBot::SetCurrentPath( const PathFollower *path ) +{ + m_currentPath = path; +} + +inline void INextBot::NotifyPathDestruction( const PathFollower *path ) +{ + if ( m_currentPath == path ) + m_currentPath = NULL; +} + + +inline ILocomotion *INextBot::GetLocomotionInterface( void ) const +{ + // these base interfaces are lazy-allocated (instead of being fully instanced classes) for two reasons: + // 1) so the memory is only used if needed + // 2) so the component is registered properly + if ( m_baseLocomotion == NULL ) + { + m_baseLocomotion = new ILocomotion( const_cast< INextBot * >( this ) ); + } + + return m_baseLocomotion; +} + +inline IBody *INextBot::GetBodyInterface( void ) const +{ + if ( m_baseBody == NULL ) + { + m_baseBody = new IBody( const_cast< INextBot * >( this ) ); + } + + return m_baseBody; +} + +inline IIntention *INextBot::GetIntentionInterface( void ) const +{ + if ( m_baseIntention == NULL ) + { + m_baseIntention = new IIntention( const_cast< INextBot * >( this ) ); + } + + return m_baseIntention; +} + +inline IVision *INextBot::GetVisionInterface( void ) const +{ + if ( m_baseVision == NULL ) + { + m_baseVision = new IVision( const_cast< INextBot * >( this ) ); + } + + return m_baseVision; +} + +inline int INextBot::GetBotId() const +{ + return m_id; +} + +inline void INextBot::FlagForUpdate( bool b ) +{ + m_bFlaggedForUpdate = b; +} + +inline bool INextBot::IsFlaggedForUpdate() +{ + return m_bFlaggedForUpdate; +} + +inline int INextBot::GetTickLastUpdate() const +{ + return m_tickLastUpdate; +} + +inline void INextBot::SetTickLastUpdate( int tick ) +{ + m_tickLastUpdate = tick; +} + +inline bool INextBot::IsImmobile( void ) const +{ + return m_immobileTimer.HasStarted(); +} + +inline float INextBot::GetImmobileDuration( void ) const +{ + return m_immobileTimer.GetElapsedTime(); +} + +inline void INextBot::ClearImmobileStatus( void ) +{ + m_immobileTimer.Invalidate(); + m_immobileAnchor = GetEntity()->GetAbsOrigin(); +} + +inline float INextBot::GetImmobileSpeedThreshold( void ) const +{ + return 30.0f; +} + +inline INextBotEventResponder *INextBot::FirstContainedResponder( void ) const +{ + return m_componentList; +} + + +inline INextBotEventResponder *INextBot::NextContainedResponder( INextBotEventResponder *current ) const +{ + return static_cast< INextBotComponent * >( current )->m_nextComponent; +} + + +#endif // _NEXT_BOT_INTERFACE_H_ diff --git a/game/server/NextBot/NextBotKnownEntity.h b/game/server/NextBot/NextBotKnownEntity.h new file mode 100644 index 0000000..7a4e9ed --- /dev/null +++ b/game/server/NextBot/NextBotKnownEntity.h @@ -0,0 +1,175 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// NextBotKnownEntity.h +// Encapsulation of being aware of an entity +// Author: Michael Booth, June 2009 + +#ifndef NEXT_BOT_KNOWN_ENTITY_H +#define NEXT_BOT_KNOWN_ENTITY_H + +//---------------------------------------------------------------------------- +/** + * A "known entity" is an entity that we have seen or heard at some point + * and which may or may not be immediately visible to us right now but which + * we remember the last place we encountered it, and when. + * + * TODO: Enhance interface to allow for sets of areas where an unseen entity + * could potentially be, knowing his last position and his rate of movement. + */ +class CKnownEntity +{ +public: + // constructing assumes we currently know about this entity + CKnownEntity( CBaseEntity *who ) + { + m_who = who; + m_whenLastSeen = -1.0f; + m_whenLastBecameVisible = -1.0f; + m_isVisible = false; + m_whenBecameKnown = gpGlobals->curtime; + m_hasLastKnownPositionBeenSeen = false; + UpdatePosition(); + } + + virtual ~CKnownEntity() { } + + virtual void Destroy( void ) + { + m_who = NULL; + m_isVisible = false; + } + + virtual void UpdatePosition( void ) // could be seen or heard, but now the entity's position is known + { + if ( m_who.Get() ) + { + m_lastKnownPostion = m_who->GetAbsOrigin(); + m_lastKnownArea = m_who->MyCombatCharacterPointer() ? m_who->MyCombatCharacterPointer()->GetLastKnownArea() : NULL; + m_whenLastKnown = gpGlobals->curtime; + } + } + + virtual CBaseEntity *GetEntity( void ) const + { + return m_who; + } + + virtual const Vector &GetLastKnownPosition( void ) const + { + return m_lastKnownPostion; + } + + // Have we had a clear view of the last known position of this entity? + // This encapsulates the idea of "I just saw a guy right over *there* a few seconds ago, but I don't know where he is now" + virtual bool HasLastKnownPositionBeenSeen( void ) const + { + return m_hasLastKnownPositionBeenSeen; + } + + virtual void MarkLastKnownPositionAsSeen( void ) + { + m_hasLastKnownPositionBeenSeen = true; + } + + virtual const CNavArea *GetLastKnownArea( void ) const + { + return m_lastKnownArea; + } + + virtual float GetTimeSinceLastKnown( void ) const + { + return gpGlobals->curtime - m_whenLastKnown; + } + + virtual float GetTimeSinceBecameKnown( void ) const + { + return gpGlobals->curtime - m_whenBecameKnown; + } + + virtual void UpdateVisibilityStatus( bool visible ) + { + if ( visible ) + { + if ( !m_isVisible ) + { + // just became visible + m_whenLastBecameVisible = gpGlobals->curtime; + } + + m_whenLastSeen = gpGlobals->curtime; + } + + m_isVisible = visible; + } + + virtual bool IsVisibleInFOVNow( void ) const // return true if this entity is currently visible and in my field of view + { + return m_isVisible; + } + + virtual bool IsVisibleRecently( void ) const // return true if this entity is visible or was very recently visible + { + if ( m_isVisible ) + return true; + + if ( WasEverVisible() && GetTimeSinceLastSeen() < 3.0f ) + return true; + + return false; + } + + virtual float GetTimeSinceBecameVisible( void ) const + { + return gpGlobals->curtime - m_whenLastBecameVisible; + } + + virtual float GetTimeWhenBecameVisible( void ) const + { + return m_whenLastBecameVisible; + } + + virtual float GetTimeSinceLastSeen( void ) const + { + return gpGlobals->curtime - m_whenLastSeen; + } + + virtual bool WasEverVisible( void ) const + { + return m_whenLastSeen > 0.0f; + } + + // has our knowledge of this entity become obsolete? + virtual bool IsObsolete( void ) const + { + return GetEntity() == NULL || !m_who->IsAlive() || GetTimeSinceLastKnown() > 10.0f; + } + + virtual bool operator==( const CKnownEntity &other ) const + { + if ( GetEntity() == NULL || other.GetEntity() == NULL ) + return false; + + return ( GetEntity() == other.GetEntity() ); + } + + virtual bool Is( CBaseEntity *who ) const + { + if ( GetEntity() == NULL || who == NULL ) + return false; + + return ( GetEntity() == who ); + } + +private: + CHandle< CBaseEntity > m_who; + Vector m_lastKnownPostion; + bool m_hasLastKnownPositionBeenSeen; + CNavArea *m_lastKnownArea; + float m_whenLastSeen; + float m_whenLastBecameVisible; + float m_whenLastKnown; // last seen or heard, confirming its existance + float m_whenBecameKnown; + bool m_isVisible; // flagged by IVision update as visible or not +}; + + +#endif // NEXT_BOT_KNOWN_ENTITY_H diff --git a/game/server/NextBot/NextBotLocomotionInterface.cpp b/game/server/NextBot/NextBotLocomotionInterface.cpp new file mode 100644 index 0000000..9acc865 --- /dev/null +++ b/game/server/NextBot/NextBotLocomotionInterface.cpp @@ -0,0 +1,520 @@ +// NextBotLocomotionInterface.cpp +// Common functionality for all NextBot locomotors +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "BasePropDoor.h" + +#include "nav_area.h" +#include "NextBot.h" +#include "NextBotUtil.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotBodyInterface.h" + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// how far a bot must move to not be considered "stuck" +#define STUCK_RADIUS 100.0f + + + +//---------------------------------------------------------------------------------------------------------- +/** + * Reset to initial state + */ +ILocomotion::ILocomotion( INextBot *bot ) : INextBotComponent( bot ) +{ + Reset(); +} + +ILocomotion::~ILocomotion() +{ +} + +void ILocomotion::Reset( void ) +{ + INextBotComponent::Reset(); + + m_motionVector = Vector( 1.0f, 0.0f, 0.0f ); + m_speed = 0.0f; + m_groundMotionVector = m_motionVector; + m_groundSpeed = m_speed; + + m_moveRequestTimer.Invalidate(); + + m_isStuck = false; + m_stuckTimer.Invalidate(); + m_stuckPos = vec3_origin; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Update internal state + */ +void ILocomotion::Update( void ) +{ + StuckMonitor(); + + // maintain motion vector and speed values + const Vector &vel = GetVelocity(); + m_speed = vel.Length(); + m_groundSpeed = vel.AsVector2D().Length(); + + const float velocityThreshold = 10.0f; + if ( m_speed > velocityThreshold ) + { + m_motionVector = vel / m_speed; + } + + if ( m_groundSpeed > velocityThreshold ) + { + m_groundMotionVector.x = vel.x / m_groundSpeed; + m_groundMotionVector.y = vel.y / m_groundSpeed; + m_groundMotionVector.z = 0.0f; + } + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + // show motion vector + NDebugOverlay::HorzArrow( GetFeet(), GetFeet() + 25.0f * m_groundMotionVector, 3.0f, 100, 255, 0, 255, true, 0.1f ); + NDebugOverlay::HorzArrow( GetFeet(), GetFeet() + 25.0f * m_motionVector, 5.0f, 255, 255, 0, 255, true, 0.1f ); + } +} + + +//---------------------------------------------------------------------------- +void ILocomotion::AdjustPosture( const Vector &moveGoal ) +{ + // This function has no effect if we're not standing or crouching + IBody *body = GetBot()->GetBodyInterface(); + if ( !body->IsActualPosture( IBody::STAND ) && !body->IsActualPosture( IBody::CROUCH ) ) + return; + + // + // Stand or crouch as needed + // + + // get bounding limits, ignoring step-upable height + const Vector &mins = body->GetHullMins() + Vector( 0, 0, GetStepHeight() ); + + const float halfSize = body->GetHullWidth()/2.0f; + Vector standMaxs( halfSize, halfSize, body->GetStandHullHeight() ); + + trace_t trace; + NextBotTraversableTraceFilter filter( GetBot(), ILocomotion::IMMEDIATELY ); + + // snap forward movement vector along floor + const Vector &groundNormal = GetGroundNormal(); + const Vector &feet = GetFeet(); + Vector moveDir = moveGoal - feet; + float moveLength = moveDir.NormalizeInPlace(); + Vector left( -moveDir.y, moveDir.x, 0.0f ); + Vector goal = feet + moveLength * CrossProduct( left, groundNormal ).Normalized(); + + TraceHull( feet, goal, mins, standMaxs, body->GetSolidMask(), &filter, &trace ); + + if ( trace.fraction >= 1.0f && !trace.startsolid ) + { + // no collision while standing + if ( body->IsActualPosture( IBody::CROUCH ) ) + { + body->SetDesiredPosture( IBody::STAND ); + } + return; + } + + if ( body->IsActualPosture( IBody::CROUCH ) ) + return; + + // crouch hull check + Vector crouchMaxs( halfSize, halfSize, body->GetCrouchHullHeight() ); + + TraceHull( feet, goal, mins, crouchMaxs, body->GetSolidMask(), &filter, &trace ); + + if ( trace.fraction >= 1.0f && !trace.startsolid ) + { + // no collision while crouching + body->SetDesiredPosture( IBody::CROUCH ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Move directly towards the given position + */ +void ILocomotion::Approach( const Vector &goalPos, float goalWeight ) +{ + // there is a desire to move + m_moveRequestTimer.Start(); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Move the bot to the precise given position immediately + */ +void ILocomotion::DriveTo( const Vector &pos ) +{ + // there is a desire to move + m_moveRequestTimer.Start(); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Return true if this locomotor could potentially move along the line given. + * If false is returned, fraction of walkable ray is returned in 'fraction' + */ +bool ILocomotion::IsPotentiallyTraversable( const Vector &from, const Vector &to, TraverseWhenType when, float *fraction ) const +{ + VPROF_BUDGET( "Locomotion::IsPotentiallyTraversable", "NextBotExpensive" ); + + // if 'to' is high above us, it's not directly traversable + // Adding a bit of fudge room to allow for floating point roundoff errors + if ( ( to.z - from.z ) > GetMaxJumpHeight() + 0.1f ) + { + Vector along = to - from; + along.NormalizeInPlace(); + if ( along.z > GetTraversableSlopeLimit() ) + { + if ( fraction ) + { + *fraction = 0.0f; + } + return false; + } + } + + trace_t result; + NextBotTraversableTraceFilter filter( GetBot(), when ); + + // use a small hull since we cannot simulate collision resolution and avoidance along the way + const float probeSize = 0.25f * GetBot()->GetBodyInterface()->GetHullWidth(); // Cant be TOO small, or open stairwells/grates/etc will cause problems + const float probeZ = GetStepHeight(); + + Vector hullMin( -probeSize, -probeSize, probeZ ); + Vector hullMax( probeSize, probeSize, GetBot()->GetBodyInterface()->GetCrouchHullHeight() ); + TraceHull( from, to, hullMin, hullMax, GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result ); + +/* + if ( result.DidHit() ) + { + NDebugOverlay::SweptBox( from, result.endpos, hullMin, hullMax, vec3_angle, 255, 0, 0, 255, 9999.9f ); + NDebugOverlay::SweptBox( result.endpos, to, hullMin, hullMax, vec3_angle, 255, 255, 0, 255, 9999.9f ); + } + else + { + NDebugOverlay::SweptBox( from, to, hullMin, hullMax, vec3_angle, 255, 255, 0, 255, 0.1f ); + } +*/ + + if ( fraction ) + { + *fraction = result.fraction; + } + + return ( result.fraction >= 1.0f ) && ( !result.startsolid ); +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Return true if there is a possible "gap" that will need to be jumped over + * If true is returned, fraction of ray before gap is returned in 'fraction' + */ +bool ILocomotion::HasPotentialGap( const Vector &from, const Vector &desiredTo, float *fraction ) const +{ + VPROF_BUDGET( "Locomotion::HasPotentialGap", "NextBot" ); + + // find section of this ray that is actually traversable + float traversableFraction; + IsPotentiallyTraversable( from, desiredTo, IMMEDIATELY, &traversableFraction ); + + // compute end of traversable ray + Vector to = from + ( desiredTo - from ) * traversableFraction; + + Vector forward = to - from; + float length = forward.NormalizeInPlace(); + + IBody *body = GetBot()->GetBodyInterface(); + + float step = body->GetHullWidth()/2.0f; + + // scan along the line checking for gaps + Vector pos = from; + Vector delta = step * forward; + for( float t = 0.0f; t < (length + step); t += step ) + { + if ( IsGap( pos, forward ) ) + { + if ( fraction ) + { + *fraction = ( t - step ) / ( length + step ); + } + + return true; + } + + pos += delta; + } + + if ( fraction ) + { + *fraction = 1.0f; + } + + return false; +} + + +//---------------------------------------------------------------------------------------------------------- +/** + * Return true if there is a "gap" here when moving in the given direction. + * A "gap" is a vertical dropoff that is too high to jump back up to. + */ +bool ILocomotion::IsGap( const Vector &pos, const Vector &forward ) const +{ + VPROF_BUDGET( "Locomotion::IsGap", "NextBotSpiky" ); + + IBody *body = GetBot()->GetBodyInterface(); + + //float halfWidth = ( body ) ? body->GetHullWidth()/2.0f : 1.0f; + + // can't really jump effectively when crouched anyhow + //float hullHeight = ( body ) ? body->GetStandHullHeight() : 1.0f; + + // use a small hull since we cannot simulate collision resolution and avoidance along the way + const float halfWidth = 1.0f; + const float hullHeight = 1.0f; + + unsigned int mask = ( body ) ? body->GetSolidMask() : MASK_PLAYERSOLID; + + trace_t ground; + + NextBotTraceFilterIgnoreActors filter( GetBot()->GetEntity(), COLLISION_GROUP_NONE ); + + TraceHull( pos + Vector( 0, 0, GetStepHeight() ), // start up a bit to handle rough terrain + pos + Vector( 0, 0, -GetMaxJumpHeight() ), + Vector( -halfWidth, -halfWidth, 0 ), Vector( halfWidth, halfWidth, hullHeight ), + mask, &filter, &ground ); + +// int r,g,b; +// +// if ( ground.fraction >= 1.0f && !ground.startsolid ) +// { +// r = 255, g = 0, b = 0; +// } +// else +// { +// r = 0, g = 255, b = 0; +// } +// +// NDebugOverlay::SweptBox( pos, +// pos + Vector( 0, 0, -GetStepHeight() ), +// Vector( -halfWidth, -halfWidth, 0 ), Vector( halfWidth, halfWidth, hullHeight ), +// vec3_angle, +// r, g, b, 255, 3.0f ); + + // if trace hit nothing, there's a gap ahead of us + return ( ground.fraction >= 1.0f && !ground.startsolid ); +} + + +//---------------------------------------------------------------------------------------------------------- +bool ILocomotion::IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when ) const +{ + if ( obstacle->IsWorld() ) + return false; + + // assume bot will open a door in its path + if ( FClassnameIs( obstacle, "prop_door*" ) || FClassnameIs( obstacle, "func_door*" ) ) + { + CBasePropDoor *door = dynamic_cast< CBasePropDoor * >( obstacle ); + + if ( door && door->IsDoorOpen() ) + { + // open doors are obstacles + return false; + } + + return true; + } + + // if we hit a clip brush, ignore it if it is not BRUSHSOLID_ALWAYS + if ( FClassnameIs( obstacle, "func_brush" ) ) + { + CFuncBrush *brush = (CFuncBrush *)obstacle; + + switch ( brush->m_iSolidity ) + { + case CFuncBrush::BRUSHSOLID_ALWAYS: + return false; + case CFuncBrush::BRUSHSOLID_NEVER: + return true; + case CFuncBrush::BRUSHSOLID_TOGGLE: + return true; + } + } + + if ( when == IMMEDIATELY ) + { + // special rules in specific games can immediately break some breakables, etc. + return false; + } + + // assume bot will EVENTUALLY break breakables in its path + return GetBot()->IsAbleToBreak( obstacle ); +} + + +//-------------------------------------------------------------------------------------------------------------- +bool ILocomotion::IsAreaTraversable( const CNavArea *baseArea ) const +{ + return !baseArea->IsBlocked( GetBot()->GetEntity()->GetTeamNumber() ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset stuck status to un-stuck + */ +void ILocomotion::ClearStuckStatus( const char *reason ) +{ + if ( IsStuck() ) + { + m_isStuck = false; + + // tell other components we're no longer stuck + GetBot()->OnUnStuck(); + } + + // always reset stuck monitoring data in case we cleared preemptively are were not yet stuck + m_stuckPos = GetFeet(); + m_stuckTimer.Start(); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + DevMsg( "%3.2f: ClearStuckStatus: %s %s\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier(), reason ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Stuck check + */ +void ILocomotion::StuckMonitor( void ) +{ + // a timer is needed to smooth over a few frames of inactivity due to state changes, etc. + // we only want to detect idle situations when the bot really doesn't "want" to move. + const float idleTime = 0.25f; + if ( m_moveRequestTimer.IsGreaterThen( idleTime ) ) + { + // we have no desire to move, and therefore cannot emit stuck events + + // prepare our internal state for when the bot starts to move next + m_stuckPos = GetFeet(); + m_stuckTimer.Start(); + + return; + } + +// if ( !IsOnGround() ) +// { +// // can't be stuck when in-air +// ClearStuckStatus( "Off the ground" ); +// return; +// } + +// if ( IsUsingLadder() ) +// { +// // can't be stuck when on a ladder (for now) +// ClearStuckStatus( "On a ladder" ); +// return; +// } + + if ( IsStuck() ) + { + // we are/were stuck - have we moved enough to consider ourselves "dislodged" + if ( GetBot()->IsRangeGreaterThan( m_stuckPos, STUCK_RADIUS ) ) + { + // we've just become un-stuck + ClearStuckStatus( "UN-STUCK" ); + } + else + { + // still stuck - periodically resend the event + if ( m_stillStuckTimer.IsElapsed() ) + { + m_stillStuckTimer.Start( 1.0f ); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + DevMsg( "%3.2f: %s STILL STUCK\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier() ); + NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 5.0f ), QAngle( -90.0f, 0, 0 ), 5.0f, 255, 0, 0, 255, true, 1.0f ); + } + + GetBot()->OnStuck(); + } + } + } + else + { + // we're not stuck - yet + + if ( /*IsClimbingOrJumping() || */GetBot()->IsRangeGreaterThan( m_stuckPos, STUCK_RADIUS ) ) + { + // we have moved - reset anchor + m_stuckPos = GetFeet(); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::Cross3D( m_stuckPos, 3.0f, 255, 0, 255, true, 3.0f ); + } + + m_stuckTimer.Start(); + } + else + { + // within stuck range of anchor. if we've been here too long, we're stuck + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::Line( GetBot()->GetEntity()->WorldSpaceCenter(), m_stuckPos, 255, 0, 255, true, 0.1f ); + } + + float minMoveSpeed = 0.1f * GetDesiredSpeed() + 0.1f; + float escapeTime = STUCK_RADIUS / minMoveSpeed; + if ( m_stuckTimer.IsGreaterThen( escapeTime ) ) + { + // we have taken too long - we're stuck + m_isStuck = true; + + if ( GetBot()->IsDebugging( NEXTBOT_ERRORS ) ) + { + DevMsg( "%3.2f: %s STUCK at position( %3.2f, %3.2f, %3.2f )\n", gpGlobals->curtime, GetBot()->GetDebugIdentifier(), m_stuckPos.x, m_stuckPos.y, m_stuckPos.z ); + + NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 15.0f ), QAngle( -90.0f, 0, 0 ), 3.0f, 255, 255, 0, 255, true, 1.0f ); + NDebugOverlay::Circle( m_stuckPos + Vector( 0, 0, 5.0f ), QAngle( -90.0f, 0, 0 ), 5.0f, 255, 0, 0, 255, true, 9999999.9f ); + } + + // tell other components we've become stuck + GetBot()->OnStuck(); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +const Vector &ILocomotion::GetFeet( void ) const +{ + return GetBot()->GetEntity()->GetAbsOrigin(); +} + diff --git a/game/server/NextBot/NextBotLocomotionInterface.h b/game/server/NextBot/NextBotLocomotionInterface.h new file mode 100644 index 0000000..f311728 --- /dev/null +++ b/game/server/NextBot/NextBotLocomotionInterface.h @@ -0,0 +1,335 @@ +// NextBotLocomotionInterface.h +// NextBot interface for movement through the environment +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_LOCOMOTION_INTERFACE_H_ +#define _NEXT_BOT_LOCOMOTION_INTERFACE_H_ + +#include "NextBotComponentInterface.h" + +class Path; +class INextBot; +class CNavLadder; + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface encapsulating *how* a bot moves through the world (walking? flying? etc) + */ +class ILocomotion : public INextBotComponent +{ +public: + ILocomotion( INextBot *bot ); + virtual ~ILocomotion(); + + virtual void Reset( void ); // (EXTEND) reset to initial state + virtual void Update( void ); // (EXTEND) update internal state + + // + // The primary locomotive method + // Depending on the physics of the bot's motion, it may not actually + // reach the given position precisely. + // The 'weight' can be used to combine multiple Approach() calls within + // a single frame into a single goal (ie: weighted average) + // + virtual void Approach( const Vector &goalPos, float goalWeight = 1.0f ); // (EXTEND) move directly towards the given position + + // + // Move the bot to the precise given position immediately, + // updating internal state as needed + // Collision resolution is done to prevent interpenetration, which may prevent + // the bot from reaching the given position. If no collisions occur, the + // bot will be at the given position when this method returns. + // + virtual void DriveTo( const Vector &pos ); // (EXTEND) Move the bot to the precise given position immediately, + + // + // Locomotion modifiers + // + virtual bool ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle ) { return true; } // initiate a jump to an adjacent high ledge, return false if climb can't start + virtual void JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward ) { } // initiate a jump across an empty volume of space to far side + virtual void Jump( void ) { } // initiate a simple undirected jump in the air + virtual bool IsClimbingOrJumping( void ) const; // is jumping in any form + virtual bool IsClimbingUpToLedge( void ) const; // is climbing up to a high ledge + virtual bool IsJumpingAcrossGap( void ) const; // is jumping across a gap to the far side + virtual bool IsScrambling( void ) const; // is in the middle of a complex action (climbing a ladder, climbing a ledge, jumping, etc) that shouldn't be interrupted + + virtual void Run( void ) { } // set desired movement speed to running + virtual void Walk( void ) { } // set desired movement speed to walking + virtual void Stop( void ) { } // set desired movement speed to stopped + virtual bool IsRunning( void ) const; + virtual void SetDesiredSpeed( float speed ) { } // set desired speed for locomotor movement + virtual float GetDesiredSpeed( void ) const; // returns the current desired speed + + virtual void SetSpeedLimit( float speed ) { } // set maximum speed bot can reach, regardless of desired speed + virtual float GetSpeedLimit( void ) const { return 1000.0f; } // get maximum speed bot can reach, regardless of desired speed + + virtual bool IsOnGround( void ) const; // return true if standing on something + virtual void OnLeaveGround( CBaseEntity *ground ) { } // invoked when bot leaves ground for any reason + virtual void OnLandOnGround( CBaseEntity *ground ) { } // invoked when bot lands on the ground after being in the air + virtual CBaseEntity *GetGround( void ) const; // return the current ground entity or NULL if not on the ground + virtual const Vector &GetGroundNormal( void ) const; // surface normal of the ground we are in contact with + virtual float GetGroundSpeed( void ) const; // return current world space speed in XY plane + virtual const Vector &GetGroundMotionVector( void ) const; // return unit vector in XY plane describing our direction of motion - even if we are currently not moving + + virtual void ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ) { } // climb the given ladder to the top and dismount + virtual void DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ) { } // descend the given ladder to the bottom and dismount + virtual bool IsUsingLadder( void ) const; // we are moving to get on, ascending/descending, and/or dismounting a ladder + virtual bool IsAscendingOrDescendingLadder( void ) const; // we are actually on the ladder right now, either climbing up or down + virtual bool IsAbleToAutoCenterOnLadder( void ) const { return false; } + + virtual void FaceTowards( const Vector &target ) { } // rotate body to face towards "target" + + virtual void SetDesiredLean( const QAngle &lean ) { } + virtual const QAngle &GetDesiredLean( void ) const; + + + // + // Locomotion information + // + virtual bool IsAbleToJumpAcrossGaps( void ) const; // return true if this bot can jump across gaps in its path + virtual bool IsAbleToClimb( void ) const; // return true if this bot can climb arbitrary geometry it encounters + + virtual const Vector &GetFeet( void ) const; // return position of "feet" - the driving point where the bot contacts the ground + + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + virtual float GetDeathDropHeight( void ) const; // distance at which we will die if we fall + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetWalkSpeed( void ) const; // get maximum walking speed + + virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor + virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor + + virtual const Vector &GetVelocity( void ) const; // return current world space velocity + virtual float GetSpeed( void ) const; // return current world space speed (magnitude of velocity) + virtual const Vector &GetMotionVector( void ) const; // return unit vector describing our direction of motion - even if we are currently not moving + + virtual bool IsAreaTraversable( const CNavArea *baseArea ) const; // return true if given area can be used for navigation + + virtual float GetTraversableSlopeLimit( void ) const; // return Z component of unit normal of steepest traversable slope + + // return true if the given entity can be ignored during locomotion + enum TraverseWhenType + { + IMMEDIATELY, // the entity will not block our motion - we'll carry right through + EVENTUALLY // the entity will block us until we spend effort to open/destroy it + }; + + /** + * Return true if this locomotor could potentially move along the line given. + * If false is returned, fraction of walkable ray is returned in 'fraction' + */ + virtual bool IsPotentiallyTraversable( const Vector &from, const Vector &to, TraverseWhenType when = EVENTUALLY, float *fraction = NULL ) const; + + /** + * Return true if there is a possible "gap" that will need to be jumped over + * If true is returned, fraction of ray before gap is returned in 'fraction' + */ + virtual bool HasPotentialGap( const Vector &from, const Vector &to, float *fraction = NULL ) const; + + // return true if there is a "gap" here when moving in the given direction + virtual bool IsGap( const Vector &pos, const Vector &forward ) const; + + virtual bool IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when = EVENTUALLY ) const; + + // + // Stuck state. If the locomotor cannot make progress, it becomes "stuck" and can only leave + // this stuck state by successfully moving and becoming un-stuck. + // + virtual bool IsStuck( void ) const; // return true if bot is stuck + virtual float GetStuckDuration( void ) const; // return how long we've been stuck + virtual void ClearStuckStatus( const char *reason = "" ); // reset stuck status to un-stuck + + virtual bool IsAttemptingToMove( void ) const; // return true if we have tried to Approach() or DriveTo() very recently + + void TraceHull( const Vector& start, const Vector& end, const Vector &mins, const Vector &maxs, unsigned int fMask, ITraceFilter *pFilter, trace_t *pTrace ) const; + + /** + * Should we collide with this entity? + */ + virtual bool ShouldCollideWith( const CBaseEntity *object ) const { return true; } + + +protected: + virtual void AdjustPosture( const Vector &moveGoal ); + virtual void StuckMonitor( void ); + +private: + Vector m_motionVector; + Vector m_groundMotionVector; + float m_speed; + float m_groundSpeed; + + // stuck monitoring + bool m_isStuck; // if true, we are stuck + IntervalTimer m_stuckTimer; // how long we've been stuck + CountdownTimer m_stillStuckTimer; // for resending stuck events + Vector m_stuckPos; // where we got stuck + IntervalTimer m_moveRequestTimer; +}; + + +inline bool ILocomotion::IsAbleToJumpAcrossGaps( void ) const +{ + return true; +} + +inline bool ILocomotion::IsAbleToClimb( void ) const +{ + return true; +} + +inline bool ILocomotion::IsAttemptingToMove( void ) const +{ + return m_moveRequestTimer.HasStarted() && m_moveRequestTimer.GetElapsedTime() < 0.25f; +} + +inline bool ILocomotion::IsScrambling( void ) const +{ + return !IsOnGround() || IsClimbingOrJumping() || IsAscendingOrDescendingLadder(); +} + +inline bool ILocomotion::IsClimbingOrJumping( void ) const +{ + return false; +} + +inline bool ILocomotion::IsClimbingUpToLedge( void ) const +{ + return false; +} + +inline bool ILocomotion::IsJumpingAcrossGap( void ) const +{ + return false; +} + +inline bool ILocomotion::IsRunning( void ) const +{ + return false; +} + +inline float ILocomotion::GetDesiredSpeed( void ) const +{ + return 0.0f; +} + +inline bool ILocomotion::IsOnGround( void ) const +{ + return false; +} + +inline CBaseEntity *ILocomotion::GetGround( void ) const +{ + return NULL; +} + +inline const Vector &ILocomotion::GetGroundNormal( void ) const +{ + return vec3_origin; +} + +inline float ILocomotion::GetGroundSpeed( void ) const +{ + return m_groundSpeed; +} + +inline const Vector & ILocomotion::GetGroundMotionVector( void ) const +{ + return m_groundMotionVector; +} + +inline bool ILocomotion::IsUsingLadder( void ) const +{ + return false; +} + +inline bool ILocomotion::IsAscendingOrDescendingLadder( void ) const +{ + return false; +} + +inline const QAngle &ILocomotion::GetDesiredLean( void ) const +{ + return vec3_angle; +} + +inline float ILocomotion::GetStepHeight( void ) const +{ + return 0.0f; +} + +inline float ILocomotion::GetMaxJumpHeight( void ) const +{ + return 0.0f; +} + +inline float ILocomotion::GetDeathDropHeight( void ) const +{ + return 0.0f; +} + +inline float ILocomotion::GetRunSpeed( void ) const +{ + return 0.0f; +} + +inline float ILocomotion::GetWalkSpeed( void ) const +{ + return 0.0f; +} + +inline float ILocomotion::GetMaxAcceleration( void ) const +{ + return 0.0f; +} + +inline float ILocomotion::GetMaxDeceleration( void ) const +{ + return 0.0f; +} + +inline const Vector &ILocomotion::GetVelocity( void ) const +{ + return vec3_origin; +} + +inline float ILocomotion::GetSpeed( void ) const +{ + return m_speed; +} + +inline const Vector & ILocomotion::GetMotionVector( void ) const +{ + return m_motionVector; +} + +inline float ILocomotion::GetTraversableSlopeLimit( void ) const +{ + return 0.6; +} + +inline bool ILocomotion::IsStuck( void ) const +{ + return m_isStuck; +} + +inline float ILocomotion::GetStuckDuration( void ) const +{ + return ( IsStuck() ) ? m_stuckTimer.GetElapsedTime() : 0.0f; +} + +inline void ILocomotion::TraceHull( const Vector& start, const Vector& end, const Vector &mins, const Vector &maxs, unsigned int fMask, ITraceFilter *pFilter, trace_t *pTrace ) const +{ +// VPROF_BUDGET( "ILocomotion::TraceHull", "TraceHull" ); + Ray_t ray; + ray.Init( start, end, mins, maxs ); + enginetrace->TraceRay( ray, fMask, pFilter, pTrace ); +} + + + +#endif // _NEXT_BOT_LOCOMOTION_INTERFACE_H_ + diff --git a/game/server/NextBot/NextBotManager.cpp b/game/server/NextBot/NextBotManager.cpp new file mode 100644 index 0000000..2ce9c6c --- /dev/null +++ b/game/server/NextBot/NextBotManager.cpp @@ -0,0 +1,892 @@ +// NextBotManager.cpp +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "NextBotManager.h" +#include "NextBotInterface.h" + +#ifdef TERROR +#include "ZombieBot/Infected/Infected.h" +#include "ZombieBot/Witch/Witch.h" +#include "ZombieManager.h" +#endif + +#include "SharedFunctorUtils.h" +//#include "../../common/blackbox_helper.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ConVar ZombieMobMaxSize; + +ConVar nb_update_frequency( "nb_update_frequency", ".1", FCVAR_CHEAT ); +ConVar nb_update_framelimit( "nb_update_framelimit", ( IsDebug() ) ? "30" : "15", FCVAR_CHEAT ); +ConVar nb_update_maxslide( "nb_update_maxslide", "2", FCVAR_CHEAT ); +ConVar nb_update_debug( "nb_update_debug", "0", FCVAR_CHEAT ); + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +/** + * Singleton accessor. + * By returning a reference, we guarantee construction of the + * instance before its first use. + */ +NextBotManager &TheNextBots( void ) +{ + if ( NextBotManager::GetInstance() ) + { + return *NextBotManager::GetInstance(); + } + else + { + static NextBotManager manager; + NextBotManager::SetInstance( &manager ); + return manager; + } +} + +NextBotManager* NextBotManager::sInstance = NULL; + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +static const char *debugTypeName[] = +{ + "BEHAVIOR", + "LOOK_AT", + "PATH", + "ANIMATION", + "LOCOMOTION", + "VISION", + "HEARING", + "EVENTS", + "ERRORS", + NULL +}; + + +static void CC_SetDebug( const CCommand &args ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Debugging stopped\n" ); + TheNextBots().SetDebugTypes( NEXTBOT_DEBUG_NONE ); + return; + } + + int debugType = 0; + + for( int i=1; i<args.ArgC(); ++i ) + { + int type; + for( type = 0; debugTypeName[ type ]; ++type ) + { + const char *token = args[i]; + + // special token that means "all" + if ( token[0] == '*' ) + { + debugType = NEXTBOT_DEBUG_ALL; + break; + } + + if ( !Q_strnicmp( args[i], debugTypeName[ type ], Q_strlen( args[1] ) ) ) + { + debugType |= ( 1 << type ); + break; + } + } + + if ( !debugTypeName[ type ] ) + { + Msg( "Invalid debug type '%s'\n", args[i] ); + } + } + + // enable debugging + TheNextBots().SetDebugTypes( ( NextBotDebugType ) debugType ); +} +static ConCommand SetDebug( "nb_debug", CC_SetDebug, "Debug NextBots. Categories are: BEHAVIOR, LOOK_AT, PATH, ANIMATION, LOCOMOTION, VISION, HEARING, EVENTS, ERRORS.", FCVAR_CHEAT ); + +//--------------------------------------------------------------------------------------------- +static void CC_SetDebugFilter( const CCommand &args ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Debug filter cleared.\n" ); + TheNextBots().DebugFilterClear(); + return; + } + + for( int i=1; i<args.ArgC(); ++i ) + { + int index = Q_atoi( args[i] ); + if ( index > 0 ) + { + TheNextBots().DebugFilterAdd( index ); + } + else + { + TheNextBots().DebugFilterAdd( args[i] ); + } + } +} +static ConCommand SetDebugFilter( "nb_debug_filter", CC_SetDebugFilter, "Add items to the NextBot debug filter. Items can be entindexes or part of the indentifier of one or more bots.", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +class Selector +{ +public: + Selector( CBasePlayer *player, bool useLOS ) + { + m_player = player; + player->EyeVectors( &m_forward ); + + m_pick = NULL; + m_pickRange = 99999999999999.9f; + m_useLOS = useLOS; + } + + bool operator() ( INextBot *bot ) + { + CBaseCombatCharacter *botEntity = bot->GetEntity(); + if ( botEntity->IsAlive() ) + { + Vector to = botEntity->WorldSpaceCenter() - m_player->EyePosition(); + float range = to.NormalizeInPlace(); + + if ( DotProduct( m_forward, to ) > 0.98f && range < m_pickRange ) + { + if ( !m_useLOS || m_player->IsAbleToSee( botEntity, CBaseCombatCharacter::DISREGARD_FOV ) ) + { + m_pick = bot; + m_pickRange = range; + } + } + } + return true; + } + + CBasePlayer *m_player; + Vector m_forward; + INextBot *m_pick; + float m_pickRange; + bool m_useLOS; +}; + +static void CC_SelectBot( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player ) + { + Selector select( player, false ); + TheNextBots().ForEachBot( select ); + + TheNextBots().Select( select.m_pick ); + + if ( select.m_pick ) + { + NDebugOverlay::Circle( select.m_pick->GetLocomotionInterface()->GetFeet() + Vector( 0, 0, 5 ), Vector( 1, 0, 0 ), Vector( 0, -1, 0 ), 25.0f, 0, 255, 0, 255, false, 1.0f ); + } + } +} +static ConCommand SelectBot( "nb_select", CC_SelectBot, "Select the bot you are aiming at for further debug operations.", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +static void CC_ForceLookAt( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + INextBot *pick = TheNextBots().GetSelected(); + + if ( player && pick ) + { + pick->GetBodyInterface()->AimHeadTowards( player, IBody::CRITICAL, 9999999.9f, NULL, "Aim forced" ); + } +} +static ConCommand ForceLookAt( "nb_force_look_at", CC_ForceLookAt, "Force selected bot to look at the local player's position", FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------- +void CC_WarpSelectedHere( const CCommand &args ) +{ + CBasePlayer *me = dynamic_cast< CBasePlayer * >( UTIL_GetCommandClient() ); + INextBot *pick = TheNextBots().GetSelected(); + + if ( me == NULL || pick == NULL ) + { + return; + } + + Vector forward; + me->EyeVectors( &forward ); + + trace_t result; + UTIL_TraceLine( me->EyePosition(), me->EyePosition() + 999999.9f * forward, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, me, COLLISION_GROUP_NONE, &result ); + if ( result.DidHit() ) + { + Vector spot = result.endpos + Vector( 0, 0, 10.0f ); + pick->GetEntity()->Teleport( &spot, &vec3_angle, &vec3_origin ); + } +} +static ConCommand WarpSelectedHere( "nb_warp_selected_here", CC_WarpSelectedHere, "Teleport the selected bot to your cursor position", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +NextBotManager::NextBotManager( void ) +{ + m_debugType = 0; + m_selectedBot = NULL; + + m_iUpdateTickrate = 0; +} + +//--------------------------------------------------------------------------------------------- +NextBotManager::~NextBotManager() +{ +} + + +//--------------------------------------------------------------------------------------------- +/** + * Reset to initial state + */ +void NextBotManager::Reset( void ) +{ + // remove the NextBots that should go away during a reset (they will unregister themselves as they go) + int i = m_botList.Head(); + while ( i != m_botList.InvalidIndex() ) + { + int iNext = m_botList.Next( i ); + if ( m_botList[i]->IsRemovedOnReset() ) + { + UTIL_Remove( m_botList[i]->GetEntity() ); + //Assert( !m_botList.IsInList( i ) ); // UTIL_Remove() calls UpdateOnRemove, adds EFL_KILLME, but doesn't delete until the end of the frame + } + i = iNext; + } + + m_selectedBot = NULL; +} + + +//--------------------------------------------------------------------------------------------- + +inline bool IsDead( INextBot *pBot ) +{ + CBaseCombatCharacter *pEntity = pBot->GetEntity(); + if ( pEntity ) + { + if ( pEntity->IsPlayer() && pEntity->m_lifeState == LIFE_DEAD ) + { + return true; + } + + if ( pEntity->IsMarkedForDeletion() ) + { + return true; + } + + if ( pEntity->m_pfnThink == &CBaseEntity::SUB_Remove ) + { + return true; + } + } + return false; +} + +//--------------------------------------------------------------------------------------------- + +// Debug stats for update balancing +static int g_nRun; +static int g_nSlid; +static int g_nBlockedSlides; + +void NextBotManager::Update( void ) +{ + // do lightweight upkeep every tick + for( int u=m_botList.Head(); u != m_botList.InvalidIndex(); u = m_botList.Next( u ) ) + { + m_botList[ u ]->Upkeep(); + } + + // schedule full updates + if ( m_botList.Count() ) + { + static int iCurFrame = -1; + if ( iCurFrame != gpGlobals->framecount ) + { + iCurFrame = gpGlobals->framecount; + m_SumFrameTime = 0; + } + else + { + // Don't run multiple ticks in a frame + return; + } + + int tickRate = TIME_TO_TICKS( nb_update_frequency.GetFloat() ); + if ( tickRate < 0 ) + { + tickRate = 0; + } + + if ( m_iUpdateTickrate != tickRate ) + { + Msg( "NextBot tickrate changed from %d (%.3fms) to %d (%.3fms)\n", m_iUpdateTickrate, TICKS_TO_TIME( m_iUpdateTickrate ), tickRate, TICKS_TO_TIME( tickRate ) ); + m_iUpdateTickrate = tickRate; + } + + int i = 0; + int nScheduled = 0; + int nNonResponsive = 0; + int nDead = 0; + if ( m_iUpdateTickrate > 0 ) + { + INextBot *pBot; + + // Count dead bots, they won't update and balancing calculations should exclude them + for( i = m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) ) + { + if ( IsDead( m_botList[i] ) ) + { + nDead++; + } + } + + + int nTargetToRun = ceilf( (float)( m_botList.Count() - nDead ) / (float)m_iUpdateTickrate ); + int curtickcount = gpGlobals->tickcount; + + for( i = m_botList.Head(); nTargetToRun && i != m_botList.InvalidIndex(); i = m_botList.Next( i ) ) + { + pBot = m_botList[i]; + if ( pBot->IsFlaggedForUpdate() ) + { + // Was offered a run last tick but didn't take it, push it back + // Leave the flag set so that bot will run right away later, but be ignored + // until then + nNonResponsive++; + } + else + { + if ( curtickcount - pBot->GetTickLastUpdate() < m_iUpdateTickrate ) + { + break; + } + if ( !IsDead( pBot ) ) + { + pBot->FlagForUpdate(); + nTargetToRun--; + nScheduled++; + } + } + } + } + else + { + nScheduled = m_botList.Count(); + } + + if ( nb_update_debug.GetBool() ) + { + int nIntentionalSliders = 0; + if ( m_iUpdateTickrate > 0 ) + { + for( ; i != m_botList.InvalidIndex(); i = m_botList.Next( i ) ) + { + if ( gpGlobals->tickcount - m_botList[i]->GetTickLastUpdate() >= m_iUpdateTickrate ) + { + nIntentionalSliders++; + } + } + } + + Msg( "Frame %8d/tick %8d: %3d run of %3d, %3d sliders, %3d blocked slides, scheduled %3d for next tick, %3d intentional sliders, %d nonresponsive, %d dead\n", gpGlobals->framecount - 1, gpGlobals->tickcount - 1, g_nRun, m_botList.Count() - nDead, g_nSlid, g_nBlockedSlides, nScheduled, nIntentionalSliders, nNonResponsive, nDead ); + g_nRun = g_nSlid = g_nBlockedSlides = 0; + } + + } +} + +//--------------------------------------------------------------------------------------------- +bool NextBotManager::ShouldUpdate( INextBot *bot ) +{ + if ( m_iUpdateTickrate < 1 ) + { + return true; + } + + float frameLimit = nb_update_framelimit.GetFloat(); + float sumFrameTime = 0; + if ( bot->IsFlaggedForUpdate() ) + { + bot->FlagForUpdate( false ); + sumFrameTime = m_SumFrameTime * 1000.0; + if ( frameLimit > 0.0f ) + { + if ( sumFrameTime < frameLimit ) + { + return true; + } + else if ( nb_update_debug.GetBool() ) + { + Msg( "Frame %8d/tick %8d: frame out of budget (%.2fms > %.2fms)\n", gpGlobals->framecount, gpGlobals->tickcount, sumFrameTime, frameLimit ); + } + } + } + + int nTicksSlid = ( gpGlobals->tickcount - bot->GetTickLastUpdate() ) - m_iUpdateTickrate; + + if ( nTicksSlid >= nb_update_maxslide.GetInt() ) + { + if ( frameLimit == 0.0 || sumFrameTime < nb_update_framelimit.GetFloat() * 2.0 ) + { + g_nBlockedSlides++; + return true; + } + } + + if ( nb_update_debug.GetBool() ) + { + if ( nTicksSlid > 0 ) + { + g_nSlid++; + } + } + + return false; +} + +//--------------------------------------------------------------------------------------------- +void NextBotManager::NotifyBeginUpdate( INextBot *bot ) +{ + if ( nb_update_debug.GetBool() ) + { + g_nRun++; + } + + m_botList.Unlink( bot->GetBotId() ); + m_botList.LinkToTail( bot->GetBotId() ); + bot->SetTickLastUpdate( gpGlobals->tickcount ); + + m_CurUpdateStartTime = Plat_FloatTime(); +} + +//--------------------------------------------------------------------------------------------- +void NextBotManager::NotifyEndUpdate( INextBot *bot ) +{ + // This might be a good place to detect a particular bot had spiked [3/14/2008 tom] + m_SumFrameTime += Plat_FloatTime() - m_CurUpdateStartTime; +} + +//--------------------------------------------------------------------------------------------- +/** + * When the server has changed maps + */ +void NextBotManager::OnMapLoaded( void ) +{ + Reset(); +} + + +//--------------------------------------------------------------------------------------------- +/** + * When the scenario restarts + */ +void NextBotManager::OnRoundRestart( void ) +{ + Reset(); +} + + +//--------------------------------------------------------------------------------------------- +int NextBotManager::Register( INextBot *bot ) +{ + return m_botList.AddToHead( bot ); +} + + +//--------------------------------------------------------------------------------------------- +void NextBotManager::UnRegister( INextBot *bot ) +{ + m_botList.Remove( bot->GetBotId() ); + + if ( bot == m_selectedBot) + { + // we can't access virtual methods because this is called from a destructor, so just clear it + m_selectedBot = NULL; + } +} + + +//-------------------------------------------------------------------------------------------------------- +void NextBotManager::OnBeginChangeLevel( void ) +{ +} + + +//---------------------------------------------------------------------------------------------------------- +class NextBotKilledNotifyScan +{ +public: + NextBotKilledNotifyScan( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) + { + m_victim = victim; + m_info = info; + } + + bool operator() ( INextBot *bot ) + { + if ( bot->GetEntity()->IsAlive() && !bot->IsSelf( m_victim ) ) + { + bot->OnOtherKilled( m_victim, m_info ); + } + return true; + } + + CBaseCombatCharacter *m_victim; + CTakeDamageInfo m_info; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * When an actor is killed. Propagate to all NextBots. + */ +void NextBotManager::OnKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ) +{ + NextBotKilledNotifyScan notify( victim, info ); + TheNextBots().ForEachBot( notify ); +} + + +//---------------------------------------------------------------------------------------------------------- +class NextBotSoundNotifyScan +{ +public: + NextBotSoundNotifyScan( CBaseEntity *source, const Vector &pos, KeyValues *keys ) : m_source( source ), m_pos( pos ), m_keys( keys ) + { + } + + bool operator() ( INextBot *bot ) + { + if ( bot->GetEntity()->IsAlive() && !bot->IsSelf( m_source ) ) + { + bot->OnSound( m_source, m_pos, m_keys ); + } + return true; + } + + CBaseEntity *m_source; + const Vector &m_pos; + KeyValues *m_keys; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * When an entity emits a sound + */ +void NextBotManager::OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys ) +{ + NextBotSoundNotifyScan notify( source, pos, keys ); + TheNextBots().ForEachBot( notify ); + + if ( source && IsDebugging( NEXTBOT_HEARING ) ) + { + int r,g,b; + switch( source->GetTeamNumber() ) + { + case FIRST_GAME_TEAM: r = 0; g = 255; b = 0; break; + case (FIRST_GAME_TEAM+1): r = 255; g = 0; b = 0; break; + default: r = 255; g = 255; b = 0; break; + } + NDebugOverlay::Circle( pos, Vector( 1, 0, 0 ), Vector( 0, -1, 0 ), 5.0f, r, g, b, 255, true, 3.0f ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +class NextBotResponseNotifyScan +{ +public: + NextBotResponseNotifyScan( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ) : m_who( who ), m_concept( concept ), m_response( response ) + { + } + + bool operator() ( INextBot *bot ) + { + if ( bot->GetEntity()->IsAlive() ) + { + bot->OnSpokeConcept( m_who, m_concept, m_response ); + } + return true; + } + + CBaseCombatCharacter *m_who; + AIConcept_t m_concept; + AI_Response *m_response; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * When an Actor speaks a concept + */ +void NextBotManager::OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ) +{ + NextBotResponseNotifyScan notify( who, concept, response ); + TheNextBots().ForEachBot( notify ); + + if ( IsDebugging( NEXTBOT_HEARING ) ) + { + // const char *who = response->GetCriteria()->GetValue( response->GetCriteria()->FindCriterionIndex( "Who" ) ); + + // TODO: Need concept.GetStringConcept() + DevMsg( "%3.2f: OnSpokeConcept( %s, %s )\n", gpGlobals->curtime, who->GetDebugName(), "concept.GetStringConcept()" ); + } +} + + +//---------------------------------------------------------------------------------------------------------- +class NextBotWeaponFiredNotifyScan +{ +public: + NextBotWeaponFiredNotifyScan( CBaseCombatCharacter *who, CBaseCombatWeapon *weapon ) : m_who( who ), m_weapon( weapon ) + { + } + + bool operator() ( INextBot *bot ) + { + if ( bot->GetEntity()->IsAlive() ) + { + bot->OnWeaponFired( m_who, m_weapon ); + } + return true; + } + + CBaseCombatCharacter *m_who; + CBaseCombatWeapon *m_weapon; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * When someone fires a weapon + */ +void NextBotManager::OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ) +{ + NextBotWeaponFiredNotifyScan notify( whoFired, weapon ); + TheNextBots().ForEachBot( notify ); + + if ( IsDebugging( NEXTBOT_EVENTS ) ) + { + DevMsg( "%3.2f: OnWeaponFired( %s, %s )\n", gpGlobals->curtime, whoFired->GetDebugName(), weapon->GetName() ); + } +} + + +//--------------------------------------------------------------------------------------------- +/** + * Add given entindex to the debug filter + */ +void NextBotManager::DebugFilterAdd( int index ) +{ + DebugFilter filter; + + filter.index = index; + filter.name[0] = '\000'; + + m_debugFilterList.AddToTail( filter ); +} + + +//--------------------------------------------------------------------------------------------- +/** + * Add given name to the debug filter + */ +void NextBotManager::DebugFilterAdd( const char *name ) +{ + DebugFilter filter; + + filter.index = -1; + Q_strncpy( filter.name, name, DebugFilter::MAX_DEBUG_NAME_SIZE ); + + m_debugFilterList.AddToTail( filter ); +} + + +//--------------------------------------------------------------------------------------------- +/** + * Remove given entindex from the debug filter + */ +void NextBotManager::DebugFilterRemove( int index ) +{ + for( int i=0; i<m_debugFilterList.Count(); ++i ) + { + if ( m_debugFilterList[i].index == index ) + { + m_debugFilterList.Remove( i ); + break; + } + } +} + + +//--------------------------------------------------------------------------------------------- +/** + * Remove given name from the debug filter + */ +void NextBotManager::DebugFilterRemove( const char *name ) +{ + for( int i=0; i<m_debugFilterList.Count(); ++i ) + { + if ( m_debugFilterList[i].name[0] != '\000' && + !Q_strnicmp( name, m_debugFilterList[i].name, MIN( Q_strlen( name ), sizeof( m_debugFilterList[i].name ) ) ) ) + { + m_debugFilterList.Remove( i ); + break; + } + } +} + + +//--------------------------------------------------------------------------------------------- +/** + * Clear the debug filter (remove all entries) + */ +void NextBotManager::DebugFilterClear( void ) +{ + m_debugFilterList.RemoveAll(); +} + + +//--------------------------------------------------------------------------------------------- +/** + * Return true if the given bot matches the debug filter + */ +bool NextBotManager::IsDebugFilterMatch( const INextBot *bot ) const +{ + // if the filter is empty, all bots match + if ( m_debugFilterList.Count() == 0 ) + { + return true; + } + + for( int i=0; i<m_debugFilterList.Count(); ++i ) + { + // compare entity index + if ( m_debugFilterList[i].index == const_cast< INextBot * >( bot )->GetEntity()->entindex() ) + { + return true; + } + + // compare debug filter + if ( m_debugFilterList[i].name[0] != '\000' && bot->IsDebugFilterMatch( m_debugFilterList[i].name ) ) + { + return true; + } + + // compare special keyword meaning local player is looking at them + if ( !Q_strnicmp( m_debugFilterList[i].name, "lookat", Q_strlen( m_debugFilterList[i].name ) ) ) + { + CBasePlayer *watcher = UTIL_GetListenServerHost(); + if ( watcher ) + { + CBaseEntity *subject = watcher->GetObserverTarget(); + + if ( subject && bot->IsSelf( subject ) ) + { + return true; + } + } + } + + // compare special keyword meaning NextBot is selected + if ( !Q_strnicmp( m_debugFilterList[i].name, "selected", Q_strlen( m_debugFilterList[i].name ) ) ) + { + INextBot *selected = GetSelected(); + if ( selected && bot->IsSelf( selected->GetEntity() ) ) + { + return true; + } + } + } + + return false; +} + +//--------------------------------------------------------------------------------------------- +/** + * Get the bot under the given player's crosshair + */ +INextBot *NextBotManager::GetBotUnderCrosshair( CBasePlayer *picker ) +{ + if ( !picker ) + return NULL; + + const float MaxDot = 0.7f; + const float MaxRange = 4000.0f; + TargetScan< CBaseCombatCharacter > scan( picker, TEAM_ANY, 1.0f - MaxDot, MaxRange ); + ForEachCombatCharacter( scan ); + CBaseCombatCharacter *target = scan.GetTarget(); + if ( target && target->MyNextBotPointer() ) + return target->MyNextBotPointer(); + + return NULL; +} + +#ifdef NEED_BLACK_BOX +//--------------------------------------------------------------------------------------------- +CON_COMMAND( nb_dump_debug_history, "Dumps debug history for the bot under the cursor to the blackbox" ) +{ + if ( !NextBotDebugHistory.GetBool() ) + { + BlackBox_Record( "bot", "nb_debug_history 0" ); + return; + } + + CBasePlayer *player = UTIL_GetCommandClient(); + if ( !player ) + { + player = UTIL_GetListenServerHost(); + } + INextBot *bot = TheNextBots().GetBotUnderCrosshair( player ); + if ( !bot ) + { + BlackBox_Record( "bot", "no bot under crosshairs" ); + return; + } + + CUtlVector< const INextBot::NextBotDebugLineType * > lines; + bot->GetDebugHistory( (NEXTBOT_DEBUG_ALL & (~NEXTBOT_EVENTS)), &lines ); + + for ( int i=0; i<lines.Count(); ++i ) + { + if ( IsPC() ) + { + BlackBox_Record( "bot", "%s", lines[i]->data ); + } + } +} +#endif // NEED_BLACK_BOX + + +//--------------------------------------------------------------------------------------------- +void NextBotManager::CollectAllBots( CUtlVector< INextBot * > *botVector ) +{ + if ( !botVector ) + return; + + botVector->RemoveAll(); + + for( int i=m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) ) + { + botVector->AddToTail( m_botList[i] ); + } +} + diff --git a/game/server/NextBot/NextBotManager.h b/game/server/NextBot/NextBotManager.h new file mode 100644 index 0000000..197b2ad --- /dev/null +++ b/game/server/NextBot/NextBotManager.h @@ -0,0 +1,211 @@ +// NextBotManager.h +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_MANAGER_H_ +#define _NEXT_BOT_MANAGER_H_ + +#include "NextBotInterface.h" + +class CTerrorPlayer; + +//---------------------------------------------------------------------------------------------------------------- +/** + * The NextBotManager manager + */ +class NextBotManager +{ +public: + NextBotManager( void ); + virtual ~NextBotManager(); + + void Reset( void ); // reset to initial state + virtual void Update( void ); + + bool ShouldUpdate( INextBot *bot ); + void NotifyBeginUpdate( INextBot *bot ); + void NotifyEndUpdate( INextBot *bot ); + + int GetNextBotCount( void ) const; // How many nextbots are alive right now? + + + /** + * Populate given vector with all bots in the system + */ + void CollectAllBots( CUtlVector< INextBot * > *botVector ); + + + /** + * DEPRECATED: Use CollectAllBots(). + * Execute functor for each NextBot in the system. + * If a functor returns false, stop iteration early + * and return false. + */ + template < typename Functor > + bool ForEachBot( Functor &func ) + { + for( int i=m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) ) + { + if ( !func( m_botList[i] ) ) + { + return false; + } + } + + return true; + } + + /** + * DEPRECATED: Use CollectAllBots(). + * Execute functor for each NextBot in the system as + * a CBaseCombatCharacter. + * If a functor returns false, stop iteration early + * and return false. + */ + template < typename Functor > + bool ForEachCombatCharacter( Functor &func ) + { + for( int i=m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) ) + { + if ( !func( m_botList[i]->GetEntity() ) ) + { + + return false; + } + } + + return true; + } + + /** + * Return closest bot to given point that passes the given filter + */ + template < typename Filter > + INextBot *GetClosestBot( const Vector &pos, Filter &filter ) + { + INextBot *close = NULL; + float closeRangeSq = FLT_MAX; + + for( int i=m_botList.Head(); i != m_botList.InvalidIndex(); i = m_botList.Next( i ) ) + { + float rangeSq = ( m_botList[i]->GetEntity()->GetAbsOrigin() - pos ).LengthSqr(); + if ( rangeSq < closeRangeSq && filter( m_botList[i] ) ) + { + closeRangeSq = rangeSq; + close = m_botList[i]; + } + } + + return close; + } + + /** + * Event propagators + */ + virtual void OnMapLoaded( void ); // when the server has changed maps + virtual void OnRoundRestart( void ); // when the scenario restarts + virtual void OnBeginChangeLevel( void ); // when the server is about to change maps + virtual void OnKilled( CBaseCombatCharacter *victim, const CTakeDamageInfo &info ); // when an actor is killed + virtual void OnSound( CBaseEntity *source, const Vector &pos, KeyValues *keys ); // when an entity emits a sound + virtual void OnSpokeConcept( CBaseCombatCharacter *who, AIConcept_t concept, AI_Response *response ); // when an Actor speaks a concept + virtual void OnWeaponFired( CBaseCombatCharacter *whoFired, CBaseCombatWeapon *weapon ); // when someone fires a weapon + + /** + * Debugging + */ + bool IsDebugging( unsigned int type ) const; // return true if debugging system is on for the given type(s) + void SetDebugTypes( NextBotDebugType type ); // start displaying debug info of the given type(s) + + void DebugFilterAdd( int index ); // add given entindex to the debug filter + void DebugFilterAdd( const char *name ); // add given name to the debug filter + void DebugFilterRemove( int index ); // remove given entindex from the debug filter + void DebugFilterRemove( const char *name ); // remove given name from the debug filter + void DebugFilterClear( void ); // clear the debug filter (remove all entries) + bool IsDebugFilterMatch( const INextBot *bot ) const; // return true if the given bot matches the debug filter + + void Select( INextBot *bot ); // mark bot as selected for further operations + void DeselectAll( void ); + INextBot *GetSelected( void ) const; + + INextBot *GetBotUnderCrosshair( CBasePlayer *picker ); // Get the bot under the given player's crosshair + + // + // Put these in a derived class + // + void OnSurvivorVomitedUpon( CTerrorPlayer *victim ); // when a Survivor has been hit by Boomer Vomit + + static void SetInstance( NextBotManager *pInstance ) { sInstance = pInstance; }; + static NextBotManager* GetInstance() { return sInstance; } + +protected: + static NextBotManager* sInstance; + + friend class INextBot; + + int Register( INextBot *bot ); + void UnRegister( INextBot *bot ); + + CUtlLinkedList< INextBot * > m_botList; // list of all active NextBots + + int m_iUpdateTickrate; + double m_CurUpdateStartTime; + double m_SumFrameTime; + + unsigned int m_debugType; // debug flags + + struct DebugFilter + { + int index; // entindex + enum { MAX_DEBUG_NAME_SIZE = 128 }; + char name[ MAX_DEBUG_NAME_SIZE ]; + }; + CUtlVector< DebugFilter > m_debugFilterList; + + INextBot *m_selectedBot; // selected bot for further debug operations +}; + +inline int NextBotManager::GetNextBotCount( void ) const +{ + return m_botList.Count(); +} + +inline bool NextBotManager::IsDebugging( unsigned int type ) const +{ + if ( type & m_debugType ) + { + return true; + } + + return false; +} + + +inline void NextBotManager::SetDebugTypes( NextBotDebugType type ) +{ + m_debugType = (unsigned int)type; +} + + +inline void NextBotManager::Select( INextBot *bot ) +{ + m_selectedBot = bot; +} + +inline void NextBotManager::DeselectAll( void ) +{ + m_selectedBot = NULL; +} + +inline INextBot *NextBotManager::GetSelected( void ) const +{ + return m_selectedBot; +} + + + +// singleton accessor +extern NextBotManager &TheNextBots( void ); + + +#endif // _NEXT_BOT_MANAGER_H_ + diff --git a/game/server/NextBot/NextBotUtil.h b/game/server/NextBot/NextBotUtil.h new file mode 100644 index 0000000..75a4221 --- /dev/null +++ b/game/server/NextBot/NextBotUtil.h @@ -0,0 +1,278 @@ +// NextBotUtil.h +// Utilities for the NextBot system +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_UTIL_H_ +#define _NEXT_BOT_UTIL_H_ + +#include "NextBotLocomotionInterface.h" +#include "nav_area.h" +#include "nav_mesh.h" +#include "nav_pathfind.h" + +//-------------------------------------------------------------------------------------------- +/** + * A simple filter interface for various NextBot queries + */ +class INextBotEntityFilter +{ +public: + // return true if the given entity passes this filter + virtual bool IsAllowed( CBaseEntity *entity ) const = 0; +}; + + +// trace filter callback functions. needed for use with the querycache/optimization functionality +bool VisionTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask ); +bool IgnoreActorsTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask ); + + +//-------------------------------------------------------------------------------------------- +/** + * Trace filter that skips all players and NextBots + */ +class NextBotTraceFilterIgnoreActors : public CTraceFilterSimple +{ +public: + NextBotTraceFilterIgnoreActors( const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup, IgnoreActorsTraceFilterFunction ) + { + } +}; + + +//-------------------------------------------------------------------------------------------- +/** + * Trace filter that skips all players, NextBots, and non-LOS blockers + */ +class NextBotVisionTraceFilter : public CTraceFilterSimple +{ +public: + NextBotVisionTraceFilter( const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup, VisionTraceFilterFunction ) + { + } +}; + + +//-------------------------------------------------------------------------------------------- +/** + * Trace filter that skips all NextBots, but includes Players + */ +class NextBotTraceFilterIgnoreNextBots : public CTraceFilterSimple +{ +public: + NextBotTraceFilterIgnoreNextBots( const IHandleEntity *passentity, int collisionGroup ) + : CTraceFilterSimple( passentity, collisionGroup ) + { + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) ) + { + CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); +#ifdef TERROR + CBasePlayer *player = ToBasePlayer( entity ); + if ( player && player->IsGhost() ) + return false; +#endif // TERROR + + return ( entity->MyNextBotPointer() == NULL ); + } + return false; + } +}; + + +//-------------------------------------------------------------------------------------------- +/** + * Trace filter that obeys INextBot::IsAbleToBlockMovementOf() + */ +class NextBotTraceFilter : public CTraceFilterSimple +{ +public: + NextBotTraceFilter( const IHandleEntity *passentity, int collisionGroup ) + : CTraceFilterSimple( passentity, collisionGroup ) + { + CBaseEntity *entity = const_cast<CBaseEntity *>(EntityFromEntityHandle( passentity )); + m_passBot = entity->MyNextBotPointer(); + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) ) + { + CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); +#ifdef TERROR + CBasePlayer *player = ToBasePlayer( entity ); + if ( player && player->IsGhost() ) + return false; +#endif // TERROR + + // Skip players on the same team - they're not solid to us, and we'll avoid them + if ( entity->IsPlayer() && m_passBot && m_passBot->GetEntity() && + m_passBot->GetEntity()->GetTeamNumber() == entity->GetTeamNumber() ) + return false; + + INextBot *bot = entity->MyNextBotPointer(); + + return ( !bot || bot->IsAbleToBlockMovementOf( m_passBot ) ); + } + return false; + } + + const INextBot *m_passBot; +}; + + +//-------------------------------------------------------------------------------------------- +/** + * Trace filter that only hits players and NextBots + */ +class NextBotTraceFilterOnlyActors : public CTraceFilterSimple +{ +public: + NextBotTraceFilterOnlyActors( const IHandleEntity *passentity, int collisionGroup ) + : CTraceFilterSimple( passentity, collisionGroup ) + { + } + + virtual TraceType_t GetTraceType() const + { + return TRACE_ENTITIES_ONLY; + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) ) + { + CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); + +#ifdef TERROR + CBasePlayer *player = ToBasePlayer( entity ); + if ( player && player->IsGhost() ) + return false; +#endif // TERROR + + return ( entity->MyNextBotPointer() || entity->IsPlayer() ); + } + return false; + } +}; + + +//-------------------------------------------------------------------------------------------- +/** + * Trace filter that skips "traversable" entities. The "when" argument creates + * a temporal context for asking if an entity is IMMEDIATELY traversable (like thin + * glass that just breaks as we walk through it) or EVENTUALLY traversable (like a + * breakable object that will take some time to break through) + */ +class NextBotTraversableTraceFilter : public CTraceFilterSimple +{ +public: + NextBotTraversableTraceFilter( INextBot *bot, ILocomotion::TraverseWhenType when = ILocomotion::EVENTUALLY ) : CTraceFilterSimple( bot->GetEntity(), COLLISION_GROUP_NONE ) + { + m_bot = bot; + m_when = when; + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); + + if ( m_bot->IsSelf( entity ) ) + { + return false; + } + + if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) ) + { + return !m_bot->GetLocomotionInterface()->IsEntityTraversable( entity, m_when ); + } + + return false; + } + +private: + INextBot *m_bot; + ILocomotion::TraverseWhenType m_when; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * Given a vector of entities, a nav area, and a max travel distance, return + * the entity that has the shortest travel distance. + */ +inline CBaseEntity *SelectClosestEntityByTravelDistance( INextBot *me, const CUtlVector< CBaseEntity * > &candidateEntities, CNavArea *startArea, float travelRange ) +{ + // collect nearby walkable areas within travelRange + CUtlVector< CNavArea * > nearbyAreaVector; + CollectSurroundingAreas( &nearbyAreaVector, startArea, travelRange, me->GetLocomotionInterface()->GetStepHeight(), me->GetLocomotionInterface()->GetDeathDropHeight() ); + + // find closest entity in the collected area set + CBaseEntity *closeEntity = NULL; + float closeTravelRange = FLT_MAX; + + for( int i=0; i<candidateEntities.Count(); ++i ) + { + CBaseEntity *candidate = candidateEntities[i]; + + CNavArea *area = TheNavMesh->GetNearestNavArea( candidate, GETNAVAREA_CHECK_LOS, 500.0f ); + + if ( area && area->IsMarked() && area->GetCostSoFar() < closeTravelRange ) + { + closeEntity = candidate; + closeTravelRange = area->GetCostSoFar(); + } + } + + return closeEntity; +} + + +#ifdef OBSOLETE +//-------------------------------------------------------------------------------------------- +/** + * Trace filter that skips "traversable" entities, but hits other Actors. + * Used for obstacle avoidance. + */ +class NextBotMovementAvoidanceTraceFilter : public CTraceFilterSimple +{ +public: + NextBotMovementAvoidanceTraceFilter( INextBot *bot ) : CTraceFilterSimple( bot->GetEntity(), COLLISION_GROUP_NONE ) + { + m_bot = bot; + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); + +#ifdef TERROR + CBasePlayer *player = ToBasePlayer( entity ); + if ( player && player->IsGhost() ) + return false; +#endif // TERROR + + if ( m_bot->IsSelf( entity ) ) + { + return false; + } + + if ( CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ) ) + { + return !m_bot->GetLocomotionInterface()->IsEntityTraversable( entity, ILocomotion::IMMEDIATELY ); + } + + return false; + } + +private: + INextBot *m_bot; +}; +#endif + + +#endif // _NEXT_BOT_UTIL_H_ diff --git a/game/server/NextBot/NextBotVisionInterface.cpp b/game/server/NextBot/NextBotVisionInterface.cpp new file mode 100644 index 0000000..1c9d661 --- /dev/null +++ b/game/server/NextBot/NextBotVisionInterface.cpp @@ -0,0 +1,802 @@ +// NextBotVisionInterface.cpp +// Implementation of common vision system +// Author: Michael Booth, May 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "nav.h" +#include "functorutils.h" + +#include "NextBot.h" +#include "NextBotVisionInterface.h" +#include "NextBotBodyInterface.h" +#include "NextBotUtil.h" + +#ifdef TERROR +#include "querycache.h" +#endif + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +ConVar nb_blind( "nb_blind", "0", FCVAR_CHEAT, "Disable vision" ); +ConVar nb_debug_known_entities( "nb_debug_known_entities", "0", FCVAR_CHEAT, "Show the 'known entities' for the bot that is the current spectator target" ); + + +//------------------------------------------------------------------------------------------ +IVision::IVision( INextBot *bot ) : INextBotComponent( bot ) +{ + Reset(); +} + + +//------------------------------------------------------------------------------------------ +/** + * Reset to initial state + */ +void IVision::Reset( void ) +{ + INextBotComponent::Reset(); + + m_knownEntityVector.RemoveAll(); + m_lastVisionUpdateTimestamp = 0.0f; + m_primaryThreat = NULL; + + m_FOV = GetDefaultFieldOfView(); + m_cosHalfFOV = cos( 0.5f * m_FOV * M_PI / 180.0f ); + + for( int i=0; i<MAX_TEAMS; ++i ) + { + m_notVisibleTimer[i].Invalidate(); + } +} + + +//------------------------------------------------------------------------------------------ +/** + * Ask the current behavior to select the most dangerous threat from + * our set of currently known entities + * TODO: Find a semantically better place for this to live. + */ +const CKnownEntity *IVision::GetPrimaryKnownThreat( bool onlyVisibleThreats ) const +{ + if ( m_knownEntityVector.Count() == 0 ) + return NULL; + + const CKnownEntity *threat = NULL; + int i; + + // find the first valid entity + for( i=0; i<m_knownEntityVector.Count(); ++i ) + { + const CKnownEntity &firstThreat = m_knownEntityVector[i]; + + // check in case status changes between updates + if ( IsAwareOf( firstThreat ) && !firstThreat.IsObsolete() && !IsIgnored( firstThreat.GetEntity() ) && GetBot()->IsEnemy( firstThreat.GetEntity() ) ) + { + if ( !onlyVisibleThreats || firstThreat.IsVisibleRecently() ) + { + threat = &firstThreat; + break; + } + } + } + + if ( threat == NULL ) + { + m_primaryThreat = NULL; + return NULL; + } + + for( ++i; i<m_knownEntityVector.Count(); ++i ) + { + const CKnownEntity &newThreat = m_knownEntityVector[i]; + + // check in case status changes between updates + if ( IsAwareOf( newThreat ) && !newThreat.IsObsolete() && !IsIgnored( newThreat.GetEntity() ) && GetBot()->IsEnemy( newThreat.GetEntity() ) ) + { + if ( !onlyVisibleThreats || newThreat.IsVisibleRecently() ) + { + threat = GetBot()->GetIntentionInterface()->SelectMoreDangerousThreat( GetBot(), GetBot()->GetEntity(), threat, &newThreat ); + } + } + } + + // cache off threat + m_primaryThreat = threat ? threat->GetEntity() : NULL; + + return threat; +} + + +//------------------------------------------------------------------------------------------ +/** + * Return the closest recognized entity + */ +const CKnownEntity *IVision::GetClosestKnown( int team ) const +{ + const Vector &myPos = GetBot()->GetPosition(); + + const CKnownEntity *close = NULL; + float closeRange = 999999999.9f; + + for( int i=0; i < m_knownEntityVector.Count(); ++i ) + { + const CKnownEntity &known = m_knownEntityVector[i]; + + if ( !known.IsObsolete() && IsAwareOf( known ) ) + { + if ( team == TEAM_ANY || known.GetEntity()->GetTeamNumber() == team ) + { + Vector to = known.GetLastKnownPosition() - myPos; + float rangeSq = to.LengthSqr(); + + if ( rangeSq < closeRange ) + { + close = &known; + closeRange = rangeSq; + } + } + } + } + + return close; +} + + +//------------------------------------------------------------------------------------------ +/** + * Return the closest recognized entity that passes the given filter + */ +const CKnownEntity *IVision::GetClosestKnown( const INextBotEntityFilter &filter ) const +{ + const Vector &myPos = GetBot()->GetPosition(); + + const CKnownEntity *close = NULL; + float closeRange = 999999999.9f; + + for( int i=0; i < m_knownEntityVector.Count(); ++i ) + { + const CKnownEntity &known = m_knownEntityVector[i]; + + if ( !known.IsObsolete() && IsAwareOf( known ) ) + { + if ( filter.IsAllowed( known.GetEntity() ) ) + { + Vector to = known.GetLastKnownPosition() - myPos; + float rangeSq = to.LengthSqr(); + + if ( rangeSq < closeRange ) + { + close = &known; + closeRange = rangeSq; + } + } + } + } + + return close; +} + + +//------------------------------------------------------------------------------------------ +/** + * Given an entity, return our known version of it (or NULL if we don't know of it) + */ +const CKnownEntity *IVision::GetKnown( const CBaseEntity *entity ) const +{ + if ( entity == NULL ) + return NULL; + + for( int i=0; i < m_knownEntityVector.Count(); ++i ) + { + const CKnownEntity &known = m_knownEntityVector[i]; + + if ( known.GetEntity() && known.GetEntity()->entindex() == entity->entindex() && !known.IsObsolete() ) + { + return &known; + } + } + + return NULL; +} + + +//------------------------------------------------------------------------------------------ +/** + * Introduce a known entity into the system. Its position is assumed to be known + * and will be updated, and it is assumed to not yet have been seen by us, allowing for learning + * of known entities by being told about them, hearing them, etc. + */ +void IVision::AddKnownEntity( CBaseEntity *entity ) +{ + if ( entity == NULL || entity->IsWorld() ) + { + // the world is not an entity we can deal with + return; + } + + CKnownEntity known( entity ); + + // only add it if we don't already know of it + if ( m_knownEntityVector.Find( known ) == m_knownEntityVector.InvalidIndex() ) + { + m_knownEntityVector.AddToTail( known ); + } +} + + +//------------------------------------------------------------------------------------------ +// Remove the given entity from our awareness (whether we know if it or not) +// Useful if we've moved to where we last saw the entity, but it's not there any longer. +void IVision::ForgetEntity( CBaseEntity *forgetMe ) +{ + if ( !forgetMe ) + return; + + FOR_EACH_VEC( m_knownEntityVector, it ) + { + const CKnownEntity &known = m_knownEntityVector[ it ]; + + if ( known.GetEntity() && known.GetEntity()->entindex() == forgetMe->entindex() ) + { + m_knownEntityVector.FastRemove( it ); + return; + } + } +} + + +//------------------------------------------------------------------------------------------ +void IVision::ForgetAllKnownEntities( void ) +{ + m_knownEntityVector.RemoveAll(); +} + + +//------------------------------------------------------------------------------------------ +/** + * Return the number of entity on the given team known to us closer than rangeLimit + */ +int IVision::GetKnownCount( int team, bool onlyVisible, float rangeLimit ) const +{ + int count = 0; + + FOR_EACH_VEC( m_knownEntityVector, it ) + { + const CKnownEntity &known = m_knownEntityVector[ it ]; + + if ( !known.IsObsolete() && IsAwareOf( known ) ) + { + if ( team == TEAM_ANY || known.GetEntity()->GetTeamNumber() == team ) + { + if ( !onlyVisible || known.IsVisibleRecently() ) + { + if ( rangeLimit < 0.0f || GetBot()->IsRangeLessThan( known.GetLastKnownPosition(), rangeLimit ) ) + { + ++count; + } + } + } + } + } + + return count; +} + + +//------------------------------------------------------------------------------------------ +class PopulateVisibleVector +{ +public: + PopulateVisibleVector( CUtlVector< CBaseEntity * > *potentiallyVisible ) + { + m_potentiallyVisible = potentiallyVisible; + } + + bool operator() ( CBaseEntity *actor ) + { + m_potentiallyVisible->AddToTail( actor ); + return true; + } + + CUtlVector< CBaseEntity * > *m_potentiallyVisible; +}; + + +//------------------------------------------------------------------------------------------ +/** + * Populate "potentiallyVisible" with the set of all entities we could potentially see. + * Entities in this set will be tested for visibility/recognition in IVision::Update() + */ +void IVision::CollectPotentiallyVisibleEntities( CUtlVector< CBaseEntity * > *potentiallyVisible ) +{ + potentiallyVisible->RemoveAll(); + + // by default, only consider players and other bots as potentially visible + PopulateVisibleVector populate( potentiallyVisible ); + ForEachActor( populate ); +} + + +//------------------------------------------------------------------------------------------ +class CollectVisible +{ +public: + CollectVisible( IVision *vision ) + { + m_vision = vision; + } + + bool operator() ( CBaseEntity *entity ) + { + if ( entity && + !m_vision->IsIgnored( entity ) && + entity->IsAlive() && + entity != m_vision->GetBot()->GetEntity() && + m_vision->IsAbleToSee( entity, IVision::USE_FOV ) ) + { + m_recognized.AddToTail( entity ); + } + + return true; + } + + bool Contains( CBaseEntity *entity ) const + { + for( int i=0; i < m_recognized.Count(); ++i ) + { + if ( entity->entindex() == m_recognized[ i ]->entindex() ) + { + return true; + } + } + return false; + } + + IVision *m_vision; + CUtlVector< CBaseEntity * > m_recognized; +}; + + +//------------------------------------------------------------------------------------------ +void IVision::UpdateKnownEntities( void ) +{ + VPROF_BUDGET( "IVision::UpdateKnownEntities", "NextBot" ); + + // construct set of potentially visible objects + CUtlVector< CBaseEntity * > potentiallyVisible; + CollectPotentiallyVisibleEntities( &potentiallyVisible ); + + // collect set of visible and recognized entities at this moment + CollectVisible visibleNow( this ); + FOR_EACH_VEC( potentiallyVisible, pit ) + { + VPROF_BUDGET( "IVision::UpdateKnownEntities( collect visible )", "NextBot" ); + + if ( visibleNow( potentiallyVisible[ pit ] ) == false ) + break; + } + + // update known set with new data + { VPROF_BUDGET( "IVision::UpdateKnownEntities( update status )", "NextBot" ); + + int i; + for( i=0; i < m_knownEntityVector.Count(); ++i ) + { + CKnownEntity &known = m_knownEntityVector[i]; + + // clear out obsolete knowledge + if ( known.GetEntity() == NULL || known.IsObsolete() ) + { + m_knownEntityVector.Remove( i ); + --i; + continue; + } + + if ( visibleNow.Contains( known.GetEntity() ) ) + { + // this visible entity was already known (but perhaps not visible until now) + known.UpdatePosition(); + known.UpdateVisibilityStatus( true ); + + // has our reaction time just elapsed? + if ( gpGlobals->curtime - known.GetTimeWhenBecameVisible() >= GetMinRecognizeTime() && + m_lastVisionUpdateTimestamp - known.GetTimeWhenBecameVisible() < GetMinRecognizeTime() ) + { + if ( GetBot()->IsDebugging( NEXTBOT_VISION ) ) + { + ConColorMsg( Color( 0, 255, 0, 255 ), "%3.2f: %s caught sight of %s(#%d)\n", + gpGlobals->curtime, + GetBot()->GetDebugIdentifier(), + known.GetEntity()->GetClassname(), + known.GetEntity()->entindex() ); + + NDebugOverlay::Line( GetBot()->GetBodyInterface()->GetEyePosition(), known.GetLastKnownPosition(), 255, 255, 0, false, 0.2f ); + } + + GetBot()->OnSight( known.GetEntity() ); + } + + // restart 'not seen' timer + m_notVisibleTimer[ known.GetEntity()->GetTeamNumber() ].Start(); + } + else // known entity is not currently visible + { + if ( known.IsVisibleInFOVNow() ) + { + // previously known and visible entity is now no longer visible + known.UpdateVisibilityStatus( false ); + + // lost sight of this entity + if ( GetBot()->IsDebugging( NEXTBOT_VISION ) ) + { + ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Lost sight of %s(#%d)\n", + gpGlobals->curtime, + GetBot()->GetDebugIdentifier(), + known.GetEntity()->GetClassname(), + known.GetEntity()->entindex() ); + } + + GetBot()->OnLostSight( known.GetEntity() ); + } + + if ( !known.HasLastKnownPositionBeenSeen() ) + { + // can we see the entity's last know position? + if ( IsAbleToSee( known.GetLastKnownPosition(), IVision::USE_FOV ) ) + { + known.MarkLastKnownPositionAsSeen(); + } + } + } + } + } + + // check for new recognizes that were not in the known set + { VPROF_BUDGET( "IVision::UpdateKnownEntities( new recognizes )", "NextBot" ); + + int i, j; + for( i=0; i < visibleNow.m_recognized.Count(); ++i ) + { + for( j=0; j < m_knownEntityVector.Count(); ++j ) + { + if ( visibleNow.m_recognized[i] == m_knownEntityVector[j].GetEntity() ) + { + break; + } + } + + if ( j == m_knownEntityVector.Count() ) + { + // recognized a previously unknown entity (emit OnSight() event after reaction time has passed) + CKnownEntity known( visibleNow.m_recognized[i] ); + known.UpdatePosition(); + known.UpdateVisibilityStatus( true ); + m_knownEntityVector.AddToTail( known ); + } + } + } + + // debugging + if ( nb_debug_known_entities.GetBool() ) + { + CBasePlayer *watcher = UTIL_GetListenServerHost(); + if ( watcher ) + { + CBaseEntity *subject = watcher->GetObserverTarget(); + + if ( subject && GetBot()->IsSelf( subject ) ) + { + CUtlVector< CKnownEntity > knownVector; + CollectKnownEntities( &knownVector ); + + for( int i=0; i < knownVector.Count(); ++i ) + { + CKnownEntity &known = knownVector[i]; + + if ( GetBot()->IsFriend( known.GetEntity() ) ) + { + if ( IsAwareOf( known ) ) + { + if ( known.IsVisibleInFOVNow() ) + NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 5.0f, 0, 255, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + else + NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 2.0f, 0, 100, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 1.0f, 0, 100, 0, 128, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + else + { + if ( IsAwareOf( known ) ) + { + if ( known.IsVisibleInFOVNow() ) + NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 5.0f, 255, 0, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + else + NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 2.0f, 100, 0, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + NDebugOverlay::HorzArrow( GetBot()->GetEntity()->GetAbsOrigin(), known.GetLastKnownPosition(), 1.0f, 100, 0, 0, 128, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + } + } + } +} + + +//------------------------------------------------------------------------------------------ +/** + * Update internal state + */ +void IVision::Update( void ) +{ + VPROF_BUDGET( "IVision::Update", "NextBotExpensive" ); + +/* This adds significantly to bot's reaction times + // throttle update rate + if ( !m_scanTimer.IsElapsed() ) + { + return; + } + + m_scanTimer.Start( 0.5f * GetMinRecognizeTime() ); +*/ + + if ( nb_blind.GetBool() ) + { + m_knownEntityVector.RemoveAll(); + return; + } + + UpdateKnownEntities(); + + m_lastVisionUpdateTimestamp = gpGlobals->curtime; +} + + +//------------------------------------------------------------------------------------------ +bool IVision::IsAbleToSee( CBaseEntity *subject, FieldOfViewCheckType checkFOV, Vector *visibleSpot ) const +{ + VPROF_BUDGET( "IVision::IsAbleToSee", "NextBotExpensive" ); + + if ( GetBot()->IsRangeGreaterThan( subject, GetMaxVisionRange() ) ) + { + return false; + } + + + if ( GetBot()->GetEntity()->IsHiddenByFog( subject ) ) + { + // lost in the fog + return false; + } + + if ( checkFOV == USE_FOV && !IsInFieldOfView( subject ) ) + { + return false; + } + + CBaseCombatCharacter *combat = subject->MyCombatCharacterPointer(); + if ( combat ) + { + CNavArea *subjectArea = combat->GetLastKnownArea(); + CNavArea *myArea = GetBot()->GetEntity()->GetLastKnownArea(); + if ( myArea && subjectArea ) + { + if ( !myArea->IsPotentiallyVisible( subjectArea ) ) + { + // subject is not potentially visible, skip the expensive raycast + return false; + } + } + } + + // do actual line-of-sight trace + if ( !IsLineOfSightClearToEntity( subject ) ) + { + return false; + } + + return IsVisibleEntityNoticed( subject ); +} + + +//------------------------------------------------------------------------------------------ +bool IVision::IsAbleToSee( const Vector &pos, FieldOfViewCheckType checkFOV ) const +{ + VPROF_BUDGET( "IVision::IsAbleToSee", "NextBotExpensive" ); + + + if ( GetBot()->IsRangeGreaterThan( pos, GetMaxVisionRange() ) ) + { + return false; + } + + if ( GetBot()->GetEntity()->IsHiddenByFog( pos ) ) + { + // lost in the fog + return false; + } + + if ( checkFOV == USE_FOV && !IsInFieldOfView( pos ) ) + { + return false; + } + + // do actual line-of-sight trace + return IsLineOfSightClear( pos ); +} + + +//------------------------------------------------------------------------------------------ +/** + * Angle given in degrees + */ +void IVision::SetFieldOfView( float horizAngle ) +{ + m_FOV = horizAngle; + m_cosHalfFOV = cos( 0.5f * m_FOV * M_PI / 180.0f ); +} + + +//------------------------------------------------------------------------------------------ +bool IVision::IsInFieldOfView( const Vector &pos ) const +{ +#ifdef CHECK_OLD_CODE_AGAINST_NEW + bool bCheck = PointWithinViewAngle( GetBot()->GetBodyInterface()->GetEyePosition(), pos, GetBot()->GetBodyInterface()->GetViewVector(), m_cosHalfFOV ); + Vector to = pos - GetBot()->GetBodyInterface()->GetEyePosition(); + to.NormalizeInPlace(); + + float cosDiff = DotProduct( GetBot()->GetBodyInterface()->GetViewVector(), to ); + + if ( ( cosDiff > m_cosHalfFOV ) != bCheck ) + { + Assert(0); + bool bCheck2 = + PointWithinViewAngle( GetBot()->GetBodyInterface()->GetEyePosition(), pos, GetBot()->GetBodyInterface()->GetViewVector(), m_cosHalfFOV ); + + } + + return ( cosDiff > m_cosHalfFOV ); +#else + return PointWithinViewAngle( GetBot()->GetBodyInterface()->GetEyePosition(), pos, GetBot()->GetBodyInterface()->GetViewVector(), m_cosHalfFOV ); +#endif + + return true; +} + + +//------------------------------------------------------------------------------------------ +bool IVision::IsInFieldOfView( CBaseEntity *subject ) const +{ + /// @todo check more points + if ( IsInFieldOfView( subject->WorldSpaceCenter() ) ) + { + return true; + } + + return IsInFieldOfView( subject->EyePosition() ); +} + + +//------------------------------------------------------------------------------------------ +/** + * Return true if the ray to the given point is unobstructed + */ +bool IVision::IsLineOfSightClear( const Vector &pos ) const +{ + VPROF_BUDGET( "IVision::IsLineOfSightClear", "NextBot" ); + VPROF_INCREMENT_COUNTER( "IVision::IsLineOfSightClear", 1 ); + + trace_t result; + NextBotVisionTraceFilter filter( GetBot()->GetEntity(), COLLISION_GROUP_NONE ); + + UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), pos, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + + return ( result.fraction >= 1.0f && !result.startsolid ); +} + + +//------------------------------------------------------------------------------------------ +bool IVision::IsLineOfSightClearToEntity( const CBaseEntity *subject, Vector *visibleSpot ) const +{ +#ifdef TERROR + // TODO: Integration querycache & its dependencies + + VPROF_INCREMENT_COUNTER( "IVision::IsLineOfSightClearToEntity", 1 ); + VPROF_BUDGET( "IVision::IsLineOfSightClearToEntity", "NextBotSpiky" ); + + bool bClear = IsLineOfSightBetweenTwoEntitiesClear( GetBot()->GetBodyInterface()->GetEntity(), EOFFSET_MODE_EYEPOSITION, + subject, EOFFSET_MODE_WORLDSPACE_CENTER, + subject, COLLISION_GROUP_NONE, + MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, VisionTraceFilterFunction, 1.0 ); + +#ifdef USE_NON_CACHE_QUERY + trace_t result; + NextBotTraceFilterIgnoreActors filter( subject, COLLISION_GROUP_NONE ); + + UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->WorldSpaceCenter(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + Assert( result.DidHit() != bClear ); + if ( subject->IsPlayer() && ! bClear ) + { + UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->EyePosition(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + bClear = IsLineOfSightBetweenTwoEntitiesClear( GetBot()->GetEntity(), + EOFFSET_MODE_EYEPOSITION, + subject, EOFFSET_MODE_EYEPOSITION, + subject, COLLISION_GROUP_NONE, + MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, + IgnoreActorsTraceFilterFunction, 1.0 ); + + // this WILL assert - the query interface happens at a different time, and has hysteresis. + Assert( result.DidHit() != bClear ); + } +#endif + + return bClear; + +#else + + // TODO: Use plain-old traces until querycache/etc gets integrated + VPROF_BUDGET( "IVision::IsLineOfSightClearToEntity", "NextBot" ); + + trace_t result; + NextBotTraceFilterIgnoreActors filter( subject, COLLISION_GROUP_NONE ); + + UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->WorldSpaceCenter(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + if ( result.DidHit() ) + { + UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->EyePosition(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + + if ( result.DidHit() ) + { + UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), subject->GetAbsOrigin(), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result ); + } + } + + if ( visibleSpot ) + { + *visibleSpot = result.endpos; + } + + return ( result.fraction >= 1.0f && !result.startsolid ); + +#endif +} + + +//------------------------------------------------------------------------------------------ +/** + * Are we looking directly at the given position + */ +bool IVision::IsLookingAt( const Vector &pos, float cosTolerance ) const +{ + Vector to = pos - GetBot()->GetBodyInterface()->GetEyePosition(); + to.NormalizeInPlace(); + + Vector forward; + AngleVectors( GetBot()->GetEntity()->EyeAngles(), &forward ); + + return DotProduct( to, forward ) > cosTolerance; +} + + +//------------------------------------------------------------------------------------------ +/** + * Are we looking directly at the given actor + */ +bool IVision::IsLookingAt( const CBaseCombatCharacter *actor, float cosTolerance ) const +{ + return IsLookingAt( actor->EyePosition(), cosTolerance ); +} + + diff --git a/game/server/NextBot/NextBotVisionInterface.h b/game/server/NextBot/NextBotVisionInterface.h new file mode 100644 index 0000000..c6361f9 --- /dev/null +++ b/game/server/NextBot/NextBotVisionInterface.h @@ -0,0 +1,226 @@ +// NextBotVisionInterface.h +// Visual information query interface for bots +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_VISION_INTERFACE_H_ +#define _NEXT_BOT_VISION_INTERFACE_H_ + +#include "NextBotComponentInterface.h" +#include "NextBotKnownEntity.h" + +class IBody; +class INextBotEntityFilter; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for HOW the bot sees (near sighted? night vision? etc) + */ +class IVision : public INextBotComponent +{ +public: + IVision( INextBot *bot ); + virtual ~IVision() { } + + virtual void Reset( void ); // reset to initial state + virtual void Update( void ); // update internal state + + //-- attention/short term memory interface follows ------------------------------------------ + + // + // WARNING: Do not keep CKnownEntity pointers returned by these methods, as they can be invalidated/freed + // + + /** + * Iterate each interesting entity we are aware of. + * If functor returns false, stop iterating and return false. + * NOTE: known.GetEntity() is guaranteed to be non-NULL + */ + class IForEachKnownEntity + { + public: + virtual bool Inspect( const CKnownEntity &known ) = 0; + }; + virtual bool ForEachKnownEntity( IForEachKnownEntity &func ); + + virtual void CollectKnownEntities( CUtlVector< CKnownEntity > *knownVector ); // populate given vector with all currently known entities + + virtual const CKnownEntity *GetPrimaryKnownThreat( bool onlyVisibleThreats = false ) const; // return the biggest threat to ourselves that we are aware of + virtual float GetTimeSinceVisible( int team ) const; // return time since we saw any member of the given team + + virtual const CKnownEntity *GetClosestKnown( int team = TEAM_ANY ) const; // return the closest known entity + virtual int GetKnownCount( int team, bool onlyVisible = false, float rangeLimit = -1.0f ) const; // return the number of entities on the given team known to us closer than rangeLimit + + virtual const CKnownEntity *GetClosestKnown( const INextBotEntityFilter &filter ) const; // return the closest known entity that passes the given filter + + virtual const CKnownEntity *GetKnown( const CBaseEntity *entity ) const; // given an entity, return our known version of it (or NULL if we don't know of it) + + // Introduce a known entity into the system. Its position is assumed to be known + // and will be updated, and it is assumed to not yet have been seen by us, allowing for learning + // of known entities by being told about them, hearing them, etc. + virtual void AddKnownEntity( CBaseEntity *entity ); + + virtual void ForgetEntity( CBaseEntity *forgetMe ); // remove the given entity from our awareness (whether we know if it or not) + virtual void ForgetAllKnownEntities( void ); + + //-- physical vision interface follows ------------------------------------------------------ + + /** + * Populate "potentiallyVisible" with the set of all entities we could potentially see. + * Entities in this set will be tested for visibility/recognition in IVision::Update() + */ + virtual void CollectPotentiallyVisibleEntities( CUtlVector< CBaseEntity * > *potentiallyVisible ); + + virtual float GetMaxVisionRange( void ) const; // return maximum distance vision can reach + virtual float GetMinRecognizeTime( void ) const; // return VISUAL reaction time + + /** + * IsAbleToSee() returns true if the viewer can ACTUALLY SEE the subject or position, + * taking into account blindness, smoke effects, invisibility, etc. + * If 'visibleSpot' is non-NULL, the highest priority spot on the subject that is visible is returned. + */ + enum FieldOfViewCheckType { USE_FOV, DISREGARD_FOV }; + virtual bool IsAbleToSee( CBaseEntity *subject, FieldOfViewCheckType checkFOV, Vector *visibleSpot = NULL ) const; + virtual bool IsAbleToSee( const Vector &pos, FieldOfViewCheckType checkFOV ) const; + + virtual bool IsIgnored( CBaseEntity *subject ) const; // return true to completely ignore this entity (may not be in sight when this is called) + virtual bool IsVisibleEntityNoticed( CBaseEntity *subject ) const; // return true if we 'notice' the subject, even though we have LOS to it + + /** + * Check if 'subject' is within the viewer's field of view + */ + virtual bool IsInFieldOfView( const Vector &pos ) const; + virtual bool IsInFieldOfView( CBaseEntity *subject ) const; + virtual float GetDefaultFieldOfView( void ) const; // return default FOV in degrees + virtual float GetFieldOfView( void ) const; // return FOV in degrees + virtual void SetFieldOfView( float horizAngle ); // angle given in degrees + + virtual bool IsLineOfSightClear( const Vector &pos ) const; // return true if the ray to the given point is unobstructed + + /** + * Returns true if the ray between the position and the subject is unobstructed. + * A visible spot on the subject is returned in 'visibleSpot'. + */ + virtual bool IsLineOfSightClearToEntity( const CBaseEntity *subject, Vector *visibleSpot = NULL ) const; + + /// @todo: Implement LookAt system + virtual bool IsLookingAt( const Vector &pos, float cosTolerance = 0.95f ) const; // are we looking at the given position + virtual bool IsLookingAt( const CBaseCombatCharacter *actor, float cosTolerance = 0.95f ) const; // are we looking at the given actor + +private: + CountdownTimer m_scanTimer; // for throttling update rate + + float m_FOV; // current FOV in degrees + float m_cosHalfFOV; // the cosine of FOV/2 + + CUtlVector< CKnownEntity > m_knownEntityVector; // the set of enemies/friends we are aware of + void UpdateKnownEntities( void ); + bool IsAwareOf( const CKnownEntity &known ) const; // return true if our reaction time has passed for this entity + mutable CHandle< CBaseEntity > m_primaryThreat; + + float m_lastVisionUpdateTimestamp; + IntervalTimer m_notVisibleTimer[ MAX_TEAMS ]; // for tracking interval since last saw a member of the given team +}; + +inline void IVision::CollectKnownEntities( CUtlVector< CKnownEntity > *knownVector ) +{ + if ( knownVector ) + { + knownVector->RemoveAll(); + + for( int i=0; i<m_knownEntityVector.Count(); ++i ) + { + if ( !m_knownEntityVector[i].IsObsolete() ) + { + knownVector->AddToTail( m_knownEntityVector[i] ); + } + } + } +} + +inline float IVision::GetDefaultFieldOfView( void ) const +{ + return 90.0f; +} + +inline float IVision::GetFieldOfView( void ) const +{ + return m_FOV; +} + + +inline float IVision::GetTimeSinceVisible( int team ) const +{ + if ( team == TEAM_ANY ) + { + // return minimum time + float time = 9999999999.9f; + for( int i=0; i<MAX_TEAMS; ++i ) + { + if ( m_notVisibleTimer[i].HasStarted() ) + { + if ( time > m_notVisibleTimer[i].GetElapsedTime() ) + { + team = m_notVisibleTimer[i].GetElapsedTime(); + } + } + } + return time; + } + + if ( team >= 0 && team < MAX_TEAMS ) + { + return m_notVisibleTimer[ team ].GetElapsedTime(); + } + + return 0.0f; +} + + +inline bool IVision::IsAwareOf( const CKnownEntity &known ) const +{ + return known.GetTimeSinceBecameKnown() >= GetMinRecognizeTime(); +} + + +inline bool IVision::ForEachKnownEntity( IVision::IForEachKnownEntity &func ) +{ + for( int i=0; i<m_knownEntityVector.Count(); ++i ) + { + const CKnownEntity &known = m_knownEntityVector[i]; + + if ( !known.IsObsolete() && IsAwareOf( known ) ) + { + if ( func.Inspect( known ) == false ) + { + return false; + } + } + } + + return true; +} + +inline bool IVision::IsVisibleEntityNoticed( CBaseEntity *subject ) const +{ + return true; +} + +inline bool IVision::IsIgnored( CBaseEntity *subject ) const +{ + return false; +} + +inline float IVision::GetMaxVisionRange( void ) const +{ + return 2000.0f; +} + +inline float IVision::GetMinRecognizeTime( void ) const +{ + return 0.0f; +} + + +#endif // _NEXT_BOT_VISION_INTERFACE_H_ diff --git a/game/server/NextBot/Path/NextBotChasePath.cpp b/game/server/NextBot/Path/NextBotChasePath.cpp new file mode 100644 index 0000000..1674fc3 --- /dev/null +++ b/game/server/NextBot/Path/NextBotChasePath.cpp @@ -0,0 +1,166 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" + +#include "NextBotChasePath.h" +#include "tier1/fmtstr.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//---------------------------------------------------------------------------------------------- +/** + * Try to cutoff our chase subject + */ +Vector ChasePath::PredictSubjectPosition( INextBot *bot, CBaseEntity *subject ) const +{ + ILocomotion *mover = bot->GetLocomotionInterface(); + + const Vector &subjectPos = subject->GetAbsOrigin(); + + Vector to = subjectPos - bot->GetPosition(); + to.z = 0.0f; + float flRangeSq = to.LengthSqr(); + + // don't lead if subject is very far away + float flLeadRadiusSq = GetLeadRadius(); + flLeadRadiusSq *= flLeadRadiusSq; + if ( flRangeSq > flLeadRadiusSq ) + return subjectPos; + + // Normalize in place + float range = sqrt( flRangeSq ); + to /= ( range + 0.0001f ); // avoid divide by zero + + // estimate time to reach subject, assuming maximum speed + float leadTime = 0.5f + ( range / ( mover->GetRunSpeed() + 0.0001f ) ); + + // estimate amount to lead the subject + Vector lead = leadTime * subject->GetAbsVelocity(); + lead.z = 0.0f; + + if ( DotProduct( to, lead ) < 0.0f ) + { + // the subject is moving towards us - only pay attention + // to his perpendicular velocity for leading + Vector2D to2D = to.AsVector2D(); + to2D.NormalizeInPlace(); + + Vector2D perp( -to2D.y, to2D.x ); + + float enemyGroundSpeed = lead.x * perp.x + lead.y * perp.y; + + lead.x = enemyGroundSpeed * perp.x; + lead.y = enemyGroundSpeed * perp.y; + } + + // compute our desired destination + Vector pathTarget = subjectPos + lead; + + // validate this destination + + // don't lead through walls + if ( lead.LengthSqr() > 36.0f ) + { + float fraction; + if ( !mover->IsPotentiallyTraversable( subjectPos, pathTarget, ILocomotion::IMMEDIATELY, &fraction ) ) + { + // tried to lead through an unwalkable area - clip to walkable space + pathTarget = subjectPos + fraction * ( pathTarget - subjectPos ); + } + } + + // don't lead over cliffs + CNavArea *leadArea = NULL; + +#ifdef NEED_GPGLOBALS_SERVERCOUNT_TO_DO_THIS + CBaseCombatCharacter *pBCC = subject->MyCombatCharacterPointer(); + if ( pBCC && CloseEnough( pathTarget, subjectPos, 3.0 ) ) + { + pathTarget = subjectPos; + leadArea = pBCC->GetLastKnownArea(); // can return null? + } + else + { + struct CacheEntry_t + { + CacheEntry_t() : pArea(NULL) {} + Vector target; + CNavArea *pArea; + }; + + static int iServer; + static CacheEntry_t cache[4]; + static int iNext; + int i; + + bool bFound = false; + if ( iServer != gpGlobals->serverCount ) + { + for ( i = 0; i < ARRAYSIZE(cache); i++ ) + { + cache[i].pArea = NULL; + } + iServer = gpGlobals->serverCount; + } + else + { + for ( i = 0; i < ARRAYSIZE(cache); i++ ) + { + if ( cache[i].pArea && CloseEnough( cache[i].target, pathTarget, 2.0 ) ) + { + pathTarget = cache[i].target; + leadArea = cache[i].pArea; + bFound = true; + break; + } + } + } + + if ( !bFound ) + { + leadArea = TheNavMesh->GetNearestNavArea( pathTarget ); + if ( leadArea ) + { + cache[iNext].target = pathTarget; + cache[iNext].pArea = leadArea; + iNext = ( iNext + 1 ) % ARRAYSIZE( cache ); + } + } + } +#else + leadArea = TheNavMesh->GetNearestNavArea( pathTarget ); +#endif + + + if ( !leadArea || leadArea->GetZ( pathTarget.x, pathTarget.y ) < pathTarget.z - mover->GetMaxJumpHeight() ) + { + // would fall off a cliff + return subjectPos; + } + + /** This needs more thought - it is preventing bots from using dropdowns + if ( mover->HasPotentialGap( subjectPos, pathTarget, &fraction ) ) + { + // tried to lead over a cliff - clip to safe region + pathTarget = subjectPos + fraction * ( pathTarget - subjectPos ); + } + */ + + return pathTarget; +} + +// if the victim is a player, poke them so they know they're being chased +void DirectChasePath::NotifyVictim( INextBot *me, CBaseEntity *victim ) +{ + CBaseCombatCharacter *pBCCVictim = ToBaseCombatCharacter( victim ); + if ( !pBCCVictim ) + return; + + pBCCVictim->OnPursuedBy( me ); +}
\ No newline at end of file diff --git a/game/server/NextBot/Path/NextBotChasePath.h b/game/server/NextBot/Path/NextBotChasePath.h new file mode 100644 index 0000000..1929648 --- /dev/null +++ b/game/server/NextBot/Path/NextBotChasePath.h @@ -0,0 +1,376 @@ +// NextBotChasePath.h +// Maintain and follow a "chase path" to a selected Actor +// Author: Michael Booth, September 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_CHASE_PATH_ +#define _NEXT_BOT_CHASE_PATH_ + +#include "nav.h" +#include "NextBotInterface.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotChasePath.h" +#include "NextBotUtil.h" +#include "NextBotPathFollow.h" +#include "tier0/vprof.h" + + +//---------------------------------------------------------------------------------------------- +/** + * A ChasePath extends a PathFollower to periodically recompute a path to a chase + * subject, and to move along the path towards that subject. + */ +class ChasePath : public PathFollower +{ +public: + enum SubjectChaseType + { + LEAD_SUBJECT, + DONT_LEAD_SUBJECT + }; + ChasePath( SubjectChaseType chaseHow = DONT_LEAD_SUBJECT ); + + virtual ~ChasePath() { } + + virtual void Update( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos = NULL ); // update path to chase target and move bot along path + + virtual float GetLeadRadius( void ) const; // range where movement leading begins - beyond this just head right for the subject + virtual float GetMaxPathLength( void ) const; // return maximum path length + virtual Vector PredictSubjectPosition( INextBot *bot, CBaseEntity *subject ) const; // try to cutoff our chase subject, knowing our relative positions and velocities + virtual bool IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const; // return true if situation has changed enough to warrant recomputing the current path + + virtual float GetLifetime( void ) const; // Return duration this path is valid. Path will become invalid at its earliest opportunity once this duration elapses. Zero = infinite lifetime + + virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid + +private: + void RefreshPath( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos ); + + CountdownTimer m_failTimer; // throttle re-pathing if last path attempt failed + CountdownTimer m_throttleTimer; // require a minimum time between re-paths + CountdownTimer m_lifetimeTimer; + EHANDLE m_lastPathSubject; // the subject used to compute the current/last path + SubjectChaseType m_chaseHow; +}; + +inline ChasePath::ChasePath( SubjectChaseType chaseHow ) +{ + m_failTimer.Invalidate(); + m_throttleTimer.Invalidate(); + m_lifetimeTimer.Invalidate(); + m_lastPathSubject = NULL; + m_chaseHow = chaseHow; +} + +inline float ChasePath::GetLeadRadius( void ) const +{ + return 500.0f; // 1000.0f; +} + +inline float ChasePath::GetMaxPathLength( void ) const +{ + // no limit + return 0.0f; +} + +inline float ChasePath::GetLifetime( void ) const +{ + // infinite duration + return 0.0f; +} + +inline void ChasePath::Invalidate( void ) +{ + // path is gone, repath at earliest opportunity + m_throttleTimer.Invalidate(); + m_lifetimeTimer.Invalidate(); + + // extend + PathFollower::Invalidate(); +} + + + + +//---------------------------------------------------------------------------------------------- +/** + * Maintain a path to our chase subject and move along that path + */ +inline void ChasePath::Update( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos ) +{ + VPROF_BUDGET( "ChasePath::Update", "NextBot" ); + + // maintain the path to the subject + RefreshPath( bot, subject, cost, pPredictedSubjectPos ); + + // move along the path towards the subject + PathFollower::Update( bot ); +} + + +//---------------------------------------------------------------------------------------------- +/** + * Return true if situation has changed enough to warrant recomputing the current path + */ +inline bool ChasePath::IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const +{ + // the closer we get, the more accurate our path needs to be + Vector to = subject->GetAbsOrigin() - bot->GetPosition(); + + const float minTolerance = 0.0f; // 25.0f; + const float toleranceRate = 0.33f; // 1.0f; // 0.15f; + + float tolerance = minTolerance + toleranceRate * to.Length(); + + return ( subject->GetAbsOrigin() - GetEndPosition() ).IsLengthGreaterThan( tolerance ); +} + + +//---------------------------------------------------------------------------------------------- +/** + * Periodically rebuild the path to our victim + */ +inline void ChasePath::RefreshPath( INextBot *bot, CBaseEntity *subject, const IPathCost &cost, Vector *pPredictedSubjectPos ) +{ + VPROF_BUDGET( "ChasePath::RefreshPath", "NextBot" ); + + ILocomotion *mover = bot->GetLocomotionInterface(); + + // don't change our path if we're on a ladder + if ( IsValid() && mover->IsUsingLadder() ) + { + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Bot is on a ladder.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); + } + + // don't allow repath until a moment AFTER we have left the ladder + m_throttleTimer.Start( 1.0f ); + + return; + } + + if ( subject == NULL ) + { + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%3.2f: bot(#%d) CasePath::RefreshPath failed. No subject.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); + } + return; + } + + if ( !m_failTimer.IsElapsed() ) + { +// if ( bot->IsDebugging( NEXTBOT_PATH ) ) +// { +// DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Fail timer not elapsed.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); +// } + return; + } + + // if our path subject changed, repath immediately + if ( subject != m_lastPathSubject ) + { + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%3.2f: bot(#%d) Chase path subject changed (from %p to %p).\n", gpGlobals->curtime, bot->GetEntity()->entindex(), m_lastPathSubject.Get(), subject ); + } + + Invalidate(); + + // new subject, fresh attempt + m_failTimer.Invalidate(); + } + + if ( IsValid() && !m_throttleTimer.IsElapsed() ) + { + // require a minimum time between repaths, as long as we have a path to follow +// if ( bot->IsDebugging( NEXTBOT_PATH ) ) +// { +// DevMsg( "%3.2f: bot(#%d) ChasePath::RefreshPath failed. Rate throttled.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); +// } + return; + } + + if ( IsValid() && m_lifetimeTimer.HasStarted() && m_lifetimeTimer.IsElapsed() ) + { + // this path's lifetime has elapsed + Invalidate(); + } + + if ( !IsValid() || IsRepathNeeded( bot, subject ) ) + { + // the situation has changed - try a new path + bool isPath; + Vector pathTarget = subject->GetAbsOrigin(); + + if ( m_chaseHow == LEAD_SUBJECT ) + { + pathTarget = pPredictedSubjectPos ? *pPredictedSubjectPos : PredictSubjectPosition( bot, subject ); + isPath = Compute( bot, pathTarget, cost, GetMaxPathLength() ); + } + else if ( subject->MyCombatCharacterPointer() && subject->MyCombatCharacterPointer()->GetLastKnownArea() ) + { + isPath = Compute( bot, subject->MyCombatCharacterPointer(), cost, GetMaxPathLength() ); + } + else + { + isPath = Compute( bot, pathTarget, cost, GetMaxPathLength() ); + } + + if ( isPath ) + { + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + //const float size = 20.0f; + //NDebugOverlay::VertArrow( bot->GetPosition() + Vector( 0, 0, size ), bot->GetPosition(), size, 255, RandomInt( 0, 200 ), 255, 255, true, 30.0f ); + + DevMsg( "%3.2f: bot(#%d) REPATH\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); + } + + m_lastPathSubject = subject; + + const float minRepathInterval = 0.5f; + m_throttleTimer.Start( minRepathInterval ); + + // track the lifetime of this new path + float lifetime = GetLifetime(); + if ( lifetime > 0.0f ) + { + m_lifetimeTimer.Start( lifetime ); + } + else + { + m_lifetimeTimer.Invalidate(); + } + } + else + { + // can't reach subject - throttle retry based on range to subject + m_failTimer.Start( 0.005f * ( bot->GetRangeTo( subject ) ) ); + + // allow bot to react to path failure + bot->OnMoveToFailure( this, FAIL_NO_PATH_EXISTS ); + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + //const float size = 20.0f; + const float dT = 90.0f; + int c = RandomInt( 0, 100 ); + //NDebugOverlay::VertArrow( bot->GetPosition() + Vector( 0, 0, size ), bot->GetPosition(), size, 255, c, c, 255, true, dT ); + NDebugOverlay::HorzArrow( bot->GetPosition(), pathTarget, 5.0f, 255, c, c, 255, true, dT ); + + DevMsg( "%3.2f: bot(#%d) REPATH FAILED\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); + } + + Invalidate(); + } + } +} + + +//---------------------------------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------------------------------- +/** + * Directly beeline toward victim if we have a clear shot, otherwise pathfind. + */ +class DirectChasePath : public ChasePath +{ +public: + + DirectChasePath( ChasePath::SubjectChaseType chaseHow = ChasePath::DONT_LEAD_SUBJECT ) : ChasePath( chaseHow ) + { + + } + + //------------------------------------------------------------------------------------------------------- + virtual void Update( INextBot *me, CBaseEntity *victim, const IPathCost &pathCost, Vector *pPredictedSubjectPos = NULL ) // update path to chase target and move bot along path + { + Assert( !pPredictedSubjectPos ); + bool bComputedPredictedPosition; + Vector vecPredictedPosition; + if ( !DirectChase( &bComputedPredictedPosition, &vecPredictedPosition, me, victim ) ) + { + // path around obstacles to reach our victim + ChasePath::Update( me, victim, pathCost, bComputedPredictedPosition ? &vecPredictedPosition : NULL ); + } + NotifyVictim( me, victim ); + } + + //------------------------------------------------------------------------------------------------------- + bool DirectChase( bool *pPredictedPositionComputed, Vector *pPredictedPos, INextBot *me, CBaseEntity *victim ) // if there is nothing between us and our victim, run directly at them + { + *pPredictedPositionComputed = false; + + ILocomotion *mover = me->GetLocomotionInterface(); + + if ( me->IsImmobile() || mover->IsScrambling() ) + { + return false; + } + + if ( IsDiscontinuityAhead( me, CLIMB_UP ) ) + { + return false; + } + + if ( IsDiscontinuityAhead( me, JUMP_OVER_GAP ) ) + { + return false; + } + + Vector leadVictimPos = PredictSubjectPosition( me, victim ); + + // Don't want to have to compute the predicted position twice. + *pPredictedPositionComputed = true; + *pPredictedPos = leadVictimPos; + + if ( !mover->IsPotentiallyTraversable( mover->GetFeet(), leadVictimPos ) ) + { + return false; + } + + // the way is clear - move directly towards our victim + mover->FaceTowards( leadVictimPos ); + mover->Approach( leadVictimPos ); + + me->GetBodyInterface()->AimHeadTowards( victim ); + + // old path is no longer useful since we've moved off of it + Invalidate(); + + return true; + } + + //------------------------------------------------------------------------------------------------------- + virtual bool IsRepathNeeded( INextBot *bot, CBaseEntity *subject ) const // return true if situation has changed enough to warrant recomputing the current path + { + if ( ChasePath::IsRepathNeeded( bot, subject ) ) + { + return true; + } + + return bot->GetLocomotionInterface()->IsStuck() && bot->GetLocomotionInterface()->GetStuckDuration() > 2.0f; + } + + //------------------------------------------------------------------------------------------------------- + /** + * Determine exactly where the path goes between the given two areas + * on the path. Return this point in 'crossPos'. + */ + virtual void ComputeAreaCrossing( INextBot *bot, const CNavArea *from, const Vector &fromPos, const CNavArea *to, NavDirType dir, Vector *crossPos ) const + { + Vector center; + float halfWidth; + from->ComputePortal( to, dir, ¢er, &halfWidth ); + + *crossPos = center; + } + + void NotifyVictim( INextBot *me, CBaseEntity *victim ); +}; + + + + +#endif // _NEXT_BOT_CHASE_PATH_ diff --git a/game/server/NextBot/Path/NextBotPath.cpp b/game/server/NextBot/Path/NextBotPath.cpp new file mode 100644 index 0000000..4514887 --- /dev/null +++ b/game/server/NextBot/Path/NextBotPath.cpp @@ -0,0 +1,1094 @@ +// NextBotPath.cpp +// Encapsulate and manipulate a path through the world +// Author: Michael Booth, February 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "nav_mesh.h" +#include "fmtstr.h" + +#include "NextBotPath.h" +#include "NextBotInterface.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotBodyInterface.h" +#include "NextBotUtil.h" + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar NextBotPathDrawIncrement( "nb_path_draw_inc", "100", FCVAR_CHEAT ); +ConVar NextBotPathDrawSegmentCount( "nb_path_draw_segment_count", "100", FCVAR_CHEAT ); +ConVar NextBotPathSegmentInfluenceRadius( "nb_path_segment_influence_radius", "100", FCVAR_CHEAT ); + +//-------------------------------------------------------------------------------------------------------------- +Path::Path( void ) +{ + m_segmentCount = 0; + + m_cursorPos = 0.0f; + m_isCursorDataDirty = true; + m_cursorData.segmentPrior = NULL; + m_ageTimer.Invalidate(); + m_subject = NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine actual path positions + */ +bool Path::ComputePathDetails( INextBot *bot, const Vector &start ) +{ + VPROF_BUDGET( "Path::ComputePathDetails", "NextBot" ); + + if (m_segmentCount == 0) + return false; + + IBody *body = bot->GetBodyInterface(); + ILocomotion *mover = bot->GetLocomotionInterface(); + + const float stepHeight = ( mover ) ? mover->GetStepHeight() : 18.0f; + + // inflate hull width slightly as a safety margin + const float hullWidth = ( body ) ? body->GetHullWidth() + 5.0f : 1.0f; + + // set first path position + if ( m_path[0].area->Contains( start ) ) + { + m_path[0].pos = start; + } + else + { + // start in first area's center + m_path[0].pos = m_path[0].area->GetCenter(); + } + m_path[0].ladder = NULL; + m_path[0].how = NUM_TRAVERSE_TYPES; + m_path[0].type = ON_GROUND; + + // set positions along the path + for( int i=1; i<m_segmentCount; ++i ) + { + Segment *from = &m_path[ i-1 ]; + Segment *to = &m_path[ i ]; + + if ( to->how <= GO_WEST ) // walk along the floor to the next area + { + to->ladder = NULL; + + from->area->ComputePortal( to->area, (NavDirType)to->how, &to->m_portalCenter, &to->m_portalHalfWidth ); + + // compute next point + ComputeAreaCrossing( bot, from->area, from->pos, to->area, (NavDirType)to->how, &to->pos ); + + // we need to walk out of "from" area, so keep Z where we can reach it + to->pos.z = from->area->GetZ( to->pos ); + + // if this is a "jump down" connection, we must insert an additional point on the path + //float expectedHeightDrop = from->area->GetZ( from->pos ) - to->area->GetZ( to->pos ); + + // measure the drop distance relative to the actual slope of the ground + Vector fromPos = from->pos; + fromPos.z = from->area->GetZ( fromPos ); + + Vector toPos = to->pos; + toPos.z = to->area->GetZ( toPos ); + + Vector groundNormal; + from->area->ComputeNormal( &groundNormal ); + + Vector alongPath = toPos - fromPos; + + float expectedHeightDrop = -DotProduct( alongPath, groundNormal ); + + if ( expectedHeightDrop > mover->GetStepHeight() ) + { + // NOTE: We can't know this is a drop-down yet, because of subtle interactions + // between nav area links and "portals" and "area crossings" + + // compute direction of path just prior to "jump down" + Vector2D dir; + DirectionToVector2D( (NavDirType)to->how, &dir ); + + // shift top of "jump down" out a bit to "get over the ledge" + const float inc = 10.0f; // 0.25f * hullWidth; + const float maxPushDist = 2.0f * hullWidth; // 75.0f; + float halfWidth = hullWidth/2.0f; + float hullHeight = ( body ) ? body->GetCrouchHullHeight() : 1.0f; + + float pushDist; + for( pushDist = 0.0f; pushDist <= maxPushDist; pushDist += inc ) + { + Vector pos = to->pos + Vector( pushDist * dir.x, pushDist * dir.y, 0.0f ); + Vector lowerPos = Vector( pos.x, pos.y, toPos.z ); + + trace_t result; + NextBotTraceFilterIgnoreActors filter( bot->GetEntity(), COLLISION_GROUP_NONE ); + UTIL_TraceHull( pos, lowerPos, + Vector( -halfWidth, -halfWidth, stepHeight ), Vector( halfWidth, halfWidth, hullHeight ), + bot->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + if ( result.fraction >= 1.0f ) + { + // found clearance to drop + break; + } + } + + Vector startDrop( to->pos.x + pushDist * dir.x, to->pos.y + pushDist * dir.y, to->pos.z ); + Vector endDrop( startDrop.x, startDrop.y, to->area->GetZ( to->pos ) ); + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Cross3D( startDrop, 5.0f, 255, 0, 255, true, 5.0f ); + NDebugOverlay::Cross3D( endDrop, 5.0f, 255, 255, 0, true, 5.0f ); + NDebugOverlay::VertArrow( startDrop, endDrop, 5.0f, 255, 100, 0, 255, true, 5.0f ); + } + + // verify that there is actually ground down there in case this is a far jump dropdown + float ground; + if ( TheNavMesh->GetGroundHeight( endDrop, &ground ) ) + { + if ( startDrop.z > ground + stepHeight ) + { + // if "ground" is lower than the next segment along the path + // there is a chasm between - this is not a drop down + // NOTE next->pos is not yet valid - this loop is computing it! + // const Segment *next = NextSegment( to ); + // if ( !next || next->area->GetCenter().z < ground + stepHeight ) + { + // this is a "jump down" link + to->pos = startDrop; + to->type = DROP_DOWN; + + // insert a duplicate node to represent the bottom of the fall + if ( m_segmentCount < MAX_PATH_SEGMENTS-1 ) + { + // copy nodes down + for( int j=m_segmentCount; j>i; --j ) + m_path[j] = m_path[j-1]; + + // path is one node longer + ++m_segmentCount; + + // move index ahead into the new node we just duplicated + ++i; + + m_path[i].pos.x = endDrop.x; + m_path[i].pos.y = endDrop.y; + m_path[i].pos.z = ground; + + m_path[i].type = ON_GROUND; + } + } + } + } + } + } + else if ( to->how == GO_LADDER_UP ) // to get to next area, must go up a ladder + { + // find our ladder + const NavLadderConnectVector *ladders = from->area->GetLadders( CNavLadder::LADDER_UP ); + int it; + for( it=0; it<ladders->Count(); ++it ) + { + CNavLadder *ladder = (*ladders)[ it ].ladder; + + // can't use "behind" area when ascending... + if (ladder->m_topForwardArea == to->area || + ladder->m_topLeftArea == to->area || + ladder->m_topRightArea == to->area) + { + to->ladder = ladder; + to->pos = ladder->m_bottom + ladder->GetNormal() * 2.0f * HalfHumanWidth; + to->type = LADDER_UP; + break; + } + } + + if (it == ladders->Count()) + { + //PrintIfWatched( "ERROR: Can't find ladder in path\n" ); + return false; + } + } + else if ( to->how == GO_LADDER_DOWN ) // to get to next area, must go down a ladder + { + // find our ladder + const NavLadderConnectVector *ladders = from->area->GetLadders( CNavLadder::LADDER_DOWN ); + int it; + for( it=0; it<ladders->Count(); ++it ) + { + CNavLadder *ladder = (*ladders)[ it ].ladder; + + if (ladder->m_bottomArea == to->area) + { + to->ladder = ladder; + to->pos = ladder->m_top; + to->pos = ladder->m_top - ladder->GetNormal() * 2.0f * HalfHumanWidth; + to->type = LADDER_DOWN; + break; + } + } + + if (it == ladders->Count()) + { + //PrintIfWatched( "ERROR: Can't find ladder in path\n" ); + return false; + } + } + else if ( to->how == GO_ELEVATOR_UP || to->how == GO_ELEVATOR_DOWN ) + { + to->pos = to->area->GetCenter(); + to->ladder = NULL; + } + } + + + // + // Scan for non-adjacent nav areas and add gap-jump-target nodes + // and jump-up target nodes for adjacent ledge mantling + // @todo Adjacency should be baked into the mesh data + // + for( int i=0; i<m_segmentCount-1; ++i ) + { + Segment *from = &m_path[ i ]; + Segment *to = &m_path[ i+1 ]; + + // first segment doesnt have a direction + if ( from->how != NUM_TRAVERSE_TYPES && from->how > GO_WEST ) + continue; + + if ( to->how > GO_WEST || !to->type == ON_GROUND ) + continue; + + // if areas are separated, we may need to 'gap jump' between them + // add a node to minimize the jump distance + Vector closeFrom, closeTo; + to->area->GetClosestPointOnArea( from->pos, &closeTo ); + from->area->GetClosestPointOnArea( closeTo, &closeFrom ); + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Line( closeFrom, closeTo, 255, 0, 255, true, 5.0f ); + } + + + const float separationTolerance = 1.9f * GenerationStepSize; + if ( (closeFrom - closeTo).AsVector2D().IsLengthGreaterThan( separationTolerance ) && ( closeTo - closeFrom ).AsVector2D().IsLengthGreaterThan( 0.5f * fabs( closeTo.z - closeFrom.z ) ) ) + { + // areas are disjoint and mostly level - add gap jump target + + // compute landing spot in 'to' area + Vector landingPos; + to->area->GetClosestPointOnArea( to->pos, &landingPos ); + + // compute launch spot in 'from' area + Vector launchPos; + from->area->GetClosestPointOnArea( landingPos, &launchPos ); + + Vector forward = landingPos - launchPos; + forward.NormalizeInPlace(); + + const float halfWidth = hullWidth/2.0f; + + // adjust path position to landing spot + to->pos = landingPos + forward * halfWidth; + + // insert launch position just before that segment to ensure bot is + // positioned for minimal jump distance + Segment newSegment = *from; + + newSegment.pos = launchPos - forward * halfWidth; + newSegment.type = JUMP_OVER_GAP; + + InsertSegment( newSegment, i+1 ); + + ++i; + } + else if ( (closeTo.z - closeFrom.z) > stepHeight ) + { + // areas are adjacent, but need a jump-up - add a jump-to target + + // adjust goal to be at top of ledge + //to->pos.z = to->area->GetZ( to->pos.x, to->pos.y ); + // use center of climb-up destination area to make sure bot moves onto actual ground once they finish their climb + to->pos = to->area->GetCenter(); + + // add launch position at base of jump + Segment newSegment = *from; + + Vector launchPos; + from->area->GetClosestPointOnArea( to->pos, &launchPos ); + + newSegment.pos = launchPos; + newSegment.type = CLIMB_UP; + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Cross3D( newSegment.pos, 15.0f, 255, 100, 255, true, 3.0f ); + } + + InsertSegment( newSegment, i+1 ); + + ++i; + } + + /** RETHINK THIS. It doesn't work in general cases, and messes up on doorways + else if ( from->type == ON_GROUND && from->how <= GO_WEST ) + { + // if any segment is not directly walkable, add a segment + // fixup corners that are being cut too tightly + if ( mover && !mover->IsPotentiallyTraversable( from->pos, to->pos ) ) + { + Segment newSegment = *from; + + if ( bot->IsDebugging( INextBot::PATH ) ) + { + NDebugOverlay::HorzArrow( from->pos, to->pos, 3.0f, 255, 0, 0, 255, true, 3.0f ); + } + + //newSegment.pos = from->area->GetCenter(); + + Vector2D shift; + DirectionToVector2D( OppositeDirection( (NavDirType)to->how ), &shift ); + + newSegment.pos = to->pos; + newSegment.pos.x += hullWidth * shift.x; + newSegment.pos.y += hullWidth * shift.y; + + newSegment.type = ON_GROUND; + + if ( bot->IsDebugging( INextBot::PATH ) ) + { + NDebugOverlay::Cross3D( newSegment.pos, 15.0f, 255, 0, 255, true, 3.0f ); + } + + InsertSegment( newSegment, i+1 ); + + i += 2; + } + } + */ + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Insert new segment at index i + */ +void Path::InsertSegment( Segment newSegment, int i ) +{ + if (m_segmentCount < MAX_PATH_SEGMENTS-1) + { + // shift segments to make room for new one + for( int j=m_segmentCount; j>i; --j ) + m_path[j] = m_path[j-1]; + + // path is one node longer + ++m_segmentCount; + + m_path[i] = newSegment; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Build trivial path when start and goal are in the same nav area + */ +bool Path::BuildTrivialPath( INextBot *bot, const Vector &goal ) +{ + const Vector &start = bot->GetPosition(); + + m_segmentCount = 0; + + /// @todo Dangerous to use "nearset" nav area - could be far away + CNavArea *startArea = TheNavMesh->GetNearestNavArea( start ); + if (startArea == NULL) + return false; + + CNavArea *goalArea = TheNavMesh->GetNearestNavArea( goal ); + if (goalArea == NULL) + return false; + + m_segmentCount = 2; + + m_path[0].area = startArea; + m_path[0].pos.x = start.x; + m_path[0].pos.y = start.y; + m_path[0].pos.z = startArea->GetZ( start ); + m_path[0].ladder = NULL; + m_path[0].how = NUM_TRAVERSE_TYPES; + m_path[0].type = ON_GROUND; + + m_path[1].area = goalArea; + m_path[1].pos.x = goal.x; + m_path[1].pos.y = goal.y; + m_path[1].pos.z = goalArea->GetZ( goal ); + m_path[1].ladder = NULL; + m_path[1].how = NUM_TRAVERSE_TYPES; + m_path[1].type = ON_GROUND; + + m_path[0].forward = m_path[1].pos - m_path[0].pos; + m_path[0].length = m_path[0].forward.NormalizeInPlace(); + m_path[0].distanceFromStart = 0.0f; + m_path[0].curvature = 0.0f; + + m_path[1].forward = m_path[0].forward; + m_path[1].length = 0.0f; + m_path[1].distanceFromStart = m_path[0].length; + m_path[1].curvature = 0.0f; + + OnPathChanged( bot, COMPLETE_PATH ); + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw the path for debugging. + */ +void Path::Draw( const Path::Segment *start ) const +{ + if ( !IsValid() ) + return; + + CFmtStr msg; + + // limit length of path we draw + int count = NextBotPathDrawSegmentCount.GetInt(); + + const Segment *s = start ? start : FirstSegment(); + int i=0; + while( s && count-- ) + { + const Segment *next = NextSegment( s ); + if ( next == NULL ) + { + // end of the path + break; + } + + Vector to = next->pos - s->pos; + float horiz = MAX( abs(to.x), abs(to.y) ); + float vert = abs( to.z ); + + int r,g,b; + switch( s->type ) + { + case DROP_DOWN: r = 255; g = 0; b = 255; break; + case CLIMB_UP: r = 0; g = 0; b = 255; break; + case JUMP_OVER_GAP: r = 0; g = 255; b = 255; break; + case LADDER_UP: r = 0; g = 255; b = 0; break; + case LADDER_DOWN: r = 0; g = 100; b = 0; break; + default: r = 255; g = 77; b = 0; break; // ON_GROUND + } + + if ( s->ladder ) + { + NDebugOverlay::VertArrow( s->ladder->m_bottom, s->ladder->m_top, 5.0f, r, g, b, 255, true, 0.1f ); + } + else + { + NDebugOverlay::Line( s->pos, next->pos, r, g, b, true, 0.1f ); + } + + const float nodeLength = 25.0f; + if ( horiz > vert ) + { + NDebugOverlay::HorzArrow( s->pos, s->pos + nodeLength * s->forward, 5.0f, r, g, b, 255, true, 0.1f ); + } + else + { + NDebugOverlay::VertArrow( s->pos, s->pos + nodeLength * s->forward, 5.0f, r, g, b, 255, true, 0.1f ); + } + + NDebugOverlay::Text( s->pos, msg.sprintf( "%d", i ), true, 0.1f ); + + //NDebugOverlay::Text( s->pos, msg.sprintf( "%d (%3.2f)", i, s->curvature ), false, 0.1f ); + + s = next; + ++i; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw the path for debugging - MODIFIES cursor position + */ +void Path::DrawInterpolated( float from, float to ) +{ + if ( !IsValid() ) + { + return; + } + + float t = from; + + MoveCursor( t ); + const Data &data = GetCursorData(); + Vector lastPos = data.pos; + + do + { + t += NextBotPathDrawIncrement.GetFloat(); + + MoveCursor( t ); + const Data &data = GetCursorData(); + + float curvePower = 3.0f * data.curvature; + + int r = 255 * ( 1.0f - curvePower ); + r = clamp( r, 0, 255 ); + + int g = 255 * ( 1.0f + curvePower ); + g = clamp( g, 0, 255 ); + + NDebugOverlay::Line( lastPos, data.pos, r, g, 0, true, 0.1f ); + + /* + int i = 0xFF & (int)( data.pos.x + data.pos.y + data.pos.z ); + i >>= 1; + i += 128; + + NDebugOverlay::Line( data.pos, data.pos + 10.0f * data.forward, 0, i, i, true, 0.1f ); + */ + + lastPos = data.pos; + } + while( t < to ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check line of sight from 'anchor' node on path to subsequent nodes until + * we find a node that can't been seen from 'anchor'. + */ +int Path::FindNextOccludedNode( INextBot *bot, int anchorIndex ) +{ + ILocomotion *mover = bot->GetLocomotionInterface(); + if ( mover == NULL) + { + return m_segmentCount; + } + + Segment *anchor = &m_path[ anchorIndex ]; + + for( int i=anchorIndex+1; i<m_segmentCount; ++i ) + { + Segment *to = &m_path[i]; + + // if this segment is not on the ground, or is precise, don't skip past it + if ( !to->type == ON_GROUND || (to->area->GetAttributes() & NAV_MESH_PRECISE) ) + { + return i; + } + + if ( !mover->IsPotentiallyTraversable( anchor->pos, to->pos, ILocomotion::IMMEDIATELY ) ) + { + // cant reach this node directly from anchor node + return i; + } + + if ( mover->HasPotentialGap( anchor->pos, to->pos ) ) + { + // we would fall into a gap if we took this cutoff + return i; + } + } + + return m_segmentCount; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Smooth out path, removing redundant nodes + */ +void Path::Optimize( INextBot *bot ) +{ + // this is SUPER expensive - especially the IsGap() check + return; + + VPROF_BUDGET( "Path::Optimize", "NextBot" ); + + if (m_segmentCount < 3) + return; + + int anchor = 0; + + while( anchor < m_segmentCount ) + { + int occluded = FindNextOccludedNode( bot, anchor ); + int nextAnchor = occluded-1; + + if (nextAnchor > anchor) + { + // remove redundant nodes between anchor and nextAnchor + int removeCount = nextAnchor - anchor - 1; + if (removeCount > 0) + { + for( int i=nextAnchor; i<m_segmentCount; ++i ) + { + m_path[i-removeCount] = m_path[i]; + } + m_segmentCount -= removeCount; + } + } + + ++anchor; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute final data for completed path + */ +void Path::PostProcess( void ) +{ + VPROF_BUDGET( "Path::PostProcess", "NextBot" ); + + m_ageTimer.Start(); + + if (m_segmentCount == 0) + return; + + if (m_segmentCount == 1) + { + m_path[0].forward = vec3_origin; + m_path[0].length = 0.0f; + m_path[0].distanceFromStart = 0.0f; + m_path[0].curvature = 0.0f; + return; + } + + float distanceSoFar = 0.0f; + int i; + for( i=0; i < m_segmentCount-1; ++i ) + { + Segment *from = &m_path[ i ]; + Segment *to = &m_path[ i+1 ]; + + from->forward = to->pos - from->pos; + from->length = from->forward.NormalizeInPlace(); + + from->distanceFromStart = distanceSoFar; + + distanceSoFar += from->length; + } + + + // compute curvature in XY plane + Vector2D from, to; + for( i=1; i < m_segmentCount-1; ++i ) + { + if (m_path[ i ].type != ON_GROUND) + { + m_path[ i ].curvature = 0.0f; + } + else + { + from = m_path[ i-1 ].forward.AsVector2D(); + from.NormalizeInPlace(); + + to = m_path[ i ].forward.AsVector2D(); + to.NormalizeInPlace(); + + m_path[ i ].curvature = 0.5f * ( 1.0f - from.Dot( to ) ); + + Vector2D right( -from.y, from.x ); + if ( to.Dot( right ) < 0.0f ) + { + m_path[ i ].curvature = -m_path[ i ].curvature; + } + } + } + + // first segment has no curvature + m_path[ 0 ].curvature = 0.0f; + + // last segment maintains direction + m_path[ i ].forward = m_path[ i-1 ].forward; + m_path[ i ].length = 0.0f; + m_path[ i ].distanceFromStart = distanceSoFar; + m_path[ i ].curvature = 0.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return a position on the path at the given distance from the path start + */ +const Vector &Path::GetPosition( float distanceFromStart, const Segment *start ) const +{ + if (!IsValid()) + { + return vec3_origin; + } + + float lengthSoFar; + const Segment *segment; + + if (start) + { + segment = start; + lengthSoFar = start->distanceFromStart; + } + else + { + segment = &m_path[0]; + lengthSoFar = 0.0f; + } + + if (segment->distanceFromStart > distanceFromStart) + { + // clamp to path start + return segment->pos; + } + + + const Segment *next = NextSegment( segment ); + + Vector delta; + float length; + + while( next ) + { + delta = next->pos - segment->pos; + length = segment->length; + + if (lengthSoFar + length >= distanceFromStart) + { + // desired point is on this segment of the path + float overlap = distanceFromStart - lengthSoFar; + float t = overlap / length; + + m_pathPos = segment->pos + t * delta; + + return m_pathPos; + } + + lengthSoFar += length; + + segment = next; + next = NextSegment( next ); + } + + // clamp to path end + return segment->pos; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest point on the path to the given position + */ +const Vector &Path::GetClosestPosition( const Vector &pos, const Segment *start, float alongLimit ) const +{ + const Segment *segment = (start) ? start : &m_path[0]; + + if (segment == NULL) + { + return pos; + } + + m_closePos = pos; + float closeRangeSq = 99999999999.9f; + + float distanceSoFar = 0.0f; + while( alongLimit == 0.0f || distanceSoFar <= alongLimit ) + { + const Segment *nextSegment = NextSegment( segment ); + + if (nextSegment) + { + Vector close; + CalcClosestPointOnLineSegment( pos, segment->pos, nextSegment->pos, close ); + float rangeSq = (close - pos).LengthSqr(); + if (rangeSq < closeRangeSq) + { + m_closePos = close; + closeRangeSq = rangeSq; + } + } + else + { + // end of the path + break; + } + + distanceSoFar += segment->length; + segment = nextSegment; + } + + return m_closePos; +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Replace this path with the given path's data + */ +void Path::Copy( INextBot *bot, const Path &path ) +{ + VPROF_BUDGET( "Path::Copy", "NextBot" ); + + Invalidate(); + + for( int i = 0; i < path.m_segmentCount; ++i ) + { + m_path[i] = path.m_path[i]; + } + m_segmentCount = path.m_segmentCount; + + OnPathChanged( bot, COMPLETE_PATH ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set cursor position to closest point on path to given position + */ +void Path::MoveCursorToClosestPosition( const Vector &pos, SeekType type, float alongLimit ) const +{ + if ( !IsValid() ) + { + return; + } + + if ( type == SEEK_ENTIRE_PATH || type == SEEK_AHEAD ) + { + const Segment *segment; + + if ( type == SEEK_AHEAD ) + { + // continue search from cursor position onward + if ( m_cursorData.segmentPrior ) + { + segment = m_cursorData.segmentPrior; + } + else + { + // no prior segment, start from the start + segment = &m_path[ 0 ]; + } + } + else + { + // search entire path from the start + segment = &m_path[ 0 ]; + } + + m_cursorData.pos = pos; + m_cursorData.segmentPrior = segment; + float closeRangeSq = 99999999999.9f; + + float distanceSoFar = 0.0f; + while( alongLimit == 0.0f || distanceSoFar <= alongLimit ) + { + const Segment *nextSegment = NextSegment( segment ); + + if ( nextSegment ) + { + Vector close; + CalcClosestPointOnLineSegment( pos, segment->pos, nextSegment->pos, close ); + + float rangeSq = ( close - pos ).LengthSqr(); + if ( rangeSq < closeRangeSq ) + { + m_cursorData.pos = close; + m_cursorData.segmentPrior = segment; + + closeRangeSq = rangeSq; + } + } + else + { + // end of the path + break; + } + + distanceSoFar += segment->length; + segment = nextSegment; + } + + // + // Move cursor to closest point on path + // + segment = m_cursorData.segmentPrior; + + float t = ( m_cursorData.pos - segment->pos ).Length() / segment->length; + + m_cursorPos = segment->distanceFromStart + t * segment->length; + m_isCursorDataDirty = true; + } + else + { + AssertMsg( false, "SEEK_BEHIND not implemented" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return path state at the current cursor position + */ +const Path::Data &Path::GetCursorData( void ) const +{ + if ( IsValid() ) + { + if ( m_isCursorDataDirty ) + { + const float epsilon = 0.0001f; + if ( m_cursorPos < epsilon || m_segmentCount < 2 ) + { + // start of path + m_cursorData.pos = m_path[0].pos; + m_cursorData.forward = m_path[0].forward; + m_cursorData.curvature = m_path[0].curvature; + m_cursorData.segmentPrior = &m_path[0]; + } + else if ( m_cursorPos > GetLength() - epsilon ) + { + // end of path + m_cursorData.pos = m_path[ m_segmentCount-1 ].pos; + m_cursorData.forward = m_path[ m_segmentCount-1 ].forward; + m_cursorData.curvature = m_path[ m_segmentCount-1 ].curvature; + m_cursorData.segmentPrior = &m_path[ m_segmentCount-1 ]; + } + else + { + // along path + float lengthSoFar = 0.0f; + const Segment *segment = &m_path[0]; + + const Segment *next = NextSegment( segment ); + + while( next ) + { + float length = segment->length; + + if ( lengthSoFar + length >= m_cursorPos ) + { + // desired point is on this segment of the path + float overlap = m_cursorPos - lengthSoFar; + float t = 1.0f; // 0-length segments are assumed to be complete, to avoid NaNs + if ( length > 0.0f ) + { + t = overlap / length; + } + + // interpolate data at this point along the path + m_cursorData.pos = segment->pos + t * ( next->pos - segment->pos ); + m_cursorData.forward = segment->forward + t * ( next->forward - segment->forward ); + m_cursorData.segmentPrior = segment; + + // curvature fades to zero along midpoint of long straight segments + // and is influenced as it nears ends of segment + if ( overlap < NextBotPathSegmentInfluenceRadius.GetFloat() ) + { + if ( length - overlap < NextBotPathSegmentInfluenceRadius.GetFloat() ) + { + // near start and end - interpolate + float startCurvature = segment->curvature * ( 1.0f - ( overlap / NextBotPathSegmentInfluenceRadius.GetFloat() ) ); + float endCurvature = next->curvature * ( 1.0f - ( ( length - overlap ) / NextBotPathSegmentInfluenceRadius.GetFloat() ) ); + + m_cursorData.curvature = ( startCurvature + endCurvature ) / 2.0f; + } + else + { + // near start only + m_cursorData.curvature = segment->curvature * ( 1.0f - ( overlap / NextBotPathSegmentInfluenceRadius.GetFloat() ) ); + } + } + else if ( length - overlap < NextBotPathSegmentInfluenceRadius.GetFloat() ) + { + // near end only + m_cursorData.curvature = next->curvature * ( 1.0f - ( ( length - overlap ) / NextBotPathSegmentInfluenceRadius.GetFloat() ) ); + } + + + break; + } + + lengthSoFar += length; + + segment = next; + next = NextSegment( next ); + } + } + + // data is up to date + m_isCursorDataDirty = false; + } + } + else + { + // path is not valid + m_cursorData.pos = vec3_origin; + m_cursorData.forward = Vector( 1.0f, 0, 0 ); + m_cursorData.curvature = 0.0f; + m_cursorData.segmentPrior = NULL; + } + + return m_cursorData; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine exactly where the path goes between the given two areas + * on the path. Return this point in 'crossPos'. + */ +void Path::ComputeAreaCrossing( INextBot *bot, const CNavArea *from, const Vector &fromPos, const CNavArea *to, NavDirType dir, Vector *crossPos ) const +{ + from->ComputeClosestPointInPortal( to, dir, fromPos, crossPos ); + + // move goal position into the goal area a bit to avoid running directly along the edge of an area against a wall, etc + // don't do this unless area is against a wall - and what if our hull is wider than the area? + // AddDirectionVector( crossPos, dir, bot->GetBodyInterface()->GetHullWidth()/2.0f ); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/server/NextBot/Path/NextBotPath.h b/game/server/NextBot/Path/NextBotPath.h new file mode 100644 index 0000000..6669d92 --- /dev/null +++ b/game/server/NextBot/Path/NextBotPath.h @@ -0,0 +1,862 @@ +// NextBotPath.h +// Encapsulate and manipulate a path through the world +// Author: Michael Booth, February 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_PATH_H_ +#define _NEXT_BOT_PATH_H_ + +#include "NextBotInterface.h" + +#include "tier0/vprof.h" + +#define PATH_NO_LENGTH_LIMIT 0.0f // non-default argument value for Path::Compute() +#define PATH_TRUNCATE_INCOMPLETE_PATH false // non-default argument value for Path::Compute() + +class INextBot; +class CNavArea; +class CNavLadder; + + +//--------------------------------------------------------------------------------------------------------------- +/** + * The interface for pathfinding costs. + * TODO: Replace all template cost functors with this interface, so we can virtualize and derive from them. + */ +class IPathCost +{ +public: + virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const = 0; +}; + + +//--------------------------------------------------------------------------------------------------------------- +/** + * The interface for selecting a goal area during "open goal" pathfinding + */ +class IPathOpenGoalSelector +{ +public: + // compare "newArea" to "currentGoal" and return the area that is the better goal area + virtual CNavArea *operator() ( CNavArea *currentGoal, CNavArea *newArea ) const = 0; +}; + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A Path through the world. + * Not only does this encapsulate a path to get from point A to point B, + * but also the selecting the decision algorithm for how to build that path. + */ +class Path +{ +public: + Path( void ); + virtual ~Path() { } + + enum SegmentType + { + ON_GROUND, + DROP_DOWN, + CLIMB_UP, + JUMP_OVER_GAP, + LADDER_UP, + LADDER_DOWN, + + NUM_SEGMENT_TYPES + }; + + // @todo Allow custom Segment classes for different kinds of paths + struct Segment + { + CNavArea *area; // the area along the path + NavTraverseType how; // how to enter this area from the previous one + Vector pos; // our movement goal position at this point in the path + const CNavLadder *ladder; // if "how" refers to a ladder, this is it + + SegmentType type; // how to traverse this segment of the path + Vector forward; // unit vector along segment + float length; // length of this segment + float distanceFromStart; // distance of this node from the start of the path + float curvature; // how much the path 'curves' at this point in the XY plane (0 = none, 1 = 180 degree doubleback) + + Vector m_portalCenter; // position of center of 'portal' between previous area and this area + float m_portalHalfWidth; // half width of 'portal' + }; + + virtual float GetLength( void ) const; // return length of path from start to finish + virtual const Vector &GetPosition( float distanceFromStart, const Segment *start = NULL ) const; // return a position on the path at the given distance from the path start + virtual const Vector &GetClosestPosition( const Vector &pos, const Segment *start = NULL, float alongLimit = 0.0f ) const; // return the closest point on the path to the given position + + virtual const Vector &GetStartPosition( void ) const; // return the position where this path starts + virtual const Vector &GetEndPosition( void ) const; // return the position where this path ends + virtual CBaseCombatCharacter *GetSubject( void ) const; // return the actor this path leads to, or NULL if there is no subject + + virtual const Path::Segment *GetCurrentGoal( void ) const; // return current goal along the path we are trying to reach + + virtual float GetAge( void ) const; // return "age" of this path (time since it was built) + + enum SeekType + { + SEEK_ENTIRE_PATH, // search the entire path length + SEEK_AHEAD, // search from current cursor position forward toward end of path + SEEK_BEHIND // search from current cursor position backward toward path start + }; + virtual void MoveCursorToClosestPosition( const Vector &pos, SeekType type = SEEK_ENTIRE_PATH, float alongLimit = 0.0f ) const; // Set cursor position to closest point on path to given position + + enum MoveCursorType + { + PATH_ABSOLUTE_DISTANCE, + PATH_RELATIVE_DISTANCE + }; + virtual void MoveCursorToStart( void ); // set seek cursor to start of path + virtual void MoveCursorToEnd( void ); // set seek cursor to end of path + virtual void MoveCursor( float value, MoveCursorType type = PATH_ABSOLUTE_DISTANCE ); // change seek cursor position + virtual float GetCursorPosition( void ) const; // return position of seek cursor (distance along path) + + struct Data + { + Vector pos; // the position along the path + Vector forward; // unit vector along path direction + float curvature; // how much the path 'curves' at this point in the XY plane (0 = none, 1 = 180 degree doubleback) + const Segment *segmentPrior; // the segment just before this position + }; + virtual const Data &GetCursorData( void ) const; // return path state at the current cursor position + + virtual bool IsValid( void ) const; + virtual void Invalidate( void ); // make path invalid (clear it) + + virtual void Draw( const Path::Segment *start = NULL ) const; // draw the path for debugging + virtual void DrawInterpolated( float from, float to ); // draw the path for debugging - MODIFIES cursor position + + virtual const Segment *FirstSegment( void ) const; // return first segment of path + virtual const Segment *NextSegment( const Segment *currentSegment ) const; // return next segment of path, given current one + virtual const Segment *PriorSegment( const Segment *currentSegment ) const; // return previous segment of path, given current one + virtual const Segment *LastSegment( void ) const; // return last segment of path + + enum ResultType + { + COMPLETE_PATH, + PARTIAL_PATH, + NO_PATH + }; + virtual void OnPathChanged( INextBot *bot, ResultType result ) { } // invoked when the path is (re)computed (path is valid at the time of this call) + + virtual void Copy( INextBot *bot, const Path &path ); // Replace this path with the given path's data + + + //----------------------------------------------------------------------------------------------------------------- + /** + * Compute shortest path from bot to given actor via A* algorithm. + * If returns true, path was found to the subject. + * If returns false, path may either be invalid (use IsValid() to check), or valid but + * doesn't reach all the way to the subject. + */ + template< typename CostFunctor > + bool Compute( INextBot *bot, CBaseCombatCharacter *subject, CostFunctor &costFunc, float maxPathLength = 0.0f, bool includeGoalIfPathFails = true ) + { + VPROF_BUDGET( "Path::Compute(subject)", "NextBot" ); + + Invalidate(); + + m_subject = subject; + + const Vector &start = bot->GetPosition(); + + CNavArea *startArea = bot->GetEntity()->GetLastKnownArea(); + if ( !startArea ) + { + OnPathChanged( bot, NO_PATH ); + return false; + } + + CNavArea *subjectArea = subject->GetLastKnownArea(); + if ( !subjectArea ) + { + OnPathChanged( bot, NO_PATH ); + return false; + } + + Vector subjectPos = subject->GetAbsOrigin(); + + // if we are already in the subject area, build trivial path + if ( startArea == subjectArea ) + { + BuildTrivialPath( bot, subjectPos ); + return true; + } + + // + // Compute shortest path to subject + // + CNavArea *closestArea = NULL; + bool pathResult = NavAreaBuildPath( startArea, subjectArea, &subjectPos, costFunc, &closestArea, maxPathLength, bot->GetEntity()->GetTeamNumber() ); + + // Failed? + if ( closestArea == NULL ) + return false; + + // + // Build actual path by following parent links back from goal area + // + + // get count + int count = 0; + CNavArea *area; + for( area = closestArea; area; area = area->GetParent() ) + { + ++count; + + if ( area == startArea ) + { + // startArea can be re-evaluated during the pathfind and given a parent... + break; + } + if ( count >= MAX_PATH_SEGMENTS-1 ) // save room for endpoint + break; + } + + if ( count == 1 ) + { + BuildTrivialPath( bot, subjectPos ); + return pathResult; + } + + // assemble path + m_segmentCount = count; + for( area = closestArea; count && area; area = area->GetParent() ) + { + --count; + m_path[ count ].area = area; + m_path[ count ].how = area->GetParentHow(); + m_path[ count ].type = ON_GROUND; + } + + if ( pathResult || includeGoalIfPathFails ) + { + // append actual subject position + m_path[ m_segmentCount ].area = closestArea; + m_path[ m_segmentCount ].pos = subjectPos; + m_path[ m_segmentCount ].ladder = NULL; + m_path[ m_segmentCount ].how = NUM_TRAVERSE_TYPES; + m_path[ m_segmentCount ].type = ON_GROUND; + ++m_segmentCount; + } + + // compute path positions + if ( ComputePathDetails( bot, start ) == false ) + { + Invalidate(); + OnPathChanged( bot, NO_PATH ); + return false; + } + + // remove redundant nodes and clean up path + Optimize( bot ); + + PostProcess(); + + OnPathChanged( bot, pathResult ? COMPLETE_PATH : PARTIAL_PATH ); + + return pathResult; + } + + + //----------------------------------------------------------------------------------------------------------------- + /** + * Compute shortest path from bot to 'goal' via A* algorithm. + * If returns true, path was found to the goal position. + * If returns false, path may either be invalid (use IsValid() to check), or valid but + * doesn't reach all the way to the goal. + */ + template< typename CostFunctor > + bool Compute( INextBot *bot, const Vector &goal, CostFunctor &costFunc, float maxPathLength = 0.0f, bool includeGoalIfPathFails = true ) + { + VPROF_BUDGET( "Path::Compute(goal)", "NextBotSpiky" ); + + Invalidate(); + + const Vector &start = bot->GetPosition(); + + CNavArea *startArea = bot->GetEntity()->GetLastKnownArea(); + if ( !startArea ) + { + OnPathChanged( bot, NO_PATH ); + return false; + } + + // check line-of-sight to the goal position when finding it's nav area + const float maxDistanceToArea = 200.0f; + CNavArea *goalArea = TheNavMesh->GetNearestNavArea( goal, true, maxDistanceToArea, true ); + + // if we are already in the goal area, build trivial path + if ( startArea == goalArea ) + { + BuildTrivialPath( bot, goal ); + return true; + } + + // make sure path end position is on the ground + Vector pathEndPosition = goal; + if ( goalArea ) + { + pathEndPosition.z = goalArea->GetZ( pathEndPosition ); + } + else + { + TheNavMesh->GetGroundHeight( pathEndPosition, &pathEndPosition.z ); + } + + // + // Compute shortest path to goal + // + CNavArea *closestArea = NULL; + bool pathResult = NavAreaBuildPath( startArea, goalArea, &goal, costFunc, &closestArea, maxPathLength, bot->GetEntity()->GetTeamNumber() ); + + // Failed? + if ( closestArea == NULL ) + return false; + + // + // Build actual path by following parent links back from goal area + // + + // get count + int count = 0; + CNavArea *area; + for( area = closestArea; area; area = area->GetParent() ) + { + ++count; + + if ( area == startArea ) + { + // startArea can be re-evaluated during the pathfind and given a parent... + break; + } + if ( count >= MAX_PATH_SEGMENTS-1 ) // save room for endpoint + break; + } + + if ( count == 1 ) + { + BuildTrivialPath( bot, goal ); + return pathResult; + } + + // assemble path + m_segmentCount = count; + for( area = closestArea; count && area; area = area->GetParent() ) + { + --count; + m_path[ count ].area = area; + m_path[ count ].how = area->GetParentHow(); + m_path[ count ].type = ON_GROUND; + } + + if ( pathResult || includeGoalIfPathFails ) + { + // append actual goal position + m_path[ m_segmentCount ].area = closestArea; + m_path[ m_segmentCount ].pos = pathEndPosition; + m_path[ m_segmentCount ].ladder = NULL; + m_path[ m_segmentCount ].how = NUM_TRAVERSE_TYPES; + m_path[ m_segmentCount ].type = ON_GROUND; + ++m_segmentCount; + } + + // compute path positions + if ( ComputePathDetails( bot, start ) == false ) + { + Invalidate(); + OnPathChanged( bot, NO_PATH ); + return false; + } + + // remove redundant nodes and clean up path + Optimize( bot ); + + PostProcess(); + + OnPathChanged( bot, pathResult ? COMPLETE_PATH : PARTIAL_PATH ); + + return pathResult; + } + + + //----------------------------------------------------------------------------------------------------------------- + /** + * Build a path from bot's current location to an undetermined goal area + * that minimizes the given cost along the final path and meets the + * goal criteria. + */ + virtual bool ComputeWithOpenGoal( INextBot *bot, const IPathCost &costFunc, const IPathOpenGoalSelector &goalSelector, float maxSearchRadius = 0.0f ) + { + VPROF_BUDGET( "ComputeWithOpenGoal", "NextBot" ); + + int teamID = bot->GetEntity()->GetTeamNumber(); + + CNavArea *startArea = bot->GetEntity()->GetLastKnownArea(); + + if ( startArea == NULL ) + return NULL; + + startArea->SetParent( NULL ); + + // start search + CNavArea::ClearSearchLists(); + + float initCost = costFunc( startArea, NULL, NULL, NULL, -1.0f ); + if ( initCost < 0.0f ) + return NULL; + + startArea->SetTotalCost( initCost ); + startArea->AddToOpenList(); + + // find our goal as we search + CNavArea *goalArea = NULL; + + // + // Dijkstra's algorithm (since we don't know our goal). + // + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CNavArea *area = CNavArea::PopOpenList(); + + area->AddToClosedList(); + + // don't consider blocked areas + if ( area->IsBlocked( teamID ) ) + continue; + + // build adjacent area array + CollectAdjacentAreas( area ); + + // search adjacent areas + for( int i=0; i<m_adjAreaIndex; ++i ) + { + CNavArea *newArea = m_adjAreaVector[ i ].area; + + // only visit each area once + if ( newArea->IsClosed() ) + continue; + + // don't consider blocked areas + if ( newArea->IsBlocked( teamID ) ) + continue; + + // don't use this area if it is out of range + if ( maxSearchRadius > 0.0f && ( newArea->GetCenter() - bot->GetEntity()->GetAbsOrigin() ).IsLengthGreaterThan( maxSearchRadius ) ) + continue; + + // determine cost of traversing this area + float newCost = costFunc( newArea, area, m_adjAreaVector[ i ].ladder, NULL, -1.0f ); + + // don't use adjacent area if cost functor says it is a dead-end + if ( newCost < 0.0f ) + continue; + + if ( newArea->IsOpen() && newArea->GetTotalCost() <= newCost ) + { + // we have already visited this area, and it has a better path + continue; + } + else + { + // whether this area has been visited or not, we now have a better path to it + newArea->SetParent( area, m_adjAreaVector[ i ].how ); + newArea->SetTotalCost( newCost ); + + // use 'cost so far' to hold cumulative cost + newArea->SetCostSoFar( newCost ); + + // tricky bit here - relying on OpenList being sorted by cost + if ( newArea->IsOpen() ) + { + // area already on open list, update the list order to keep costs sorted + newArea->UpdateOnOpenList(); + } + else + { + newArea->AddToOpenList(); + } + + // keep track of best goal so far + goalArea = goalSelector( goalArea, newArea ); + } + } + } + + if ( goalArea ) + { + // compile the path details into a usable path + AssemblePrecomputedPath( bot, goalArea->GetCenter(), goalArea ); + return true; + } + + // all adjacent areas are likely too far away + return false; + } + + + //----------------------------------------------------------------------------------------------------------------- + /** + * Given the last area in a path with valid parent pointers, + * construct the actual path. + */ + void AssemblePrecomputedPath( INextBot *bot, const Vector &goal, CNavArea *endArea ) + { + VPROF_BUDGET( "AssemblePrecomputedPath", "NextBot" ); + + const Vector &start = bot->GetPosition(); + + // get count + int count = 0; + CNavArea *area; + for( area = endArea; area; area = area->GetParent() ) + { + ++count; + } + + // save room for endpoint + if ( count > MAX_PATH_SEGMENTS-1 ) + { + count = MAX_PATH_SEGMENTS-1; + } + else if ( count == 0 ) + { + return; + } + + if ( count == 1 ) + { + BuildTrivialPath( bot, goal ); + return; + } + + // assemble path + m_segmentCount = count; + for( area = endArea; count && area; area = area->GetParent() ) + { + --count; + m_path[ count ].area = area; + m_path[ count ].how = area->GetParentHow(); + m_path[ count ].type = ON_GROUND; + } + + // append actual goal position + m_path[ m_segmentCount ].area = endArea; + m_path[ m_segmentCount ].pos = goal; + m_path[ m_segmentCount ].ladder = NULL; + m_path[ m_segmentCount ].how = NUM_TRAVERSE_TYPES; + m_path[ m_segmentCount ].type = ON_GROUND; + ++m_segmentCount; + + // compute path positions + if ( ComputePathDetails( bot, start ) == false ) + { + Invalidate(); + OnPathChanged( bot, NO_PATH ); + return; + } + + // remove redundant nodes and clean up path + Optimize( bot ); + + PostProcess(); + + OnPathChanged( bot, COMPLETE_PATH ); + } + + /** + * Utility function for when start and goal are in the same area + */ + bool BuildTrivialPath( INextBot *bot, const Vector &goal ); + + /** + * Determine exactly where the path goes between the given two areas + * on the path. Return this point in 'crossPos'. + */ + virtual void ComputeAreaCrossing( INextBot *bot, const CNavArea *from, const Vector &fromPos, const CNavArea *to, NavDirType dir, Vector *crossPos ) const; + + +private: + enum { MAX_PATH_SEGMENTS = 256 }; + Segment m_path[ MAX_PATH_SEGMENTS ]; + int m_segmentCount; + + bool ComputePathDetails( INextBot *bot, const Vector &start ); // determine actual path positions + + void Optimize( INextBot *bot ); + void PostProcess( void ); + int FindNextOccludedNode( INextBot *bot, int anchor ); // used by Optimize() + + void InsertSegment( Segment newSegment, int i ); // insert new segment at index i + + mutable Vector m_pathPos; // used by GetPosition() + mutable Vector m_closePos; // used by GetClosestPosition() + + mutable float m_cursorPos; // current cursor position (distance along path) + mutable Data m_cursorData; // used by GetCursorData() + mutable bool m_isCursorDataDirty; + + IntervalTimer m_ageTimer; // how old is this path? + CHandle< CBaseCombatCharacter > m_subject; // the subject this path leads to + + /** + * Build a vector of adjacent areas reachable from the given area + */ + void CollectAdjacentAreas( CNavArea *area ) + { + m_adjAreaIndex = 0; + + const NavConnectVector &adjNorth = *area->GetAdjacentAreas( NORTH ); + FOR_EACH_VEC( adjNorth, it ) + { + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + m_adjAreaVector[ m_adjAreaIndex ].area = adjNorth[ it ].area; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_NORTH; + m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL; + ++m_adjAreaIndex; + } + + const NavConnectVector &adjSouth = *area->GetAdjacentAreas( SOUTH ); + FOR_EACH_VEC( adjSouth, it ) + { + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + m_adjAreaVector[ m_adjAreaIndex ].area = adjSouth[ it ].area; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_SOUTH; + m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL; + ++m_adjAreaIndex; + } + + const NavConnectVector &adjWest = *area->GetAdjacentAreas( WEST ); + FOR_EACH_VEC( adjWest, it ) + { + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + m_adjAreaVector[ m_adjAreaIndex ].area = adjWest[ it ].area; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_WEST; + m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL; + ++m_adjAreaIndex; + } + + const NavConnectVector &adjEast = *area->GetAdjacentAreas( EAST ); + FOR_EACH_VEC( adjEast, it ) + { + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + m_adjAreaVector[ m_adjAreaIndex ].area = adjEast[ it ].area; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_EAST; + m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL; + ++m_adjAreaIndex; + } + + const NavLadderConnectVector &adjUpLadder = *area->GetLadders( CNavLadder::LADDER_UP ); + FOR_EACH_VEC( adjUpLadder, it ) + { + CNavLadder *ladder = adjUpLadder[ it ].ladder; + + if ( ladder->m_topForwardArea && m_adjAreaIndex < MAX_ADJ_AREAS ) + { + m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topForwardArea; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP; + m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder; + ++m_adjAreaIndex; + } + + if ( ladder->m_topLeftArea && m_adjAreaIndex < MAX_ADJ_AREAS ) + { + m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topLeftArea; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP; + m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder; + ++m_adjAreaIndex; + } + + if ( ladder->m_topRightArea && m_adjAreaIndex < MAX_ADJ_AREAS ) + { + m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topRightArea; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP; + m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder; + ++m_adjAreaIndex; + } + } + + const NavLadderConnectVector &adjDownLadder = *area->GetLadders( CNavLadder::LADDER_DOWN ); + FOR_EACH_VEC( adjDownLadder, it ) + { + CNavLadder *ladder = adjDownLadder[ it ].ladder; + + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + if ( ladder->m_bottomArea ) + { + m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_bottomArea; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_DOWN; + m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder; + ++m_adjAreaIndex; + } + } + } + + enum { MAX_ADJ_AREAS = 64 }; + + struct AdjInfo + { + CNavArea *area; + CNavLadder *ladder; + NavTraverseType how; + }; + + AdjInfo m_adjAreaVector[ MAX_ADJ_AREAS ]; + int m_adjAreaIndex; + +}; + + +inline float Path::GetLength( void ) const +{ + if (m_segmentCount <= 0) + { + return 0.0f; + } + + return m_path[ m_segmentCount-1 ].distanceFromStart; +} + +inline bool Path::IsValid( void ) const +{ + return (m_segmentCount > 0); +} + +inline void Path::Invalidate( void ) +{ + m_segmentCount = 0; + + m_cursorPos = 0.0f; + + m_cursorData.pos = vec3_origin; + m_cursorData.forward = Vector( 1.0f, 0, 0 ); + m_cursorData.curvature = 0.0f; + m_cursorData.segmentPrior = NULL; + + m_isCursorDataDirty = true; + + m_subject = NULL; +} + +inline const Path::Segment *Path::FirstSegment( void ) const +{ + return (IsValid()) ? &m_path[0] : NULL; +} + +inline const Path::Segment *Path::NextSegment( const Segment *currentSegment ) const +{ + if (currentSegment == NULL || !IsValid()) + return NULL; + + int i = currentSegment - m_path; + + if (i < 0 || i >= m_segmentCount-1) + { + return NULL; + } + + return &m_path[ i+1 ]; +} + +inline const Path::Segment *Path::PriorSegment( const Segment *currentSegment ) const +{ + if (currentSegment == NULL || !IsValid()) + return NULL; + + int i = currentSegment - m_path; + + if (i < 1 || i >= m_segmentCount) + { + return NULL; + } + + return &m_path[ i-1 ]; +} + +inline const Path::Segment *Path::LastSegment( void ) const +{ + return ( IsValid() ) ? &m_path[ m_segmentCount-1 ] : NULL; +} + +inline const Vector &Path::GetStartPosition( void ) const +{ + return ( IsValid() ) ? m_path[ 0 ].pos : vec3_origin; +} + +inline const Vector &Path::GetEndPosition( void ) const +{ + return ( IsValid() ) ? m_path[ m_segmentCount-1 ].pos : vec3_origin; +} + +inline CBaseCombatCharacter *Path::GetSubject( void ) const +{ + return m_subject; +} + +inline void Path::MoveCursorToStart( void ) +{ + m_cursorPos = 0.0f; + m_isCursorDataDirty = true; +} + +inline void Path::MoveCursorToEnd( void ) +{ + m_cursorPos = GetLength(); + m_isCursorDataDirty = true; +} + +inline void Path::MoveCursor( float value, MoveCursorType type ) +{ + if ( type == PATH_ABSOLUTE_DISTANCE ) + { + m_cursorPos = value; + } + else // relative distance + { + m_cursorPos += value; + } + + if ( m_cursorPos < 0.0f ) + { + m_cursorPos = 0.0f; + } + else if ( m_cursorPos > GetLength() ) + { + m_cursorPos = GetLength(); + } + + m_isCursorDataDirty = true; +} + +inline float Path::GetCursorPosition( void ) const +{ + return m_cursorPos; +} + +inline const Path::Segment *Path::GetCurrentGoal( void ) const +{ + return NULL; +} + +inline float Path::GetAge( void ) const +{ + return m_ageTimer.GetElapsedTime(); +} + + +#endif // _NEXT_BOT_PATH_H_ + diff --git a/game/server/NextBot/Path/NextBotPathFollow.cpp b/game/server/NextBot/Path/NextBotPathFollow.cpp new file mode 100644 index 0000000..58e2e85 --- /dev/null +++ b/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -0,0 +1,1923 @@ +// NextBotPathFollow.cpp +// Path following +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "BasePropDoor.h" + +#include "nav_mesh.h" +#include "NextBot.h" +#include "NextBotPathFollow.h" +#include "NextBotUtil.h" + +#include "NextBotLocomotionInterface.h" +#include "NextBotBodyInterface.h" +#include "NextBotVisionInterface.h" + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar NextBotSpeedLookAheadRange( "nb_speed_look_ahead_range", "150", FCVAR_CHEAT ); +ConVar NextBotGoalLookAheadRange( "nb_goal_look_ahead_range", "50", FCVAR_CHEAT ); +ConVar NextBotLadderAlignRange( "nb_ladder_align_range", "50", FCVAR_CHEAT ); + +ConVar NextBotAllowAvoiding( "nb_allow_avoiding", "1", FCVAR_CHEAT ); +ConVar NextBotAllowClimbing( "nb_allow_climbing", "1", FCVAR_CHEAT ); +ConVar NextBotAllowGapJumping( "nb_allow_gap_jumping", "1", FCVAR_CHEAT ); + +ConVar NextBotDebugClimbing( "nb_debug_climbing", "0", FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor + */ +PathFollower::PathFollower( void ) +{ + m_goal = NULL; + m_didAvoidCheck = false; + + m_avoidTimer.Invalidate(); + m_waitTimer.Invalidate(); + m_hindrance = NULL; + + m_minLookAheadRange = -1.0f; + + // was 10.0f for L4D - need a better solution here (MSB 5/15/09) + m_goalTolerance = 25.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +class CDetachPath +{ +public: + CDetachPath( PathFollower *path ) + { + m_path = path; + } + + bool operator() ( INextBot *bot ) + { + bot->NotifyPathDestruction( m_path ); + return true; + } + + PathFollower *m_path; +}; + +//-------------------------------------------------------------------------------------------------------------- +PathFollower::~PathFollower() +{ + // allow bots to detach pointer to me + CDetachPath detach( this ); + TheNextBots().ForEachBot( detach ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When the path is invalidated, the follower is also reset + */ +void PathFollower::Invalidate( void ) +{ + // extend + Path::Invalidate(); + + m_goal = NULL; + + m_avoidTimer.Invalidate(); + m_waitTimer.Invalidate(); + m_hindrance = NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when the path is (re)computed (path is valid at the time of this call) + */ +void PathFollower::OnPathChanged( INextBot *bot, Path::ResultType result ) +{ + // start from the beginning + m_goal = FirstSegment(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Adjust speed based on path curvature + */ +void PathFollower::AdjustSpeed( INextBot *bot ) +{ + ILocomotion *mover = bot->GetLocomotionInterface(); + + // if we're coming up on a gap jump, or we're in the air, use maximum speed + if ( ( m_goal && m_goal->type == JUMP_OVER_GAP ) || !mover->IsOnGround() ) + { + mover->SetDesiredSpeed( mover->GetRunSpeed() ); + return; + } + + MoveCursorToClosestPosition( bot->GetPosition() ); + const Path::Data &data = GetCursorData(); + + // speed based on curvature + mover->SetDesiredSpeed( mover->GetRunSpeed() + fabs( data.curvature ) * ( mover->GetWalkSpeed() - mover->GetRunSpeed() ) ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if reached current goal along path + * NOTE: Ladder goals are handled elsewhere + */ +bool PathFollower::IsAtGoal( INextBot *bot ) const +{ + VPROF_BUDGET( "PathFollower::IsAtGoal", "NextBot" ); + + ILocomotion *mover = bot->GetLocomotionInterface(); + IBody *body = bot->GetBodyInterface(); + + // + // m_goal is the node we are moving toward along the path + // current is the node just behind us + // + const Segment *current = PriorSegment( m_goal ); + Vector toGoal = m_goal->pos - mover->GetFeet(); + +// if ( m_goal->type == JUMP_OVER_GAP && !mover->IsOnGround() ) +// { +// // jumping over a gap, don't skip ahead until we land +// return false; +// } + + if ( current == NULL ) + { + // passed goal + return true; + } + else if ( m_goal->type == DROP_DOWN ) + { + // m_goal is the top of the drop-down, and the following segment is the landing point + const Segment *landing = NextSegment( m_goal ); + + if ( landing == NULL ) + { + // passed goal or corrupt path + return true; + } + else + { + // did we reach the ground + if ( mover->GetFeet().z - landing->pos.z < mover->GetStepHeight() ) + { + // reached goal + return true; + } + } + + /// @todo: it is possible to fall into a bad place and get stuck - should move back onto the path + + } + else if ( m_goal->type == CLIMB_UP ) + { + // once jump is started, assume it is successful, since + // nav mesh may be substantially off from actual ground height at landing + const Segment *landing = NextSegment( m_goal ); + + if ( landing == NULL ) + { + // passed goal or corrupt path + return true; + } + else if ( /*!mover->IsOnGround() && */ mover->GetFeet().z > m_goal->pos.z + mover->GetStepHeight() ) + { + // we're off the ground, presumably climbing - assume we reached the goal + return true; + } + /* This breaks infected climbing up holes in the ceiling - they can get within 2D range of m_goal before finding a ledge to climb up to + else if ( mover->IsOnGround() ) + { + // proximity check + // Z delta can be anything, since we may be climbing over a tall fence, a physics prop, etc. + const float rangeTolerance = 10.0f; + if ( toGoal.AsVector2D().IsLengthLessThan( rangeTolerance ) ) + { + // reached goal + return true; + } + } + */ + } + else + { + const Segment *next = NextSegment( m_goal ); + + if ( next ) + { + // because mover may be off the path, check if it crossed the plane of the goal + // check against average of current and next forward vectors + Vector2D dividingPlane; + + if ( current->ladder ) + { + dividingPlane = m_goal->forward.AsVector2D(); + } + else + { + dividingPlane = current->forward.AsVector2D() + m_goal->forward.AsVector2D(); + } + + if ( DotProduct2D( toGoal.AsVector2D(), dividingPlane ) < 0.0001f && + abs( toGoal.z ) < body->GetStandHullHeight() ) + { + // only skip higher Z goal if next goal is directly reachable + // can't use this for positions below us because we need to be able + // to climb over random objects along our path that we can't actually + // move *through* + if ( toGoal.z < mover->GetStepHeight() && ( mover->IsPotentiallyTraversable( mover->GetFeet(), next->pos ) && !mover->HasPotentialGap( mover->GetFeet(), next->pos ) ) ) + { + // passed goal + return true; + } + } + } + + // proximity check + // Z delta can be anything, since we may be climbing over a tall fence, a physics prop, etc. + if ( toGoal.AsVector2D().IsLengthLessThan( m_goalTolerance ) ) + { + // reached goal + return true; + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move bot along ladder. Return true if ladder motion is in progress, false if complete. + */ +bool PathFollower::LadderUpdate( INextBot *bot ) +{ + VPROF_BUDGET( "PathFollower::LadderUpdate", "NextBot" ); + + ILocomotion *mover = bot->GetLocomotionInterface(); + IBody *body = bot->GetBodyInterface(); + + if ( mover->IsUsingLadder() ) + { + // wait for locomotor to finish traversing ladder + return true; + } + + if ( m_goal->ladder == NULL ) + { + // Check if we have somehow ended up on a ladder, if so, and its a tall down-ladder we are expecting, jump the path ahead. + // This happens for players, who run off ledges and the gamemovement sticks them onto ladders. We only care about + // tall down-ladders, because up ladders work without this, and short ladders aren't dangerous to miss and drop down + // instead of climbing down. + if ( bot->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) + { + // 'current' is the segment we are on/just passed over + const Segment *current = PriorSegment( m_goal ); + if ( current == NULL ) + { + return false; + } + + // Start with current, the segment we are currently traversing. Skip the distance check for that segment, because + // the pos is (hopefully) behind us. And if it's a long path segment, it's already outside the climbLookAheadRange, + // and thus it would prevent us looking at m_goal and further for imminent planned climbs. + // 'current' is the segment we are on/just passed over + const float ladderLookAheadRange = 50.0f; + for( const Segment *s = current; s; s = NextSegment( s ) ) + { + if ( s != current && ( s->pos - mover->GetFeet() ).AsVector2D().IsLengthGreaterThan( ladderLookAheadRange ) ) + { + break; + } + + // Only consider reasonably tall down ladders - if we don't grab onto a short ladder, it hopefully won't be a bad fall. + if ( s->ladder != NULL && s->how == GO_LADDER_DOWN && s->ladder->m_length > mover->GetMaxJumpHeight() ) + { + float destinationHeightDelta = s->pos.z - mover->GetFeet().z; + if ( fabs(destinationHeightDelta) < mover->GetMaxJumpHeight() ) + { + // Advance the goal, and fall through to the normal codepath. + m_goal = s; + break; + } + } + } + } + + if ( m_goal->ladder == NULL ) + { + // no ladder to use + return false; + } + } + + + // start using the ladder + const float mountRange = 25.0f; + + if ( m_goal->how == GO_LADDER_UP ) + { + // check if we're off the ladder and at the top + if ( !mover->IsUsingLadder() && mover->GetFeet().z > m_goal->ladder->m_top.z - mover->GetStepHeight() ) + { + // we're up + m_goal = NextSegment( m_goal ); + return false; + } + + // approach the ladder + Vector2D to = ( m_goal->ladder->m_bottom - mover->GetFeet() ).AsVector2D(); + + body->AimHeadTowards( m_goal->ladder->m_top - 50.0f * m_goal->ladder->GetNormal() + Vector( 0, 0, body->GetCrouchHullHeight() ), + IBody::CRITICAL, + 2.0f, + NULL, + "Mounting upward ladder" ); + + float range = to.NormalizeInPlace(); + if ( range < NextBotLadderAlignRange.GetFloat() ) + { + // getting close - line up + Vector2D ladderNormal2D = m_goal->ladder->GetNormal().AsVector2D(); + float dot = DotProduct2D( ladderNormal2D, to ); + + const float cos5 = 0.9f; + if ( dot < -cos5 ) + { + // lined up - continue approach + mover->Approach( m_goal->ladder->m_bottom ); + + if ( range < mountRange ) + { + // go up ladder + mover->ClimbLadder( m_goal->ladder, m_goal->area ); + } + } + else + { + // rotate around ladder and maintain distance from it + Vector myPerp( -to.y, to.x, 0.0f ); + Vector2D ladderPerp2D( -ladderNormal2D.y, ladderNormal2D.x ); + + Vector goal = m_goal->ladder->m_bottom; + + float alignRange = NextBotLadderAlignRange.GetFloat(); + + if ( dot < 0.0f ) + { + // we are on the correct side of the ladder + // align range should drop off as we reach alignment + alignRange = mountRange + (1.0f + dot) * (alignRange - mountRange); + } + + goal.x -= alignRange * to.x; + goal.y -= alignRange * to.y; + + if ( DotProduct2D( to, ladderPerp2D ) < 0.0f ) + { + goal += 10.0f * myPerp; + } + else + { + goal -= 10.0f * myPerp; + } + + mover->Approach( goal ); + } + } + else + { + // approach the base of the ladder - use normal path following in case there are jumps/climbs on the way to the ladder + return false; + } + } + else // go down ladder + { + // check if we fell off and are now below the ladder + if ( mover->GetFeet().z < m_goal->ladder->m_bottom.z + mover->GetStepHeight() ) + { + // we fell + m_goal = NextSegment( m_goal ); + } + else + { + // approach the ladder + Vector mountPoint = m_goal->ladder->m_top + 0.5f * body->GetHullWidth() * m_goal->ladder->GetNormal(); + Vector2D to = ( mountPoint - mover->GetFeet() ).AsVector2D(); + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + const float size = 5.0f; + NDebugOverlay::Sphere( mountPoint, size, 255, 0, 255, true, 0.1f ); + } + + body->AimHeadTowards( m_goal->ladder->m_bottom + 50.0f * m_goal->ladder->GetNormal() + Vector( 0, 0, body->GetCrouchHullHeight() ), + IBody::CRITICAL, + 1.0f, + NULL, + "Mounting downward ladder" ); + + float range = to.NormalizeInPlace(); + + // Approach the top of the ladder. If we're already on the ladder, start descending. + if ( range < mountRange || bot->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) + { + // go down ladder + mover->DescendLadder( m_goal->ladder, m_goal->area ); + + // increment goal segment since locomotor will move us along the ladder + m_goal = NextSegment( m_goal ); + } + else + { + // approach the top of the ladder - use normal path following in case there are jumps/climbs on the way to the ladder + return false; + } + } + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check if we have reached our current path goal and + * iterate to next goal or finish the path + */ +bool PathFollower::CheckProgress( INextBot *bot ) +{ + ILocomotion *mover = bot->GetLocomotionInterface(); + + // skip nearby goal points that are redundant to smooth path following motion + const Path::Segment *pSkipToGoal = NULL; + if ( m_minLookAheadRange > 0.0f ) + { + pSkipToGoal = m_goal; + const Vector &myFeet = mover->GetFeet(); + while( pSkipToGoal && pSkipToGoal->type == ON_GROUND && mover->IsOnGround() ) + { + if ( ( pSkipToGoal->pos - myFeet ).IsLengthLessThan( m_minLookAheadRange ) ) + { + // goal is too close - step to next segment + const Path::Segment *nextSegment = NextSegment( pSkipToGoal ); + + if ( !nextSegment || nextSegment->type != ON_GROUND ) + { + // can't skip ahead to next segment - head towards current goal + break; + } + + if ( nextSegment->pos.z > myFeet.z + mover->GetStepHeight() ) + { + // going uphill or up stairs tends to cause problems if we skip ahead, so don't + break; + } + +#ifdef DOTA_DLL + if ( DotProduct( mover->GetMotionVector(), nextSegment->forward ) <= 0.1f ) + { + // don't skip sharp turns + break; + } +#endif + + // can we reach the next path segment directly + if ( mover->IsPotentiallyTraversable( myFeet, nextSegment->pos ) && !mover->HasPotentialGap( myFeet, nextSegment->pos ) ) + { + pSkipToGoal = nextSegment; + } + else + { + // can't directly reach next segment - keep heading towards current goal + break; + } + } + else + { + // goal is farther than min lookahead + break; + } + } + + // didn't find any goal to skip to + if ( pSkipToGoal == m_goal ) + { + pSkipToGoal = NULL; + } + } + + if ( IsAtGoal( bot ) ) + { + // iterate to next segment of the path + const Path::Segment *nextSegment = pSkipToGoal ? pSkipToGoal : NextSegment( m_goal ); + + if ( nextSegment == NULL ) + { + // must be on ground to complete the path + if ( mover->IsOnGround() ) + { + // the end of the path has been reached + mover->GetBot()->OnMoveToSuccess( this ); + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "PathFollower: OnMoveToSuccess\n" ); + } + + // don't invalidate if OnMoveToSuccess just recomputed a new path + if ( GetAge() > 0.0f ) + { + Invalidate(); + } + + return false; + } + } + else + { + // keep moving + m_goal = nextSegment; + + if ( bot->IsDebugging( NEXTBOT_PATH ) && !mover->IsPotentiallyTraversable( mover->GetFeet(), nextSegment->pos ) ) + { + Warning( "PathFollower: path to my goal is blocked by something\n" ); + NDebugOverlay::Sphere( m_goal->pos, 5.f, 255, 0, 0, true, 3.f ); + } + } + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move mover along path + */ +void PathFollower::Update( INextBot *bot ) +{ + VPROF_BUDGET( "PathFollower::Update", "NextBotSpiky" ); + + // track most recent path followed + bot->SetCurrentPath( this ); + + + ILocomotion *mover = bot->GetLocomotionInterface(); + + if ( !IsValid() || m_goal == NULL ) + { + return; + } + + if ( !m_waitTimer.IsElapsed() ) + { + // still waiting + //mover->ClearStuckStatus( "Waiting for blocker to move" ); + return; + } + +// m_didAvoidCheck = false; + + + if ( LadderUpdate( bot ) ) + { + // we are traversing a ladder + return; + } + + + // adjust speed based on path curvature + AdjustSpeed( bot ); + + if ( CheckProgress( bot ) == false ) + { + // goal reached + return; + } + + // use the direction towards the goal as 'forward' direction + Vector forward = m_goal->pos - mover->GetFeet(); + + if ( m_goal->type == CLIMB_UP ) + { + const Segment *next = NextSegment( m_goal ); + if ( next ) + { + // use landing of climb up as forward to help ledge detection + forward = next->pos - mover->GetFeet(); + } + } + + forward.z = 0.0f; + + float goalRange = forward.NormalizeInPlace(); + + Vector left( -forward.y, forward.x, 0.0f ); + + if ( left.IsZero() ) + { + // if left is zero, forward must also be - path follow failure + mover->GetBot()->OnMoveToFailure( this, FAIL_STUCK ); + + // don't invalidate if OnMoveToFailure just recomputed a new path + if ( GetAge() > 0.0f ) + { + Invalidate(); + } + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "PathFollower: OnMoveToFailure( FAIL_STUCK ) because forward and left are ZERO\n" ); + } + + return; + } + + // unit vectors must follow floor slope + const Vector &normal = mover->GetGroundNormal(); + + // get forward vector along floor + forward = CrossProduct( left, normal ); + + // correct the sideways vector + left = CrossProduct( normal, forward ); + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + float axisSize = 25.0f; + NDebugOverlay::Line( mover->GetFeet(), mover->GetFeet() + axisSize * forward, 255, 0, 0, true, 0.1f ); + NDebugOverlay::Line( mover->GetFeet(), mover->GetFeet() + axisSize * normal, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Line( mover->GetFeet(), mover->GetFeet() + axisSize * left, 0, 0, 255, true, 0.1f ); + } + + // climb up ledges + if ( !Climbing( bot, m_goal, forward, left, goalRange ) ) + { + // a failed climb could mean an invalid path + if ( !IsValid() ) + { + return; + } + + // jump over gaps + JumpOverGaps( bot, m_goal, forward, left, goalRange ); + } + + // event callbacks from the above climbs and jumps may invalidate the path + if ( !IsValid() ) + { + return; + } + + // if our movement goal is high above us, we must have fallen + CNavArea *myArea = bot->GetEntity()->GetLastKnownArea(); + bool isOnStairs = ( myArea && myArea->HasAttributes( NAV_MESH_STAIRS ) ); + + // limit too high distance to reasonable value for bots that can climb very high + float tooHighDistance = mover->GetMaxJumpHeight(); + + if ( !m_goal->ladder && !mover->IsClimbingOrJumping() && !isOnStairs && m_goal->pos.z > mover->GetFeet().z + tooHighDistance ) + { + const float closeRange = 25.0f; // 75.0f; + Vector2D to( mover->GetFeet().x - m_goal->pos.x, mover->GetFeet().y - m_goal->pos.y ); + if ( mover->IsStuck() || to.IsLengthLessThan( closeRange ) ) + { + // the goal is too high to reach + + // check if we can reach the next segment, in case this was a "jump down" situation + const Path::Segment *next = NextSegment( m_goal ); + if ( mover->IsStuck() || !next || ( next->pos.z - mover->GetFeet().z > mover->GetMaxJumpHeight() ) || !mover->IsPotentiallyTraversable( mover->GetFeet(), next->pos ) ) + { + // the next node is too high, too - we really did fall off the path + mover->GetBot()->OnMoveToFailure( this, FAIL_FELL_OFF ); + + // don't invalidate if OnMoveToFailure just recomputed a new path + if ( GetAge() > 0.0f ) + { + Invalidate(); + } + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "PathFollower: OnMoveToFailure( FAIL_FELL_OFF )\n" ); + } + + // reset stuck status since we're (likely) repathing anyways. otherwise, we could be stuck in a loop here and not move + mover->ClearStuckStatus( "Fell off path" ); + + return; + } + } + } + + + Vector goalPos = m_goal->pos; + + // avoid small obstacles + forward = goalPos - mover->GetFeet(); + forward.z = 0.0f; + float rangeToGoal = forward.NormalizeInPlace(); + + left.x = -forward.y; + left.y = forward.x; + left.z = 0.0f; + + if ( true || m_goal != LastSegment() ) // think more about this - we often need to avoid to reach the final goal pos, too (MSB 5/15/09) + { + const float nearLedgeRange = 50.0f; + if ( rangeToGoal > nearLedgeRange || ( m_goal && m_goal->type != CLIMB_UP ) ) + { + goalPos = Avoid( bot, goalPos, forward, left ); + } + } + + // face towards movement goal + if ( mover->IsOnGround() ) + { + mover->FaceTowards( goalPos ); + } + + // move bot along path + mover->Approach( goalPos ); + + // Currently, Approach determines STAND or CROUCH. + // Override this if we're approaching a climb or a jump + if ( m_goal && ( m_goal->type == CLIMB_UP || m_goal->type == JUMP_OVER_GAP ) ) + { + bot->GetBodyInterface()->SetDesiredPosture( IBody::STAND ); + } + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + const Segment *start = GetCurrentGoal(); + if ( start ) + { + start = PriorSegment( start ); + } + + Draw( start ); + + /* + else + { + DrawInterpolated( 0.0f, GetLength() ); + } + */ + + NDebugOverlay::Cross3D( goalPos, 5.0f, 150, 150, 255, true, 0.1f ); + NDebugOverlay::Line( bot->GetEntity()->WorldSpaceCenter(), goalPos, 255, 255, 0, true, 0.1f ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * If entity is returned, it is blocking us from continuing along our path + */ +CBaseEntity *PathFollower::FindBlocker( INextBot *bot ) +{ + IIntention *think = bot->GetIntentionInterface(); + + // if we don't care about hindrances, don't do the expensive tests + if ( think->IsHindrance( bot, IS_ANY_HINDRANCE_POSSIBLE ) != ANSWER_YES ) + return NULL; + + ILocomotion *mover = bot->GetLocomotionInterface(); + IBody *body = bot->GetBodyInterface(); + + trace_t result; + NextBotTraceFilterOnlyActors filter( bot->GetEntity(), COLLISION_GROUP_NONE ); + + const float size = body->GetHullWidth()/4.0f; // keep this small to avoid lockups when groups of bots get close + Vector blockerMins( -size, -size, mover->GetStepHeight() ); + Vector blockerMaxs( size, size, body->GetCrouchHullHeight() ); + + Vector from = mover->GetFeet(); + float range = 0.0f; + + const float maxHindranceRangeAlong = 750.0f; + + // because our path goal may be far ahead of us if the way to there is unobstructed, we + // need to start looking from the point of the path we are actually standing on + MoveCursorToClosestPosition( mover->GetFeet() ); + + for( const Segment *s = GetCursorData().segmentPrior; s && range < maxHindranceRangeAlong; s = NextSegment( s ) ) + { + // trace along direction toward goal a minimum range, in case goal and hindrance are + // very close, but goal is closer + + Vector traceForward = s->pos - from; + float traceRange = traceForward.NormalizeInPlace(); + + const float minTraceRange = 2.0f * body->GetHullWidth(); + if ( traceRange < minTraceRange ) + { + traceRange = minTraceRange; + } + + mover->TraceHull( from, from + traceRange * traceForward, blockerMins, blockerMaxs, body->GetSolidMask(), &filter, &result ); + + if ( result.DidHitNonWorldEntity() ) + { + // if blocker is close, they could be behind us - check + Vector toBlocker = result.m_pEnt->GetAbsOrigin() - bot->GetLocomotionInterface()->GetFeet(); + + Vector alongPath = s->pos - from; + alongPath.z = 0.0f; + + if ( DotProduct( toBlocker, alongPath ) > 0.0f ) + { + // ask the bot if this really is a hindrance + if ( think->IsHindrance( bot, result.m_pEnt ) == ANSWER_YES ) + { + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Circle( bot->GetLocomotionInterface()->GetFeet(), QAngle( -90.0f, 0, 0 ), 10.0f, 255, 0, 0, 255, true, 1.0f ); + NDebugOverlay::HorzArrow( bot->GetLocomotionInterface()->GetFeet(), result.m_pEnt->GetAbsOrigin(), 1.0f, 255, 0, 0, 255, true, 1.0f ); + } + + // we are blocked + return result.m_pEnt; + } + } + } + + from = s->pos; + range += s->length; + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do reflex avoidance movements of very nearby obstacles. + * Return adjusted goal. + */ +Vector PathFollower::Avoid( INextBot *bot, const Vector &goalPos, const Vector &forward, const Vector &left ) +{ + VPROF_BUDGET( "PathFollower::Avoid", "NextBotExpensive" ); + + if ( !NextBotAllowAvoiding.GetBool() ) + { + return goalPos; + } + + if ( !m_avoidTimer.IsElapsed() ) + { + return goalPos; + } + + // low frequency check until we actually hit something we need to avoid + const float avoidInterval = 0.5f; // 1.0f; + m_avoidTimer.Start( avoidInterval ); + + ILocomotion *mover = bot->GetLocomotionInterface(); + + if ( mover->IsClimbingOrJumping() || !mover->IsOnGround() ) + { + return goalPos; + } + + // + // Check for potential blockers along our path and wait if we're blocked + // + m_hindrance = FindBlocker( bot ); + if ( m_hindrance != NULL ) + { + // wait + m_waitTimer.Start( avoidInterval * RandomFloat( 1.0f, 2.0f ) ); + + return mover->GetFeet(); + } + + + // if we are in a "precise" area, do not use avoid volumes + CNavArea *area = bot->GetEntity()->GetLastKnownArea(); + if ( area && ( area->GetAttributes() & NAV_MESH_PRECISE ) ) + { + return goalPos; + } + + m_didAvoidCheck = true; + + // we want to avoid other players, etc + trace_t result; + NextBotTraceFilterOnlyActors filter( bot->GetEntity(), COLLISION_GROUP_NONE ); + + IBody *body = bot->GetBodyInterface(); + unsigned int mask = body->GetSolidMask(); + + const float size = body->GetHullWidth()/4.0f; + const float offset = size + 2.0f; + + float range = mover->IsRunning() ? 50.0f : 30.0f; + range *= bot->GetEntity()->GetModelScale(); + + m_hullMin = Vector( -size, -size, mover->GetStepHeight()+0.1f ); + + // only use crouch-high avoid volumes, since we'll just crouch if higher obstacles are near + m_hullMax = Vector( size, size, body->GetCrouchHullHeight() ); + + Vector nextStepHullMin( -size, -size, 2.0f * mover->GetStepHeight() + 0.1f ); + + // avoid any open doors in our way + CBasePropDoor *door = NULL; + + // check left side + m_leftFrom = mover->GetFeet() + offset * left; + m_leftTo = m_leftFrom + range * forward; + + m_isLeftClear = true; + float leftAvoid = 0.0f; + + NextBotTraversableTraceFilter traverseFilter( bot ); + mover->TraceHull( m_leftFrom, m_leftTo, m_hullMin, m_hullMax, mask, &traverseFilter, &result ); + if ( result.fraction < 1.0f || result.startsolid ) + { + // if this sensor is starting in a solid, set fraction to emulate being against a wall + if ( result.startsolid ) + { + result.fraction = 0.0f; + } + + leftAvoid = clamp( 1.0f - result.fraction, 0.0f, 1.0f ); + + m_isLeftClear = false; + + // track any doors we need to avoid + if ( result.DidHitNonWorldEntity() ) + { + door = dynamic_cast< CBasePropDoor * >( result.m_pEnt ); + } + + // check for steps +// float firstHit = result.fraction; +// mover->TraceHull( m_leftFrom, m_leftTo, nextStepHullMin, m_hullMax, mask, &filter, &result ); +// if ( result.fraction <= firstHit ) //+ mover->GetStepHeight()/2.0f ) +// { +// // it's not a step - we hit something +// m_isLeftClear = false; +// } + } + + // check right side + m_rightFrom = mover->GetFeet() - offset * left; + m_rightTo = m_rightFrom + range * forward; + + m_isRightClear = true; + float rightAvoid = 0.0f; + + mover->TraceHull( m_rightFrom, m_rightTo, m_hullMin, m_hullMax, mask, &traverseFilter, &result ); + if ( result.fraction < 1.0f || result.startsolid ) + { + // if this sensor is starting in a solid, set fraction to emulate being against a wall + if ( result.startsolid ) + { + result.fraction = 0.0f; + } + + rightAvoid = clamp( 1.0f - result.fraction, 0.0f, 1.0f ); + + m_isRightClear = false; + + // track any doors we need to avoid + if ( !door && result.DidHitNonWorldEntity() ) + { + door = dynamic_cast< CBasePropDoor * >( result.m_pEnt ); + } + + // check for steps +// float firstHit = result.fraction; +// mover->TraceHull( m_rightFrom, m_rightTo, nextStepHullMin, m_hullMax, mask, &filter, &result ); +// if ( result.fraction <= firstHit ) // + mover->GetStepHeight()/2.0f) +// { +// // it's not a step - we hit something +// m_isRightClear = false; +// } + } + + Vector adjustedGoal = goalPos; + + // avoid doors directly in our way + if ( door && !m_isLeftClear && !m_isRightClear ) + { + Vector forward, right, up; + AngleVectors( door->GetAbsAngles(), &forward, &right, &up ); + + const float doorWidth = 100.0f; + Vector doorEdge = door->GetAbsOrigin() - doorWidth * right; + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Axis( door->GetAbsOrigin(), door->GetAbsAngles(), 20.0f, true, 10.0f ); + NDebugOverlay::Line( door->GetAbsOrigin(), doorEdge, 255, 255, 0, true, 10.0f ); + } + + // move around door + adjustedGoal.x = doorEdge.x; + adjustedGoal.y = doorEdge.y; + + // do avoid check again next frame + m_avoidTimer.Invalidate(); + } + else if ( !m_isLeftClear || !m_isRightClear ) + { + // adjust goal to avoid small obstacle + float avoidResult = 0.0f; + if ( m_isLeftClear ) + { + avoidResult = -rightAvoid; + } + else if (m_isRightClear) + { + avoidResult = leftAvoid; + } + else + { + // both left and right are blocked, avoid nearest + const float equalTolerance = 0.01f; + if ( fabs( rightAvoid - leftAvoid ) < equalTolerance ) + { + // squarely against a wall, etc + return adjustedGoal; + } + else if ( rightAvoid > leftAvoid ) + { + avoidResult = -rightAvoid; + } + else + { + avoidResult = leftAvoid; + } + } + + // adjust goal to avoid obstacle + Vector avoidDir = 0.5f * forward - left * avoidResult; + avoidDir.NormalizeInPlace(); + + adjustedGoal = mover->GetFeet() + 100.0f * avoidDir; + + // do avoid check again next frame + m_avoidTimer.Invalidate(); + } + + return adjustedGoal; +} + + +#ifdef EXPERIMENTAL_LEDGE_FINDER +//-------------------------------------------------------------------------------------------------------------- +/** + * Given a hull that defines the area of space that may contain a climbable ledge, + * subdivide it until we find the ledge. + */ +bool PathFollower::FindClimbLedge( INextBot *bot, Vector startTracePos, Vector ledgeRegionMins, Vector ledgeRegionMaxs ) +{ + float deltaZ = ledgeRegionMaxs.z - ledgeRegionMins.z; + + if ( deltaZ <= bot->GetLocomotionInterface()->GetStepHeight() ) + { + // reached minimum subdivision limit - stop + return false; + } + + trace_t result; + NextBotTraversableTraceFilter filter( bot, ILocomotion::IMMEDIATELY ); + + mover->TraceHull( startTracePos, startTracePos, + ledgeRegionMins, ledgeRegionMaxs, + bot->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + + if ( result.DidHit() ) + { + // volume is blocked - split into upper and lower volumes and try again + float midZ = ( ledgeRegionMins.z + ledgeRegionMaxs.z ) / 2.0f; + + Vector upperLedgeRegionMins( ledgeRegionMins.x, ledgeRegionMins.y, midZ ); + Vector upperLedgeRegionMaxs = ledgeRegionMaxs; + FindClimbLedge( bot, startTracePos, upperLedgeRegionMins, upperLedgeRegionMaxs ); + + + Vector lowerLedgeRegionMins = ledgeRegionMins; + Vector lowerLedgeRegionMaxs( ledgeRegionMaxs.x, ledgeRegionMaxs.y, midZ ); + FindClimbLedge( bot, startTracePos, lowerLedgeRegionMins, lowerLedgeRegionMaxs ); + } + else + { + // volume is clear, trace straight down to find ledge and keep lowest one we've found + mover->TraceHull( startTracePos, + startTracePos + Vector( 0, 0, -100.0f ), + ledgeRegionMins, ledgeRegionMaxs, + bot->GetBodyInterface()->GetSolidMask(), &filter, &result ); + } +} +#endif // _DEBUG + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Climb up ledges + */ +bool PathFollower::Climbing( INextBot *bot, const Path::Segment *goal, const Vector &forward, const Vector &right, float goalRange ) +{ + VPROF_BUDGET( "PathFollower::Climbing", "NextBot" ); + + ILocomotion *mover = bot->GetLocomotionInterface(); + IBody *body = bot->GetBodyInterface(); + CNavArea *myArea = bot->GetEntity()->GetLastKnownArea(); + + if ( !mover->IsAbleToClimb() || !NextBotAllowClimbing.GetBool() ) + { + return false; + } + + // use the 2D direction towards our goal + Vector climbDirection = forward; + climbDirection.z = 0.0f; + climbDirection.NormalizeInPlace(); + + // we can't have this as large as our hull width, or we'll find ledges ahead of us + // that we will fall from when we climb up because our hull wont actually touch at the top. + const float ledgeLookAheadRange = body->GetHullWidth() - 1; + + if ( mover->IsClimbingOrJumping() || mover->IsAscendingOrDescendingLadder() || !mover->IsOnGround() ) + { + return false; + } + + // can be in any posture when we climb + + if ( m_goal == NULL ) + { + return false; + } + + if ( TheNavMesh->IsAuthoritative() ) + { + // + // Trust what that nav mesh tells us. + // No need for expensive ledge-finding for games with simpler geometry (like TF2) + // + if ( m_goal->type == CLIMB_UP ) + { + const Segment *afterClimb = NextSegment( m_goal ); + if ( afterClimb && afterClimb->area ) + { + // find closest point on climb-destination area + Vector nearClimbGoal; + afterClimb->area->GetClosestPointOnArea( mover->GetFeet(), &nearClimbGoal ); + + climbDirection = nearClimbGoal - mover->GetFeet(); + climbDirection.z = 0.0f; + climbDirection.NormalizeInPlace(); + + if ( mover->ClimbUpToLedge( nearClimbGoal, climbDirection, NULL ) ) + return true; + } + } + + return false; + } + + + // If we're approaching a CLIMB_UP link, save off the height delta for it, and trust the nav *just* enough + // to climb up to that ledge and only that ledge. We keep as large a tolerance as possible, to trust + // the nav as little as possible. There's no valid way to have another CLIMB_UP link within crouch height, + // because we can't actually fit in between the two areas, so one climb is invalid. + float climbUpLedgeHeightDelta = -1.0f; + const float climbUpLedgeTolerance = body->GetCrouchHullHeight(); + + if ( m_goal->type == CLIMB_UP ) + { + const Segment *afterClimb = NextSegment( m_goal ); + if ( afterClimb && afterClimb->area ) + { + // find closest point on climb-destination area + Vector nearClimbGoal; + afterClimb->area->GetClosestPointOnArea( mover->GetFeet(), &nearClimbGoal ); + + climbDirection = nearClimbGoal - mover->GetFeet(); + climbUpLedgeHeightDelta = climbDirection.z; + climbDirection.z = 0.0f; + climbDirection.NormalizeInPlace(); + } + } + + // don't try to climb up stairs + if ( m_goal->area->HasAttributes( NAV_MESH_STAIRS ) || ( myArea && myArea->HasAttributes( NAV_MESH_STAIRS ) ) ) + { + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Cross3D( mover->GetFeet(), 5.0f, 0, 255, 255, true, 5.0f ); + DevMsg( "%3.2f: %s ON STAIRS\n", gpGlobals->curtime, bot->GetDebugIdentifier() ); + } + return false; + } + + // 'current' is the segment we are on/just passed over + const Segment *current = PriorSegment( m_goal ); + if ( current == NULL ) + { + return false; + } + + // If path segment immediately ahead of us is not obstructed, don't try to climb. + // This is required to try to avoid accidentally climbing onto valid high ledges when we really want to run UNDER them to our destination. + // We need to check "immediate" traversability to pay attention to breakable objects in our way that we should climb over. + // We also need to check traversability out to 2 * ledgeLookAheadRange in case our goal is just before a tricky ledge climb and once we pass the goal it will be too late. + // When we're in a CLIMB_UP segment, allow us to look for ledges - we know the destination ledge height, and will only grab the correct ledge. + Vector toGoal = m_goal->pos - mover->GetFeet(); + toGoal.NormalizeInPlace(); + + if ( toGoal.z < mover->GetTraversableSlopeLimit() && + !mover->IsStuck() && m_goal->type != CLIMB_UP && + mover->IsPotentiallyTraversable( mover->GetFeet(), mover->GetFeet() + 2.0f * ledgeLookAheadRange * toGoal, ILocomotion::IMMEDIATELY ) ) + { + return false; + } + + + // can't do this - we have to find the ledge to deal with breakable railings +#if 0 + // If our path requires a climb, do the climb. + // This solves some issues where there are several possible climbable ledges at a given + // location, and we need to know which ledge to climb - just use the preplanned path's choice. + const Segment *ledge = NextSegment( m_goal ); + if ( m_goal->type == CLIMB_UP && ledge ) + { + const float startClimbRange = body->GetHullWidth(); + if ( ( m_goal->pos - mover->GetFeet() ).IsLengthLessThan( startClimbRange ) ) + { + mover->ClimbUpToLedge( ledge->pos, climbDirection ); + return true; + } + } +#endif + + + + // Determine if we're approaching a planned climb. + // Start with current, the segment we are currently traversing. Skip the distance check for that segment, because + // the pos is (hopefully) behind us. And if it's a long path segment, it's already outside the climbLookAheadRange, + // and thus it would prevent us looking at m_goal and further for imminent planned climbs. + const float climbLookAheadRange = 150.0f; + bool isPlannedClimbImminent = false; + float plannedClimbZ = 0.0f; + for( const Segment *s = current; s; s = NextSegment( s ) ) + { + if ( s != current && ( s->pos - mover->GetFeet() ).AsVector2D().IsLengthGreaterThan( climbLookAheadRange ) ) + { + break; + } + + if ( s->type == CLIMB_UP ) + { + isPlannedClimbImminent = true; + + const Segment *next = NextSegment( s ); + if ( next ) + { + plannedClimbZ = next->pos.z; + } + break; + } + } + + unsigned int mask = body->GetSolidMask(); + trace_t result; + NextBotTraversableTraceFilter filter( bot, ILocomotion::IMMEDIATELY ); + + const float hullWidth = body->GetHullWidth(); + const float halfSize = hullWidth / 2.0f; + const float minHullHeight = body->GetCrouchHullHeight(); + const float minLedgeHeight = mover->GetStepHeight() + 0.1f; + + Vector skipStepHeightHullMin( -halfSize, -halfSize, minLedgeHeight ); + + // need to use minimum actual hull height here to catch porous fences and railings + Vector skipStepHeightHullMax( halfSize, halfSize, minHullHeight + 0.1f ); + + + // Find the highest height we can stand at our current location. + // Using the full width hull catches on small lips/ledges, so back up and try again. + float ceilingFraction; + + // We can't use IsPotentiallyTraversable to test for ledges, because it's smaller Hull can cause the + // next trace (trace the ceiling height forward) to start solid. + // mover->IsPotentiallyTraversable( mover->GetFeet(), mover->GetFeet() + Vector( 0, 0, mover->GetMaxJumpHeight() ), ILocomotion::IMMEDIATELY, &ceilingFraction ); + + // Instead of IsPotentiallyTraversable, we back up the same distance and use a second upward trace + // to see if that one finds a higher ceiling. If so, we use that ceiling height, and use the + // backed-up feet position for the ledge finding traces. + Vector feet( mover->GetFeet() ); + Vector ceiling( feet + Vector( 0, 0, mover->GetMaxJumpHeight() ) ); + mover->TraceHull( feet, ceiling, + skipStepHeightHullMin, skipStepHeightHullMax, mask, &filter, &result ); + ceilingFraction = result.fraction; + bool isBackupTraceUsed = false; + if ( ceilingFraction < 1.0f || result.startsolid ) + { + trace_t backupTrace; + const float backupDistance = hullWidth * 0.25f; // The IsPotentiallyTraversable check this replaces uses a 1/4 hull width trace + Vector backupFeet( feet - climbDirection * backupDistance ); + Vector backupCeiling( backupFeet + Vector( 0, 0, mover->GetMaxJumpHeight() ) ); + mover->TraceHull( backupFeet, backupCeiling, + skipStepHeightHullMin, skipStepHeightHullMax, mask, &filter, &backupTrace ); + if ( !backupTrace.startsolid && backupTrace.fraction > ceilingFraction ) + { + bot->DebugConColorMsg( NEXTBOT_PATH, Color( 255, 255, 255, 255 ), "%s backing up when looking for max ledge height\n", bot->GetDebugIdentifier() ); + result = backupTrace; + ceilingFraction = result.fraction; + feet = backupFeet; + ceiling = backupCeiling; + isBackupTraceUsed = true; + } + } + + float maxLedgeHeight = ceilingFraction * mover->GetMaxJumpHeight(); + + if ( maxLedgeHeight <= mover->GetStepHeight() ) + { + return false; // early out when we can't even climb StepHeight. + } + + // + // Check for ledge climbs over things in our way. + // Even if we have a CLIMB_UP link in our path, we still need + // to find the actual ledge by tracing the local geometry. + // + Vector climbHullMax( halfSize, halfSize, maxLedgeHeight ); + + Vector ledgePos = feet; // to be computed below + + mover->TraceHull( feet, + feet + climbDirection * ledgeLookAheadRange, + skipStepHeightHullMin, climbHullMax, mask, &filter, &result ); + + if ( bot->IsDebugging( NEXTBOT_PATH ) && NextBotDebugClimbing.GetBool() ) + { + // show ledge-finding hull as we move + NDebugOverlay::SweptBox( feet, + feet + climbDirection * ledgeLookAheadRange, + skipStepHeightHullMin, climbHullMax, vec3_angle, + 255, 100, 0, 255, 0.1f ); + } + + bool wasPotentialLedgeFound = result.DidHit() && !result.startsolid; + // To test climbing up past small lips on walls, we can force the bot to run past the overhang and use the backup trace: + // wasPotentialLedgeFound = wasPotentialLedgeFound && (result.fraction == 0 || isBackupTraceUsed); + if ( wasPotentialLedgeFound ) + { + VPROF_BUDGET( "PathFollower::Climbing( Search for ledge to climb )", "NextBot" ); + + if ( bot->IsDebugging( NEXTBOT_PATH ) && NextBotDebugClimbing.GetBool() ) + { + // show ledge-finding hull that found a ledge candidate + NDebugOverlay::SweptBox( feet, + feet + climbDirection * ledgeLookAheadRange, + skipStepHeightHullMin, climbHullMax, vec3_angle, + 255, 100, 0, 100, 999.9f ); + + // show primary climb direction + NDebugOverlay::HorzArrow( feet, feet + 50.0f * climbDirection, 2.0f, 0, 255, 0, 255, true, 9999.9f ); + } + + // what are we climbing over? + CBaseEntity *obstacle = result.m_pEnt; + + if ( !result.DidHitNonWorldEntity() || bot->IsAbleToClimbOnto( obstacle ) ) + { + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%3.2f: %s at potential ledge climb\n", gpGlobals->curtime, bot->GetDebugIdentifier() ); + } + + // the low hull sweep hit an obstacle - note how 'far in' this is + float ledgeFrontWallDepth = ledgeLookAheadRange * result.fraction; + + float minLedgeDepth = body->GetHullWidth()/2.0f; // 5.0f; + if ( m_goal->type == CLIMB_UP ) + { + // Climbing up to a narrow nav area indicates a narrow ledge. We need to reduce our minLedgeDepth + // here or our path will say we should climb but we'll forever fail to find a wide enough ledge. + const Segment *afterClimb = NextSegment( m_goal ); + if ( afterClimb && afterClimb->area ) + { + Vector depthVector = climbDirection * minLedgeDepth; + depthVector.z = 0; + if ( fabs( depthVector.x ) > afterClimb->area->GetSizeX() ) + { + depthVector.x = (depthVector.x > 0) ? afterClimb->area->GetSizeX() : -afterClimb->area->GetSizeX(); + } + if ( fabs( depthVector.y ) > afterClimb->area->GetSizeY() ) + { + depthVector.y = (depthVector.y > 0) ? afterClimb->area->GetSizeY() : -afterClimb->area->GetSizeY(); + } + + float areaDepth = depthVector.NormalizeInPlace(); + minLedgeDepth = MIN( minLedgeDepth, areaDepth ); + } + } + + // + // Find the ledge. Start at the lowest jump we can make + // and step up until we find the actual ledge. + // + // The scan is limited to maxLedgeHeight in case our max + // jump/climb height is so tall the highest horizontal hull + // trace could be on the other side of the ceiling above us + // + + float ledgeHeight = minLedgeHeight; + const float ledgeHeightIncrement = 0.5f * mover->GetStepHeight(); + + bool foundWall = false; + bool foundLedge = false; + + // once we have found the ledge's front wall, we must look at least minLedgeDepth farther in to verify it is a ledge + // NOTE: This *must* be ledgeLookAheadRange since ledges are compared against the initial trace which was ledgeLookAheadRange deep + float ledgeTopLookAheadRange = ledgeLookAheadRange; + + // TODO: we also must look far enough ahead in case the ledge we actually find is "deeper" than the initial wall at the base + + Vector climbHullMin( -halfSize, -halfSize, 0.0f ); + Vector climbHullMax( halfSize, halfSize, minHullHeight ); + + Vector wallPos; + float wallDepth = 0.0f; + + bool isLastIteration = false; + while( true ) + { + // trace forward to find the wall in front of us, or the empty space of the ledge above us + mover->TraceHull( feet + Vector( 0, 0, ledgeHeight ), + feet + Vector( 0, 0, ledgeHeight ) + climbDirection * ledgeTopLookAheadRange, + climbHullMin, climbHullMax, mask, &filter, &result ); + + float traceDepth = ledgeTopLookAheadRange * result.fraction; + + if ( !result.startsolid ) + { + // if trace reached minLedgeDepth farther, this is a potential ledge + if ( foundWall ) + { + // TODO: test that potential ledge is flat enough to stand on + if ( ( traceDepth - ledgeFrontWallDepth ) > minLedgeDepth ) + { + bool isUsable = true; + + // initialize ledgePos from result of last trace + ledgePos = result.endpos; + + // Find the actual ground level on the potential ledge + // Only trace back down to the previous ledge height trace. + // The ledge can be no lower, or we would've found it in the last iteration. + mover->TraceHull( ledgePos, + ledgePos + Vector( 0, 0, -ledgeHeightIncrement ), + climbHullMin, climbHullMax, mask, &filter, &result ); + + ledgePos = result.endpos; + + // if the whole trace is in solid, we're out of luck, but + // if the trace just started solid, 'ledgePos' should still be valid + // since the trace left the solid and then hit. + // if the trace hit nothing, the potential ledge is actually deeper in + const float MinGroundNormal = 0.7f; // players can't stand on ground steeper than 0.7 + if ( result.allsolid || !result.DidHit() || result.plane.normal.z < MinGroundNormal ) + { + // not a usable ledge, try again + isUsable = false; + } + else + { + if ( climbUpLedgeHeightDelta > 0.0f ) + { + // if we're climbing to a specific ledge via a CLIMB_UP link, only climb to that ledge. + // Do this only for the world (which includes static props) so we can still opportunistically + // climb up onto breakable railings and physics props. + if ( result.DidHitWorld() ) + { + float potentialLedgeHeight = result.endpos.z - feet.z; + if ( fabs(potentialLedgeHeight - climbUpLedgeHeightDelta) > climbUpLedgeTolerance ) + { + isUsable = false; + } + } + } + } + + if ( isUsable ) + { + // back up until we no longer are hitting the ledge to determine the + // exact ledge edge position + Vector validLedgePos = ledgePos; // save off a valid ledge pos + const float edgeTolerance = 4.0f; + const float maxBackUp = hullWidth; + float backUpSoFar = edgeTolerance; + Vector testPos = ledgePos; + + while( backUpSoFar < maxBackUp ) + { + testPos -= edgeTolerance * climbDirection; + backUpSoFar += edgeTolerance; + + mover->TraceHull( testPos, + testPos + Vector( 0, 0, -ledgeHeightIncrement ), + climbHullMin, climbHullMax, mask, &filter, &result ); + + + if ( bot->IsDebugging( NEXTBOT_PATH ) && NextBotDebugClimbing.GetBool() ) + { + // show edge-finder hulls + NDebugOverlay::SweptBox( testPos, + testPos + Vector( 0, 0, -mover->GetStepHeight() ), + climbHullMin, climbHullMax, vec3_angle, 255, 0, 0, 255, 999.9f ); + } + + if ( result.DidHit() && result.plane.normal.z >= MinGroundNormal ) + { + // we hit, this is closer to the actual ledge edge + ledgePos = result.endpos; + } + else + { + // nothing but air or a steep slope below us, we have found the edge + break; + } + } + + // we want ledgePos to be right on the edge itself, so move + // it ahead by half of the hull width + ledgePos += climbDirection * halfSize; + + // Make sure this doesn't embed us in the far wall if the ledge is narrow, since we would + // have backed up less than halfSize. + Vector climbHullMinStep( climbHullMin ); // skip StepHeight for sloped ledges + mover->TraceHull( validLedgePos, + ledgePos, + climbHullMinStep, climbHullMax, mask, &filter, &result ); + + ledgePos = result.endpos; + + // Now since ledgePos + StepHeight is valid, trace down to find ground on sloped ledges. + mover->TraceHull( ledgePos + Vector( 0, 0, StepHeight ), + ledgePos, + climbHullMin, climbHullMax, mask, &filter, &result ); + if ( !result.startsolid ) + { + ledgePos = result.endpos; + } + } + + +/*** NOTE: While this saves us from climbing into a window below the window we want to get in, + *** it also causes us to climb in midair high over crates sitting against walls we need to climb over. + if ( isUsable && m_goal->type == CLIMB_UP ) + { + // we can only accept ledges at least as high as our current CLIMB_UP destination + // NOTE: Can't use plannedClimbZ here, since that could be 2 or 3 short climbs ahead + const Segment *ledge = NextSegment( m_goal ); + + if ( !ledge || ledgeHeight < ledge->pos.z - feet.z - mover->GetStepHeight() ) + { + // this ledge is below the CLIMB_UP destination - can't use it + isUsable = false; + } + } +*/ + + + if ( isUsable ) + { + // found a usable ledge here + foundLedge = true; + break; + } + } + } + else if ( result.DidHit() ) + { + // this iteration hit the wall under the ledge, + // meaning the next iteration that reaches far enough will be our ledge + + // Since we know that our desired route is likely blocked (via the + // IsTraversable check above) - any ledge we hit we must climb. + + // found a valid ledge wall + foundWall = true; + wallDepth = traceDepth; + + // make sure the subsequent traces are at least minLedgeDepth deeper than + // the wall we just found, or all ledge checks will fail + float minTraceDepth = traceDepth + minLedgeDepth + 0.1f; + + if ( ledgeTopLookAheadRange < minTraceDepth ) + { + ledgeTopLookAheadRange = minTraceDepth; + } + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%3.2f: Climbing - found wall.\n", gpGlobals->curtime ); + if ( NextBotDebugClimbing.GetBool() ) + { + NDebugOverlay::HorzArrow( result.endpos, result.endpos + 20.0f * result.plane.normal, 5.0f, 255, 100, 0, 255, true, 9999.9f ); + } + wallPos = result.endpos; + } + } + else if ( ledgeHeight > body->GetCrouchHullHeight() && !isPlannedClimbImminent ) + { + // we haven't hit anything yet, and we're already above our heads - no obstacle + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%3.2f: Climbing - skipping overhead climb we can walk/crawl under.\n", gpGlobals->curtime ); + } + break; + } + } + + ledgeHeight += ledgeHeightIncrement; + + if ( ledgeHeight >= maxLedgeHeight ) + { + if ( isLastIteration ) + { + // tested at max height + break; + } + + // check one more time at max jump height + isLastIteration = true; + ledgeHeight = maxLedgeHeight; + } + } + + if ( foundLedge ) + { + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%3.2f: STARTING LEDGE CLIMB UP\n", gpGlobals->curtime ); + + if ( NextBotDebugClimbing.GetBool() ) + { + NDebugOverlay::Cross3D( ledgePos, 10.0f, 0, 255, 0, true, 9999.9f ); + + // display approximation of idealized ledge that has been found + Vector side( -climbDirection.y, climbDirection.x, 0.0f ); + + // this is an approximation, since AABB can hit at any angle + Vector base = feet + halfSize * climbDirection; + + Vector wallBottomLeft = base + halfSize * side; + Vector wallBottomRight = base - halfSize * side; + Vector wallTopLeft = wallBottomLeft + Vector( 0, 0, ledgeHeight ); + Vector wallTopRight = wallBottomRight + Vector( 0, 0, ledgeHeight ); + + NDebugOverlay::Triangle( wallBottomRight, wallBottomLeft, wallTopLeft, 255, 100, 0, 100, true, 9999.9f ); + NDebugOverlay::Triangle( wallBottomRight, wallTopLeft, wallTopRight, 255, 100, 0, 100, true, 9999.9f ); + + Vector ledgeLeft = ledgePos + halfSize * side; + Vector ledgeRight = ledgePos - halfSize * side; + + NDebugOverlay::Triangle( wallTopRight, wallTopLeft, ledgeLeft, 0, 100, 255, 100, true, 9999.9f ); + NDebugOverlay::Triangle( wallTopRight, ledgeLeft, ledgeRight, 0, 100, 255, 100, true, 9999.9f ); + } + } + + if ( !mover->ClimbUpToLedge( ledgePos, climbDirection, obstacle ) ) + { + // climb failed - build a new path in case we're now stuck + //Invalidate(); + return false; + } + + return true; + } + else if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "%3.2f: CANT FIND LEDGE TO CLIMB\n", gpGlobals->curtime ); + } + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Jump over gaps + */ +bool PathFollower::JumpOverGaps( INextBot *bot, const Path::Segment *goal, const Vector &forward, const Vector &right, float goalRange ) +{ + VPROF_BUDGET( "PathFollower::JumpOverGaps", "NextBot" ); + + ILocomotion *mover = bot->GetLocomotionInterface(); + IBody *body = bot->GetBodyInterface(); + + if ( !mover->IsAbleToJumpAcrossGaps() || !NextBotAllowGapJumping.GetBool() ) + { + return false; + } + + if ( mover->IsClimbingOrJumping() || mover->IsAscendingOrDescendingLadder() || !mover->IsOnGround() ) + { + return false; + } + + if ( !body->IsActualPosture( IBody::STAND ) ) + { + // can't jump if we're not standing + return false; + } + + if ( m_goal == NULL ) + { + return false; + } + + trace_t result; + NextBotTraversableTraceFilter filter( bot, ILocomotion::IMMEDIATELY ); + + const float hullWidth = ( body ) ? body->GetHullWidth() : 1.0f; + + // 'current' is the segment we are on/just passed over + const Segment *current = PriorSegment( m_goal ); + if ( current == NULL ) + { + return false; + } + + const float minGapJumpRange = 2.0f * hullWidth; + + const Segment *gap = NULL; + + if ( current->type == JUMP_OVER_GAP ) + { + gap = current; + } + else + { + float searchRange = goalRange; + for( const Segment *s = m_goal; s; s = NextSegment( s ) ) + { + if ( searchRange > minGapJumpRange ) + { + break; + } + + if ( s->type == JUMP_OVER_GAP ) + { + gap = s; + break; + } + + searchRange += s->length; + } + } + + if ( gap ) + { + VPROF_BUDGET( "PathFollower::GapJumping", "NextBot" ); + + float halfWidth = hullWidth/2.0f; + + if ( mover->IsGap( mover->GetFeet() + halfWidth * gap->forward, gap->forward ) ) + { + // there is a gap to jump over + const Segment *landing = NextSegment( gap ); + if ( landing ) + { + mover->JumpAcrossGap( landing->pos, landing->forward ); + + // if we're jumping over this gap, make sure our goal is the landing so we aim for it + m_goal = landing; + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + NDebugOverlay::Cross3D( m_goal->pos, 5.0f, 0, 255, 255, true, 5.0f ); + DevMsg( "%3.2f: GAP JUMP\n", gpGlobals->curtime ); + } + return true; + } + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw the path for debugging + */ +void PathFollower::Draw( const Path::Segment *start ) const +{ + if ( m_goal == NULL ) + return; + + // show avoid volumes + if ( m_didAvoidCheck ) + { + QAngle angles( 0, 0, 0 ); + + if (m_isLeftClear) + NDebugOverlay::SweptBox( m_leftFrom, m_leftTo, m_hullMin, m_hullMax, angles, 0, 255, 0, 255, 0.1f ); + else + NDebugOverlay::SweptBox( m_leftFrom, m_leftTo, m_hullMin, m_hullMax, angles, 255, 0, 0, 255, 0.1f ); + + if (m_isRightClear) + NDebugOverlay::SweptBox( m_rightFrom, m_rightTo, m_hullMin, m_hullMax, angles, 0, 255, 0, 255, 0.1f ); + else + NDebugOverlay::SweptBox( m_rightFrom, m_rightTo, m_hullMin, m_hullMax, angles, 255, 0, 0, 255, 0.1f ); + + const_cast< PathFollower * >( this )->m_didAvoidCheck = false; + } + + // highlight current goal segment + if ( m_goal ) + { + const float size = 5.0f; + NDebugOverlay::Sphere( m_goal->pos, size, 255, 255, 0, true, 0.1f ); + + switch( m_goal->how ) + { + case GO_NORTH: + case GO_SOUTH: + NDebugOverlay::Line( m_goal->m_portalCenter - Vector( m_goal->m_portalHalfWidth, 0, 0 ), m_goal->m_portalCenter + Vector( m_goal->m_portalHalfWidth, 0, 0 ), 255, 0, 255, true, 0.1f ); + break; + + default: + NDebugOverlay::Line( m_goal->m_portalCenter - Vector( 0, m_goal->m_portalHalfWidth, 0 ), m_goal->m_portalCenter + Vector( 0, m_goal->m_portalHalfWidth, 0 ), 255, 0, 255, true, 0.1f ); + break; + } + + // 'current' is the segment we are on/just passed over + const Segment *current = PriorSegment( m_goal ); + if ( current ) + { + NDebugOverlay::Line( current->pos, m_goal->pos, 255, 255, 0, true, 0.1f ); + } + } + + // extend + Path::Draw(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if there is a the given discontinuity ahead in the path within the given range (-1 = entire remaining path) + */ +bool PathFollower::IsDiscontinuityAhead( INextBot *bot, Path::SegmentType type, float range ) const +{ + if ( m_goal ) + { + const Path::Segment *current = PriorSegment( m_goal ); + if ( current && current->type == type ) + { + // we're on the discontinuity now + return true; + } + + float rangeSoFar = ( m_goal->pos - bot->GetLocomotionInterface()->GetFeet() ).Length(); + + for( const Segment *s = m_goal; s; s = NextSegment( s ) ) + { + if ( rangeSoFar >= range ) + { + break; + } + + if ( s->type == type ) + { + return true; + } + + rangeSoFar += s->length; + } + } + + return false; +} + + diff --git a/game/server/NextBot/Path/NextBotPathFollow.h b/game/server/NextBot/Path/NextBotPathFollow.h new file mode 100644 index 0000000..edb27b5 --- /dev/null +++ b/game/server/NextBot/Path/NextBotPathFollow.h @@ -0,0 +1,106 @@ +// NextBotPathFollow.h +// Path following +// Author: Michael Booth, April 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_PATH_FOLLOWER_ +#define _NEXT_BOT_PATH_FOLLOWER_ + +#include "nav_mesh.h" +#include "nav_pathfind.h" +#include "NextBotPath.h" + +class INextBot; +class ILocomotion; + + +//-------------------------------------------------------------------------------------------------------- +/** + * A PathFollower extends a Path to include mechanisms to move along (follow) it + */ +class PathFollower : public Path +{ +public: + PathFollower( void ); + virtual ~PathFollower(); + + virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid + virtual void Draw( const Path::Segment *start = NULL ) const; // (EXTEND) draw the path for debugging + virtual void OnPathChanged( INextBot *bot, Path::ResultType result ); // invoked when the path is (re)computed (path is valid at the time of this call) + + virtual void Update( INextBot *bot ); // move bot along path + + virtual const Path::Segment *GetCurrentGoal( void ) const; // return current goal along the path we are trying to reach + + virtual void SetMinLookAheadDistance( float value ); // minimum range movement goal must be along path + + virtual CBaseEntity *GetHindrance( void ) const; // returns entity that is hindering our progress along the path + + virtual bool IsDiscontinuityAhead( INextBot *bot, Path::SegmentType type, float range = -1.0f ) const; // return true if there is a the given discontinuity ahead in the path within the given range (-1 = entire remaining path) + + void SetGoalTolerance( float range ); // set tolerance within at which we're considered to be at our goal + +private: + const Path::Segment *m_goal; // our current goal along the path + float m_minLookAheadRange; + + bool CheckProgress( INextBot *bot ); + bool IsAtGoal( INextBot *bot ) const; // return true if reached current path goal + + //bool IsOnStairs( INextBot *bot ) const; // return true if bot is standing on a stairway + bool m_isOnStairs; + + CountdownTimer m_avoidTimer; // do avoid check more often if we recently avoided + + CountdownTimer m_waitTimer; // for waiting for a blocker to move off our path + CHandle< CBaseEntity > m_hindrance; + + // debug display data for avoid volumes + bool m_didAvoidCheck; + Vector m_leftFrom; + Vector m_leftTo; + bool m_isLeftClear; + Vector m_rightFrom; + Vector m_rightTo; + bool m_isRightClear; + Vector m_hullMin, m_hullMax; + + void AdjustSpeed( INextBot *bot ); // adjust speed based on path curvature + + Vector Avoid( INextBot *bot, const Vector &goalPos, const Vector &forward, const Vector &left ); // avoidance movements for very nearby obstacles. returns modified goal position + bool Climbing( INextBot *bot, const Path::Segment *goal, const Vector &forward, const Vector &left, float goalRange ); // climb up ledges + bool JumpOverGaps( INextBot *bot, const Path::Segment *goal, const Vector &forward, const Vector &left, float goalRange ); // jump over gaps + + bool LadderUpdate( INextBot *bot ); // move bot along ladder + CBaseEntity *FindBlocker( INextBot *bot ); // if entity is returned, it is blocking us from continuing along our path + + float m_goalTolerance; +}; + + +inline void PathFollower::SetGoalTolerance( float range ) +{ + m_goalTolerance = range; +} + + +inline const Path::Segment *PathFollower::GetCurrentGoal( void ) const +{ + return m_goal; +} + + +inline void PathFollower::SetMinLookAheadDistance( float value ) +{ + m_minLookAheadRange = value; +} + +inline CBaseEntity *PathFollower::GetHindrance( void ) const +{ + return m_hindrance; +} + + +#endif // _NEXT_BOT_PATH_FOLLOWER_ + + diff --git a/game/server/NextBot/Path/NextBotRetreatPath.h b/game/server/NextBot/Path/NextBotRetreatPath.h new file mode 100644 index 0000000..8d57cb4 --- /dev/null +++ b/game/server/NextBot/Path/NextBotRetreatPath.h @@ -0,0 +1,573 @@ +// NextBotRetreatPath.h +// Maintain and follow a path that leads safely away from the given Actor +// Author: Michael Booth, February 2007 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_RETREAT_PATH_ +#define _NEXT_BOT_RETREAT_PATH_ + +#include "nav.h" +#include "NextBotInterface.h" +#include "NextBotLocomotionInterface.h" +#include "NextBotRetreatPath.h" +#include "NextBotUtil.h" +#include "NextBotPathFollow.h" +#include "tier0/vprof.h" + + +//---------------------------------------------------------------------------------------------- +/** + * A RetreatPath extends a PathFollower to periodically recompute a path + * away from a threat, and to move along the path away from that threat. + */ +class RetreatPath : public PathFollower +{ +public: + RetreatPath( void ); + virtual ~RetreatPath() { } + + void Update( INextBot *bot, CBaseEntity *threat ); // update path away from threat and move bot along path + + virtual float GetMaxPathLength( void ) const; // return maximum path length + + virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid + +private: + void RefreshPath( INextBot *bot, CBaseEntity *threat ); + + CountdownTimer m_throttleTimer; // require a minimum time between re-paths + EHANDLE m_pathThreat; // the threat of our existing path + Vector m_pathThreatPos; // where the threat was when the path was built +}; + +inline RetreatPath::RetreatPath( void ) +{ + m_throttleTimer.Invalidate(); + m_pathThreat = NULL; +} + +inline float RetreatPath::GetMaxPathLength( void ) const +{ + return 1000.0f; +} + +inline void RetreatPath::Invalidate( void ) +{ + // path is gone, repath at earliest opportunity + m_throttleTimer.Invalidate(); + m_pathThreat = NULL; + + // extend + PathFollower::Invalidate(); +} + + + +//---------------------------------------------------------------------------------------------- +/** + * Maintain a path to our chase threat and move along that path + */ +inline void RetreatPath::Update( INextBot *bot, CBaseEntity *threat ) +{ + VPROF_BUDGET( "RetreatPath::Update", "NextBot" ); + + if ( threat == NULL ) + { + return; + } + + // if our path threat changed, repath immediately + if ( threat != m_pathThreat ) + { + if ( bot->IsDebugging( INextBot::PATH ) ) + { + DevMsg( "%3.2f: bot(#%d) Chase path threat changed (from %X to %X).\n", gpGlobals->curtime, bot->GetEntity()->entindex(), m_pathThreat.Get(), threat ); + } + + Invalidate(); + } + + // maintain the path away from the threat + RefreshPath( bot, threat ); + + // move along the path towards the threat + PathFollower::Update( bot ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Build a path away from retreatFromArea up to retreatRange in length. + */ +class RetreatPathBuilder +{ +public: + RetreatPathBuilder( INextBot *me, CBaseEntity *threat, float retreatRange = 500.0f ) + { + m_me = me; + m_mover = me->GetLocomotionInterface(); + + m_threat = threat; + m_retreatRange = retreatRange; + } + + CNavArea *ComputePath( void ) + { + VPROF_BUDGET( "NavAreaBuildRetreatPath", "NextBot" ); + + if ( m_mover == NULL ) + return NULL; + + CNavArea *startArea = m_me->GetEntity()->GetLastKnownArea(); + + if ( startArea == NULL ) + return NULL; + + CNavArea *retreatFromArea = TheNavMesh->GetNearestNavArea( m_threat->GetAbsOrigin() ); + if ( retreatFromArea == NULL ) + return NULL; + + startArea->SetParent( NULL ); + + // start search + CNavArea::ClearSearchLists(); + + float initCost = Cost( startArea, NULL, NULL ); + if ( initCost < 0.0f ) + return NULL; + + int teamID = m_me->GetEntity()->GetTeamNumber(); + + startArea->SetTotalCost( initCost ); + + startArea->AddToOpenList(); + + // keep track of the area farthest away from the threat + CNavArea *farthestArea = NULL; + float farthestRange = 0.0f; + + // + // Dijkstra's algorithm (since we don't know our goal). + // Build a path as far away from the retreat area as possible. + // Minimize total path length and danger. + // Maximize distance to threat of end of path. + // + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CNavArea *area = CNavArea::PopOpenList(); + + area->AddToClosedList(); + + // don't consider blocked areas + if ( area->IsBlocked( teamID ) ) + continue; + + // build adjacent area array + CollectAdjacentAreas( area ); + + // search adjacent areas + for( int i=0; i<m_adjAreaIndex; ++i ) + { + CNavArea *newArea = m_adjAreaVector[ i ].area; + + // only visit each area once + if ( newArea->IsClosed() ) + continue; + + // don't consider blocked areas + if ( newArea->IsBlocked( teamID ) ) + continue; + + // don't use this area if it is out of range + if ( ( newArea->GetCenter() - m_me->GetEntity()->GetAbsOrigin() ).IsLengthGreaterThan( m_retreatRange ) ) + continue; + + // determine cost of traversing this area + float newCost = Cost( newArea, area, m_adjAreaVector[ i ].ladder ); + + // don't use adjacent area if cost functor says it is a dead-end + if ( newCost < 0.0f ) + continue; + + if ( newArea->IsOpen() && newArea->GetTotalCost() <= newCost ) + { + // we have already visited this area, and it has a better path + continue; + } + else + { + // whether this area has been visited or not, we now have a better path + newArea->SetParent( area, m_adjAreaVector[ i ].how ); + newArea->SetTotalCost( newCost ); + + // use 'cost so far' to hold cumulative cost + newArea->SetCostSoFar( newCost ); + + // tricky bit here - relying on OpenList being sorted by cost + if ( newArea->IsOpen() ) + { + // area already on open list, update the list order to keep costs sorted + newArea->UpdateOnOpenList(); + } + else + { + newArea->AddToOpenList(); + } + + // keep track of area farthest from threat + float threatRange = ( newArea->GetCenter() - m_threat->GetAbsOrigin() ).Length(); + if ( threatRange > farthestRange ) + { + farthestArea = newArea; + farthestRange = threatRange; + } + } + } + } + + return farthestArea; + } + + + /** + * Build a vector of adjacent areas reachable from the given area + */ + void CollectAdjacentAreas( CNavArea *area ) + { + m_adjAreaIndex = 0; + + const NavConnectVector &adjNorth = *area->GetAdjacentAreas( NORTH ); + FOR_EACH_VEC( adjNorth, it ) + { + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + m_adjAreaVector[ m_adjAreaIndex ].area = adjNorth[ it ].area; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_NORTH; + m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL; + ++m_adjAreaIndex; + } + + const NavConnectVector &adjSouth = *area->GetAdjacentAreas( SOUTH ); + FOR_EACH_VEC( adjSouth, it ) + { + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + m_adjAreaVector[ m_adjAreaIndex ].area = adjSouth[ it ].area; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_SOUTH; + m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL; + ++m_adjAreaIndex; + } + + const NavConnectVector &adjWest = *area->GetAdjacentAreas( WEST ); + FOR_EACH_VEC( adjWest, it ) + { + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + m_adjAreaVector[ m_adjAreaIndex ].area = adjWest[ it ].area; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_WEST; + m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL; + ++m_adjAreaIndex; + } + + const NavConnectVector &adjEast = *area->GetAdjacentAreas( EAST ); + FOR_EACH_VEC( adjEast, it ) + { + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + m_adjAreaVector[ m_adjAreaIndex ].area = adjEast[ it ].area; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_EAST; + m_adjAreaVector[ m_adjAreaIndex ].ladder = NULL; + ++m_adjAreaIndex; + } + + const NavLadderConnectVector &adjUpLadder = *area->GetLadders( CNavLadder::LADDER_UP ); + FOR_EACH_VEC( adjUpLadder, it ) + { + CNavLadder *ladder = adjUpLadder[ it ].ladder; + + if ( ladder->m_topForwardArea && m_adjAreaIndex < MAX_ADJ_AREAS ) + { + m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topForwardArea; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP; + m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder; + ++m_adjAreaIndex; + } + + if ( ladder->m_topLeftArea && m_adjAreaIndex < MAX_ADJ_AREAS ) + { + m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topLeftArea; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP; + m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder; + ++m_adjAreaIndex; + } + + if ( ladder->m_topRightArea && m_adjAreaIndex < MAX_ADJ_AREAS ) + { + m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_topRightArea; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_UP; + m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder; + ++m_adjAreaIndex; + } + } + + const NavLadderConnectVector &adjDownLadder = *area->GetLadders( CNavLadder::LADDER_DOWN ); + FOR_EACH_VEC( adjDownLadder, it ) + { + CNavLadder *ladder = adjDownLadder[ it ].ladder; + + if ( m_adjAreaIndex >= MAX_ADJ_AREAS ) + break; + + if ( ladder->m_bottomArea ) + { + m_adjAreaVector[ m_adjAreaIndex ].area = ladder->m_bottomArea; + m_adjAreaVector[ m_adjAreaIndex ].how = GO_LADDER_DOWN; + m_adjAreaVector[ m_adjAreaIndex ].ladder = ladder; + ++m_adjAreaIndex; + } + } + } + + /** + * Cost minimizes path length traveled thus far and "danger" (proximity to threat(s)) + */ + float Cost( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder ) + { + // check if we can use this area + if ( !m_mover->IsAreaTraversable( area ) ) + { + return -1.0f; + } + + int teamID = m_me->GetEntity()->GetTeamNumber(); + if ( area->IsBlocked( teamID ) ) + { + return -1.0f; + } + + const float debugDeltaT = 3.0f; + + float cost; + + const float maxThreatRange = 500.0f; + const float dangerDensity = 1000.0f; + + if ( fromArea == NULL ) + { + cost = 0.0f; + + if ( area->Contains( m_threat->GetAbsOrigin() ) ) + { + // maximum danger - threat is in the area with us + cost += 10.0f * dangerDensity; + + if ( m_me->IsDebugging( INextBot::PATH ) ) + { + area->DrawFilled( 255, 0, 0, 128 ); + } + } + else + { + // danger proportional to range to us + float rangeToThreat = ( m_threat->GetAbsOrigin() - m_me->GetEntity()->GetAbsOrigin() ).Length(); + + if ( rangeToThreat < maxThreatRange ) + { + cost += dangerDensity * ( 1.0f - ( rangeToThreat / maxThreatRange ) ); + + if ( m_me->IsDebugging( INextBot::PATH ) ) + { + NDebugOverlay::Line( m_me->GetEntity()->GetAbsOrigin(), m_threat->GetAbsOrigin(), 255, 0, 0, true, debugDeltaT ); + } + } + } + } + else + { + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + const float ladderCostFactor = 100.0f; + dist = ladderCostFactor * ladder->m_length; + } + else + { + Vector to = area->GetCenter() - fromArea->GetCenter(); + + dist = to.Length(); + + // check for vertical discontinuities + Vector closeFrom, closeTo; + area->GetClosestPointOnArea( fromArea->GetCenter(), &closeTo ); + fromArea->GetClosestPointOnArea( area->GetCenter(), &closeFrom ); + + float deltaZ = closeTo.z - closeFrom.z; + + if ( deltaZ > m_mover->GetMaxJumpHeight() ) + { + // too high to jump + return -1.0f; + } + else if ( -deltaZ > m_mover->GetDeathDropHeight() ) + { + // too far down to drop + return -1.0f; + } + + // prefer to maintain our level + const float climbCost = 10.0f; + dist += climbCost * fabs( deltaZ ); + } + + cost = dist + fromArea->GetTotalCost(); + + + // Add in danger cost due to threat + // Assume straight line between areas and find closest point + // to the threat along that line segment. The distance between + // the threat and closest point on the line is the danger cost. + + // path danger is CUMULATIVE + float dangerCost = fromArea->GetCostSoFar(); + + Vector close; + float t; + CalcClosestPointOnLineSegment( m_threat->GetAbsOrigin(), area->GetCenter(), fromArea->GetCenter(), close, &t ); + if ( t < 0.0f ) + { + close = area->GetCenter(); + } + else if ( t > 1.0f ) + { + close = fromArea->GetCenter(); + } + + float rangeToThreat = ( m_threat->GetAbsOrigin() - close ).Length(); + + if ( rangeToThreat < maxThreatRange ) + { + float dangerFactor = 1.0f - ( rangeToThreat / maxThreatRange ); + dangerCost = dangerDensity * dangerFactor; + + if ( m_me->IsDebugging( INextBot::PATH ) ) + { + NDebugOverlay::HorzArrow( fromArea->GetCenter(), area->GetCenter(), 5, 255 * dangerFactor, 0, 0, 255, true, debugDeltaT ); + + Vector to = close - m_threat->GetAbsOrigin(); + to.NormalizeInPlace(); + + NDebugOverlay::Line( close, close - 50.0f * to, 255, 0, 0, true, debugDeltaT ); + } + } + + cost += dangerCost; + } + + return cost; + } + +private: + INextBot *m_me; + ILocomotion *m_mover; + + CBaseEntity *m_threat; + float m_retreatRange; + + enum { MAX_ADJ_AREAS = 64 }; + + struct AdjInfo + { + CNavArea *area; + CNavLadder *ladder; + NavTraverseType how; + }; + + AdjInfo m_adjAreaVector[ MAX_ADJ_AREAS ]; + int m_adjAreaIndex; + +}; + + +//---------------------------------------------------------------------------------------------- +/** + * Periodically rebuild the path away from our threat + */ +inline void RetreatPath::RefreshPath( INextBot *bot, CBaseEntity *threat ) +{ + VPROF_BUDGET( "RetreatPath::RefreshPath", "NextBot" ); + + if ( threat == NULL ) + { + if ( bot->IsDebugging( INextBot::PATH ) ) + { + DevMsg( "%3.2f: bot(#%d) CasePath::RefreshPath failed. No threat.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); + } + return; + } + + // don't change our path if we're on a ladder + ILocomotion *mover = bot->GetLocomotionInterface(); + if ( IsValid() && mover && mover->IsUsingLadder() ) + { + if ( bot->IsDebugging( INextBot::PATH ) ) + { + DevMsg( "%3.2f: bot(#%d) RetreatPath::RefreshPath failed. Bot is on a ladder.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); + } + return; + } + + // the closer we get, the more accurate our path needs to be + Vector to = threat->GetAbsOrigin() - bot->GetPosition(); + + const float minTolerance = 0.0f; + const float toleranceRate = 0.33f; + + float tolerance = minTolerance + toleranceRate * to.Length(); + + if ( !IsValid() || ( threat->GetAbsOrigin() - m_pathThreatPos ).IsLengthGreaterThan( tolerance ) ) + { + if ( !m_throttleTimer.IsElapsed() ) + { + // require a minimum time between repaths, as long as we have a path to follow + if ( bot->IsDebugging( INextBot::PATH ) ) + { + DevMsg( "%3.2f: bot(#%d) RetreatPath::RefreshPath failed. Rate throttled.\n", gpGlobals->curtime, bot->GetEntity()->entindex() ); + } + return; + } + + // remember our path threat + m_pathThreat = threat; + m_pathThreatPos = threat->GetAbsOrigin(); + + RetreatPathBuilder retreat( bot, threat, GetMaxPathLength() ); + + CNavArea *goalArea = retreat.ComputePath(); + + if ( goalArea ) + { + AssemblePrecomputedPath( bot, goalArea->GetCenter(), goalArea ); + } + else + { + // all adjacent areas are too far away - just move directly away from threat + Vector to = threat->GetAbsOrigin() - bot->GetPosition(); + + BuildTrivialPath( bot, bot->GetPosition() - to ); + } + + const float minRepathInterval = 0.5f; + m_throttleTimer.Start( minRepathInterval ); + } +} + + + +#endif // _NEXT_BOT_RETREAT_PATH_ diff --git a/game/server/NextBot/Player/NextBotPlayer.cpp b/game/server/NextBot/Player/NextBotPlayer.cpp new file mode 100644 index 0000000..a2121f7 --- /dev/null +++ b/game/server/NextBot/Player/NextBotPlayer.cpp @@ -0,0 +1,22 @@ +// NextBotPlayer.cpp +// A CBasePlayer bot based on the NextBot technology +// Author: Michael Booth, November 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "nav_mesh.h" + +#include "NextBot.h" +#include "NextBotPlayer.h" + +#include "in_buttons.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar NextBotPlayerStop( "nb_player_stop", "0", FCVAR_CHEAT, "Stop all NextBotPlayers from updating" ); +ConVar NextBotPlayerWalk( "nb_player_walk", "0", FCVAR_CHEAT, "Force bots to walk" ); +ConVar NextBotPlayerCrouch( "nb_player_crouch", "0", FCVAR_CHEAT, "Force bots to crouch" ); +ConVar NextBotPlayerMove( "nb_player_move", "1", FCVAR_CHEAT, "Prevents bots from moving" ); + diff --git a/game/server/NextBot/Player/NextBotPlayer.h b/game/server/NextBot/Player/NextBotPlayer.h new file mode 100644 index 0000000..2032c72 --- /dev/null +++ b/game/server/NextBot/Player/NextBotPlayer.h @@ -0,0 +1,910 @@ +// NextBotPlayer.h +// A CBasePlayer bot based on the NextBot technology +// Author: Michael Booth, November 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_PLAYER_H_ +#define _NEXT_BOT_PLAYER_H_ + +#include "cbase.h" +#include "gameinterface.h" + +#include "NextBot.h" +#include "Path/NextBotPathFollow.h" +//#include "NextBotPlayerBody.h" +#include "NextBotBehavior.h" + +#include "in_buttons.h" + +extern ConVar NextBotPlayerStop; +extern ConVar NextBotPlayerWalk; +extern ConVar NextBotPlayerCrouch; +extern ConVar NextBotPlayerMove; + + + +//-------------------------------------------------------------------------------------------------- +/** + * Instantiate a NextBot derived from CBasePlayer and spawn it into the environment. + * Assumes class T is derived from CBasePlayer, and has the following method that + * creates a new entity of type T and returns it: + * + * static CBasePlayer *T::AllocatePlayerEntity( edict_t *pEdict, const char *playerName ) + * + */ +template < typename T > +T * NextBotCreatePlayerBot( const char *name, bool bReportFakeClient = true ) +{ + /* + if ( UTIL_ClientsInGame() >= gpGlobals->maxClients ) + { + Msg( "CreatePlayerBot: Failed - server is full (%d/%d clients).\n", UTIL_ClientsInGame(), gpGlobals->maxClients ); + return NULL; + } + */ + + // This is a "back door" for allocating a custom player bot entity when + // the engine calls ClientPutInServer (from CreateFakeClient) + ClientPutInServerOverride( T::AllocatePlayerEntity ); + + // create the bot and spawn it into the environment + edict_t *botEdict = engine->CreateFakeClientEx( name, bReportFakeClient ); + + // close the "back door" + ClientPutInServerOverride( NULL ); + + if ( botEdict == NULL ) + { + Msg( "CreatePlayerBot: Unable to create bot %s - CreateFakeClient() returned NULL.\n", name ); + return NULL; + } + + // create an instance of the bot's class and bind it to the edict + T *bot = dynamic_cast< T * >( CBaseEntity::Instance( botEdict ) ); + + if ( bot == NULL ) + { + Assert( false ); + Error( "CreatePlayerBot: Could not Instance() from the bot edict.\n" ); + return NULL; + } + + bot->SetPlayerName( name ); + + // flag this as a fakeclient (bot) + bot->ClearFlags(); + bot->AddFlag( FL_CLIENT | FL_FAKECLIENT ); + + return bot; +} + + +//-------------------------------------------------------------------------------------------------- +/** + * Interface to access player input buttons. + * Unless a duration is given, each button is released at the start of the next frame. + * The release methods allow releasing a button before its duration has elapsed. + */ +class INextBotPlayerInput +{ +public: + virtual void PressFireButton( float duration = -1.0f ) = 0; + virtual void ReleaseFireButton( void ) = 0; + + virtual void PressAltFireButton( float duration = -1.0f ) = 0; + virtual void ReleaseAltFireButton( void ) = 0; + + virtual void PressMeleeButton( float duration = -1.0f ) = 0; + virtual void ReleaseMeleeButton( void ) = 0; + + virtual void PressSpecialFireButton( float duration = -1.0f ) = 0; + virtual void ReleaseSpecialFireButton( void ) = 0; + + virtual void PressUseButton( float duration = -1.0f ) = 0; + virtual void ReleaseUseButton( void ) = 0; + + virtual void PressReloadButton( float duration = -1.0f ) = 0; + virtual void ReleaseReloadButton( void ) = 0; + + virtual void PressForwardButton( float duration = -1.0f ) = 0; + virtual void ReleaseForwardButton( void ) = 0; + + virtual void PressBackwardButton( float duration = -1.0f ) = 0; + virtual void ReleaseBackwardButton( void ) = 0; + + virtual void PressLeftButton( float duration = -1.0f ) = 0; + virtual void ReleaseLeftButton( void ) = 0; + + virtual void PressRightButton( float duration = -1.0f ) = 0; + virtual void ReleaseRightButton( void ) = 0; + + virtual void PressJumpButton( float duration = -1.0f ) = 0; + virtual void ReleaseJumpButton( void ) = 0; + + virtual void PressCrouchButton( float duration = -1.0f ) = 0; + virtual void ReleaseCrouchButton( void ) = 0; + + virtual void PressWalkButton( float duration = -1.0f ) = 0; + virtual void ReleaseWalkButton( void ) = 0; + + virtual void SetButtonScale( float forward, float right ) = 0; +}; + + +//-------------------------------------------------------------------------------------------------- +/** + * Drive a CBasePlayer-derived entity via NextBot logic + */ +template < typename PlayerType > +class NextBotPlayer : public PlayerType, public INextBot, public INextBotPlayerInput +{ +public: + DECLARE_CLASS( NextBotPlayer, PlayerType ); + + NextBotPlayer( void ); + virtual ~NextBotPlayer(); + + virtual void Spawn( void ); + + virtual void SetSpawnPoint( CBaseEntity *spawnPoint ); // define place in environment where bot will (re)spawn + virtual CBaseEntity *EntSelectSpawnPoint( void ); + + virtual void PhysicsSimulate( void ); + + virtual bool IsNetClient( void ) const { return false; } // Bots should return FALSE for this, they can't receive NET messages + virtual bool IsFakeClient( void ) const { return true; } + virtual bool IsBot( void ) const { return true; } + virtual INextBot *MyNextBotPointer( void ) { return this; } + + // this is valid because the templatized PlayerType must be derived from CBasePlayer, which is derived from CBaseCombatCharacter + virtual CBaseCombatCharacter *GetEntity( void ) const { return ( PlayerType * )this; } + + virtual bool IsRemovedOnReset( void ) const { return false; } // remove this bot when the NextBot manager calls Reset + + virtual bool IsDormantWhenDead( void ) const { return true; } // should this player-bot continue to update itself when dead (respawn logic, etc) + + // allocate a bot and bind it to the edict + static CBasePlayer *AllocatePlayerEntity( edict_t *edict, const char *playerName ); + + //------------------------------------------------------------------------ + // utility methods + float GetDistanceBetween( CBaseEntity *other ) const; // return distance between us and the given entity + bool IsDistanceBetweenLessThan( CBaseEntity *other, float range ) const; // return true if distance between is less than the given value + bool IsDistanceBetweenGreaterThan( CBaseEntity *other, float range ) const; // return true if distance between is greater than the given value + + float GetDistanceBetween( const Vector &target ) const; // return distance between us and the given entity + bool IsDistanceBetweenLessThan( const Vector &target, float range ) const; // return true if distance between is less than the given value + bool IsDistanceBetweenGreaterThan( const Vector &target, float range ) const; // return true if distance between is greater than the given value + + //------------------------------------------------------------------------ + // INextBotPlayerInput + virtual void PressFireButton( float duration = -1.0f ); + virtual void ReleaseFireButton( void ); + + virtual void PressAltFireButton( float duration = -1.0f ); + virtual void ReleaseAltFireButton( void ); + + virtual void PressMeleeButton( float duration = -1.0f ); + virtual void ReleaseMeleeButton( void ); + + virtual void PressSpecialFireButton( float duration = -1.0f ); + virtual void ReleaseSpecialFireButton( void ); + + virtual void PressUseButton( float duration = -1.0f ); + virtual void ReleaseUseButton( void ); + + virtual void PressReloadButton( float duration = -1.0f ); + virtual void ReleaseReloadButton( void ); + + virtual void PressForwardButton( float duration = -1.0f ); + virtual void ReleaseForwardButton( void ); + + virtual void PressBackwardButton( float duration = -1.0f ); + virtual void ReleaseBackwardButton( void ); + + virtual void PressLeftButton( float duration = -1.0f ); + virtual void ReleaseLeftButton( void ); + + virtual void PressRightButton( float duration = -1.0f ); + virtual void ReleaseRightButton( void ); + + virtual void PressJumpButton( float duration = -1.0f ); + virtual void ReleaseJumpButton( void ); + + virtual void PressCrouchButton( float duration = -1.0f ); + virtual void ReleaseCrouchButton( void ); + + virtual void PressWalkButton( float duration = -1.0f ); + virtual void ReleaseWalkButton( void ); + + virtual void SetButtonScale( float forward, float right ); + + //------------------------------------------------------------------------ + // Event hooks into NextBot system + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual int OnTakeDamage_Dying( const CTakeDamageInfo &info ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void HandleAnimEvent( animevent_t *event ); + virtual void OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ); // invoked (by UpdateLastKnownArea) when we enter a new nav area (or it is reset to NULL) + virtual void Touch( CBaseEntity *other ); + virtual void Weapon_Equip( CBaseCombatWeapon *weapon ); // for OnPickUp + virtual void Weapon_Drop( CBaseCombatWeapon *weapon, const Vector *target, const Vector *velocity ); // for OnDrop + virtual void OnMainActivityComplete( Activity newActivity, Activity oldActivity ); + virtual void OnMainActivityInterrupted( Activity newActivity, Activity oldActivity ); + //------------------------------------------------------------------------ + + bool IsAbleToAutoCenterOnLadders( void ) const; + + virtual void AvoidPlayers( CUserCmd *pCmd ) { } // some game types allow players to pass through each other, this method pushes them apart + +public: + // begin INextBot ------------------------------------------------------------------------------------------------------------------ + virtual void Update( void ); // (EXTEND) update internal state + +protected: + int m_inputButtons; // this is still needed to guarantee each button press is captured at least once + int m_prevInputButtons; + CountdownTimer m_fireButtonTimer; + CountdownTimer m_meleeButtonTimer; + CountdownTimer m_specialFireButtonTimer; + CountdownTimer m_useButtonTimer; + CountdownTimer m_reloadButtonTimer; + CountdownTimer m_forwardButtonTimer; + CountdownTimer m_backwardButtonTimer; + CountdownTimer m_leftButtonTimer; + CountdownTimer m_rightButtonTimer; + CountdownTimer m_jumpButtonTimer; + CountdownTimer m_crouchButtonTimer; + CountdownTimer m_walkButtonTimer; + CountdownTimer m_buttonScaleTimer; + IntervalTimer m_burningTimer; // how long since we were last burning + float m_forwardScale; + float m_rightScale; + CHandle< CBaseEntity > m_spawnPointEntity; +}; + + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::SetSpawnPoint( CBaseEntity *spawnPoint ) +{ + m_spawnPointEntity = spawnPoint; +} + +template < typename PlayerType > +inline CBaseEntity *NextBotPlayer< PlayerType >::EntSelectSpawnPoint( void ) +{ + if ( m_spawnPointEntity != NULL ) + return m_spawnPointEntity; + + return BaseClass::EntSelectSpawnPoint(); +} + +template < typename PlayerType > +inline float NextBotPlayer< PlayerType >::GetDistanceBetween( CBaseEntity *other ) const +{ + return (this->GetAbsOrigin() - other->GetAbsOrigin()).Length(); +} + +template < typename PlayerType > +inline bool NextBotPlayer< PlayerType >::IsDistanceBetweenLessThan( CBaseEntity *other, float range ) const +{ + return (this->GetAbsOrigin() - other->GetAbsOrigin()).IsLengthLessThan( range ); +} + +template < typename PlayerType > +inline bool NextBotPlayer< PlayerType >::IsDistanceBetweenGreaterThan( CBaseEntity *other, float range ) const +{ + return (this->GetAbsOrigin() - other->GetAbsOrigin()).IsLengthGreaterThan( range ); +} + +template < typename PlayerType > +inline float NextBotPlayer< PlayerType >::GetDistanceBetween( const Vector &target ) const +{ + return (this->GetAbsOrigin() - target).Length(); +} + +template < typename PlayerType > +inline bool NextBotPlayer< PlayerType >::IsDistanceBetweenLessThan( const Vector &target, float range ) const +{ + return (this->GetAbsOrigin() - target).IsLengthLessThan( range ); +} + +template < typename PlayerType > +inline bool NextBotPlayer< PlayerType >::IsDistanceBetweenGreaterThan( const Vector &target, float range ) const +{ + return (this->GetAbsOrigin() - target).IsLengthGreaterThan( range ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressFireButton( float duration ) +{ + m_inputButtons |= IN_ATTACK; + m_fireButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseFireButton( void ) +{ + m_inputButtons &= ~IN_ATTACK; + m_fireButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressAltFireButton( float duration ) +{ + PressMeleeButton( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseAltFireButton( void ) +{ + ReleaseMeleeButton(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressMeleeButton( float duration ) +{ + m_inputButtons |= IN_ATTACK2; + m_meleeButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseMeleeButton( void ) +{ + m_inputButtons &= ~IN_ATTACK2; + m_meleeButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressSpecialFireButton( float duration ) +{ + m_inputButtons |= IN_ATTACK3; + m_specialFireButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseSpecialFireButton( void ) +{ + m_inputButtons &= ~IN_ATTACK3; + m_specialFireButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressUseButton( float duration ) +{ + m_inputButtons |= IN_USE; + m_useButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseUseButton( void ) +{ + m_inputButtons &= ~IN_USE; + m_useButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressReloadButton( float duration ) +{ + m_inputButtons |= IN_RELOAD; + m_reloadButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseReloadButton( void ) +{ + m_inputButtons &= ~IN_RELOAD; + m_reloadButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressJumpButton( float duration ) +{ + m_inputButtons |= IN_JUMP; + m_jumpButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseJumpButton( void ) +{ + m_inputButtons &= ~IN_JUMP; + m_jumpButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressCrouchButton( float duration ) +{ + m_inputButtons |= IN_DUCK; + m_crouchButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseCrouchButton( void ) +{ + m_inputButtons &= ~IN_DUCK; + m_crouchButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressWalkButton( float duration ) +{ + m_inputButtons |= IN_SPEED; + m_walkButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseWalkButton( void ) +{ + m_inputButtons &= ~IN_SPEED; + m_walkButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressForwardButton( float duration ) +{ + m_inputButtons |= IN_FORWARD; + m_forwardButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseForwardButton( void ) +{ + m_inputButtons &= ~IN_FORWARD; + m_forwardButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressBackwardButton( float duration ) +{ + m_inputButtons |= IN_BACK; + m_backwardButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseBackwardButton( void ) +{ + m_inputButtons &= ~IN_BACK; + m_backwardButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressLeftButton( float duration ) +{ + m_inputButtons |= IN_MOVELEFT; + m_leftButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseLeftButton( void ) +{ + m_inputButtons &= ~IN_MOVELEFT; + m_leftButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressRightButton( float duration ) +{ + m_inputButtons |= IN_MOVERIGHT; + m_rightButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseRightButton( void ) +{ + m_inputButtons &= ~IN_MOVERIGHT; + m_rightButtonTimer.Invalidate(); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::SetButtonScale( float forward, float right ) +{ + m_forwardScale = forward; + m_rightScale = right; + m_buttonScaleTimer.Start( 0.01 ); +} + + + +//----------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline NextBotPlayer< PlayerType >::NextBotPlayer( void ) +{ + m_prevInputButtons = 0; + m_inputButtons = 0; + m_burningTimer.Invalidate(); + m_spawnPointEntity = NULL; +} + + +//----------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline NextBotPlayer< PlayerType >::~NextBotPlayer() +{ +} + + +//----------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::Spawn( void ) +{ + engine->SetFakeClientConVarValue( this->edict(), "cl_autohelp", "0" ); + + m_prevInputButtons = m_inputButtons = 0; + m_fireButtonTimer.Invalidate(); + m_meleeButtonTimer.Invalidate(); + m_specialFireButtonTimer.Invalidate(); + m_useButtonTimer.Invalidate(); + m_reloadButtonTimer.Invalidate(); + m_forwardButtonTimer.Invalidate(); + m_backwardButtonTimer.Invalidate(); + m_leftButtonTimer.Invalidate(); + m_rightButtonTimer.Invalidate(); + m_jumpButtonTimer.Invalidate(); + m_crouchButtonTimer.Invalidate(); + m_walkButtonTimer.Invalidate(); + m_buttonScaleTimer.Invalidate(); + m_forwardScale = m_rightScale = 0.04; + m_burningTimer.Invalidate(); + + // reset first, because Spawn() may access various interfaces + INextBot::Reset(); + + BaseClass::Spawn(); +} + + + +//----------------------------------------------------------------------------------------------------- +inline void _NextBot_BuildUserCommand( CUserCmd *cmd, const QAngle &viewangles, float forwardmove, float sidemove, float upmove, int buttons, byte impulse ) +{ + Q_memset( cmd, 0, sizeof( CUserCmd ) ); + + cmd->command_number = gpGlobals->tickcount; + cmd->forwardmove = forwardmove; + cmd->sidemove = sidemove; + cmd->upmove = upmove; + cmd->buttons = buttons; + cmd->impulse = impulse; + + VectorCopy( viewangles, cmd->viewangles ); + + cmd->random_seed = random->RandomInt( 0, 0x7fffffff ); +} + + +//----------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void ) +{ + VPROF( "NextBotPlayer::PhysicsSimulate" ); + + // Make sure not to simulate this guy twice per frame + if ( PlayerType::m_nSimulationTick == gpGlobals->tickcount ) + { + return; + } + + if ( engine->IsPaused() ) + { + // We're paused - don't add new commands + PlayerType::PhysicsSimulate(); + return; + } + + if ( ( IsDormantWhenDead() && PlayerType::m_lifeState == LIFE_DEAD ) || NextBotStop.GetBool() ) + { + // death animation complete - nothing left to do except let PhysicsSimulate run PreThink etc + PlayerType::PhysicsSimulate(); + return; + } + + int inputButtons; + // + // Update bot behavior + // + if ( BeginUpdate() ) + { + Update(); + + // build button bits + if ( !m_fireButtonTimer.IsElapsed() ) + m_inputButtons |= IN_ATTACK; + + if ( !m_meleeButtonTimer.IsElapsed() ) + m_inputButtons |= IN_ATTACK2; + + if ( !m_specialFireButtonTimer.IsElapsed() ) + m_inputButtons |= IN_ATTACK3; + + if ( !m_useButtonTimer.IsElapsed() ) + m_inputButtons |= IN_USE; + + if ( !m_reloadButtonTimer.IsElapsed() ) + m_inputButtons |= IN_RELOAD; + + if ( !m_forwardButtonTimer.IsElapsed() ) + m_inputButtons |= IN_FORWARD; + + if ( !m_backwardButtonTimer.IsElapsed() ) + m_inputButtons |= IN_BACK; + + if ( !m_leftButtonTimer.IsElapsed() ) + m_inputButtons |= IN_MOVELEFT; + + if ( !m_rightButtonTimer.IsElapsed() ) + m_inputButtons |= IN_MOVERIGHT; + + if ( !m_jumpButtonTimer.IsElapsed() ) + m_inputButtons |= IN_JUMP; + + if ( !m_crouchButtonTimer.IsElapsed() ) + m_inputButtons |= IN_DUCK; + + if ( !m_walkButtonTimer.IsElapsed() ) + m_inputButtons |= IN_SPEED; + + m_prevInputButtons = m_inputButtons; + inputButtons = m_inputButtons; + + EndUpdate(); + } + else + { + // HACK: Smooth out body animations + GetBodyInterface()->Update(); + + // keep buttons pressed between Update() calls (m_prevInputButtons), + // and include any button presses that occurred this tick (m_inputButtons). + inputButtons = m_prevInputButtons | m_inputButtons; + } + + // + // Convert NextBot locomotion and posture into + // player commands + // + IBody *body = GetBodyInterface(); + ILocomotion *mover = GetLocomotionInterface(); + + if ( body->IsActualPosture( IBody::CROUCH ) ) + { + inputButtons |= IN_DUCK; + } + + float forwardSpeed = 0.0f; + float strafeSpeed = 0.0f; + float verticalSpeed = ( m_inputButtons & IN_JUMP ) ? mover->GetRunSpeed() : 0.0f; + + if ( inputButtons & IN_FORWARD ) + { + forwardSpeed = mover->GetRunSpeed(); + } + else if ( inputButtons & IN_BACK ) + { + forwardSpeed = -mover->GetRunSpeed(); + } + + if ( inputButtons & IN_MOVELEFT ) + { + strafeSpeed = -mover->GetRunSpeed(); + } + else if ( inputButtons & IN_MOVERIGHT ) + { + strafeSpeed = mover->GetRunSpeed(); + } + + if ( NextBotPlayerWalk.GetBool() ) + { + inputButtons |= IN_SPEED; + } + + if ( NextBotPlayerCrouch.GetBool() ) + { + inputButtons |= IN_DUCK; + } + + if ( !m_buttonScaleTimer.IsElapsed() ) + { + forwardSpeed = mover->GetRunSpeed() * m_forwardScale; + strafeSpeed = mover->GetRunSpeed() * m_rightScale; + } + + if ( !NextBotPlayerMove.GetBool() ) + { + inputButtons &= ~(IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_JUMP ); + forwardSpeed = 0.0f; + strafeSpeed = 0.0f; + verticalSpeed = 0.0f; + } + + QAngle angles = this->EyeAngles(); + +#ifdef TERROR + if ( IsStunned() ) + { + inputButtons &= ~(IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_JUMP | IN_DUCK ); + } + + // "Look" in the direction we're climbing/stumbling etc. We can't do anything anyway, and it + // keeps motion extraction working. + if ( IsRenderYawOverridden() && IsMotionControlledXY( GetMainActivity() ) ) + { + angles[YAW] = GetOverriddenRenderYaw(); + } +#endif + + // construct a "command" to move the player + CUserCmd userCmd; + _NextBot_BuildUserCommand( &userCmd, angles, forwardSpeed, strafeSpeed, verticalSpeed, inputButtons, 0 ); + + AvoidPlayers( &userCmd ); + + // allocate a new command and add it to the player's list of command to process + this->ProcessUsercmds( &userCmd, 1, 1, 0, false ); + + m_inputButtons = 0; + + // actually execute player commands and do player physics + PlayerType::PhysicsSimulate(); +} + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ) +{ + // propagate into NextBot responders + INextBotEventResponder::OnNavAreaChanged( enteredArea, leftArea ); + + BaseClass::OnNavAreaChanged( enteredArea, leftArea ); +} + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::Touch( CBaseEntity *other ) +{ + if ( ShouldTouch( other ) ) + { + // propagate touch into NextBot event responders + trace_t result; + result = this->GetTouchTrace(); + OnContact( other, &result ); + } + + BaseClass::Touch( other ); +} + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::Weapon_Equip( CBaseCombatWeapon *weapon ) +{ +#ifdef TERROR + // TODO: Reimplement GetDroppingPlayer() into GetLastOwner() + OnPickUp( weapon, weapon->GetDroppingPlayer() ); +#else + OnPickUp( weapon, NULL ); +#endif + + BaseClass::Weapon_Equip( weapon ); +} + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::Weapon_Drop( CBaseCombatWeapon *weapon, const Vector *target, const Vector *velocity ) +{ + OnDrop( weapon ); + + BaseClass::Weapon_Drop( weapon, target, velocity ); +} + + +//-------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::OnMainActivityComplete( Activity newActivity, Activity oldActivity ) +{ +#ifdef TERROR + BaseClass::OnMainActivityComplete( newActivity, oldActivity ); +#endif + OnAnimationActivityComplete( oldActivity ); +} + + +//-------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::OnMainActivityInterrupted( Activity newActivity, Activity oldActivity ) +{ +#ifdef TERROR + BaseClass::OnMainActivityInterrupted( newActivity, oldActivity ); +#endif + OnAnimationActivityInterrupted( oldActivity ); +} + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::Update( void ) +{ + // don't spend CPU updating if this Survivor is dead + if ( ( this->IsAlive() || !IsDormantWhenDead() ) && !NextBotPlayerStop.GetBool() ) + { + INextBot::Update(); + } +} + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline bool NextBotPlayer< PlayerType >::IsAbleToAutoCenterOnLadders( void ) const +{ + const ILocomotion *locomotion = GetLocomotionInterface(); + return locomotion && locomotion->IsAbleToAutoCenterOnLadder(); +} + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline int NextBotPlayer< PlayerType >::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + if ( info.GetDamageType() & DMG_BURN ) + { + if ( !m_burningTimer.HasStarted() || m_burningTimer.IsGreaterThen( 1.0f ) ) + { + // emit ignite event periodically as long as we are burning + OnIgnite(); + m_burningTimer.Start(); + } + } + + // propagate event to components + OnInjured( info ); + + return BaseClass::OnTakeDamage_Alive( info ); +} + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline int NextBotPlayer< PlayerType >::OnTakeDamage_Dying( const CTakeDamageInfo &info ) +{ + if ( info.GetDamageType() & DMG_BURN ) + { + if ( !m_burningTimer.HasStarted() || m_burningTimer.IsGreaterThen( 1.0f ) ) + { + // emit ignite event periodically as long as we are burning + OnIgnite(); + m_burningTimer.Start(); + } + } + + // propagate event to components + OnInjured( info ); + + return BaseClass::OnTakeDamage_Dying( info ); +} + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::Event_Killed( const CTakeDamageInfo &info ) +{ + // propagate event to my components + OnKilled( info ); + + BaseClass::Event_Killed( info ); +} + + + +//---------------------------------------------------------------------------------------------------------- +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::HandleAnimEvent( animevent_t *event ) +{ + // propagate event to components + OnAnimationEvent( event ); + + BaseClass::HandleAnimEvent( event ); +} + + +#endif // _NEXT_BOT_PLAYER_H_ diff --git a/game/server/NextBot/Player/NextBotPlayerBody.cpp b/game/server/NextBot/Player/NextBotPlayerBody.cpp new file mode 100644 index 0000000..246b6fa --- /dev/null +++ b/game/server/NextBot/Player/NextBotPlayerBody.cpp @@ -0,0 +1,881 @@ +// NextBotPlayerBody.cpp +// Implementation of Body interface for CBasePlayer-derived classes +// Author: Michael Booth, October 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" + +#include "NextBot.h" +#include "NextBotPlayerBody.h" +#include "NextBotPlayer.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +ConVar nb_saccade_time( "nb_saccade_time", "0.1", FCVAR_CHEAT ); +ConVar nb_saccade_speed( "nb_saccade_speed", "1000", FCVAR_CHEAT ); +ConVar nb_head_aim_steady_max_rate( "nb_head_aim_steady_max_rate", "100", FCVAR_CHEAT ); +ConVar nb_head_aim_settle_duration( "nb_head_aim_settle_duration", "0.3", FCVAR_CHEAT ); +ConVar nb_head_aim_resettle_angle( "nb_head_aim_resettle_angle", "100", FCVAR_CHEAT, "After rotating through this angle, the bot pauses to 'recenter' its virtual mouse on its virtual mousepad" ); +ConVar nb_head_aim_resettle_time( "nb_head_aim_resettle_time", "0.3", FCVAR_CHEAT, "How long the bot pauses to 'recenter' its virtual mouse on its virtual mousepad" ); + + +//----------------------------------------------------------------------------------------------- +/** + * A useful reply for IBody::AimHeadTowards. When the + * head is aiming on target, press the fire button. + */ +void PressFireButtonReply::OnSuccess( INextBot *bot ) +{ + INextBotPlayerInput *playerInput = dynamic_cast< INextBotPlayerInput * >( bot->GetEntity() ); + if ( playerInput ) + { + playerInput->PressFireButton(); + } +} + + +//----------------------------------------------------------------------------------------------- +/** + * A useful reply for IBody::AimHeadTowards. When the + * head is aiming on target, press the alternate fire button. + */ +void PressAltFireButtonReply::OnSuccess( INextBot *bot ) +{ + INextBotPlayerInput *playerInput = dynamic_cast< INextBotPlayerInput * >( bot->GetEntity() ); + if ( playerInput ) + { + playerInput->PressMeleeButton(); + } +} + + +//----------------------------------------------------------------------------------------------- +/** + * A useful reply for IBody::AimHeadTowards. When the + * head is aiming on target, press the jump button. + */ +void PressJumpButtonReply::OnSuccess( INextBot *bot ) +{ + INextBotPlayerInput *playerInput = dynamic_cast< INextBotPlayerInput * >( bot->GetEntity() ); + if ( playerInput ) + { + playerInput->PressJumpButton(); + } +} + + +//----------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------------------------- +PlayerBody::PlayerBody( INextBot *bot ) : IBody( bot ) +{ + m_player = static_cast< CBasePlayer * >( bot->GetEntity() ); +} + + +//----------------------------------------------------------------------------------------------- +PlayerBody::~PlayerBody() +{ +} + + +//----------------------------------------------------------------------------------------------- +/** + * reset to initial state + */ +void PlayerBody::Reset( void ) +{ + m_posture = STAND; + + m_lookAtPos = vec3_origin; + m_lookAtSubject = NULL; + m_lookAtReplyWhenAimed = NULL; + m_lookAtVelocity = vec3_origin; + m_lookAtExpireTimer.Invalidate(); + + m_lookAtPriority = BORING; + m_lookAtExpireTimer.Invalidate(); + m_lookAtDurationTimer.Invalidate(); + m_isSightedIn = false; + m_hasBeenSightedIn = false; + m_headSteadyTimer.Invalidate(); + m_priorAngles = vec3_angle; + m_anchorRepositionTimer.Invalidate(); + m_anchorForward = vec3_origin; +} + +ConVar bot_mimic( "bot_mimic", "0", 0, "Bot uses usercmd of player by index." ); + +//----------------------------------------------------------------------------------------------- +/** + * Update internal state. + * Do this every tick to keep head aims smooth and accurate + */ +void PlayerBody::Upkeep( void ) +{ + // If mimicking the player, don't modify the view angles. + static ConVarRef bot_mimic( "bot_mimic" ); + if ( bot_mimic.IsValid() && bot_mimic.GetBool() ) + return; + + const float deltaT = gpGlobals->frametime; + if ( deltaT < 0.00001f ) + { + return; + } + + CBasePlayer *player = ( CBasePlayer * )GetBot()->GetEntity(); + + // get current view angles + QAngle currentAngles = player->EyeAngles() + player->GetPunchAngle(); + + // track when our head is "steady" + bool isSteady = true; + + float actualPitchRate = AngleDiff( currentAngles.x, m_priorAngles.x ); + if ( abs( actualPitchRate ) > nb_head_aim_steady_max_rate.GetFloat() * deltaT ) + { + isSteady = false; + } + else + { + float actualYawRate = AngleDiff( currentAngles.y, m_priorAngles.y ); + + if ( abs( actualYawRate ) > nb_head_aim_steady_max_rate.GetFloat() * deltaT ) + { + isSteady = false; + } + } + + if ( isSteady ) + { + if ( !m_headSteadyTimer.HasStarted() ) + { + m_headSteadyTimer.Start(); + } + } + else + { + m_headSteadyTimer.Invalidate(); + } + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + if ( IsHeadSteady() ) + { + const float maxTime = 3.0f; + float t = GetHeadSteadyDuration() / maxTime; + t = clamp( t, 0.f, 1.0f ); + NDebugOverlay::Circle( player->EyePosition(), t * 10.0f, 0, 255, 0, 255, true, 2.0f * deltaT ); + } + } + + m_priorAngles = currentAngles; + + + // if our current look-at has expired, don't change our aim further + if ( m_hasBeenSightedIn && m_lookAtExpireTimer.IsElapsed() ) + { + return; + } + + // simulate limited range of mouse movements + // compute the angle change from "center" + const Vector &forward = GetViewVector(); + float deltaAngle = RAD2DEG( acos( DotProduct( forward, m_anchorForward ) ) ); + if ( deltaAngle > nb_head_aim_resettle_angle.GetFloat() ) + { + // time to recenter our 'virtual mouse' + m_anchorRepositionTimer.Start( RandomFloat( 0.9f, 1.1f ) * nb_head_aim_resettle_time.GetFloat() ); + m_anchorForward = forward; + return; + } + + // if we're currently recentering our "virtual mouse", wait + if ( m_anchorRepositionTimer.HasStarted() && !m_anchorRepositionTimer.IsElapsed() ) + { + return; + } + m_anchorRepositionTimer.Invalidate(); + + + // if we have a subject, update lookat point + CBaseEntity *subject = m_lookAtSubject; + if ( subject ) + { + if ( m_lookAtTrackingTimer.IsElapsed() ) + { + // update subject tracking by periodically estimating linear aim velocity, allowing for "slop" between updates + Vector desiredLookAtPos; + + if ( subject->MyCombatCharacterPointer() ) + { + desiredLookAtPos = GetBot()->GetIntentionInterface()->SelectTargetPoint( GetBot(), subject->MyCombatCharacterPointer() ); + } + else + { + desiredLookAtPos = subject->WorldSpaceCenter(); + } + + desiredLookAtPos += GetHeadAimSubjectLeadTime() * subject->GetAbsVelocity(); + + Vector errorVector = desiredLookAtPos - m_lookAtPos; + float error = errorVector.NormalizeInPlace(); + + float trackingInterval = GetHeadAimTrackingInterval(); + if ( trackingInterval < deltaT ) + { + trackingInterval = deltaT; + } + + float errorVel = error / trackingInterval; + + m_lookAtVelocity = ( errorVel * errorVector ) + subject->GetAbsVelocity(); + + m_lookAtTrackingTimer.Start( RandomFloat( 0.8f, 1.2f ) * trackingInterval ); + } + + m_lookAtPos += deltaT * m_lookAtVelocity; + } + + + // aim view towards last look at point + Vector to = m_lookAtPos - GetEyePosition(); + to.NormalizeInPlace(); + + QAngle desiredAngles; + VectorAngles( to, desiredAngles ); + + QAngle angles; + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + NDebugOverlay::Line( GetEyePosition(), GetEyePosition() + 100.0f * forward, 255, 255, 0, false, 2.0f * deltaT ); + + float thickness = isSteady ? 2.0f : 3.0f; + int r = m_isSightedIn ? 255 : 0; + int g = subject ? 255 : 0; + NDebugOverlay::HorzArrow( GetEyePosition(), m_lookAtPos, thickness, r, g, 255, 255, false, 2.0f * deltaT ); + } + + + const float onTargetTolerance = 0.98f; + float dot = DotProduct( forward, to ); + if ( dot > onTargetTolerance ) + { + // on target + m_isSightedIn = true; + + if ( !m_hasBeenSightedIn ) + { + m_hasBeenSightedIn = true; + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + ConColorMsg( Color( 255, 100, 0, 255 ), "%3.2f: %s Look At SIGHTED IN\n", + gpGlobals->curtime, + m_player->GetPlayerName() ); + } + } + + if ( m_lookAtReplyWhenAimed ) + { + m_lookAtReplyWhenAimed->OnSuccess( GetBot() ); + m_lookAtReplyWhenAimed = NULL; + } + } + else + { + // off target + m_isSightedIn = false; + } + + + // rotate view at a rate proportional to how far we have to turn + // max rate if we need to turn around + // want first derivative continuity of rate as our aim hits to avoid pop + float approachRate = GetMaxHeadAngularVelocity(); + + const float easeOut = 0.7f; + if ( dot > easeOut ) + { + float t = RemapVal( dot, easeOut, 1.0f, 1.0f, 0.02f ); + const float halfPI = 1.57f; + approachRate *= sin( halfPI * t ); + } + + const float easeInTime = 0.25f; + if ( m_lookAtDurationTimer.GetElapsedTime() < easeInTime ) + { + approachRate *= m_lookAtDurationTimer.GetElapsedTime() / easeInTime; + } + + angles.y = ApproachAngle( desiredAngles.y, currentAngles.y, approachRate * deltaT ); + angles.x = ApproachAngle( desiredAngles.x, currentAngles.x, 0.5f * approachRate * deltaT ); + angles.z = 0.0f; + + // back out "punch angle" + angles -= player->GetPunchAngle(); + + angles.x = AngleNormalize( angles.x ); + angles.y = AngleNormalize( angles.y ); + + player->SnapEyeAngles( angles ); +} + + +//----------------------------------------------------------------------------------------------- +bool PlayerBody::SetPosition( const Vector &pos ) +{ + m_player->SetAbsOrigin( pos ); + return true; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return the eye position of the bot in world coordinates + */ +const Vector &PlayerBody::GetEyePosition( void ) const +{ + m_eyePos = m_player->EyePosition(); + return m_eyePos; +} + + +CBaseEntity *PlayerBody::GetEntity( void ) +{ + return m_player; +} + +//----------------------------------------------------------------------------------------------- +/** + * Return the view unit direction vector in world coordinates + */ +const Vector &PlayerBody::GetViewVector( void ) const +{ + m_player->EyeVectors( &m_viewVector ); + return m_viewVector; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Aim the bot's head towards the given goal + */ +void PlayerBody::AimHeadTowards( const Vector &lookAtPos, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason ) +{ + if ( duration <= 0.0f ) + { + duration = 0.1f; + } + + // don't spaz our aim around + if ( m_lookAtPriority == priority ) + { + if ( !IsHeadSteady() || GetHeadSteadyDuration() < nb_head_aim_settle_duration.GetFloat() ) + { + // we're still finishing a look-at at the same priority + if ( replyWhenAimed ) + { + replyWhenAimed->OnFail( GetBot(), INextBotReply::DENIED ); + } + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Look At '%s' rejected - previous aim not %s\n", + gpGlobals->curtime, + m_player->GetPlayerName(), + reason, + IsHeadSteady() ? "settled long enough" : "head-steady" ); + } + return; + } + } + + // don't short-circuit if "sighted in" to avoid rapid view jitter + if ( m_lookAtPriority > priority && !m_lookAtExpireTimer.IsElapsed() ) + { + // higher priority lookat still ongoing + if ( replyWhenAimed ) + { + replyWhenAimed->OnFail( GetBot(), INextBotReply::DENIED ); + } + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Look At '%s' rejected - higher priority aim in progress\n", + gpGlobals->curtime, + m_player->GetPlayerName(), + reason ); + } + return; + } + + if ( m_lookAtReplyWhenAimed ) + { + // in-process aim was interrupted + m_lookAtReplyWhenAimed->OnFail( GetBot(), INextBotReply::INTERRUPTED ); + } + + m_lookAtReplyWhenAimed = replyWhenAimed; + m_lookAtExpireTimer.Start( duration ); + + // if given the same point, just update priority + const float epsilon = 1.0f; + if ( ( m_lookAtPos - lookAtPos ).IsLengthLessThan( epsilon ) ) + { + m_lookAtPriority = priority; + return; + } + + // new look-at point + + m_lookAtPos = lookAtPos; + m_lookAtSubject = NULL; + + m_lookAtPriority = priority; + m_lookAtDurationTimer.Start(); + + // do NOT clear this here, or continuous calls to AimHeadTowards will keep IsHeadAimingOnTarget returning false all of the time + // m_isSightedIn = false; + + m_hasBeenSightedIn = false; + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + NDebugOverlay::Cross3D( lookAtPos, 2.0f, 255, 255, 100, true, 2.0f * duration ); + + const char *priName = ""; + switch( priority ) + { + case BORING: priName = "BORING"; break; + case INTERESTING: priName = "INTERESTING"; break; + case IMPORTANT: priName = "IMPORTANT"; break; + case CRITICAL: priName = "CRITICAL"; break; + } + + ConColorMsg( Color( 255, 100, 0, 255 ), "%3.2f: %s Look At ( %g, %g, %g ) for %3.2f s, Pri = %s, Reason = %s\n", + gpGlobals->curtime, + m_player->GetPlayerName(), + lookAtPos.x, lookAtPos.y, lookAtPos.z, + duration, + priName, + ( reason ) ? reason : "" ); + } +} + + +//----------------------------------------------------------------------------------------------- +/** + * Aim the bot's head towards the given goal + */ +void PlayerBody::AimHeadTowards( CBaseEntity *subject, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason ) +{ + if ( duration <= 0.0f ) + { + duration = 0.1f; + } + + if ( subject == NULL ) + { + return; + } + + // don't spaz our aim around + if ( m_lookAtPriority == priority ) + { + if ( !IsHeadSteady() || GetHeadSteadyDuration() < nb_head_aim_settle_duration.GetFloat() ) + { + // we're still finishing a look-at at the same priority + if ( replyWhenAimed ) + { + replyWhenAimed->OnFail( GetBot(), INextBotReply::DENIED ); + } + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Look At '%s' rejected - previous aim not %s\n", + gpGlobals->curtime, + m_player->GetPlayerName(), + reason, + IsHeadSteady() ? "head-steady" : "settled long enough" ); + } + return; + } + } + + // don't short-circuit if "sighted in" to avoid rapid view jitter + if ( m_lookAtPriority > priority && !m_lookAtExpireTimer.IsElapsed() ) + { + // higher priority lookat still ongoing + if ( replyWhenAimed ) + { + replyWhenAimed->OnFail( GetBot(), INextBotReply::DENIED ); + } + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + ConColorMsg( Color( 255, 0, 0, 255 ), "%3.2f: %s Look At '%s' rejected - higher priority aim in progress\n", + gpGlobals->curtime, + m_player->GetPlayerName(), + reason ); + } + return; + } + + if ( m_lookAtReplyWhenAimed ) + { + // in-process aim was interrupted + m_lookAtReplyWhenAimed->OnFail( GetBot(), INextBotReply::INTERRUPTED ); + } + + m_lookAtReplyWhenAimed = replyWhenAimed; + m_lookAtExpireTimer.Start( duration ); + + // if given the same subject, just update priority + if ( subject == m_lookAtSubject ) + { + m_lookAtPriority = priority; + return; + } + + // new subject + m_lookAtSubject = subject; + +#ifdef REFACTOR_FOR_CLIENT_SIDE_EYE_TRACKING + CBasePlayer *pMyPlayer = static_cast< CBasePlayer * >( GetEntity() ); + if ( subject->IsPlayer() ) + { + // looking at a player, look at their eye position + TerrorPlayer *pMyTarget = ToTerrorPlayer( subject ); + m_lookAtPos = subject->EyePosition(); + if(pMyPlayer) + { + pMyPlayer->SetLookatPlayer( pMyTarget ); + } + } + else + { + // not looking at a player + m_lookAtPos = subject->WorldSpaceCenter(); + if(pMyPlayer) + { + pMyPlayer->SetLookatPlayer( NULL ); + } + } +#endif + + m_lookAtPriority = priority; + m_lookAtDurationTimer.Start(); + + // do NOT clear this here, or continuous calls to AimHeadTowards will keep IsHeadAimingOnTarget returning false all of the time + // m_isSightedIn = false; + + m_hasBeenSightedIn = false; + + if ( GetBot()->IsDebugging( NEXTBOT_LOOK_AT ) ) + { + NDebugOverlay::Cross3D( m_lookAtPos, 2.0f, 100, 100, 100, true, duration ); + + const char *priName = ""; + switch( priority ) + { + case BORING: priName = "BORING"; break; + case INTERESTING: priName = "INTERESTING"; break; + case IMPORTANT: priName = "IMPORTANT"; break; + case CRITICAL: priName = "CRITICAL"; break; + } + + ConColorMsg( Color( 255, 100, 0, 255 ), "%3.2f: %s Look At subject %s for %3.2f s, Pri = %s, Reason = %s\n", + gpGlobals->curtime, + m_player->GetPlayerName(), + subject->GetClassname(), + duration, + priName, + ( reason ) ? reason : "" ); + } +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if head is not rapidly turning to look somewhere else + */ +bool PlayerBody::IsHeadSteady( void ) const +{ + return m_headSteadyTimer.HasStarted(); +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return the duration that the bot's head has been on-target + */ +float PlayerBody::GetHeadSteadyDuration( void ) const +{ + // return ( IsHeadAimingOnTarget() ) ? m_headSteadyTimer.GetElapsedTime() : 0.0f; + return m_headSteadyTimer.HasStarted() ? m_headSteadyTimer.GetElapsedTime() : 0.0f; +} + + +//----------------------------------------------------------------------------------------------- +// Clear out currently pending replyWhenAimed callback +void PlayerBody::ClearPendingAimReply( void ) +{ + m_lookAtReplyWhenAimed = NULL; +} + + +//----------------------------------------------------------------------------------------------- +float PlayerBody::GetMaxHeadAngularVelocity( void ) const +{ + return nb_saccade_speed.GetFloat(); +} + + +//----------------------------------------------------------------------------------------------- +bool PlayerBody::StartActivity( Activity act, unsigned int flags ) +{ + // player animation state is controlled on the client + return false; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return currently animating activity + */ +Activity PlayerBody::GetActivity( void ) const +{ + return ACT_INVALID; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if currently animating activity matches the given one + */ +bool PlayerBody::IsActivity( Activity act ) const +{ + return false; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if currently animating activity has any of the given flags + */ +bool PlayerBody::HasActivityType( unsigned int flags ) const +{ + return false; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Request a posture change + */ +void PlayerBody::SetDesiredPosture( PostureType posture ) +{ + m_posture = posture; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Get posture body is trying to assume + */ +IBody::PostureType PlayerBody::GetDesiredPosture( void ) const +{ + return m_posture; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if body is trying to assume this posture + */ +bool PlayerBody::IsDesiredPosture( PostureType posture ) const +{ + return ( posture == m_posture ); +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if body's actual posture matches its desired posture + */ +bool PlayerBody::IsInDesiredPosture( void ) const +{ + return true; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return body's current actual posture + */ +IBody::PostureType PlayerBody::GetActualPosture( void ) const +{ + return m_posture; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if body is actually in the given posture + */ +bool PlayerBody::IsActualPosture( PostureType posture ) const +{ + return ( posture == m_posture ); +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if body's current posture allows it to move around the world + */ +bool PlayerBody::IsPostureMobile( void ) const +{ + return true; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if body's posture is in the process of changing to new posture + */ +bool PlayerBody::IsPostureChanging( void ) const +{ + return false; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Arousal level change + */ +void PlayerBody::SetArousal( ArousalType arousal ) +{ + m_arousal = arousal; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Get arousal level + */ +IBody::ArousalType PlayerBody::GetArousal( void ) const +{ + return m_arousal; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return true if body is at this arousal level + */ +bool PlayerBody::IsArousal( ArousalType arousal ) const +{ + return ( arousal == m_arousal ); +} + + +//----------------------------------------------------------------------------------------------- +/** + * Width of bot's collision hull in XY plane + */ +float PlayerBody::GetHullWidth( void ) const +{ + return VEC_HULL_MAX_SCALED( m_player ).x - VEC_HULL_MIN_SCALED( m_player ).x; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Height of bot's current collision hull based on posture + */ +float PlayerBody::GetHullHeight( void ) const +{ + if ( m_posture == CROUCH ) + { + return GetCrouchHullHeight(); + } + + return GetStandHullHeight(); +} + + +//----------------------------------------------------------------------------------------------- +/** + * Height of bot's collision hull when standing + */ +float PlayerBody::GetStandHullHeight( void ) const +{ + return VEC_HULL_MAX_SCALED( m_player ).z - VEC_HULL_MIN_SCALED( m_player ).z; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Height of bot's collision hull when crouched + */ +float PlayerBody::GetCrouchHullHeight( void ) const +{ + return VEC_DUCK_HULL_MAX_SCALED( m_player ).z - VEC_DUCK_HULL_MIN_SCALED( m_player ).z; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return current collision hull minimums based on actual body posture + */ +const Vector &PlayerBody::GetHullMins( void ) const +{ + if ( m_posture == CROUCH ) + { + m_hullMins = VEC_DUCK_HULL_MIN_SCALED( m_player ); + } + else + { + m_hullMins = VEC_HULL_MIN_SCALED( m_player ); + } + + return m_hullMins; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return current collision hull maximums based on actual body posture + */ +const Vector &PlayerBody::GetHullMaxs( void ) const +{ + if ( m_posture == CROUCH ) + { + m_hullMaxs = VEC_DUCK_HULL_MAX_SCALED( m_player ); + } + else + { + m_hullMaxs = VEC_HULL_MAX_SCALED( m_player ); + } + + return m_hullMaxs; +} + + +//----------------------------------------------------------------------------------------------- +/** + * Return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) + */ +unsigned int PlayerBody::GetSolidMask( void ) const +{ + return ( m_player ) ? m_player->PlayerSolidMask() : MASK_PLAYERSOLID; +} + + + + + diff --git a/game/server/NextBot/Player/NextBotPlayerBody.h b/game/server/NextBot/Player/NextBotPlayerBody.h new file mode 100644 index 0000000..eda3643 --- /dev/null +++ b/game/server/NextBot/Player/NextBotPlayerBody.h @@ -0,0 +1,153 @@ +// NextBotPlayerBody.h +// Control and information about the bot's body state (posture, animation state, etc) +// Author: Michael Booth, October 2006 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_PLAYER_BODY_H_ +#define _NEXT_BOT_PLAYER_BODY_H_ + +#include "NextBotBodyInterface.h" + + +//---------------------------------------------------------------------------------------------------------------- +/** + * A useful reply for IBody::AimHeadTowards. When the + * head is aiming on target, press the fire button. + */ +class PressFireButtonReply : public INextBotReply +{ +public: + virtual void OnSuccess( INextBot *bot ); // invoked when process completed successfully +}; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * A useful reply for IBody::AimHeadTowards. When the + * head is aiming on target, press the alt-fire button. + */ +class PressAltFireButtonReply : public INextBotReply +{ +public: + virtual void OnSuccess( INextBot *bot ); // invoked when process completed successfully +}; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * A useful reply for IBody::AimHeadTowards. When the + * head is aiming on target, press the jump button. + */ +class PressJumpButtonReply : public INextBotReply +{ +public: + virtual void OnSuccess( INextBot *bot ); // invoked when process completed successfully +}; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for control and information about the bot's body state (posture, animation state, etc) + */ +class PlayerBody : public IBody +{ +public: + PlayerBody( INextBot *bot ); + virtual ~PlayerBody(); + + virtual void Reset( void ); // reset to initial state + virtual void Upkeep( void ); // lightweight update guaranteed to occur every server tick + + virtual bool SetPosition( const Vector &pos ); + + virtual const Vector &GetEyePosition( void ) const; // return the eye position of the bot in world coordinates + virtual const Vector &GetViewVector( void ) const; // return the view unit direction vector in world coordinates + + virtual void AimHeadTowards( const Vector &lookAtPos, + LookAtPriorityType priority = BORING, + float duration = 0.0f, + INextBotReply *replyWhenAimed = NULL, + const char *reason = NULL ); // aim the bot's head towards the given goal + + virtual void AimHeadTowards( CBaseEntity *subject, + LookAtPriorityType priority = BORING, + float duration = 0.0f, + INextBotReply *replyWhenAimed = NULL, + const char *reason = NULL ); // continually aim the bot's head towards the given subject + + virtual bool IsHeadAimingOnTarget( void ) const; // return true if the bot's head has achieved its most recent lookat target + virtual bool IsHeadSteady( void ) const; // return true if head is not rapidly turning to look somewhere else + virtual float GetHeadSteadyDuration( void ) const; // return the duration that the bot's head has been on-target + virtual void ClearPendingAimReply( void ); // clear out currently pending replyWhenAimed callback + + virtual float GetMaxHeadAngularVelocity( void ) const; // return max turn rate of head in degrees/second + + virtual bool StartActivity( Activity act, unsigned int flags ); + virtual Activity GetActivity( void ) const; // return currently animating activity + virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one + virtual bool HasActivityType( unsigned int flags ) const; // return true if currently animating activity has any of the given flags + + virtual void SetDesiredPosture( PostureType posture ); // request a posture change + virtual PostureType GetDesiredPosture( void ) const; // get posture body is trying to assume + virtual bool IsDesiredPosture( PostureType posture ) const; // return true if body is trying to assume this posture + virtual bool IsInDesiredPosture( void ) const; // return true if body's actual posture matches its desired posture + + virtual PostureType GetActualPosture( void ) const; // return body's current actual posture + virtual bool IsActualPosture( PostureType posture ) const; // return true if body is actually in the given posture + + virtual bool IsPostureMobile( void ) const; // return true if body's current posture allows it to move around the world + virtual bool IsPostureChanging( void ) const; // return true if body's posture is in the process of changing to new posture + + virtual void SetArousal( ArousalType arousal ); // arousal level change + virtual ArousalType GetArousal( void ) const; // get arousal level + virtual bool IsArousal( ArousalType arousal ) const; // return true if body is at this arousal level + + virtual float GetHullWidth( void ) const; // width of bot's collision hull in XY plane + virtual float GetHullHeight( void ) const; // height of bot's current collision hull based on posture + virtual float GetStandHullHeight( void ) const; // height of bot's collision hull when standing + virtual float GetCrouchHullHeight( void ) const; // height of bot's collision hull when crouched + virtual const Vector &GetHullMins( void ) const; // return current collision hull minimums based on actual body posture + virtual const Vector &GetHullMaxs( void ) const; // return current collision hull maximums based on actual body posture + + virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) + + virtual CBaseEntity *GetEntity( void ); // get the entity +private: + CBasePlayer *m_player; + + PostureType m_posture; + ArousalType m_arousal; + + mutable Vector m_eyePos; // for use with GetEyePosition() ONLY + mutable Vector m_viewVector; // for use with GetViewVector() ONLY + mutable Vector m_hullMins; // for use with GetHullMins() ONLY + mutable Vector m_hullMaxs; // for use with GetHullMaxs() ONLY + + Vector m_lookAtPos; // if m_lookAtSubject is non-NULL, it continually overwrites this position with its own + EHANDLE m_lookAtSubject; + Vector m_lookAtVelocity; // world velocity of lookat point, for tracking moving subjects + CountdownTimer m_lookAtTrackingTimer; + + LookAtPriorityType m_lookAtPriority; + CountdownTimer m_lookAtExpireTimer; // how long until this lookat expired + IntervalTimer m_lookAtDurationTimer; // how long have we been looking at this target + INextBotReply *m_lookAtReplyWhenAimed; + bool m_isSightedIn; // true if we are looking at our last lookat target + bool m_hasBeenSightedIn; // true if we have hit the current lookat target + + IntervalTimer m_headSteadyTimer; + QAngle m_priorAngles; // last update's head angles + QAngle m_desiredAngles; + + CountdownTimer m_anchorRepositionTimer; // the time is takes us to recenter our virtual mouse + Vector m_anchorForward; +}; + +inline bool PlayerBody::IsHeadAimingOnTarget( void ) const +{ + // TODO: Calling this immediately after AimHeadTowards will always return false until next Upkeep() (MSB) + return m_isSightedIn; +} + + +#endif // _NEXT_BOT_PLAYER_BODY_H_ diff --git a/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp b/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp new file mode 100644 index 0000000..ea30ee7 --- /dev/null +++ b/game/server/NextBot/Player/NextBotPlayerLocomotion.cpp @@ -0,0 +1,826 @@ +// NextBotPlayerLocomotion.cpp +// Implementation of Locomotion interface for CBasePlayer-derived classes +// Author: Michael Booth, November 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" +#include "nav_mesh.h" +#include "in_buttons.h" +#include "NextBot.h" +#include "NextBotUtil.h" +#include "NextBotPlayer.h" +#include "NextBotPlayerLocomotion.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar NextBotPlayerMoveDirect( "nb_player_move_direct", "0" ); + +//----------------------------------------------------------------------------------------------------- +PlayerLocomotion::PlayerLocomotion( INextBot *bot ) : ILocomotion( bot ) +{ + m_player = NULL; + Reset(); +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Reset locomotor to initial state + */ +void PlayerLocomotion::Reset( void ) +{ + m_player = static_cast< CBasePlayer * >( GetBot()->GetEntity() ); + + m_isJumping = false; + m_isClimbingUpToLedge = false; + m_isJumpingAcrossGap = false; + m_hasLeftTheGround = false; + m_desiredSpeed = 0.0f; + + + m_ladderState = NO_LADDER; + m_ladderInfo = NULL; + m_ladderDismountGoal = NULL; + m_ladderTimer.Invalidate(); + + m_minSpeedLimit = 0.0f; + m_maxSpeedLimit = 9999999.9f; + + BaseClass::Reset(); +} + + +//----------------------------------------------------------------------------------------------------- +bool PlayerLocomotion::TraverseLadder( void ) +{ + switch( m_ladderState ) + { + case APPROACHING_ASCENDING_LADDER: + m_ladderState = ApproachAscendingLadder(); + return true; + + case APPROACHING_DESCENDING_LADDER: + m_ladderState = ApproachDescendingLadder(); + return true; + + case ASCENDING_LADDER: + m_ladderState = AscendLadder(); + return true; + + case DESCENDING_LADDER: + m_ladderState = DescendLadder(); + return true; + + case DISMOUNTING_LADDER_TOP: + m_ladderState = DismountLadderTop(); + return true; + + case DISMOUNTING_LADDER_BOTTOM: + m_ladderState = DismountLadderBottom(); + return true; + + case NO_LADDER: + default: + m_ladderInfo = NULL; + + if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) + { + // on ladder and don't want to be + GetBot()->GetEntity()->SetMoveType( MOVETYPE_WALK ); + } + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * We're close, but not yet on, this ladder - approach it + */ +PlayerLocomotion::LadderState PlayerLocomotion::ApproachAscendingLadder( void ) +{ + if ( m_ladderInfo == NULL ) + { + return NO_LADDER; + } + + // sanity check - are we already at the end of this ladder? + if ( GetFeet().z >= m_ladderInfo->m_top.z - GetStepHeight() ) + { + m_ladderTimer.Start( 2.0f ); + return DISMOUNTING_LADDER_TOP; + } + + // sanity check - are we too far below this ladder to reach it? + if ( GetFeet().z <= m_ladderInfo->m_bottom.z - GetMaxJumpHeight() ) + { + return NO_LADDER; + } + + FaceTowards( m_ladderInfo->m_bottom ); + + // it is important to approach precisely, so use a very large weight to wash out all other Approaches + Approach( m_ladderInfo->m_bottom, 9999999.9f ); + + if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) + { + // we're on the ladder + return ASCENDING_LADDER; + } + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Approach ascending ladder", 0.1f, 255, 255, 255, 255 ); + } + + return APPROACHING_ASCENDING_LADDER; +} + + +//----------------------------------------------------------------------------------------------------- +PlayerLocomotion::LadderState PlayerLocomotion::ApproachDescendingLadder( void ) +{ + if ( m_ladderInfo == NULL ) + { + return NO_LADDER; + } + + // sanity check - are we already at the end of this ladder? + if ( GetFeet().z <= m_ladderInfo->m_bottom.z + GetMaxJumpHeight() ) + { + m_ladderTimer.Start( 2.0f ); + return DISMOUNTING_LADDER_BOTTOM; + } + + Vector mountPoint = m_ladderInfo->m_top + 0.25f * GetBot()->GetBodyInterface()->GetHullWidth() * m_ladderInfo->GetNormal(); + Vector to = mountPoint - GetFeet(); + to.z = 0.0f; + + float mountRange = to.NormalizeInPlace(); + Vector moveGoal; + + const float veryClose = 10.0f; + if ( mountRange < veryClose ) + { + // we're right at the ladder - just keep moving forward until we grab it + const Vector &forward = GetMotionVector(); + moveGoal = GetFeet() + 100.0f * forward; + } + else + { + if ( DotProduct( to, m_ladderInfo->GetNormal() ) < 0.0f ) + { + // approaching front of downward ladder + // ## + // ->+ ## + // | ## + // | ## + // | ## + // <-+ ## + // ###### + // + moveGoal = m_ladderInfo->m_top - 100.0f * m_ladderInfo->GetNormal(); + } + else + { + // approaching back of downward ladder + // + // ->+ + // ##| + // ##| + // ##+--> + // ###### + // + moveGoal = m_ladderInfo->m_top + 100.0f * m_ladderInfo->GetNormal(); + } + } + + FaceTowards( moveGoal ); + + // it is important to approach precisely, so use a very large weight to wash out all other Approaches + Approach( moveGoal, 9999999.9f ); + + if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) + { + // we're on the ladder + return DESCENDING_LADDER; + } + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Approach descending ladder", 0.1f, 255, 255, 255, 255 ); + } + + return APPROACHING_DESCENDING_LADDER; +} + + +//----------------------------------------------------------------------------------------------------- +PlayerLocomotion::LadderState PlayerLocomotion::AscendLadder( void ) +{ + if ( m_ladderInfo == NULL ) + { + return NO_LADDER; + } + + if ( GetBot()->GetEntity()->GetMoveType() != MOVETYPE_LADDER ) + { + // slipped off ladder + m_ladderInfo = NULL; + return NO_LADDER; + } + + if ( GetFeet().z >= m_ladderInfo->m_top.z ) + { + // reached top of ladder + m_ladderTimer.Start( 2.0f ); + return DISMOUNTING_LADDER_TOP; + } + + // climb up this ladder - look up + Vector goal = GetFeet() + 100.0f * ( -m_ladderInfo->GetNormal() + Vector( 0, 0, 2 ) ); + + GetBot()->GetBodyInterface()->AimHeadTowards( goal, IBody::MANDATORY, 0.1f, NULL, "Ladder" ); + + // it is important to approach precisely, so use a very large weight to wash out all other Approaches + Approach( goal, 9999999.9f ); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Ascend", 0.1f, 255, 255, 255, 255 ); + } + + return ASCENDING_LADDER; +} + + +//----------------------------------------------------------------------------------------------------- +PlayerLocomotion::LadderState PlayerLocomotion::DescendLadder( void ) +{ + if ( m_ladderInfo == NULL ) + { + return NO_LADDER; + } + + if ( GetBot()->GetEntity()->GetMoveType() != MOVETYPE_LADDER ) + { + // slipped off ladder + m_ladderInfo = NULL; + return NO_LADDER; + } + + if ( GetFeet().z <= m_ladderInfo->m_bottom.z + GetBot()->GetLocomotionInterface()->GetStepHeight() ) + { + // reached bottom of ladder + m_ladderTimer.Start( 2.0f ); + return DISMOUNTING_LADDER_BOTTOM; + } + + // climb down this ladder - look down + Vector goal = GetFeet() + 100.0f * ( m_ladderInfo->GetNormal() + Vector( 0, 0, -2 ) ); + + GetBot()->GetBodyInterface()->AimHeadTowards( goal, IBody::MANDATORY, 0.1f, NULL, "Ladder" ); + + // it is important to approach precisely, so use a very large weight to wash out all other Approaches + Approach( goal, 9999999.9f ); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Descend", 0.1f, 255, 255, 255, 255 ); + } + + return DESCENDING_LADDER; +} + + +//----------------------------------------------------------------------------------------------------- +PlayerLocomotion::LadderState PlayerLocomotion::DismountLadderTop( void ) +{ + if ( m_ladderInfo == NULL || m_ladderTimer.IsElapsed() ) + { + m_ladderInfo = NULL; + return NO_LADDER; + } + + IBody *body = GetBot()->GetBodyInterface(); + Vector toGoal = m_ladderDismountGoal->GetCenter() - GetFeet(); + toGoal.z = 0.0f; + float range = toGoal.NormalizeInPlace(); + toGoal.z = 1.0f; + + body->AimHeadTowards( body->GetEyePosition() + 100.0f * toGoal, IBody::MANDATORY, 0.1f, NULL, "Ladder dismount" ); + + // it is important to approach precisely, so use a very large weight to wash out all other Approaches + Approach( GetFeet() + 100.0f * toGoal, 9999999.9f ); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::EntityText( GetBot()->GetEntity()->entindex(), 0, "Dismount top", 0.1f, 255, 255, 255, 255 ); + NDebugOverlay::HorzArrow( GetFeet(), m_ladderDismountGoal->GetCenter(), 5.0f, 255, 255, 0, 255, true, 0.1f ); + } + + // test 2D vector here in case nav area is under the geometry a bit + const float tolerance = 10.0f; + if ( GetBot()->GetEntity()->GetLastKnownArea() == m_ladderDismountGoal && range < tolerance ) + { + // reached dismount goal + m_ladderInfo = NULL; + return NO_LADDER; + } + + return DISMOUNTING_LADDER_TOP; +} + + +//----------------------------------------------------------------------------------------------------- +PlayerLocomotion::LadderState PlayerLocomotion::DismountLadderBottom( void ) +{ + if ( m_ladderInfo == NULL || m_ladderTimer.IsElapsed() ) + { + m_ladderInfo = NULL; + return NO_LADDER; + } + + if ( GetBot()->GetEntity()->GetMoveType() == MOVETYPE_LADDER ) + { + // near the bottom - just let go + GetBot()->GetEntity()->SetMoveType( MOVETYPE_WALK ); + m_ladderInfo = NULL; + } + + return NO_LADDER; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Update internal state + */ +void PlayerLocomotion::Update( void ) +{ + if ( TraverseLadder() ) + { + return BaseClass::Update(); + } + + if ( m_isJumpingAcrossGap || m_isClimbingUpToLedge ) + { + // force a run + SetMinimumSpeedLimit( GetRunSpeed() ); + + Vector toLanding = m_landingGoal - GetFeet(); + toLanding.z = 0.0f; + toLanding.NormalizeInPlace(); + + if ( m_hasLeftTheGround ) + { + // face into the jump/climb + GetBot()->GetBodyInterface()->AimHeadTowards( GetBot()->GetEntity()->EyePosition() + 100.0 * toLanding, IBody::MANDATORY, 0.25f, NULL, "Facing impending jump/climb" ); + + if ( IsOnGround() ) + { + // back on the ground - jump is complete + m_isClimbingUpToLedge = false; + m_isJumpingAcrossGap = false; + SetMinimumSpeedLimit( 0.0f ); + } + } + else + { + // haven't left the ground yet - just starting the jump + + if ( !IsClimbingOrJumping() ) + { + Jump(); + } + + Vector vel = GetBot()->GetEntity()->GetAbsVelocity(); + + if ( m_isJumpingAcrossGap ) + { + // cheat and max our velocity in case we were stopped at the edge of this gap + vel.x = GetRunSpeed() * toLanding.x; + vel.y = GetRunSpeed() * toLanding.y; + // leave vel.z unchanged + } + + GetBot()->GetEntity()->SetAbsVelocity( vel ); + + if ( !IsOnGround() ) + { + // jump has begun + m_hasLeftTheGround = true; + } + } + + + Approach( m_landingGoal ); + } + + BaseClass::Update(); +} + + +//----------------------------------------------------------------------------------------------------- +void PlayerLocomotion::AdjustPosture( const Vector &moveGoal ) +{ + // This function has no effect if we're not standing or crouching + IBody *body = GetBot()->GetBodyInterface(); + if ( !body->IsActualPosture( IBody::STAND ) && !body->IsActualPosture( IBody::CROUCH ) ) + return; + + // not all games have auto-crouch, so don't assume it here + BaseClass::AdjustPosture( moveGoal ); +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Build a user command to move this player towards the goal position + */ +void PlayerLocomotion::Approach( const Vector &pos, float goalWeight ) +{ + VPROF_BUDGET( "PlayerLocomotion::Approach", "NextBot" ); + + BaseClass::Approach( pos ); + + AdjustPosture( pos ); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::Line( GetFeet(), pos, 255, 255, 0, true, 0.1f ); + } + + INextBotPlayerInput *playerButtons = dynamic_cast< INextBotPlayerInput * >( GetBot() ); + + if ( !playerButtons ) + { + DevMsg( "PlayerLocomotion::Approach: No INextBotPlayerInput\n " ); + return; + } + + Vector forward3D; + m_player->EyeVectors( &forward3D ); + + Vector2D forward( forward3D.x, forward3D.y ); + forward.NormalizeInPlace(); + + Vector2D right( forward.y, -forward.x ); + + // compute unit vector to goal position + Vector2D to = ( pos - GetFeet() ).AsVector2D(); + float goalDistance = to.NormalizeInPlace(); + + float ahead = to.Dot( forward ); + float side = to.Dot( right ); + +#ifdef NEED_TO_INTEGRATE_MOTION_CONTROLLED_CODE_FROM_L4D_PLAYERS + // If we're climbing ledges, we need to stay crouched to prevent player movement code from messing + // with our origin. + CTerrorPlayer *player = ToTerrorPlayer(m_player); + if ( player && player->IsMotionControlledZ( player->GetMainActivity() ) ) + { + playerButtons->PressCrouchButton(); + return; + } +#endif + + if ( m_player->IsOnLadder() && IsUsingLadder() && ( m_ladderState == ASCENDING_LADDER || m_ladderState == DESCENDING_LADDER ) ) + { + // we are on a ladder and WANT to be on a ladder. + playerButtons->PressForwardButton(); + + // Stay in center of ladder. The gamemovement will autocenter us in most cases, but this is needed in case it doesn't. + if ( m_ladderInfo ) + { + Vector posOnLadder; + CalcClosestPointOnLine( GetFeet(), m_ladderInfo->m_bottom, m_ladderInfo->m_top, posOnLadder ); + + Vector alongLadder = m_ladderInfo->m_top - m_ladderInfo->m_bottom; + alongLadder.NormalizeInPlace(); + + Vector rightLadder = CrossProduct( alongLadder, m_ladderInfo->GetNormal() ); + + Vector away = GetFeet() - posOnLadder; + + // we only want error in plane of ladder + float error = DotProduct( away, rightLadder ); + away.NormalizeInPlace(); + + const float tolerance = 5.0f + 0.25f * GetBot()->GetBodyInterface()->GetHullWidth(); + if ( error > tolerance ) + { + if ( DotProduct( away, rightLadder ) > 0.0f ) + { + playerButtons->PressLeftButton(); + } + else + { + playerButtons->PressRightButton(); + } + } + } + } + else + { + const float epsilon = 0.25f; + if ( NextBotPlayerMoveDirect.GetBool() ) + { + if ( goalDistance > epsilon ) + { + playerButtons->SetButtonScale( ahead, side ); + } + } + + if ( ahead > epsilon ) + { + playerButtons->PressForwardButton(); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::HorzArrow( m_player->GetAbsOrigin(), m_player->GetAbsOrigin() + 50.0f * Vector( forward.x, forward.y, 0.0f ), 15.0f, 0, 255, 0, 255, true, 0.1f ); + } + } + else if ( ahead < -epsilon ) + { + playerButtons->PressBackwardButton(); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::HorzArrow( m_player->GetAbsOrigin(), m_player->GetAbsOrigin() - 50.0f * Vector( forward.x, forward.y, 0.0f ), 15.0f, 255, 0, 0, 255, true, 0.1f ); + } + } + + if ( side <= -epsilon ) + { + playerButtons->PressLeftButton(); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::HorzArrow( m_player->GetAbsOrigin(), m_player->GetAbsOrigin() - 50.0f * Vector( right.x, right.y, 0.0f ), 15.0f, 255, 0, 255, 255, true, 0.1f ); + } + } + else if ( side >= epsilon ) + { + playerButtons->PressRightButton(); + + if ( GetBot()->IsDebugging( NEXTBOT_LOCOMOTION ) ) + { + NDebugOverlay::HorzArrow( m_player->GetAbsOrigin(), m_player->GetAbsOrigin() + 50.0f * Vector( right.x, right.y, 0.0f ), 15.0f, 0, 255, 255, 255, true, 0.1f ); + } + } + } + + if ( !IsRunning() ) + { + playerButtons->PressWalkButton(); + } +} + + +//---------------------------------------------------------------------------------------------------- +/** + * Move the bot to the precise given position immediately, + */ +void PlayerLocomotion::DriveTo( const Vector &pos ) +{ + BaseClass::DriveTo( pos ); + + Approach( pos ); +} + + +//---------------------------------------------------------------------------------------------------- +bool PlayerLocomotion::IsClimbPossible( INextBot *me, const CBaseEntity *obstacle ) const +{ + // don't jump unless we have to + const PathFollower *path = GetBot()->GetCurrentPath(); + if ( path ) + { + const float watchForClimbRange = 75.0f; + if ( !path->IsDiscontinuityAhead( GetBot(), Path::CLIMB_UP, watchForClimbRange ) ) + { + // we are not planning on climbing + + // always allow climbing over movable obstacles + if ( obstacle && !const_cast< CBaseEntity * >( obstacle )->IsWorld() ) + { + IPhysicsObject *physics = obstacle->VPhysicsGetObject(); + if ( physics && physics->IsMoveable() ) + { + // movable physics object - climb over it + return true; + } + } + + if ( !GetBot()->GetLocomotionInterface()->IsStuck() ) + { + // we're not stuck - don't try to jump up yet + return false; + } + } + } + + return true; +} + + +//---------------------------------------------------------------------------------------------------- +bool PlayerLocomotion::ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle ) +{ + if ( !IsClimbPossible( GetBot(), obstacle ) ) + { + return false; + } + + Jump(); + + m_isClimbingUpToLedge = true; + m_landingGoal = landingGoal; + m_hasLeftTheGround = false; + + return true; +} + + +//---------------------------------------------------------------------------------------------------- +void PlayerLocomotion::JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward ) +{ + Jump(); + + // face forward + GetBot()->GetBodyInterface()->AimHeadTowards( landingGoal, IBody::MANDATORY, 1.0f, NULL, "Looking forward while jumping a gap" ); + + m_isJumpingAcrossGap = true; + m_landingGoal = landingGoal; + m_hasLeftTheGround = false; +} + + +//---------------------------------------------------------------------------------------------------- +void PlayerLocomotion::Jump( void ) +{ + m_isJumping = true; + m_jumpTimer.Start( 0.5f ); + + INextBotPlayerInput *playerButtons = dynamic_cast< INextBotPlayerInput * >( GetBot() ); + if ( playerButtons ) + { + playerButtons->PressJumpButton(); + } +} + + +//---------------------------------------------------------------------------------------------------- +bool PlayerLocomotion::IsClimbingOrJumping( void ) const +{ + if ( !m_isJumping ) + return false; + + if ( m_jumpTimer.IsElapsed() && IsOnGround() ) + { + m_isJumping = false; + return false; + } + + return true; +} + + +//---------------------------------------------------------------------------------------------------- +bool PlayerLocomotion::IsClimbingUpToLedge( void ) const +{ + return m_isClimbingUpToLedge; +} + + +//---------------------------------------------------------------------------------------------------- +bool PlayerLocomotion::IsJumpingAcrossGap( void ) const +{ + return m_isJumpingAcrossGap; +} + + +//---------------------------------------------------------------------------------------------------- +/** + * Return true if standing on something + */ +bool PlayerLocomotion::IsOnGround( void ) const +{ + return (m_player->GetGroundEntity() != NULL); +} + + +//---------------------------------------------------------------------------------------------------- +/** + * Return the current ground entity or NULL if not on the ground + */ +CBaseEntity *PlayerLocomotion::GetGround( void ) const +{ + return m_player->GetGroundEntity(); +} + + +//---------------------------------------------------------------------------------------------------- +/** + * Surface normal of the ground we are in contact with + */ +const Vector &PlayerLocomotion::GetGroundNormal( void ) const +{ + static Vector up( 0, 0, 1.0f ); + return up; + + // TODO: Integrate movehelper_server for this: return m_player->GetGroundNormal(); +} + + +//---------------------------------------------------------------------------------------------------- +/** + * Climb the given ladder to the top and dismount + */ +void PlayerLocomotion::ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ) +{ + // look up and push forward +// Vector goal = GetBot()->GetPosition() + 100.0f * ( Vector( 0, 0, 1.0f ) - ladder->GetNormal() ); +// Approach( goal ); +// FaceTowards( goal ); + + m_ladderState = APPROACHING_ASCENDING_LADDER; + m_ladderInfo = ladder; + m_ladderDismountGoal = dismountGoal; +} + + +//---------------------------------------------------------------------------------------------------- +/** + * Descend the given ladder to the bottom and dismount + */ +void PlayerLocomotion::DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ) +{ + // look down and push forward +// Vector goal = GetBot()->GetPosition() + 100.0f * ( Vector( 0, 0, -1.0f ) - ladder->GetNormal() ); +// Approach( goal ); +// FaceTowards( goal ); + + m_ladderState = APPROACHING_DESCENDING_LADDER; + m_ladderInfo = ladder; + m_ladderDismountGoal = dismountGoal; +} + + +//---------------------------------------------------------------------------------------------------- +bool PlayerLocomotion::IsUsingLadder( void ) const +{ + return ( m_ladderState != NO_LADDER ); +} + + +//---------------------------------------------------------------------------------------------------- +/** + * Rotate body to face towards "target" + */ +void PlayerLocomotion::FaceTowards( const Vector &target ) +{ + // player body follows view direction + Vector look( target.x, target.y, GetBot()->GetEntity()->EyePosition().z ); + + GetBot()->GetBodyInterface()->AimHeadTowards( look, IBody::BORING, 0.1f, NULL, "Body facing" ); +} + + +//----------------------------------------------------------------------------------------------------- +/** +* Return position of "feet" - point below centroid of bot at feet level +*/ +const Vector &PlayerLocomotion::GetFeet( void ) const +{ + return m_player->GetAbsOrigin(); +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Return current world space velocity + */ +const Vector &PlayerLocomotion::GetVelocity( void ) const +{ + return m_player->GetAbsVelocity(); +} + + +//----------------------------------------------------------------------------------------------------- +float PlayerLocomotion::GetRunSpeed( void ) const +{ + return m_player->MaxSpeed(); +} + + +//----------------------------------------------------------------------------------------------------- +float PlayerLocomotion::GetWalkSpeed( void ) const +{ + return 0.5f * m_player->MaxSpeed(); +} + diff --git a/game/server/NextBot/Player/NextBotPlayerLocomotion.h b/game/server/NextBot/Player/NextBotPlayerLocomotion.h new file mode 100644 index 0000000..5da7260 --- /dev/null +++ b/game/server/NextBot/Player/NextBotPlayerLocomotion.h @@ -0,0 +1,223 @@ +// NextBotPlayerLocomotion.h +// Locomotor for CBasePlayer derived bots +// Author: Michael Booth, November 2005 +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _NEXT_BOT_PLAYER_LOCOMOTION_H_ +#define _NEXT_BOT_PLAYER_LOCOMOTION_H_ + +#include "NextBot.h" +#include "NextBotLocomotionInterface.h" +#include "Path/NextBotPathFollow.h" + +class CBasePlayer; + +//-------------------------------------------------------------------------------------------------- +/** + * Basic player locomotion implementation + */ +class PlayerLocomotion : public ILocomotion +{ +public: + DECLARE_CLASS( PlayerLocomotion, ILocomotion ); + + PlayerLocomotion( INextBot *bot ); + virtual ~PlayerLocomotion() { } + + virtual void Reset( void ); // reset to initial state + virtual void Update( void ); // update internal state + + virtual void Approach( const Vector &pos, float goalWeight = 1.0f ); // move directly towards the given position + virtual void DriveTo( const Vector &pos ); // Move the bot to the precise given position immediately, + + // + // ILocomotion modifiers + // + virtual bool ClimbUpToLedge( const Vector &landingGoal, const Vector &landingForward, const CBaseEntity *obstacle ); // initiate a jump to an adjacent high ledge, return false if climb can't start + virtual void JumpAcrossGap( const Vector &landingGoal, const Vector &landingForward ); // initiate a jump across an empty volume of space to far side + virtual void Jump( void ); // initiate a simple undirected jump in the air + virtual bool IsClimbingOrJumping( void ) const; // is jumping in any form + virtual bool IsClimbingUpToLedge( void ) const; // is climbing up to a high ledge + virtual bool IsJumpingAcrossGap( void ) const; // is jumping across a gap to the far side + + virtual void Run( void ); // set desired movement speed to running + virtual void Walk( void ); // set desired movement speed to walking + virtual void Stop( void ); // set desired movement speed to stopped + virtual bool IsRunning( void ) const; + virtual void SetDesiredSpeed( float speed ); // set desired speed for locomotor movement + virtual float GetDesiredSpeed( void ) const; // returns the current desired speed + virtual void SetMinimumSpeedLimit( float limit ); // speed cannot drop below this + virtual void SetMaximumSpeedLimit( float limit ); // speed cannot rise above this + + virtual bool IsOnGround( void ) const; // return true if standing on something + virtual CBaseEntity *GetGround( void ) const; // return the current ground entity or NULL if not on the ground + virtual const Vector &GetGroundNormal( void ) const; // surface normal of the ground we are in contact with + + virtual void ClimbLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ); // climb the given ladder to the top and dismount + virtual void DescendLadder( const CNavLadder *ladder, const CNavArea *dismountGoal ); // descend the given ladder to the bottom and dismount + virtual bool IsUsingLadder( void ) const; + virtual bool IsAscendingOrDescendingLadder( void ) const; // we are actually on the ladder right now, either climbing up or down + virtual bool IsAbleToAutoCenterOnLadder( void ) const; + + virtual void FaceTowards( const Vector &target ); // rotate body to face towards "target" + + virtual void SetDesiredLean( const QAngle &lean ) { } + virtual const QAngle &GetDesiredLean( void ) const { static QAngle junk; return junk; } + + // + // ILocomotion information + // + virtual const Vector &GetFeet( void ) const; // return position of "feet" - point below centroid of bot at feet level + + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + virtual float GetDeathDropHeight( void ) const; // distance at which we will die if we fall + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetWalkSpeed( void ) const; // get maximum walking speed + + virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor + virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor + + virtual const Vector &GetVelocity( void ) const; // return current world space velocity + +protected: + virtual void AdjustPosture( const Vector &moveGoal ); + +private: + CBasePlayer *m_player; // the player we are locomoting + + mutable bool m_isJumping; + CountdownTimer m_jumpTimer; + + bool m_isClimbingUpToLedge; + bool m_isJumpingAcrossGap; + Vector m_landingGoal; + bool m_hasLeftTheGround; + + float m_desiredSpeed; + float m_minSpeedLimit; + float m_maxSpeedLimit; + + bool TraverseLadder( void ); // when climbing/descending a ladder + + enum LadderState + { + NO_LADDER, // not using a ladder + APPROACHING_ASCENDING_LADDER, + APPROACHING_DESCENDING_LADDER, + ASCENDING_LADDER, + DESCENDING_LADDER, + DISMOUNTING_LADDER_TOP, + DISMOUNTING_LADDER_BOTTOM, + }; + + LadderState m_ladderState; + LadderState ApproachAscendingLadder( void ); + LadderState ApproachDescendingLadder( void ); + LadderState AscendLadder( void ); + LadderState DescendLadder( void ); + LadderState DismountLadderTop( void ); + LadderState DismountLadderBottom( void ); + + const CNavLadder *m_ladderInfo; + const CNavArea *m_ladderDismountGoal; + CountdownTimer m_ladderTimer; // a "give up" timer if things go awry + + bool IsClimbPossible( INextBot *me, const CBaseEntity *obstacle ) const; +}; + + +inline float PlayerLocomotion::GetStepHeight( void ) const +{ + return 18.0f; +} + + +inline float PlayerLocomotion::GetMaxJumpHeight( void ) const +{ + return 57.0f; +} + + +inline float PlayerLocomotion::GetDeathDropHeight( void ) const +{ + return 200.0f; +} + + +inline float PlayerLocomotion::GetMaxAcceleration( void ) const +{ + return 100.0f; +} + +inline float PlayerLocomotion::GetMaxDeceleration( void ) const +{ + return 200.0f; +} + +inline void PlayerLocomotion::Run( void ) +{ + m_desiredSpeed = GetRunSpeed(); +} + +inline void PlayerLocomotion::Walk( void ) +{ + m_desiredSpeed = GetWalkSpeed(); +} + +inline void PlayerLocomotion::Stop( void ) +{ + m_desiredSpeed = 0.0f; +} + +inline bool PlayerLocomotion::IsRunning( void ) const +{ + return true; +} + +inline void PlayerLocomotion::SetDesiredSpeed( float speed ) +{ + m_desiredSpeed = speed; +} + +inline float PlayerLocomotion::GetDesiredSpeed( void ) const +{ + return clamp( m_desiredSpeed, m_minSpeedLimit, m_maxSpeedLimit ); +} + +inline void PlayerLocomotion::SetMinimumSpeedLimit( float limit ) +{ + m_minSpeedLimit = limit; +} + +inline void PlayerLocomotion::SetMaximumSpeedLimit( float limit ) +{ + m_maxSpeedLimit = limit; +} + +inline bool PlayerLocomotion::IsAbleToAutoCenterOnLadder( void ) const +{ + return IsUsingLadder() && (m_ladderState == ASCENDING_LADDER || m_ladderState == DESCENDING_LADDER); +} + +inline bool PlayerLocomotion::IsAscendingOrDescendingLadder( void ) const +{ + switch( m_ladderState ) + { + case ASCENDING_LADDER: + case DESCENDING_LADDER: + case DISMOUNTING_LADDER_TOP: + case DISMOUNTING_LADDER_BOTTOM: + return true; + default: + // Explicitly handle the default so that clang knows not to warn us. + // warning: enumeration values 'NO_LADDER', 'APPROACHING_ASCENDING_LADDER', and 'APPROACHING_DESCENDING_LADDER' not handled in switch [-Wswitch-enum] + break; + } + + return false; +} + + +#endif // _NEXT_BOT_PLAYER_LOCOMOTION_H_ diff --git a/game/server/NextBot/simple_bot.cpp b/game/server/NextBot/simple_bot.cpp new file mode 100644 index 0000000..dbae239 --- /dev/null +++ b/game/server/NextBot/simple_bot.cpp @@ -0,0 +1,199 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// simple_bot.cpp +// A simple bot +// Michael Booth, February 2009 + +#include "cbase.h" +#include "simple_bot.h" +#include "nav_mesh.h" + + +//----------------------------------------------------------------------------------------------------- +// Command to add a Simple Bot where your crosshairs are aiming +//----------------------------------------------------------------------------------------------------- +CON_COMMAND_F( simple_bot_add, "Add a simple bot.", FCVAR_CHEAT ) +{ + CBasePlayer *player = UTIL_GetCommandClient(); + if ( !player ) + { + return; + } + + Vector forward; + player->EyeVectors( &forward ); + + trace_t result; + UTIL_TraceLine( player->EyePosition(), player->EyePosition() + 999999.9f * forward, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, player, COLLISION_GROUP_NONE, &result ); + if ( !result.DidHit() ) + { + return; + } + + CSimpleBot *bot = static_cast< CSimpleBot * >( CreateEntityByName( "simple_bot" ) ); + if ( bot ) + { + Vector forward = player->GetAbsOrigin() - result.endpos; + forward.z = 0.0f; + forward.NormalizeInPlace(); + + QAngle angles; + VectorAngles( forward, angles ); + + bot->SetAbsAngles( angles ); + bot->SetAbsOrigin( result.endpos + Vector( 0, 0, 10.0f ) ); + + DispatchSpawn( bot ); + } +} + + +//----------------------------------------------------------------------------------------------------- +// The Simple Bot +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( simple_bot, CSimpleBot ); + +#ifndef TF_DLL +PRECACHE_REGISTER( simple_bot ); +#endif + + +//----------------------------------------------------------------------------------------------------- +CSimpleBot::CSimpleBot() +{ + ALLOCATE_INTENTION_INTERFACE( CSimpleBot ); + + m_locomotor = new NextBotGroundLocomotion( this ); +} + + +//----------------------------------------------------------------------------------------------------- +CSimpleBot::~CSimpleBot() +{ + DEALLOCATE_INTENTION_INTERFACE; + + if ( m_locomotor ) + delete m_locomotor; +} + +//----------------------------------------------------------------------------------------------------- +void CSimpleBot::Precache() +{ + BaseClass::Precache(); + +#ifndef DOTA_DLL + PrecacheModel( "models/humans/group01/female_01.mdl" ); +#endif +} + + +//----------------------------------------------------------------------------------------------------- +void CSimpleBot::Spawn( void ) +{ + BaseClass::Spawn(); + +#ifndef DOTA_DLL + SetModel( "models/humans/group01/female_01.mdl" ); +#endif +} + + +//--------------------------------------------------------------------------------------------- +// The Simple Bot behaviors +//--------------------------------------------------------------------------------------------- +/** + * For use with TheNavMesh->ForAllAreas() + * Find the Nth area in the sequence + */ +class SelectNthAreaFunctor +{ +public: + SelectNthAreaFunctor( int count ) + { + m_count = count; + m_area = NULL; + } + + bool operator() ( CNavArea *area ) + { + m_area = area; + return ( m_count-- > 0 ); + } + + int m_count; + CNavArea *m_area; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * This action causes the bot to pick a random nav area in the mesh and move to it, then + * pick another, etc. + * Actions usually each have their own .cpp/.h file and are organized into folders since there + * are often many of them. For this example, we're keeping everything to a single .cpp/.h file. + */ +class CSimpleBotRoam : public Action< CSimpleBot > +{ +public: + //---------------------------------------------------------------------------------- + // OnStart is called once when the Action first becomes active + virtual ActionResult< CSimpleBot > OnStart( CSimpleBot *me, Action< CSimpleBot > *priorAction ) + { + // smooth out the bot's path following by moving toward a point farther down the path + m_path.SetMinLookAheadDistance( 300.0f ); + + return Continue(); + } + + + //---------------------------------------------------------------------------------- + // Update is called repeatedly (usually once per server frame) while the Action is active + virtual ActionResult< CSimpleBot > Update( CSimpleBot *me, float interval ) + { + if ( m_path.IsValid() && !m_timer.IsElapsed() ) + { + // PathFollower::Update() moves the bot along the path using the bot's ILocomotion and IBody interfaces + m_path.Update( me ); + } + else + { + SelectNthAreaFunctor pick( RandomInt( 0, TheNavMesh->GetNavAreaCount() - 1 ) ); + TheNavMesh->ForAllAreas( pick ); + + if ( pick.m_area ) + { + CSimpleBotPathCost cost( me ); + m_path.Compute( me, pick.m_area->GetCenter(), cost ); + } + + // follow this path for a random duration (or until we reach the end) + m_timer.Start( RandomFloat( 5.0f, 10.0f ) ); + } + + return Continue(); + } + + + //---------------------------------------------------------------------------------- + // this is an event handler - many more are available (see declaration of Action< Actor > in NextBotBehavior.h) + virtual EventDesiredResult< CSimpleBot > OnStuck( CSimpleBot *me ) + { + // we are stuck trying to follow the current path - invalidate it so a new one is chosen + m_path.Invalidate(); + + return TryContinue(); + } + + + virtual const char *GetName( void ) const { return "Roam"; } // return name of this action + +private: + PathFollower m_path; + CountdownTimer m_timer; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * Instantiate the bot's Intention interface and start the initial Action (CSimpleBotRoam in this case) + */ +IMPLEMENT_INTENTION_INTERFACE( CSimpleBot, CSimpleBotRoam ) diff --git a/game/server/NextBot/simple_bot.h b/game/server/NextBot/simple_bot.h new file mode 100644 index 0000000..b679b6c --- /dev/null +++ b/game/server/NextBot/simple_bot.h @@ -0,0 +1,116 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// simple_bot.h +// A mininal example of a NextBotCombatCharacter (ie: non-player) bot +// Michael Booth, February 2009 + +#ifndef SIMPLE_BOT_H +#define SIMPLE_BOT_H + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "Path/NextBotPathFollow.h" + + +//---------------------------------------------------------------------------- +/** + * A Simple Bot + */ +class CSimpleBot : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CSimpleBot, NextBotCombatCharacter ); + + CSimpleBot(); + virtual ~CSimpleBot(); + + virtual void Precache(); + virtual void Spawn( void ); + + // INextBot + DECLARE_INTENTION_INTERFACE( CSimpleBot ) + virtual NextBotGroundLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + +private: + NextBotGroundLocomotion *m_locomotor; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor used with the A* algorithm of NavAreaBuildPath() to determine the "cost" of moving from one area to another. + * "Cost" is generally the weighted distance between the centers of the areas. If you want the bot + * to avoid an area/ladder/elevator, increase the cost. If you want to disallow an area/ladder/elevator, return -1. + */ +class CSimpleBotPathCost : public IPathCost +{ +public: + CSimpleBotPathCost( CSimpleBot *me ) + { + m_me = me; + } + + // return the cost (weighted distance between) of moving from "fromArea" to "area", or -1 if the move is not allowed + virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const + { + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + { + // our locomotor says we can't move here + return -1.0f; + } + + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + // optimization to avoid recomputing length + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area ); + if ( deltaZ >= m_me->GetLocomotionInterface()->GetStepHeight() ) + { + if ( deltaZ >= m_me->GetLocomotionInterface()->GetMaxJumpHeight() ) + { + // too high to reach + return -1.0f; + } + + // jumping is slower than flat ground + const float jumpPenalty = 5.0f; + cost += jumpPenalty * dist; + } + else if ( deltaZ < -m_me->GetLocomotionInterface()->GetDeathDropHeight() ) + { + // too far to drop + return -1.0f; + } + + return cost; + } + } + + CSimpleBot *m_me; +}; + + +#endif // SIMPLE_BOT_H |