diff options
Diffstat (limited to 'game/shared/cstrike/bot')
| -rw-r--r-- | game/shared/cstrike/bot/bot.cpp | 109 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot.h | 1053 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot_constants.h | 40 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot_hide.cpp | 490 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot_manager.cpp | 402 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot_manager.h | 195 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot_profile.cpp | 704 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot_profile.h | 251 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot_util.cpp | 604 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/bot_util.h | 167 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/improv_locomotor.h | 57 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/nav_path.cpp | 1208 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/nav_path.h | 246 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/shared_util.cpp | 207 | ||||
| -rw-r--r-- | game/shared/cstrike/bot/shared_util.h | 83 |
15 files changed, 5816 insertions, 0 deletions
diff --git a/game/shared/cstrike/bot/bot.cpp b/game/shared/cstrike/bot/bot.cpp new file mode 100644 index 0000000..4b17ddf --- /dev/null +++ b/game/shared/cstrike/bot/bot.cpp @@ -0,0 +1,109 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), Leon Hartwig, 2003 + +#include "cbase.h" +#include "basegrenade_shared.h" + +#include "bot.h" +#include "bot_util.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +/// @todo Remove this nasty hack - CreateFakeClient() calls CBot::Spawn, which needs the profile and team +const BotProfile *g_botInitProfile = NULL; +int g_botInitTeam = 0; + +// +// NOTE: Because CBot had to be templatized, the code was moved into bot.h +// + + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- + +ActiveGrenade::ActiveGrenade( CBaseGrenade *grenadeEntity ) +{ + m_entity = grenadeEntity; + m_detonationPosition = grenadeEntity->GetAbsOrigin(); + m_dieTimestamp = 0.0f; + m_radius = HEGrenadeRadius; + + m_isSmoke = FStrEq( grenadeEntity->GetClassname(), "smokegrenade_projectile" ); + if ( m_isSmoke ) + { + m_radius = SmokeGrenadeRadius; + } + + m_isFlashbang = FStrEq( grenadeEntity->GetClassname(), "flashbang_projectile" ); + if ( m_isFlashbang ) + { + m_radius = FlashbangGrenadeRadius; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called when the grenade in the world goes away + */ +void ActiveGrenade::OnEntityGone( void ) +{ + if (m_isSmoke) + { + // smoke lingers after grenade is gone + const float smokeLingerTime = 4.0f; + m_dieTimestamp = gpGlobals->curtime + smokeLingerTime; + } + + m_entity = NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +void ActiveGrenade::Update( void ) +{ + if (m_entity != NULL) + { + m_detonationPosition = m_entity->GetAbsOrigin(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this grenade is valid + */ +bool ActiveGrenade::IsValid( void ) const +{ + if ( m_isSmoke ) + { + if ( m_entity == NULL && gpGlobals->curtime > m_dieTimestamp ) + { + return false; + } + } + else + { + if ( m_entity == NULL ) + { + return false; + } + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +const Vector &ActiveGrenade::GetPosition( void ) const +{ + // smoke grenades can vanish before the smoke itself does - refer to the detonation position + if (m_entity == NULL) + return GetDetonationPosition(); + + return m_entity->GetAbsOrigin(); +} + diff --git a/game/shared/cstrike/bot/bot.h b/game/shared/cstrike/bot/bot.h new file mode 100644 index 0000000..3067c2c --- /dev/null +++ b/game/shared/cstrike/bot/bot.h @@ -0,0 +1,1053 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// +// Author: Michael S. Booth ([email protected]), 2003 +// +// NOTE: The CS Bot code uses Doxygen-style comments. If you run Doxygen over this code, it will +// auto-generate documentation. Visit www.doxygen.org to download the system for free. +// + +#ifndef BOT_H +#define BOT_H + +#include "cbase.h" +#include "in_buttons.h" +#include "movehelper_server.h" +#include "mathlib/mathlib.h" + +#include "bot_manager.h" +#include "bot_util.h" +#include "bot_constants.h" +#include "nav_mesh.h" +#include "gameinterface.h" +#include "weapon_csbase.h" +#include "shared_util.h" +#include "util.h" +#include "shareddefs.h" + +#include "tier0/vprof.h" + +class BotProfile; + + +extern bool AreBotsAllowed(); + + +//-------------------------------------------------------------------------------------------------------- +// BOTPORT: Convert everything to assume "origin" means "feet" + +// +// Utility function to get "centroid" or center of player or player equivalent +// +inline Vector GetCentroid( const CBaseEntity *player ) +{ + Vector centroid = player->GetAbsOrigin(); + + const Vector &mins = player->WorldAlignMins(); + const Vector &maxs = player->WorldAlignMaxs(); + + centroid.z += (maxs.z - mins.z)/2.0f; + + //centroid.z += HalfHumanHeight; + + return centroid; +} + + +CBasePlayer* ClientPutInServerOverride_Bot( edict_t *pEdict, const char *playername ); + +/// @todo Remove this nasty hack - CreateFakeClient() calls CBot::Spawn, which needs the profile +extern const BotProfile *g_botInitProfile; +extern int g_botInitTeam; +extern int g_nClientPutInServerOverrides; + +//-------------------------------------------------------------------------------------------------------- +template < class T > T * CreateBot( const BotProfile *profile, int team ) +{ + if ( !AreBotsAllowed() ) + return NULL; + + if ( UTIL_ClientsInGame() >= gpGlobals->maxClients ) + { + CONSOLE_ECHO( "Unable to create bot: Server is full (%d/%d clients).\n", UTIL_ClientsInGame(), gpGlobals->maxClients ); + return NULL; + } + + // set the bot's name + char botName[64]; + UTIL_ConstructBotNetName( botName, 64, profile ); + + // This is a backdoor we use so when the engine calls ClientPutInServer (from CreateFakeClient), + // expecting the game to make an entity for the fake client, we can make our special bot class + // instead of a CCSPlayer. + g_nClientPutInServerOverrides = 0; + ClientPutInServerOverride( ClientPutInServerOverride_Bot ); + + // get an edict for the bot + // NOTE: This will ultimately invoke CBot::Spawn(), so set the profile now + g_botInitProfile = profile; + g_botInitTeam = team; + edict_t *botEdict = engine->CreateFakeClient( botName ); + + ClientPutInServerOverride( NULL ); + Assert( g_nClientPutInServerOverrides == 1 ); + + + if ( botEdict == NULL ) + { + CONSOLE_ECHO( "Unable to create bot: CreateFakeClient() returned null.\n" ); + 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( "Could not allocate and bind entity to bot edict.\n" ); + return NULL; + } + + bot->ClearFlags(); + bot->AddFlag( FL_CLIENT | FL_FAKECLIENT ); + + return bot; +} + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- +/** + * The base bot class from which bots for specific games are derived + * A template is needed here because the CBot class must be derived from CBasePlayer, + * but also may need to be derived from a more specific player class, such as CCSPlayer + */ +template < class PlayerType > +class CBot : public PlayerType +{ +public: + DECLARE_CLASS( CBot, PlayerType ); + + CBot( void ); ///< constructor initializes all values to zero + virtual ~CBot(); + virtual bool Initialize( const BotProfile *profile, int team ); ///< (EXTEND) prepare bot for action + + unsigned int GetID( void ) const { return m_id; } ///< return bot's unique ID + + virtual bool IsBot( void ) const { return true; } + virtual bool IsNetClient( void ) const { return false; } // Bots should return FALSE for this, they can't receive NET messages + + virtual void Spawn( void ); ///< (EXTEND) spawn the bot into the game + + virtual void Upkeep( void ) = 0; ///< lightweight maintenance, invoked frequently + virtual void Update( void ) = 0; ///< heavyweight algorithms, invoked less often + + + virtual void Run( void ); + virtual void Walk( void ); + virtual bool IsRunning( void ) const { return m_isRunning; } + + virtual void Crouch( void ); + virtual void StandUp( void ); + bool IsCrouching( void ) const { return m_isCrouching; } + + void PushPostureContext( void ); ///< push the current posture context onto the top of the stack + void PopPostureContext( void ); ///< restore the posture context to the next context on the stack + + virtual void MoveForward( void ); + virtual void MoveBackward( void ); + virtual void StrafeLeft( void ); + virtual void StrafeRight( void ); + + #define MUST_JUMP true + virtual bool Jump( bool mustJump = false ); ///< returns true if jump was started + bool IsJumping( void ); ///< returns true if we are in the midst of a jump + float GetJumpTimestamp( void ) const { return m_jumpTimestamp; } ///< return time last jump began + + virtual void ClearMovement( void ); ///< zero any MoveForward(), Jump(), etc + + const Vector &GetViewVector( void ); ///< return the actual view direction + + + //------------------------------------------------------------------------------------ + // Weapon interface + // + virtual void UseEnvironment( void ); + virtual void PrimaryAttack( void ); + virtual void ClearPrimaryAttack( void ); + virtual void TogglePrimaryAttack( void ); + virtual void SecondaryAttack( void ); + virtual void Reload( void ); + + float GetActiveWeaponAmmoRatio( void ) const; ///< returns ratio of ammo left to max ammo (1 = full clip, 0 = empty) + bool IsActiveWeaponClipEmpty( void ) const; ///< return true if active weapon has any empty clip + bool IsActiveWeaponOutOfAmmo( void ) const; ///< return true if active weapon has no ammo at all + bool IsActiveWeaponRecoilHigh( void ) const; ///< return true if active weapon's bullet spray has become large and inaccurate + bool IsUsingScope( void ); ///< return true if looking thru weapon's scope + + + //------------------------------------------------------------------------------------ + // Event hooks + // + + /// invoked when injured by something (EXTEND) - returns the amount of damage inflicted + virtual int OnTakeDamage( const CTakeDamageInfo &info ) + { + return PlayerType::OnTakeDamage( info ); + } + + /// invoked when killed (EXTEND) + virtual void Event_Killed( const CTakeDamageInfo &info ) + { + PlayerType::Event_Killed( info ); + } + + bool IsEnemy( CBaseEntity *ent ) const; ///< returns TRUE if given entity is our enemy + int GetEnemiesRemaining( void ) const; ///< return number of enemies left alive + int GetFriendsRemaining( void ) const; ///< return number of friends left alive + + bool IsPlayerFacingMe( CBasePlayer *enemy ) const; ///< return true if player is facing towards us + bool IsPlayerLookingAtMe( CBasePlayer *enemy, float cosTolerance = 0.9f ) const; ///< returns true if other player is pointing right at us + bool IsLookingAtPosition( const Vector &pos, float angleTolerance = 20.0f ) const; ///< returns true if looking (roughly) at given position + + bool IsLocalPlayerWatchingMe( void ) const; ///< return true if local player is observing this bot + + void PrintIfWatched( PRINTF_FORMAT_STRING const char *format, ... ) const; ///< output message to console if we are being watched by the local player + + virtual void UpdatePlayer( void ); ///< update player physics, movement, weapon firing commands, etc + virtual void BuildUserCmd( CUserCmd& cmd, const QAngle& viewangles, float forwardmove, float sidemove, float upmove, int buttons, byte impulse ); + virtual void SetModel( const char *modelName ); + + int Save( CSave &save ) const { return 0; } + int Restore( CRestore &restore ) const { return 0; } + virtual void Think( void ) { } + + const BotProfile *GetProfile( void ) const { return m_profile; } ///< return our personality profile + + virtual bool ClientCommand( const CCommand &args ); ///< Do a "client command" - useful for invoking menu choices, etc. + virtual int Cmd_Argc( void ); ///< Returns the number of tokens in the command string + virtual char *Cmd_Argv( int argc ); ///< Retrieves a specified token + +private: + CUtlVector< char * > m_args; + +protected: + const BotProfile *m_profile; ///< the "personality" profile of this bot + +private: + friend class CBotManager; + + unsigned int m_id; ///< unique bot ID + + CUserCmd m_userCmd; + bool m_isRunning; ///< run/walk mode + bool m_isCrouching; ///< true if crouching (ducking) + float m_forwardSpeed; + float m_strafeSpeed; + float m_verticalSpeed; + int m_buttonFlags; ///< bitfield of movement buttons + + float m_jumpTimestamp; ///< time when we last began a jump + + Vector m_viewForward; ///< forward view direction (only valid when GetViewVector() is used) + + /// the PostureContext represents the current settings of walking and crouching + struct PostureContext + { + bool isRunning; + bool isCrouching; + }; + enum { MAX_POSTURE_STACK = 8 }; + PostureContext m_postureStack[ MAX_POSTURE_STACK ]; + int m_postureStackIndex; ///< index of top of stack + + void ResetCommand( void ); + //byte ThrottledMsec( void ) const; + +protected: + virtual float GetMoveSpeed( void ); ///< returns current movement speed (for walk/run) +}; + + +//----------------------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------------------------------------- +// +// Inlines +// + +//-------------------------------------------------------------------------------------------------------------- +template < class T > +inline void CBot<T>::SetModel( const char *modelName ) +{ + BaseClass::SetModel( modelName ); +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline float CBot<T>::GetMoveSpeed( void ) +{ + return this->MaxSpeed(); +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline void CBot<T>::Run( void ) +{ + m_isRunning = true; +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline void CBot<T>::Walk( void ) +{ + m_isRunning = false; +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline bool CBot<T>::IsActiveWeaponRecoilHigh( void ) const +{ + const QAngle &angles = const_cast< CBot<T> * >( this )->GetPunchAngle(); + const float highRecoil = -1.5f; + return (angles.x < highRecoil); +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline void CBot<T>::PushPostureContext( void ) +{ + if (m_postureStackIndex == MAX_POSTURE_STACK) + { + PrintIfWatched( "PushPostureContext() overflow error!\n" ); + return; + } + + m_postureStack[ m_postureStackIndex ].isRunning = m_isRunning; + m_postureStack[ m_postureStackIndex ].isCrouching = m_isCrouching; + ++m_postureStackIndex; +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline void CBot<T>::PopPostureContext( void ) +{ + if (m_postureStackIndex == 0) + { + PrintIfWatched( "PopPostureContext() underflow error!\n" ); + m_isRunning = true; + m_isCrouching = false; + return; + } + + --m_postureStackIndex; + m_isRunning = m_postureStack[ m_postureStackIndex ].isRunning; + m_isCrouching = m_postureStack[ m_postureStackIndex ].isCrouching; +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline bool CBot<T>::IsPlayerFacingMe( CBasePlayer *other ) const +{ + Vector toOther = other->GetAbsOrigin() - this->GetAbsOrigin(); + + Vector otherForward; + AngleVectors( other->EyeAngles() + other->GetPunchAngle(), &otherForward ); + + if (DotProduct( otherForward, toOther ) < 0.0f) + return true; + + return false; +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline bool CBot<T>::IsPlayerLookingAtMe( CBasePlayer *other, float cosTolerance ) const +{ + Vector toOther = other->GetAbsOrigin() - this->GetAbsOrigin(); + toOther.NormalizeInPlace(); + + Vector otherForward; + AngleVectors( other->EyeAngles() + other->GetPunchAngle(), &otherForward ); + + // other player must be pointing nearly right at us to be "looking at" us + if (DotProduct( otherForward, toOther ) < -cosTolerance) + return true; + + return false; +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline const Vector &CBot<T>::GetViewVector( void ) +{ + AngleVectors( this->EyeAngles() + this->GetPunchAngle(), &m_viewForward ); + return m_viewForward; +} + +//----------------------------------------------------------------------------------------------------------- +template < class T > +inline bool CBot<T>::IsLookingAtPosition( const Vector &pos, float angleTolerance ) const +{ + // forced to do this since many methods in CBaseEntity are not const, but should be + CBot< T > *me = const_cast< CBot< T > * >( this ); + + Vector to = pos - me->EyePosition(); + + QAngle idealAngles; + VectorAngles( to, idealAngles ); + + QAngle viewAngles = me->EyeAngles(); + + float deltaYaw = AngleNormalize( idealAngles.y - viewAngles.y ); + float deltaPitch = AngleNormalize( idealAngles.x - viewAngles.x ); + + if (fabs( deltaYaw ) < angleTolerance && abs( deltaPitch ) < angleTolerance) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline CBot< PlayerType >::CBot( void ) +{ + // the profile will be attached after this instance is constructed + m_profile = NULL; + + // assign this bot a unique ID + static unsigned int nextID = 1; + + // wraparound (highly unlikely) + if (nextID == 0) + ++nextID; + + m_id = nextID; + ++nextID; + + m_postureStackIndex = 0; +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline CBot< PlayerType >::~CBot( void ) +{ +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Prepare bot for action + */ +template < class PlayerType > +inline bool CBot< PlayerType >::Initialize( const BotProfile *profile, int team ) +{ + m_profile = profile; + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::Spawn( void ) +{ + // initialize the bot (thus setting its profile) + if (m_profile == NULL) + Initialize( g_botInitProfile, g_botInitTeam ); + + // let the base class set some things up + PlayerType::Spawn(); + + // Make sure everyone knows we are a bot + this->AddFlag( FL_CLIENT | FL_FAKECLIENT ); + + // Bots use their own thinking mechanism + this->SetThink( NULL ); + + m_isRunning = true; + m_isCrouching = false; + m_postureStackIndex = 0; + + m_jumpTimestamp = 0.0f; + + // Command interface variable initialization + ResetCommand(); +} + + +/* +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::BotThink( void ) +{ +float g_flBotFullThinkInterval = 1.0 / 15.0; // full AI at lower frequency (was 10 in GoldSrc) + + + Upkeep(); + + if (gpGlobals->curtime >= m_flNextFullBotThink) + { + m_flNextFullBotThink = gpGlobals->curtime + g_flBotFullThinkInterval; + + ResetCommand(); + Update(); + } + + UpdatePlayer(); +} +*/ + + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::MoveForward( void ) +{ + m_forwardSpeed = GetMoveSpeed(); + SETBITS( m_buttonFlags, IN_FORWARD ); + + // make mutually exclusive + CLEARBITS( m_buttonFlags, IN_BACK ); +} + + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::MoveBackward( void ) +{ + m_forwardSpeed = -GetMoveSpeed(); + SETBITS( m_buttonFlags, IN_BACK ); + + // make mutually exclusive + CLEARBITS( m_buttonFlags, IN_FORWARD ); +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::StrafeLeft( void ) +{ + m_strafeSpeed = -GetMoveSpeed(); + SETBITS( m_buttonFlags, IN_MOVELEFT ); + + // make mutually exclusive + CLEARBITS( m_buttonFlags, IN_MOVERIGHT ); +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::StrafeRight( void ) +{ + m_strafeSpeed = GetMoveSpeed(); + SETBITS( m_buttonFlags, IN_MOVERIGHT ); + + // make mutually exclusive + CLEARBITS( m_buttonFlags, IN_MOVELEFT ); +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline bool CBot< PlayerType >::Jump( bool mustJump ) +{ + if (IsJumping() || IsCrouching()) + return false; + + if (!mustJump) + { + const float minJumpInterval = 0.9f; // 1.5f; + if (gpGlobals->curtime - m_jumpTimestamp < minJumpInterval) + return false; + } + + // still need sanity check for jumping frequency + const float sanityInterval = 0.3f; + if (gpGlobals->curtime - m_jumpTimestamp < sanityInterval) + return false; + + // jump + SETBITS( m_buttonFlags, IN_JUMP ); + m_jumpTimestamp = gpGlobals->curtime; + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Zero any MoveForward(), Jump(), etc + */ +template < class PlayerType > +void CBot< PlayerType >::ClearMovement( void ) +{ + m_forwardSpeed = 0.0; + m_strafeSpeed = 0.0; + m_verticalSpeed = 100.0; // stay at the top of water, so we don't drown. TODO: swim logic + m_buttonFlags &= ~(IN_FORWARD | IN_BACK | IN_LEFT | IN_RIGHT | IN_JUMP); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we are in the midst of a jump + */ +template < class PlayerType > +inline bool CBot< PlayerType >::IsJumping( void ) +{ + // if long time after last jump, we can't be jumping + if (gpGlobals->curtime - m_jumpTimestamp > 3.0f) + return false; + + // if we just jumped, we're still jumping + if (gpGlobals->curtime - m_jumpTimestamp < 0.9f) // 1.0f + return true; + + // a little after our jump, we're jumping until we hit the ground + if (FBitSet( this->GetFlags(), FL_ONGROUND )) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::Crouch( void ) +{ + m_isCrouching = true; +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::StandUp( void ) +{ + m_isCrouching = false; +} + + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::UseEnvironment( void ) +{ + SETBITS( m_buttonFlags, IN_USE ); +} + + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::PrimaryAttack( void ) +{ + SETBITS( m_buttonFlags, IN_ATTACK ); +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::ClearPrimaryAttack( void ) +{ + CLEARBITS( m_buttonFlags, IN_ATTACK ); +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::TogglePrimaryAttack( void ) +{ + if (FBitSet( m_buttonFlags, IN_ATTACK )) + { + CLEARBITS( m_buttonFlags, IN_ATTACK ); + } + else + { + SETBITS( m_buttonFlags, IN_ATTACK ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::SecondaryAttack( void ) +{ + SETBITS( m_buttonFlags, IN_ATTACK2 ); +} + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::Reload( void ) +{ + SETBITS( m_buttonFlags, IN_RELOAD ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns ratio of ammo left to max ammo (1 = full clip, 0 = empty) + */ +template < class PlayerType > +inline float CBot< PlayerType >::GetActiveWeaponAmmoRatio( void ) const +{ + CWeaponCSBase *weapon = this->GetActiveCSWeapon(); + + if (weapon == NULL) + return 0.0f; + + // weapons with no ammo are always full + if (weapon->Clip1() < 0) + return 1.0f; + + return (float)weapon->Clip1() / (float)weapon->GetMaxClip1(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if active weapon has an empty clip + */ +template < class PlayerType > +inline bool CBot< PlayerType >::IsActiveWeaponClipEmpty( void ) const +{ + CWeaponCSBase *gun = this->GetActiveCSWeapon(); + + if (gun && gun->Clip1() == 0) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if active weapon has no ammo at all + */ +template < class PlayerType > +inline bool CBot< PlayerType >::IsActiveWeaponOutOfAmmo( void ) const +{ + CWeaponCSBase *weapon = this->GetActiveCSWeapon(); + + if (weapon == NULL) + return true; + + return !weapon->HasAnyAmmo(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if looking thru weapon's scope + */ +template < class PlayerType > +inline bool CBot< PlayerType >::IsUsingScope( void ) +{ + // if our field of view is less than 90, we're looking thru a scope (maybe only true for CS...) + if (this->GetFOV() < this->GetDefaultFOV()) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Fill in a CUserCmd with our data + */ +template < class PlayerType > +inline void CBot< PlayerType >::BuildUserCmd( CUserCmd& cmd, const QAngle& viewangles, float forwardmove, float sidemove, float upmove, int buttons, byte impulse ) +{ + Q_memset( &cmd, 0, sizeof( cmd ) ); + 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 ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update player physics, movement, weapon firing commands, etc + */ +template < class PlayerType > +inline void CBot< PlayerType >::UpdatePlayer( void ) +{ + if (m_isCrouching) + { + SETBITS( m_buttonFlags, IN_DUCK ); + } + else if (!m_isRunning) + { + SETBITS( m_buttonFlags, IN_SPEED ); + } + + if ( this->IsEFlagSet(EFL_BOT_FROZEN) ) + { + m_buttonFlags = 0; // Freeze. + m_forwardSpeed = 0; + m_strafeSpeed = 0; + m_verticalSpeed = 0; + } + + // Fill in a CUserCmd with our data + this->BuildUserCmd( m_userCmd, this->EyeAngles(), m_forwardSpeed, m_strafeSpeed, m_verticalSpeed, m_buttonFlags, 0 ); + + // Save off the CUserCmd to execute later + this->ProcessUsercmds( &m_userCmd, 1, 1, 0, false ); +} + + +//-------------------------------------------------------------------------------------------------------------- +template < class PlayerType > +inline void CBot< PlayerType >::ResetCommand( void ) +{ + m_forwardSpeed = 0.0; + m_strafeSpeed = 0.0; + m_verticalSpeed = 100.0; // stay at the top of water, so we don't drown. TODO: swim logic + m_buttonFlags = 0; +} + + +//-------------------------------------------------------------------------------------------------------------- +/* +template < class PlayerType > +inline byte CBot< PlayerType >::ThrottledMsec( void ) const +{ + int iNewMsec; + + // Estimate Msec to use for this command based on time passed from the previous command + iNewMsec = (int)( (gpGlobals->curtime - m_flPreviousCommandTime) * 1000 ); + if (iNewMsec > 255) // Doh, bots are going to be slower than they should if this happens. + iNewMsec = 255; // Upgrade that CPU or use less bots! + + return (byte)iNewMsec; +} +*/ + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do a "client command" - useful for invoking menu choices, etc. + */ +template < class PlayerType > +inline bool CBot< PlayerType >::ClientCommand( const CCommand &args ) +{ + // Remove old args + int i; + for ( i=0; i<m_args.Count(); ++i ) + { + delete[] m_args[i]; + } + m_args.RemoveAll(); + + // parse individual args + const char *cmd = args.GetCommandString(); + while (1) + { + // skip whitespace up to a /n + while (*cmd && *cmd <= ' ' && *cmd != '\n') + { + cmd++; + } + + if (*cmd == '\n') + { // a newline seperates commands in the buffer + cmd++; + break; + } + + if (!*cmd) + break; + + cmd = SharedParse (cmd); + if (!cmd) + break; + + m_args.AddToTail( CloneString( SharedGetToken() ) ); + } + + // and pass to the base class + return PlayerType::ClientCommand( args ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns the number of tokens in the command string + */ +template < class PlayerType > +inline int CBot< PlayerType >::Cmd_Argc() +{ + return m_args.Count(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Retrieves a specified token + */ +template < class PlayerType > +inline char * CBot< PlayerType >::Cmd_Argv( int argc ) +{ + if ( argc < 0 || argc >= m_args.Count() ) + return NULL; + return m_args[argc]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns TRUE if given entity is our enemy + */ +template < class PlayerType > +inline bool CBot< PlayerType >::IsEnemy( CBaseEntity *ent ) const +{ + // only Players (real and AI) can be enemies + if (!ent->IsPlayer()) + return false; + + // corpses are no threat + if (!ent->IsAlive()) + return false; + + CBasePlayer *player = static_cast<CBasePlayer *>( ent ); + + // if they are on our team, they are our friends + if (player->GetTeamNumber() == this->GetTeamNumber()) + return false; + + // yep, we hate 'em + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return number of enemies left alive + */ +template < class PlayerType > +inline int CBot< PlayerType >::GetEnemiesRemaining( void ) const +{ + int count = 0; + + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBaseEntity *player = UTIL_PlayerByIndex( i ); + + if (player == NULL) + continue; + + if (!IsEnemy( player )) + continue; + + if (!player->IsAlive()) + continue; + + count++; + } + + return count; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return number of friends left alive + */ +template < class PlayerType > +inline int CBot< PlayerType >::GetFriendsRemaining( void ) const +{ + int count = 0; + + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBaseEntity *player = UTIL_PlayerByIndex( i ); + + if (player == NULL) + continue; + + if (IsEnemy( player )) + continue; + + if (!player->IsAlive()) + continue; + + if (player == static_cast<CBaseEntity *>( const_cast<CBot *>( this ) )) + continue; + + count++; + } + + return count; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the local player is currently in observer mode watching this bot. + */ +template < class PlayerType > +inline bool CBot< PlayerType >::IsLocalPlayerWatchingMe( void ) const +{ + if ( engine->IsDedicatedServer() ) + return false; + + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player == NULL ) + return false; + + if ( cv_bot_debug_target.GetInt() > 0 ) + { + return this->entindex() == cv_bot_debug_target.GetInt(); + } + + if ( player->IsObserver() || !player->IsAlive() ) + { + if ( const_cast< CBot< PlayerType > * >(this) == player->GetObserverTarget() ) + { + switch( player->GetObserverMode() ) + { + case OBS_MODE_IN_EYE: + case OBS_MODE_CHASE: + return true; + } + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Output message to console if we are being watched by the local player + */ +template < class PlayerType > +inline void CBot< PlayerType >::PrintIfWatched( PRINTF_FORMAT_STRING const char *format, ... ) const +{ + if (cv_bot_debug.GetInt() == 0) + { + return; + } + + if ((IsLocalPlayerWatchingMe() && (cv_bot_debug.GetInt() == 1 || cv_bot_debug.GetInt() == 3)) || + (cv_bot_debug.GetInt() == 2 || cv_bot_debug.GetInt() == 4)) + { + va_list varg; + char buffer[ CBotManager::MAX_DBG_MSG_SIZE ]; + const char *name = const_cast< CBot< PlayerType > * >( this )->GetPlayerName(); + + va_start( varg, format ); + vsprintf( buffer, format, varg ); + va_end( varg ); + + // prefix the console message with the bot's name (this can be NULL if bot was just added) + ClientPrint( UTIL_GetListenServerHost(), + HUD_PRINTCONSOLE, + UTIL_VarArgs( "%s: %s", + (name) ? name : "(NULL netname)", buffer ) ); + + TheBots->AddDebugMessage( buffer ); + } +} + +//----------------------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------------------------------------- + +extern void InstallBotControl( void ); +extern void RemoveBotControl( void ); +extern void Bot_ServerCommand( void ); +extern void Bot_RegisterCvars( void ); + +extern bool IsSpotOccupied( CBaseEntity *me, const Vector &pos ); // if a player is at the given spot, return true +extern const Vector *FindNearbyHidingSpot( CBaseEntity *me, const Vector &pos, float maxRange = 1000.0f, bool isSniper = false, bool useNearest = false ); +extern const Vector *FindRandomHidingSpot( CBaseEntity *me, Place place, bool isSniper = false ); +extern const Vector *FindNearbyRetreatSpot( CBaseEntity *me, const Vector &start, float maxRange = 1000.0f, int avoidTeam = 0 ); + + +#endif // BOT_H diff --git a/game/shared/cstrike/bot/bot_constants.h b/game/shared/cstrike/bot/bot_constants.h new file mode 100644 index 0000000..94f5090 --- /dev/null +++ b/game/shared/cstrike/bot/bot_constants.h @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Matthew D. Campbell ([email protected]), 2003 + +#ifndef BOT_CONSTANTS_H +#define BOT_CONSTANTS_H + +/// version number is MAJOR.MINOR +#define BOT_VERSION_MAJOR 1 +#define BOT_VERSION_MINOR 50 + +//-------------------------------------------------------------------------------------------------------- +/** + * Difficulty levels + */ +enum BotDifficultyType +{ + BOT_EASY = 0, + BOT_NORMAL = 1, + BOT_HARD = 2, + BOT_EXPERT = 3, + + NUM_DIFFICULTY_LEVELS +}; + +#ifdef DEFINE_DIFFICULTY_NAMES + const char *BotDifficultyName[] = + { + "EASY", "NORMAL", "HARD", "EXPERT", NULL + }; +#else + extern const char *BotDifficultyName[]; +#endif + +#endif // BOT_CONSTANTS_H diff --git a/game/shared/cstrike/bot/bot_hide.cpp b/game/shared/cstrike/bot/bot_hide.cpp new file mode 100644 index 0000000..9a2b02c --- /dev/null +++ b/game/shared/cstrike/bot/bot_hide.cpp @@ -0,0 +1,490 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// bot_hide.cpp +// Mechanisms for using Hiding Spots in the Navigation Mesh +// Author: Michael Booth, 2003-2004 + +#include "cbase.h" +#include "bot.h" +#include "cs_nav_pathfind.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * If a player is at the given spot, return true + */ +bool IsSpotOccupied( CBaseEntity *me, const Vector &pos ) +{ + const float closeRange = 75.0f; // 50 + + // is there a player in this spot + float range; + CBasePlayer *player = UTIL_GetClosestPlayer( pos, &range ); + + if (player != me) + { + if (player && range < closeRange) + return true; + } + + // is there is a hostage in this spot + // BOTPORT: Implement hostage manager + /* + if (g_pHostages) + { + CHostage *hostage = g_pHostages->GetClosestHostage( *pos, &range ); + if (hostage && hostage != me && range < closeRange) + return true; + } + */ + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +class CollectHidingSpotsFunctor +{ +public: + CollectHidingSpotsFunctor( CBaseEntity *me, const Vector &origin, float range, int flags, Place place = UNDEFINED_PLACE ) : m_origin( origin ) + { + m_me = me; + m_count = 0; + m_range = range; + m_flags = (unsigned char)flags; + m_place = place; + m_totalWeight = 0; + } + + enum { MAX_SPOTS = 256 }; + + bool operator() ( CNavArea *area ) + { + // if a place is specified, only consider hiding spots from areas in that place + if (m_place != UNDEFINED_PLACE && area->GetPlace() != m_place) + return true; + + // collect all the hiding spots in this area + const HidingSpotVector *pSpots = area->GetHidingSpots(); + + FOR_EACH_VEC( (*pSpots), it ) + { + const HidingSpot *spot = (*pSpots)[ it ]; + + // if we've filled up, stop searching + if (m_count == MAX_SPOTS) + { + return false; + } + + // make sure hiding spot is in range + if (m_range > 0.0f) + { + if ((spot->GetPosition() - m_origin).IsLengthGreaterThan( m_range )) + { + continue; + } + } + + // if a Player is using this hiding spot, don't consider it + if (IsSpotOccupied( m_me, spot->GetPosition() )) + { + // player is in hiding spot + /// @todo Check if player is moving or sitting still + continue; + } + + if (spot->GetArea() && (spot->GetArea()->GetAttributes() & NAV_MESH_DONT_HIDE)) + { + // the area has been marked as DONT_HIDE since the last analysis, so let's ignore it + continue; + } + + // only collect hiding spots with matching flags + if (m_flags & spot->GetFlags()) + { + m_hidingSpot[ m_count ] = &spot->GetPosition(); + m_hidingSpotWeight[ m_count ] = m_totalWeight; + + // if it's an 'avoid' area, give it a low weight + if ( spot->GetArea() && ( spot->GetArea()->GetAttributes() & NAV_MESH_AVOID ) ) + { + m_totalWeight += 1; + } + else + { + m_totalWeight += 2; + } + + ++m_count; + } + } + + return (m_count < MAX_SPOTS); + } + + /** + * Remove the spot at index "i" + */ + void RemoveSpot( int i ) + { + if (m_count == 0) + return; + + for( int j=i+1; j<m_count; ++j ) + m_hidingSpot[j-1] = m_hidingSpot[j]; + + --m_count; + } + + + int GetRandomHidingSpot( void ) + { + int weight = RandomInt( 0, m_totalWeight-1 ); + for ( int i=0; i<m_count-1; ++i ) + { + // if the next spot's starting weight is over the target weight, this spot is the one + if ( m_hidingSpotWeight[i+1] >= weight ) + { + return i; + } + } + + // if we didn't find any, it's the last one + return m_count - 1; + } + + CBaseEntity *m_me; + const Vector &m_origin; + float m_range; + + const Vector *m_hidingSpot[ MAX_SPOTS ]; + int m_hidingSpotWeight[ MAX_SPOTS ]; + int m_totalWeight; + int m_count; + + unsigned char m_flags; + + Place m_place; +}; + +/** + * Do a breadth-first search to find a nearby hiding spot and return it. + * Don't pick a hiding spot that a Player is currently occupying. + * @todo Clean up this mess + */ +const Vector *FindNearbyHidingSpot( CBaseEntity *me, const Vector &pos, float maxRange, bool isSniper, bool useNearest ) +{ + CNavArea *startArea = TheNavMesh->GetNearestNavArea( pos ); + if (startArea == NULL) + return NULL; + + // collect set of nearby hiding spots + if (isSniper) + { + CollectHidingSpotsFunctor collector( me, pos, maxRange, HidingSpot::IDEAL_SNIPER_SPOT ); + SearchSurroundingAreas( startArea, pos, collector, maxRange ); + + if (collector.m_count) + { + int which = collector.GetRandomHidingSpot(); + return collector.m_hidingSpot[ which ]; + } + else + { + // no ideal sniping spots, look for "good" sniping spots + CollectHidingSpotsFunctor collector( me, pos, maxRange, HidingSpot::GOOD_SNIPER_SPOT ); + SearchSurroundingAreas( startArea, pos, collector, maxRange ); + + if (collector.m_count) + { + int which = collector.GetRandomHidingSpot(); + return collector.m_hidingSpot[ which ]; + } + + // no sniping spots at all.. fall through and pick a normal hiding spot + } + } + + // collect hiding spots with decent "cover" + CollectHidingSpotsFunctor collector( me, pos, maxRange, HidingSpot::IN_COVER ); + SearchSurroundingAreas( startArea, pos, collector, maxRange ); + + if (collector.m_count == 0) + { + // no hiding spots at all - if we're not a sniper, try to find a sniper spot to use instead + if (!isSniper) + { + return FindNearbyHidingSpot( me, pos, maxRange, true, useNearest ); + } + + return NULL; + } + + if (useNearest) + { + // return closest hiding spot + const Vector *closest = NULL; + float closeRangeSq = 9999999999.9f; + for( int i=0; i<collector.m_count; ++i ) + { + float rangeSq = (*collector.m_hidingSpot[i] - pos).LengthSqr(); + if (rangeSq < closeRangeSq) + { + closeRangeSq = rangeSq; + closest = collector.m_hidingSpot[i]; + } + } + + return closest; + } + + // select a hiding spot at random + int which = collector.GetRandomHidingSpot(); + return collector.m_hidingSpot[ which ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Select a random hiding spot among the nav areas that are tagged with the given place + */ +const Vector *FindRandomHidingSpot( CBaseEntity *me, Place place, bool isSniper ) +{ + // collect set of nearby hiding spots + if (isSniper) + { + CollectHidingSpotsFunctor collector( me, me->GetAbsOrigin(), -1.0f, HidingSpot::IDEAL_SNIPER_SPOT, place ); + TheNavMesh->ForAllAreas( collector ); + + if (collector.m_count) + { + int which = RandomInt( 0, collector.m_count-1 ); + return collector.m_hidingSpot[ which ]; + } + else + { + // no ideal sniping spots, look for "good" sniping spots + CollectHidingSpotsFunctor collector( me, me->GetAbsOrigin(), -1.0f, HidingSpot::GOOD_SNIPER_SPOT, place ); + TheNavMesh->ForAllAreas( collector ); + + if (collector.m_count) + { + int which = RandomInt( 0, collector.m_count-1 ); + return collector.m_hidingSpot[ which ]; + } + + // no sniping spots at all.. fall through and pick a normal hiding spot + } + } + + // collect hiding spots with decent "cover" + CollectHidingSpotsFunctor collector( me, me->GetAbsOrigin(), -1.0f, HidingSpot::IN_COVER, place ); + TheNavMesh->ForAllAreas( collector ); + + if (collector.m_count == 0) + return NULL; + + // select a hiding spot at random + int which = RandomInt( 0, collector.m_count-1 ); + return collector.m_hidingSpot[ which ]; +} + + +//-------------------------------------------------------------------------------------------------------------------- +/** + * Select a nearby retreat spot. + * Don't pick a hiding spot that a Player is currently occupying. + * If "avoidTeam" is nonzero, avoid getting close to members of that team. + */ +const Vector *FindNearbyRetreatSpot( CBaseEntity *me, const Vector &start, float maxRange, int avoidTeam ) +{ + CNavArea *startArea = TheNavMesh->GetNearestNavArea( start ); + if (startArea == NULL) + return NULL; + + // collect hiding spots with decent "cover" + CollectHidingSpotsFunctor collector( me, start, maxRange, HidingSpot::IN_COVER ); + SearchSurroundingAreas( startArea, start, collector, maxRange ); + + if (collector.m_count == 0) + return NULL; + + // find the closest unoccupied hiding spot that crosses the least lines of fire and has the best cover + for( int i=0; i<collector.m_count; ++i ) + { + // check if we would have to cross a line of fire to reach this hiding spot + if (IsCrossingLineOfFire( start, *collector.m_hidingSpot[i], me )) + { + collector.RemoveSpot( i ); + + // back up a step, so iteration won't skip a spot + --i; + + continue; + } + + // check if there is someone on the avoidTeam near this hiding spot + if (avoidTeam) + { + float range; + if (UTIL_GetClosestPlayer( *collector.m_hidingSpot[i], avoidTeam, &range )) + { + const float dangerRange = 150.0f; + if (range < dangerRange) + { + // there is an avoidable player too near this spot - remove it + collector.RemoveSpot( i ); + + // back up a step, so iteration won't skip a spot + --i; + + continue; + } + } + } + } + + if (collector.m_count <= 0) + return NULL; + + // all remaining spots are ok - pick one at random + int which = RandomInt( 0, collector.m_count-1 ); + return collector.m_hidingSpot[ which ]; +} + + +//-------------------------------------------------------------------------------------------------------------------- +/** + * Functor to collect all hiding spots in range that we can reach before the enemy arrives. + * NOTE: This only works for the initial rush. + */ +class CollectArriveFirstSpotsFunctor +{ +public: + CollectArriveFirstSpotsFunctor( CBaseEntity *me, const Vector &searchOrigin, float enemyArriveTime, float range, int flags ) : m_searchOrigin( searchOrigin ) + { + m_me = me; + m_count = 0; + m_range = range; + m_flags = (unsigned char)flags; + m_enemyArriveTime = enemyArriveTime; + } + + enum { MAX_SPOTS = 256 }; + + bool operator() ( CNavArea *area ) + { + // collect all the hiding spots in this area + const HidingSpotVector *pSpots = area->GetHidingSpots(); + + FOR_EACH_VEC( (*pSpots), it ) + { + const HidingSpot *spot = (*pSpots)[ it ]; + + // make sure hiding spot is in range + if (m_range > 0.0f) + { + if ((spot->GetPosition() - m_searchOrigin).IsLengthGreaterThan( m_range )) + { + continue; + } + } + + // if a Player is using this hiding spot, don't consider it + if (IsSpotOccupied( m_me, spot->GetPosition() )) + { + // player is in hiding spot + /// @todo Check if player is moving or sitting still + continue; + } + + // only collect hiding spots with matching flags + if (!(m_flags & spot->GetFlags())) + { + continue; + } + + // only collect this hiding spot if we can reach it before the enemy arrives + // NOTE: This assumes the area is fairly small and the difference of moving to the corner vs the center is small + const float settleTime = 1.0f; + if (spot->GetArea()->GetEarliestOccupyTime( m_me->GetTeamNumber() ) + settleTime < m_enemyArriveTime) + { + m_hidingSpot[ m_count++ ] = spot; + } + } + + // if we've filled up, stop searching + if (m_count == MAX_SPOTS) + return false; + + return true; + } + + CBaseEntity *m_me; + const Vector &m_searchOrigin; + + float m_range; + float m_enemyArriveTime; + unsigned char m_flags; + + const HidingSpot *m_hidingSpot[ MAX_SPOTS ]; + int m_count; +}; + + +/** + * Select a hiding spot that we can reach before the enemy arrives. + * NOTE: This only works for the initial rush. + */ +const HidingSpot *FindInitialEncounterSpot( CBaseEntity *me, const Vector &searchOrigin, float enemyArriveTime, float maxRange, bool isSniper ) +{ + CNavArea *startArea = TheNavMesh->GetNearestNavArea( searchOrigin ); + if (startArea == NULL) + return NULL; + + // collect set of nearby hiding spots + if (isSniper) + { + CollectArriveFirstSpotsFunctor collector( me, searchOrigin, enemyArriveTime, maxRange, HidingSpot::IDEAL_SNIPER_SPOT ); + SearchSurroundingAreas( startArea, searchOrigin, collector, maxRange ); + + if (collector.m_count) + { + int which = RandomInt( 0, collector.m_count-1 ); + return collector.m_hidingSpot[ which ]; + } + else + { + // no ideal sniping spots, look for "good" sniping spots + CollectArriveFirstSpotsFunctor collector( me, searchOrigin, enemyArriveTime, maxRange, HidingSpot::GOOD_SNIPER_SPOT ); + SearchSurroundingAreas( startArea, searchOrigin, collector, maxRange ); + + if (collector.m_count) + { + int which = RandomInt( 0, collector.m_count-1 ); + return collector.m_hidingSpot[ which ]; + } + + // no sniping spots at all.. fall through and pick a normal hiding spot + } + } + + // collect hiding spots with decent "cover" + CollectArriveFirstSpotsFunctor collector( me, searchOrigin, enemyArriveTime, maxRange, HidingSpot::IN_COVER | HidingSpot::EXPOSED ); + SearchSurroundingAreas( startArea, searchOrigin, collector, maxRange ); + + if (collector.m_count == 0) + return NULL; + + // select a hiding spot at random + int which = RandomInt( 0, collector.m_count-1 ); + return collector.m_hidingSpot[ which ]; +} + diff --git a/game/shared/cstrike/bot/bot_manager.cpp b/game/shared/cstrike/bot/bot_manager.cpp new file mode 100644 index 0000000..ce15823 --- /dev/null +++ b/game/shared/cstrike/bot/bot_manager.cpp @@ -0,0 +1,402 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" + +#include "bot.h" +#include "bot_manager.h" +#include "nav_area.h" +#include "bot_util.h" +#include "basegrenade_shared.h" + +#include "cs_bot.h" + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +float g_BotUpkeepInterval = 0.0f; +float g_BotUpdateInterval = 0.0f; + + +//-------------------------------------------------------------------------------------------------------------- +CBotManager::CBotManager() +{ + InitBotTrig(); +} + + +//-------------------------------------------------------------------------------------------------------------- +CBotManager::~CBotManager() +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when the round is restarting + */ +void CBotManager::RestartRound( void ) +{ + DestroyAllGrenades(); + ClearDebugMessages(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked at the start of each frame + */ +void CBotManager::StartFrame( void ) +{ + VPROF_BUDGET( "CBotManager::StartFrame", VPROF_BUDGETGROUP_NPCS ); + + ValidateActiveGrenades(); + + // debug smoke grenade visualization + if (cv_bot_debug.GetInt() == 5) + { + Vector edge, lastEdge; + + FOR_EACH_LL( m_activeGrenadeList, it ) + { + ActiveGrenade *ag = m_activeGrenadeList[ it ]; + + const Vector &pos = ag->GetDetonationPosition(); + + UTIL_DrawBeamPoints( pos, pos + Vector( 0, 0, 50 ), 1, 255, 100, 0 ); + + lastEdge = Vector( ag->GetRadius() + pos.x, pos.y, pos.z ); + float angle; + for( angle=0.0f; angle <= 180.0f; angle += 22.5f ) + { + edge.x = ag->GetRadius() * BotCOS( angle ) + pos.x; + edge.y = pos.y; + edge.z = ag->GetRadius() * BotSIN( angle ) + pos.z; + + UTIL_DrawBeamPoints( edge, lastEdge, 1, 255, 50, 0 ); + + lastEdge = edge; + } + + lastEdge = Vector( pos.x, ag->GetRadius() + pos.y, pos.z ); + for( angle=0.0f; angle <= 180.0f; angle += 22.5f ) + { + edge.x = pos.x; + edge.y = ag->GetRadius() * BotCOS( angle ) + pos.y; + edge.z = ag->GetRadius() * BotSIN( angle ) + pos.z; + + UTIL_DrawBeamPoints( edge, lastEdge, 1, 255, 50, 0 ); + + lastEdge = edge; + } + } + } + + // set frame duration + g_BotUpkeepInterval = m_frameTimer.GetElapsedTime(); + m_frameTimer.Start(); + + g_BotUpdateInterval = (g_BotUpdateSkipCount+1) * g_BotUpkeepInterval; + + // + // Process each active bot + // + for( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (!player) + continue; + + // Hack for now so the temp bot code works. The temp bots are very useful for debugging + // because they can be setup to mimic the player's usercmds. + if (player->IsBot() && IsEntityValid( player ) ) + { + // EVIL: Messes up vtables + //CBot< CBasePlayer > *bot = static_cast< CBot< CBasePlayer > * >( player ); + CCSBot *bot = dynamic_cast< CCSBot * >( player ); + + if ( bot ) + { + bot->Upkeep(); + + if (((gpGlobals->tickcount + bot->entindex()) % g_BotUpdateSkipCount) == 0) + { + bot->ResetCommand(); + bot->Update(); + } + + bot->UpdatePlayer(); + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add an active grenade to the bot's awareness + */ +void CBotManager::AddGrenade( CBaseGrenade *grenade ) +{ + ActiveGrenade *ag = new ActiveGrenade( grenade ); + m_activeGrenadeList.AddToTail( ag ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * The grenade entity in the world is going away + */ +void CBotManager::RemoveGrenade( CBaseGrenade *grenade ) +{ + FOR_EACH_LL( m_activeGrenadeList, it ) + { + ActiveGrenade *ag = m_activeGrenadeList[ it ]; + + if (ag->IsEntity( grenade )) + { + ag->OnEntityGone(); + return; + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * The grenade entity has changed its radius + */ +void CBotManager::SetGrenadeRadius( CBaseGrenade *grenade, float radius ) +{ + FOR_EACH_LL( m_activeGrenadeList, it ) + { + ActiveGrenade *ag = m_activeGrenadeList[ it ]; + + if (ag->IsEntity( grenade )) + { + ag->SetRadius( radius ); + return; + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Destroy any invalid active grenades + */ +void CBotManager::ValidateActiveGrenades( void ) +{ + int it = m_activeGrenadeList.Head(); + + while( it != m_activeGrenadeList.InvalidIndex() ) + { + ActiveGrenade *ag = m_activeGrenadeList[ it ]; + + int current = it; + it = m_activeGrenadeList.Next( it ); + + // lazy validation + if (!ag->IsValid()) + { + m_activeGrenadeList.Remove( current ); + delete ag; + continue; + } + else + { + ag->Update(); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CBotManager::DestroyAllGrenades( void ) +{ + m_activeGrenadeList.PurgeAndDeleteElements(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if position is inside a smoke cloud + */ +bool CBotManager::IsInsideSmokeCloud( const Vector *pos ) +{ + int it = m_activeGrenadeList.Head(); + + while( it != m_activeGrenadeList.InvalidIndex() ) + { + ActiveGrenade *ag = m_activeGrenadeList[ it ]; + + int current = it; + it = m_activeGrenadeList.Next( it ); + + // lazy validation + if (!ag->IsValid()) + { + m_activeGrenadeList.Remove( current ); + delete ag; + continue; + } + + if (ag->IsSmoke()) + { + const Vector &smokeOrigin = ag->GetDetonationPosition(); + + if ((smokeOrigin - *pos).IsLengthLessThan( ag->GetRadius() )) + return true; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if line intersects smoke volume + * Determine the length of the line of sight covered by each smoke cloud, + * and sum them (overlap is additive for obstruction). + * If the overlap exceeds the threshold, the bot can't see through. + */ +bool CBotManager::IsLineBlockedBySmoke( const Vector &from, const Vector &to, float grenadeBloat ) +{ + VPROF_BUDGET( "CBotManager::IsLineBlockedBySmoke", VPROF_BUDGETGROUP_NPCS ); + + float totalSmokedLength = 0.0f; // distance along line of sight covered by smoke + + // compute unit vector and length of line of sight segment + Vector sightDir = to - from; + float sightLength = sightDir.NormalizeInPlace(); + + FOR_EACH_LL( m_activeGrenadeList, it ) + { + ActiveGrenade *ag = m_activeGrenadeList[ it ]; + const float smokeRadiusSq = ag->GetRadius() * ag->GetRadius() * grenadeBloat * grenadeBloat; + + if (ag->IsSmoke()) + { + const Vector &smokeOrigin = ag->GetDetonationPosition(); + + Vector toGrenade = smokeOrigin - from; + + float alongDist = DotProduct( toGrenade, sightDir ); + + // compute closest point to grenade along line of sight ray + Vector close; + + // constrain closest point to line segment + if (alongDist < 0.0f) + close = from; + else if (alongDist >= sightLength) + close = to; + else + close = from + sightDir * alongDist; + + // if closest point is within smoke radius, the line overlaps the smoke cloud + Vector toClose = close - smokeOrigin; + float lengthSq = toClose.LengthSqr(); + + if (lengthSq < smokeRadiusSq) + { + // some portion of the ray intersects the cloud + + float fromSq = toGrenade.LengthSqr(); + float toSq = (smokeOrigin - to).LengthSqr(); + + if (fromSq < smokeRadiusSq) + { + if (toSq < smokeRadiusSq) + { + // both 'from' and 'to' lie within the cloud + // entire length is smoked + totalSmokedLength += (to - from).Length(); + } + else + { + // 'from' is inside the cloud, 'to' is outside + // compute half of total smoked length as if ray crosses entire cloud chord + float halfSmokedLength = (float)sqrt( smokeRadiusSq - lengthSq ); + + if (alongDist > 0.0f) + { + // ray goes thru 'close' + totalSmokedLength += halfSmokedLength + (close - from).Length(); + } + else + { + // ray starts after 'close' + totalSmokedLength += halfSmokedLength - (close - from).Length(); + } + + } + } + else if (toSq < smokeRadiusSq) + { + // 'from' is outside the cloud, 'to' is inside + // compute half of total smoked length as if ray crosses entire cloud chord + float halfSmokedLength = (float)sqrt( smokeRadiusSq - lengthSq ); + + Vector v = to - smokeOrigin; + if (DotProduct( v, sightDir ) > 0.0f) + { + // ray goes thru 'close' + totalSmokedLength += halfSmokedLength + (close - to).Length(); + } + else + { + // ray ends before 'close' + totalSmokedLength += halfSmokedLength - (close - to).Length(); + } + } + else + { + // 'from' and 'to' lie outside of the cloud - the line of sight completely crosses it + // determine the length of the chord that crosses the cloud + float smokedLength = 2.0f * (float)sqrt( smokeRadiusSq - lengthSq ); + + totalSmokedLength += smokedLength; + } + } + } + } + + // define how much smoke a bot can see thru + const float maxSmokedLength = 0.7f * SmokeGrenadeRadius; + + // return true if the total length of smoke-covered line-of-sight is too much + return (totalSmokedLength > maxSmokedLength); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CBotManager::ClearDebugMessages( void ) +{ + m_debugMessageCount = 0; + m_currentDebugMessage = -1; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add a new debug message to the message history + */ +void CBotManager::AddDebugMessage( const char *msg ) +{ + if (++m_currentDebugMessage >= MAX_DBG_MSGS) + { + m_currentDebugMessage = 0; + } + + if (m_debugMessageCount < MAX_DBG_MSGS) + { + ++m_debugMessageCount; + } + + Q_strncpy( m_debugMessage[ m_currentDebugMessage ].m_string, msg, MAX_DBG_MSG_SIZE ); + m_debugMessage[ m_currentDebugMessage ].m_age.Start(); +} diff --git a/game/shared/cstrike/bot/bot_manager.h b/game/shared/cstrike/bot/bot_manager.h new file mode 100644 index 0000000..b7d2bd1 --- /dev/null +++ b/game/shared/cstrike/bot/bot_manager.h @@ -0,0 +1,195 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#ifndef BASE_CONTROL_H +#define BASE_CONTROL_H + +#pragma warning( disable : 4530 ) // STL uses exceptions, but we are not compiling with them - ignore warning + +extern float g_BotUpkeepInterval; ///< duration between bot upkeeps +extern float g_BotUpdateInterval; ///< duration between bot updates +const int g_BotUpdateSkipCount = 2; ///< number of upkeep periods to skip update + +class CNavArea; + +/// TODO: move CS-specific defines into CSBot files +enum +{ + SmokeGrenadeRadius = 155, + FlashbangGrenadeRadius = 115, + HEGrenadeRadius = 115, +}; + +//-------------------------------------------------------------------------------------------------------------- +class CBaseGrenade; + +/** + * An ActiveGrenade is a representation of a grenade in the world + * NOTE: Currently only used for smoke grenade line-of-sight testing + * @todo Use system allow bots to avoid HE and Flashbangs + */ +class ActiveGrenade +{ +public: + ActiveGrenade( CBaseGrenade *grenadeEntity ); + + void OnEntityGone( void ); ///< called when the grenade in the world goes away + void Update( void ); ///< called every frame + bool IsValid( void ) const ; ///< return true if this grenade is valid + + bool IsEntity( CBaseGrenade *grenade ) const { return (grenade == m_entity) ? true : false; } + CBaseGrenade *GetEntity( void ) const { return m_entity; } + + const Vector &GetDetonationPosition( void ) const { return m_detonationPosition; } + const Vector &GetPosition( void ) const; + bool IsSmoke( void ) const { return m_isSmoke; } + bool IsFlashbang( void ) const { return m_isFlashbang; } + CBaseGrenade *GetGrenade( void ) { return m_entity; } + float GetRadius( void ) const { return m_radius; } + void SetRadius( float radius ) { m_radius = radius; } + +private: + CBaseGrenade *m_entity; ///< the entity + Vector m_detonationPosition; ///< the location where the grenade detonated (smoke) + float m_dieTimestamp; ///< time this should go away after m_entity is NULL + bool m_isSmoke; ///< true if this is a smoke grenade + bool m_isFlashbang; ///< true if this is a flashbang grenade + float m_radius; +}; + +typedef CUtlLinkedList<ActiveGrenade *> ActiveGrenadeList; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * This class manages all active bots, propagating events to them and updating them. + */ +class CBotManager +{ +public: + CBotManager(); + virtual ~CBotManager(); + + CBasePlayer *AllocateAndBindBotEntity( edict_t *ed ); ///< allocate the appropriate entity for the bot and bind it to the given edict + virtual CBasePlayer *AllocateBotEntity( void ) = 0; ///< factory method to allocate the appropriate entity for the bot + + virtual void ClientDisconnect( CBaseEntity *entity ) = 0; + virtual bool ClientCommand( CBasePlayer *player, const CCommand &args ) = 0; + + virtual void ServerActivate( void ) = 0; + virtual void ServerDeactivate( void ) = 0; + virtual bool ServerCommand( const char * pcmd ) = 0; + + virtual void RestartRound( void ); ///< (EXTEND) invoked when a new round begins + virtual void StartFrame( void ); ///< (EXTEND) called each frame + + virtual unsigned int GetPlayerPriority( CBasePlayer *player ) const = 0; ///< return priority of player (0 = max pri) + + + void AddGrenade( CBaseGrenade *grenade ); ///< add an active grenade to the bot's awareness + void RemoveGrenade( CBaseGrenade *grenade ); ///< the grenade entity in the world is going away + void SetGrenadeRadius( CBaseGrenade *grenade, float radius ); ///< the radius of the grenade entity (or associated smoke cloud) + void ValidateActiveGrenades( void ); ///< destroy any invalid active grenades + void DestroyAllGrenades( void ); + bool IsLineBlockedBySmoke( const Vector &from, const Vector &to, float grenadeBloat = 1.0f ); ///< return true if line intersects smoke volume, with grenade radius increased by the grenadeBloat factor + bool IsInsideSmokeCloud( const Vector *pos ); ///< return true if position is inside a smoke cloud + + // + // Invoke functor on all active grenades. + // If any functor call return false, return false. Otherwise, return true. + // + template < typename T > + bool ForEachGrenade( T &func ) + { + int it = m_activeGrenadeList.Head(); + + while( it != m_activeGrenadeList.InvalidIndex() ) + { + ActiveGrenade *ag = m_activeGrenadeList[ it ]; + + int current = it; + it = m_activeGrenadeList.Next( it ); + + // lazy validation + if (!ag->IsValid()) + { + m_activeGrenadeList.Remove( current ); + delete ag; + continue; + } + else + { + if (func( ag ) == false) + { + return false; + } + } + } + + return true; + } + + enum { MAX_DBG_MSG_SIZE = 1024 }; + struct DebugMessage + { + char m_string[ MAX_DBG_MSG_SIZE ]; + IntervalTimer m_age; + }; + + // debug message history ------------------------------------------------------------------------------- + int GetDebugMessageCount( void ) const; ///< get number of debug messages in history + const DebugMessage *GetDebugMessage( int which = 0 ) const; ///< return the debug message emitted by the bot (0 = most recent) + void ClearDebugMessages( void ); + void AddDebugMessage( const char *msg ); + + +private: + ActiveGrenadeList m_activeGrenadeList;///< the list of active grenades the bots are aware of + + enum { MAX_DBG_MSGS = 6 }; + DebugMessage m_debugMessage[ MAX_DBG_MSGS ]; ///< debug message history + int m_debugMessageCount; + int m_currentDebugMessage; + + IntervalTimer m_frameTimer; ///< for measuring each frame's duration +}; + + +inline CBasePlayer *CBotManager::AllocateAndBindBotEntity( edict_t *ed ) +{ + CBasePlayer::s_PlayerEdict = ed; + return AllocateBotEntity(); +} + +inline int CBotManager::GetDebugMessageCount( void ) const +{ + return m_debugMessageCount; +} + +inline const CBotManager::DebugMessage *CBotManager::GetDebugMessage( int which ) const +{ + if (which >= m_debugMessageCount) + return NULL; + + int i = m_currentDebugMessage - which; + if (i < 0) + i += MAX_DBG_MSGS; + + return &m_debugMessage[ i ]; +} + + + + + +// global singleton to create and control bots +extern CBotManager *TheBots; + + +#endif diff --git a/game/shared/cstrike/bot/bot_profile.cpp b/game/shared/cstrike/bot/bot_profile.cpp new file mode 100644 index 0000000..13da6b0 --- /dev/null +++ b/game/shared/cstrike/bot/bot_profile.cpp @@ -0,0 +1,704 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" + +#pragma warning( disable : 4530 ) // STL uses exceptions, but we are not compiling with them - ignore warning + +#define DEFINE_DIFFICULTY_NAMES +#include "bot_profile.h" +#include "shared_util.h" + +#include "bot.h" +#include "bot_util.h" +#include "cs_bot.h" // BOTPORT: Remove this CS dependency + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +BotProfileManager *TheBotProfiles = NULL; + + +//-------------------------------------------------------------------------------------------------------- +/** + * Generates a filename-decorated skin name + */ +static const char * GetDecoratedSkinName( const char *name, const char *filename ) +{ + const int BufLen = _MAX_PATH + 64; + static char buf[BufLen]; + Q_snprintf( buf, sizeof( buf ), "%s/%s", filename, name ); + return buf; +} + +//-------------------------------------------------------------------------------------------------------------- +const char* BotProfile::GetWeaponPreferenceAsString( int i ) const +{ + if ( i < 0 || i >= m_weaponPreferenceCount ) + return NULL; + + return WeaponIDToAlias( m_weaponPreference[ i ] ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this profile has a primary weapon preference + */ +bool BotProfile::HasPrimaryPreference( void ) const +{ + for( int i=0; i<m_weaponPreferenceCount; ++i ) + { + if (IsPrimaryWeapon( m_weaponPreference[i] )) + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this profile has a pistol weapon preference + */ +bool BotProfile::HasPistolPreference( void ) const +{ + for( int i=0; i<m_weaponPreferenceCount; ++i ) + if (IsSecondaryWeapon( m_weaponPreference[i] )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this profile is valid for the specified team + */ +bool BotProfile::IsValidForTeam( int team ) const +{ + return ( team == TEAM_UNASSIGNED || m_teams == TEAM_UNASSIGNED || team == m_teams ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Return true if this profile inherits from the specified template +*/ +bool BotProfile::InheritsFrom( const char *name ) const +{ + if ( WildcardMatch( name, GetName() ) ) + return true; + + for ( int i=0; i<m_templates.Count(); ++i ) + { + const BotProfile *queryTemplate = m_templates[i]; + if ( queryTemplate->InheritsFrom( name ) ) + { + return true; + } + } + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor + */ +BotProfileManager::BotProfileManager( void ) +{ + m_nextSkin = 0; + for (int i=0; i<NumCustomSkins; ++i) + { + m_skins[i] = NULL; + m_skinFilenames[i] = NULL; + m_skinModelnames[i] = NULL; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Load the bot profile database + */ +void BotProfileManager::Init( const char *filename, unsigned int *checksum ) +{ + FileHandle_t file = filesystem->Open( filename, "r" ); + + if (!file) + { + if ( true ) // UTIL_IsGame( "czero" ) ) + { + CONSOLE_ECHO( "WARNING: Cannot access bot profile database '%s'\n", filename ); + } + return; + } + + int dataLength = filesystem->Size( filename ); + char *dataPointer = new char[ dataLength ]; + int dataReadLength = filesystem->Read( dataPointer, dataLength, file ); + filesystem->Close( file ); + if ( dataReadLength > 0 ) + { + // NULL-terminate based on the length read in, since Read() can transform \r\n to \n and + // return fewer bytes than we were expecting. + dataPointer[ dataReadLength - 1 ] = 0; + } + + const char *dataFile = dataPointer; + + // compute simple checksum + if (checksum) + { + *checksum = 0; // ComputeSimpleChecksum( (const unsigned char *)dataPointer, dataLength ); + } + + BotProfile defaultProfile; + + // + // Parse the BotProfile.db into BotProfile instances + // + while( true ) + { + dataFile = SharedParse( dataFile ); + if (!dataFile) + break; + + char *token = SharedGetToken(); + + bool isDefault = (!stricmp( token, "Default" )); + bool isTemplate = (!stricmp( token, "Template" )); + bool isCustomSkin = (!stricmp( token, "Skin" )); + + if ( isCustomSkin ) + { + const int BufLen = 64; + char skinName[BufLen]; + + // get skin name + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing %s - expected skin name\n", filename ); + delete [] dataPointer; + return; + } + token = SharedGetToken(); + Q_snprintf( skinName, sizeof( skinName ), "%s", token ); + + // get attribute name + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing %s - expected 'Model'\n", filename ); + delete [] dataPointer; + return; + } + token = SharedGetToken(); + if (stricmp( "Model", token )) + { + CONSOLE_ECHO( "Error parsing %s - expected 'Model'\n", filename ); + delete [] dataPointer; + return; + } + + // eat '=' + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename ); + delete [] dataPointer; + return; + } + token = SharedGetToken(); + if (strcmp( "=", token )) + { + CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename ); + delete [] dataPointer; + return; + } + + // get attribute value + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing %s - expected attribute value\n", filename ); + delete [] dataPointer; + return; + } + token = SharedGetToken(); + + const char *decoratedName = GetDecoratedSkinName( skinName, filename ); + bool skinExists = GetCustomSkinIndex( decoratedName ) > 0; + if ( m_nextSkin < NumCustomSkins && !skinExists ) + { + // decorate the name + m_skins[ m_nextSkin ] = CloneString( decoratedName ); + + // construct the model filename + m_skinModelnames[ m_nextSkin ] = CloneString( token ); + m_skinFilenames[ m_nextSkin ] = new char[ strlen(token)*2 + strlen("models/player//.mdl") + 1 ]; + Q_snprintf( m_skinFilenames[ m_nextSkin ], sizeof( m_skinFilenames[ m_nextSkin ] ), "models/player/%s/%s.mdl", token, token ); + ++m_nextSkin; + } + + // eat 'End' + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename ); + delete [] dataPointer; + return; + } + token = SharedGetToken(); + if (strcmp( "End", token )) + { + CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename ); + delete [] dataPointer; + return; + } + + continue; // it's just a custom skin - no need to do inheritance on a bot profile, etc. + } + + // encountered a new profile + BotProfile *profile; + + if (isDefault) + { + profile = &defaultProfile; + } + else + { + profile = new BotProfile; + + // always inherit from Default + *profile = defaultProfile; + } + + // do inheritance in order of appearance + if (!isTemplate && !isDefault) + { + const BotProfile *inherit = NULL; + + // template names are separated by "+" + while(true) + { + char *c = strchr( token, '+' ); + if (c) + *c = '\000'; + + // find the given template name + FOR_EACH_LL( m_templateList, it ) + { + BotProfile *profile = m_templateList[ it ]; + if (!stricmp( profile->GetName(), token )) + { + inherit = profile; + break; + } + } + + if (inherit == NULL) + { + CONSOLE_ECHO( "Error parsing '%s' - invalid template reference '%s'\n", filename, token ); + delete [] dataPointer; + return; + } + + // inherit the data + profile->Inherit( inherit, &defaultProfile ); + + if (c == NULL) + break; + + token = c+1; + } + } + + + // get name of this profile + if (!isDefault) + { + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing '%s' - expected name\n", filename ); + delete [] dataPointer; + return; + } + profile->m_name = CloneString( SharedGetToken() ); + + /** + * HACK HACK + * Until we have a generalized means of storing bot preferences, we're going to hardcode the bot's + * preference towards silencers based on his name. + */ + if ( profile->m_name[0] % 2 ) + { + profile->m_prefersSilencer = true; + } + } + + // read attributes for this profile + bool isFirstWeaponPref = true; + while( true ) + { + // get next token + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename ); + delete [] dataPointer; + return; + } + token = SharedGetToken(); + + // check for End delimiter + if (!stricmp( token, "End" )) + break; + + // found attribute name - keep it + char attributeName[64]; + strcpy( attributeName, token ); + + // eat '=' + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename ); + delete [] dataPointer; + return; + } + + token = SharedGetToken(); + if (strcmp( "=", token )) + { + CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename ); + delete [] dataPointer; + return; + } + + // get attribute value + dataFile = SharedParse( dataFile ); + if (!dataFile) + { + CONSOLE_ECHO( "Error parsing %s - expected attribute value\n", filename ); + delete [] dataPointer; + return; + } + token = SharedGetToken(); + + // store value in appropriate attribute + if (!stricmp( "Aggression", attributeName )) + { + profile->m_aggression = (float)atof(token) / 100.0f; + } + else if (!stricmp( "Skill", attributeName )) + { + profile->m_skill = (float)atof(token) / 100.0f; + } + else if (!stricmp( "Skin", attributeName )) + { + profile->m_skin = atoi(token); + if ( profile->m_skin == 0 ) + { + // atoi() failed - try to look up a custom skin by name + profile->m_skin = GetCustomSkinIndex( token, filename ); + } + } + else if (!stricmp( "Teamwork", attributeName )) + { + profile->m_teamwork = (float)atof(token) / 100.0f; + } + else if (!stricmp( "Cost", attributeName )) + { + profile->m_cost = atoi(token); + } + else if (!stricmp( "VoicePitch", attributeName )) + { + profile->m_voicePitch = atoi(token); + } + else if (!stricmp( "VoiceBank", attributeName )) + { + profile->m_voiceBank = FindVoiceBankIndex( token ); + } + else if (!stricmp( "WeaponPreference", attributeName )) + { + // weapon preferences override parent prefs + if (isFirstWeaponPref) + { + isFirstWeaponPref = false; + profile->m_weaponPreferenceCount = 0; + } + + if (!stricmp( token, "none" )) + { + profile->m_weaponPreferenceCount = 0; + } + else + { + if (profile->m_weaponPreferenceCount < BotProfile::MAX_WEAPON_PREFS) + { + profile->m_weaponPreference[ profile->m_weaponPreferenceCount++ ] = AliasToWeaponID( token ); + } + } + } + else if (!stricmp( "ReactionTime", attributeName )) + { + profile->m_reactionTime = (float)atof(token); + +#ifndef GAMEUI_EXPORTS + // subtract off latency due to "think" update rate. + // In GameUI, we don't really care. + //profile->m_reactionTime -= g_BotUpdateInterval; +#endif + + } + else if (!stricmp( "AttackDelay", attributeName )) + { + profile->m_attackDelay = (float)atof(token); + } + else if (!stricmp( "Difficulty", attributeName )) + { + // override inheritance + profile->m_difficultyFlags = 0; + + // parse bit flags + while(true) + { + char *c = strchr( token, '+' ); + if (c) + *c = '\000'; + + for( int i=0; i<NUM_DIFFICULTY_LEVELS; ++i ) + if (!stricmp( BotDifficultyName[i], token )) + profile->m_difficultyFlags |= (1 << i); + + if (c == NULL) + break; + + token = c+1; + } + } + else if (!stricmp( "Team", attributeName )) + { + if ( !stricmp( token, "T" ) ) + { + profile->m_teams = TEAM_TERRORIST; + } + else if ( !stricmp( token, "CT" ) ) + { + profile->m_teams = TEAM_CT; + } + else + { + profile->m_teams = TEAM_UNASSIGNED; + } + } + else + { + CONSOLE_ECHO( "Error parsing %s - unknown attribute '%s'\n", filename, attributeName ); + } + } + + if (!isDefault) + { + if (isTemplate) + { + // add to template list + m_templateList.AddToTail( profile ); + } + else + { + // add profile to the master list + m_profileList.AddToTail( profile ); + } + } + } + + delete [] dataPointer; +} + +//-------------------------------------------------------------------------------------------------------------- +BotProfileManager::~BotProfileManager( void ) +{ + Reset(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Free all bot profiles + */ +void BotProfileManager::Reset( void ) +{ + m_profileList.PurgeAndDeleteElements(); + m_templateList.PurgeAndDeleteElements(); + + int i; + + for (i=0; i<NumCustomSkins; ++i) + { + if ( m_skins[i] ) + { + delete[] m_skins[i]; + m_skins[i] = NULL; + } + if ( m_skinFilenames[i] ) + { + delete[] m_skinFilenames[i]; + m_skinFilenames[i] = NULL; + } + if ( m_skinModelnames[i] ) + { + delete[] m_skinModelnames[i]; + m_skinModelnames[i] = NULL; + } + } + + for ( i=0; i<m_voiceBanks.Count(); ++i ) + { + delete[] m_voiceBanks[i]; + } + m_voiceBanks.RemoveAll(); +} + +//-------------------------------------------------------------------------------------------------------- +/** + * Returns custom skin name at a particular index + */ +const char * BotProfileManager::GetCustomSkin( int index ) +{ + if ( index < FirstCustomSkin || index > LastCustomSkin ) + { + return NULL; + } + + return m_skins[ index - FirstCustomSkin ]; +} + +//-------------------------------------------------------------------------------------------------------- +/** + * Returns custom skin filename at a particular index + */ +const char * BotProfileManager::GetCustomSkinFname( int index ) +{ + if ( index < FirstCustomSkin || index > LastCustomSkin ) + { + return NULL; + } + + return m_skinFilenames[ index - FirstCustomSkin ]; +} + +//-------------------------------------------------------------------------------------------------------- +/** + * Returns custom skin modelname at a particular index + */ +const char * BotProfileManager::GetCustomSkinModelname( int index ) +{ + if ( index < FirstCustomSkin || index > LastCustomSkin ) + { + return NULL; + } + + return m_skinModelnames[ index - FirstCustomSkin ]; +} + +//-------------------------------------------------------------------------------------------------------- +/** + * Looks up a custom skin index by filename-decorated name (will decorate the name if filename is given) + */ +int BotProfileManager::GetCustomSkinIndex( const char *name, const char *filename ) +{ + const char * skinName = name; + if ( filename ) + { + skinName = GetDecoratedSkinName( name, filename ); + } + + for (int i=0; i<NumCustomSkins; ++i) + { + if ( m_skins[i] ) + { + if ( !stricmp( skinName, m_skins[i] ) ) + { + return FirstCustomSkin + i; + } + } + } + return 0; +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * return index of the (custom) bot phrase db, inserting it if needed + */ +int BotProfileManager::FindVoiceBankIndex( const char *filename ) +{ + int index = 0; + + for ( int i=0; i<m_voiceBanks.Count(); ++i ) + { + if ( !stricmp( filename, m_voiceBanks[i] ) ) + { + return index; + } + } + + m_voiceBanks.AddToTail( CloneString( filename ) ); + return index; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return random unused profile that matches the given difficulty level + */ +const BotProfile *BotProfileManager::GetRandomProfile( BotDifficultyType difficulty, int team, CSWeaponType weaponType ) const +{ + // count up valid profiles + CUtlVector< const BotProfile * > profiles; + FOR_EACH_LL( m_profileList, it ) + { + const BotProfile *profile = m_profileList[ it ]; + + // Match difficulty + if ( !profile->IsDifficulty( difficulty ) ) + continue; + + // Prevent duplicate names + if ( UTIL_IsNameTaken( profile->GetName() ) ) + continue; + + // Match team choice + if ( !profile->IsValidForTeam( team ) ) + continue; + + // Match desired weapon + if ( weaponType != WEAPONTYPE_UNKNOWN ) + { + if ( !profile->GetWeaponPreferenceCount() ) + continue; + + if ( weaponType != WeaponClassFromWeaponID( (CSWeaponID)profile->GetWeaponPreference( 0 ) ) ) + continue; + } + + profiles.AddToTail( profile ); + } + + if ( !profiles.Count() ) + return NULL; + + // select one at random + int which = RandomInt( 0, profiles.Count()-1 ); + return profiles[which]; +} + diff --git a/game/shared/cstrike/bot/bot_profile.h b/game/shared/cstrike/bot/bot_profile.h new file mode 100644 index 0000000..172380a --- /dev/null +++ b/game/shared/cstrike/bot/bot_profile.h @@ -0,0 +1,251 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#ifndef _BOT_PROFILE_H_ +#define _BOT_PROFILE_H_ + +#pragma warning( disable : 4786 ) // long STL names get truncated in browse info. + +#include "bot_constants.h" +#include "bot_util.h" +#include "cs_weapon_parse.h" + +enum +{ + FirstCustomSkin = 100, + NumCustomSkins = 100, + LastCustomSkin = FirstCustomSkin + NumCustomSkins - 1, +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * A BotProfile describes the "personality" of a given bot + */ +class BotProfile +{ +public: + BotProfile( void ) + { + m_name = NULL; + m_aggression = 0.0f; + m_skill = 0.0f; + m_teamwork = 0.0f; + m_weaponPreferenceCount = 0; + m_cost = 0; + m_skin = 0; + m_difficultyFlags = 0; + m_voicePitch = 100; + m_reactionTime = 0.3f; + m_attackDelay = 0.0f; + m_teams = TEAM_UNASSIGNED; + m_voiceBank = 0; + m_prefersSilencer = false; + } + + ~BotProfile( void ) + { + if ( m_name ) + delete [] m_name; + } + + const char *GetName( void ) const { return m_name; } ///< return bot's name + float GetAggression( void ) const { return m_aggression; } + float GetSkill( void ) const { return m_skill; } + float GetTeamwork( void ) const { return m_teamwork; } + + CSWeaponID GetWeaponPreference( int i ) const { return m_weaponPreference[ i ]; } + const char *GetWeaponPreferenceAsString( int i ) const; + int GetWeaponPreferenceCount( void ) const { return m_weaponPreferenceCount; } + bool HasPrimaryPreference( void ) const; ///< return true if this profile has a primary weapon preference + bool HasPistolPreference( void ) const; ///< return true if this profile has a pistol weapon preference + + int GetCost( void ) const { return m_cost; } + int GetSkin( void ) const { return m_skin; } + bool IsDifficulty( BotDifficultyType diff ) const; ///< return true if this profile can be used for the given difficulty level + int GetVoicePitch( void ) const { return m_voicePitch; } + float GetReactionTime( void ) const { return m_reactionTime; } + float GetAttackDelay( void ) const { return m_attackDelay; } + int GetVoiceBank() const { return m_voiceBank; } + + bool IsValidForTeam( int team ) const; + + bool PrefersSilencer() const { return m_prefersSilencer; } + + bool InheritsFrom( const char *name ) const; + +private: + friend class BotProfileManager; ///< for loading profiles + + void Inherit( const BotProfile *parent, const BotProfile *baseline ); ///< copy values from parent if they differ from baseline + + char *m_name; ///< the bot's name + float m_aggression; ///< percentage: 0 = coward, 1 = berserker + float m_skill; ///< percentage: 0 = terrible, 1 = expert + float m_teamwork; ///< percentage: 0 = rogue, 1 = complete obeyance to team, lots of comm + + enum { MAX_WEAPON_PREFS = 16 }; + CSWeaponID m_weaponPreference[ MAX_WEAPON_PREFS ]; ///< which weapons this bot likes to use, in order of priority + int m_weaponPreferenceCount; + + int m_cost; ///< reputation point cost for career mode + int m_skin; ///< "skin" index + unsigned char m_difficultyFlags; ///< bits set correspond to difficulty levels this is valid for + int m_voicePitch; ///< the pitch shift for bot chatter (100 = normal) + float m_reactionTime; //< our reaction time in seconds + float m_attackDelay; ///< time in seconds from when we notice an enemy to when we open fire + int m_teams; ///< teams for which this profile is valid + + bool m_prefersSilencer; ///< does the bot prefer to use silencers? + + int m_voiceBank; ///< Index of the BotChatter.db voice bank this profile uses (0 is the default) + + CUtlVector< const BotProfile * > m_templates; ///< List of templates we inherit from +}; +typedef CUtlLinkedList<BotProfile *> BotProfileList; + + +inline bool BotProfile::IsDifficulty( BotDifficultyType diff ) const +{ + return (m_difficultyFlags & (1 << diff)) ? true : false; +} + +/** + * Copy in data from parent if it differs from the baseline + */ +inline void BotProfile::Inherit( const BotProfile *parent, const BotProfile *baseline ) +{ + if (parent->m_aggression != baseline->m_aggression) + m_aggression = parent->m_aggression; + + if (parent->m_skill != baseline->m_skill) + m_skill = parent->m_skill; + + if (parent->m_teamwork != baseline->m_teamwork) + m_teamwork = parent->m_teamwork; + + if (parent->m_weaponPreferenceCount != baseline->m_weaponPreferenceCount) + { + m_weaponPreferenceCount = parent->m_weaponPreferenceCount; + for( int i=0; i<parent->m_weaponPreferenceCount; ++i ) + m_weaponPreference[i] = parent->m_weaponPreference[i]; + } + + if (parent->m_cost != baseline->m_cost) + m_cost = parent->m_cost; + + if (parent->m_skin != baseline->m_skin) + m_skin = parent->m_skin; + + if (parent->m_difficultyFlags != baseline->m_difficultyFlags) + m_difficultyFlags = parent->m_difficultyFlags; + + if (parent->m_voicePitch != baseline->m_voicePitch) + m_voicePitch = parent->m_voicePitch; + + if (parent->m_reactionTime != baseline->m_reactionTime) + m_reactionTime = parent->m_reactionTime; + + if (parent->m_attackDelay != baseline->m_attackDelay) + m_attackDelay = parent->m_attackDelay; + + if (parent->m_teams != baseline->m_teams) + m_teams = parent->m_teams; + + if (parent->m_voiceBank != baseline->m_voiceBank) + m_voiceBank = parent->m_voiceBank; + + m_templates.AddToTail( parent ); +} + + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * The BotProfileManager defines the interface to accessing BotProfiles + */ +class BotProfileManager +{ +public: + BotProfileManager( void ); + ~BotProfileManager( void ); + + void Init( const char *filename, unsigned int *checksum = NULL ); + void Reset( void ); + + /// given a name, return a profile + const BotProfile *GetProfile( const char *name, int team ) const + { + FOR_EACH_LL( m_profileList, it ) + { + BotProfile *profile = m_profileList[ it ]; + + if ( !stricmp( name, profile->GetName() ) && profile->IsValidForTeam( team ) ) + return profile; + } + + return NULL; + } + + /// given a template name and difficulty, return a profile + const BotProfile *GetProfileMatchingTemplate( const char *profileName, int team, BotDifficultyType difficulty ) const + { + FOR_EACH_LL( m_profileList, it ) + { + BotProfile *profile = m_profileList[ it ]; + + if ( !profile->InheritsFrom( profileName ) ) + continue; + + if ( !profile->IsValidForTeam( team ) ) + continue; + + if ( !profile->IsDifficulty( difficulty ) ) + continue; + + if ( UTIL_IsNameTaken( profile->GetName() ) ) + continue; + + return profile; + } + + return NULL; + } + + const BotProfileList *GetProfileList( void ) const { return &m_profileList; } ///< return list of all profiles + + const BotProfile *GetRandomProfile( BotDifficultyType difficulty, int team, CSWeaponType weaponType ) const; ///< return random unused profile that matches the given difficulty level + + const char * GetCustomSkin( int index ); ///< Returns custom skin name at a particular index + const char * GetCustomSkinModelname( int index ); ///< Returns custom skin modelname at a particular index + const char * GetCustomSkinFname( int index ); ///< Returns custom skin filename at a particular index + int GetCustomSkinIndex( const char *name, const char *filename = NULL ); ///< Looks up a custom skin index by name + + typedef CUtlVector<char *> VoiceBankList; + const VoiceBankList *GetVoiceBanks( void ) const { return &m_voiceBanks; } + int FindVoiceBankIndex( const char *filename ); ///< return index of the (custom) bot phrase db, inserting it if needed + +protected: + BotProfileList m_profileList; ///< the list of all bot profiles + BotProfileList m_templateList; ///< the list of all bot templates + + VoiceBankList m_voiceBanks; + + char *m_skins[ NumCustomSkins ]; ///< Custom skin names + char *m_skinModelnames[ NumCustomSkins ]; ///< Custom skin modelnames + char *m_skinFilenames[ NumCustomSkins ]; ///< Custom skin filenames + int m_nextSkin; ///< Next custom skin to allocate +}; + +/// the global singleton for accessing BotProfiles +extern BotProfileManager *TheBotProfiles; + + +#endif // _BOT_PROFILE_H_ diff --git a/game/shared/cstrike/bot/bot_util.cpp b/game/shared/cstrike/bot/bot_util.cpp new file mode 100644 index 0000000..bd10ff2 --- /dev/null +++ b/game/shared/cstrike/bot/bot_util.cpp @@ -0,0 +1,604 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_shareddefs.h" +#include "engine/IEngineSound.h" +#include "KeyValues.h" + +#include "bot.h" +#include "bot_util.h" +#include "bot_profile.h" + +#include "cs_bot.h" +#include <ctype.h> +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static int s_iBeamSprite = 0; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if given name is already in use by another player + */ +bool UTIL_IsNameTaken( const char *name, bool ignoreHumans ) +{ + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (player->IsPlayer() && player->IsBot()) + { + // bots can have prefixes so we need to check the name + // against the profile name instead. + CCSBot *bot = dynamic_cast<CCSBot *>(player); + if ( bot && bot->GetProfile()->GetName() && FStrEq(name, bot->GetProfile()->GetName())) + { + return true; + } + } + else + { + if (!ignoreHumans) + { + if (FStrEq( name, player->GetPlayerName() )) + return true; + } + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +int UTIL_ClientsInGame( void ) +{ + int count = 0; + + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBaseEntity *player = UTIL_PlayerByIndex( i ); + + if (player == NULL) + continue; + + count++; + } + + return count; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the number of non-bots on the given team + */ +int UTIL_HumansOnTeam( int teamID, bool isAlive ) +{ + int count = 0; + + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBaseEntity *entity = UTIL_PlayerByIndex( i ); + + if ( entity == NULL ) + continue; + + CBasePlayer *player = static_cast<CBasePlayer *>( entity ); + + if (player->IsBot()) + continue; + + if (player->GetTeamNumber() != teamID) + continue; + + if (isAlive && !player->IsAlive()) + continue; + + count++; + } + + return count; +} + + +//-------------------------------------------------------------------------------------------------------------- +int UTIL_BotsInGame( void ) +{ + int count = 0; + + for (int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>(UTIL_PlayerByIndex( i )); + + if ( player == NULL ) + continue; + + if ( !player->IsBot() ) + continue; + + count++; + } + + return count; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Kick a bot from the given team. If no bot exists on the team, return false. + */ +bool UTIL_KickBotFromTeam( int kickTeam ) +{ + int i; + + // try to kick a dead bot first + for ( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (!player->IsBot()) + continue; + + if (!player->IsAlive() && player->GetTeamNumber() == kickTeam) + { + // its a bot on the right team - kick it + engine->ServerCommand( UTIL_VarArgs( "kick \"%s\"\n", player->GetPlayerName() ) ); + + return true; + } + } + + // no dead bots, kick any bot on the given team + for ( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (!player->IsBot()) + continue; + + if (player->GetTeamNumber() == kickTeam) + { + // its a bot on the right team - kick it + engine->ServerCommand( UTIL_VarArgs( "kick \"%s\"\n", player->GetPlayerName() ) ); + + return true; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if all of the members of the given team are bots + */ +bool UTIL_IsTeamAllBots( int team ) +{ + int botCount = 0; + + for( int i=1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + // skip players on other teams + if (player->GetTeamNumber() != team) + continue; + + // if not a bot, fail the test + if (!player->IsBot()) + return false; + + // is a bot on given team + ++botCount; + } + + // if team is empty, there are no bots + return (botCount) ? true : false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest active player to the given position. + * If 'distance' is non-NULL, the distance to the closest player is returned in it. + */ +extern CBasePlayer *UTIL_GetClosestPlayer( const Vector &pos, float *distance ) +{ + CBasePlayer *closePlayer = NULL; + float closeDistSq = 999999999999.9f; + + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (!IsEntityValid( player )) + continue; + + if (!player->IsAlive()) + continue; + + Vector playerOrigin = GetCentroid( player ); + float distSq = (playerOrigin - pos).LengthSqr(); + if (distSq < closeDistSq) + { + closeDistSq = distSq; + closePlayer = static_cast<CBasePlayer *>( player ); + } + } + + if (distance) + *distance = (float)sqrt( closeDistSq ); + + return closePlayer; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest active player on the given team to the given position. + * If 'distance' is non-NULL, the distance to the closest player is returned in it. + */ +extern CBasePlayer *UTIL_GetClosestPlayer( const Vector &pos, int team, float *distance ) +{ + CBasePlayer *closePlayer = NULL; + float closeDistSq = 999999999999.9f; + + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (!IsEntityValid( player )) + continue; + + if (!player->IsAlive()) + continue; + + if (player->GetTeamNumber() != team) + continue; + + Vector playerOrigin = GetCentroid( player ); + float distSq = (playerOrigin - pos).LengthSqr(); + if (distSq < closeDistSq) + { + closeDistSq = distSq; + closePlayer = static_cast<CBasePlayer *>( player ); + } + } + + if (distance) + *distance = (float)sqrt( closeDistSq ); + + return closePlayer; +} + +//-------------------------------------------------------------------------------------------------------------- +// Takes the bot pointer and constructs the net name using the current bot name prefix. +void UTIL_ConstructBotNetName( char *name, int nameLength, const BotProfile *profile ) +{ + if (profile == NULL) + { + name[0] = 0; + return; + } + + // if there is no bot prefix just use the profile name. + if ((cv_bot_prefix.GetString() == NULL) || (strlen(cv_bot_prefix.GetString()) == 0)) + { + Q_strncpy( name, profile->GetName(), nameLength ); + return; + } + + // find the highest difficulty + const char *diffStr = BotDifficultyName[0]; + for ( int i=BOT_EXPERT; i>0; --i ) + { + if ( profile->IsDifficulty( (BotDifficultyType)i ) ) + { + diffStr = BotDifficultyName[i]; + break; + } + } + + const char *weaponStr = NULL; + if ( profile->GetWeaponPreferenceCount() ) + { + weaponStr = profile->GetWeaponPreferenceAsString( 0 ); + + const char *translatedAlias = GetTranslatedWeaponAlias( weaponStr ); + + char wpnName[128]; + Q_snprintf( wpnName, sizeof( wpnName ), "weapon_%s", translatedAlias ); + WEAPON_FILE_INFO_HANDLE hWpnInfo = LookupWeaponInfoSlot( wpnName ); + if ( hWpnInfo != GetInvalidWeaponInfoHandle() ) + { + CCSWeaponInfo *pWeaponInfo = dynamic_cast< CCSWeaponInfo* >( GetFileWeaponInfoFromHandle( hWpnInfo ) ); + if ( pWeaponInfo ) + { + CSWeaponType weaponType = pWeaponInfo->m_WeaponType; + weaponStr = WeaponClassAsString( weaponType ); + } + } + } + if ( !weaponStr ) + { + weaponStr = ""; + } + + char skillStr[16]; + Q_snprintf( skillStr, sizeof( skillStr ), "%.0f", profile->GetSkill()*100 ); + + char temp[MAX_PLAYER_NAME_LENGTH*2]; + char prefix[MAX_PLAYER_NAME_LENGTH*2]; + Q_strncpy( temp, cv_bot_prefix.GetString(), sizeof( temp ) ); + Q_StrSubst( temp, "<difficulty>", diffStr, prefix, sizeof( prefix ) ); + Q_StrSubst( prefix, "<weaponclass>", weaponStr, temp, sizeof( temp ) ); + Q_StrSubst( temp, "<skill>", skillStr, prefix, sizeof( prefix ) ); + Q_snprintf( name, nameLength, "%s %s", prefix, profile->GetName() ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if anyone on the given team can see the given spot + */ +bool UTIL_IsVisibleToTeam( const Vector &spot, int team ) +{ + for( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (!player->IsAlive()) + continue; + + if (player->GetTeamNumber() != team) + continue; + + trace_t result; + UTIL_TraceLine( player->EyePosition(), spot, CONTENTS_SOLID, player, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f) + return true; + } + + return false; +} + + +//------------------------------------------------------------------------------------------------------------ +void UTIL_DrawBeamFromEnt( int i, Vector vecEnd, int iLifetime, byte bRed, byte bGreen, byte bBlue ) +{ +/* BOTPORT: What is the replacement for MESSAGE_BEGIN? + MESSAGE_BEGIN( MSG_PVS, SVC_TEMPENTITY, vecEnd ); // vecEnd = origin??? + WRITE_BYTE( TE_BEAMENTPOINT ); + WRITE_SHORT( i ); + WRITE_COORD( vecEnd.x ); + WRITE_COORD( vecEnd.y ); + WRITE_COORD( vecEnd.z ); + WRITE_SHORT( s_iBeamSprite ); + WRITE_BYTE( 0 ); // startframe + WRITE_BYTE( 0 ); // framerate + WRITE_BYTE( iLifetime ); // life + WRITE_BYTE( 10 ); // width + WRITE_BYTE( 0 ); // noise + WRITE_BYTE( bRed ); // r, g, b + WRITE_BYTE( bGreen ); // r, g, b + WRITE_BYTE( bBlue ); // r, g, b + WRITE_BYTE( 255 ); // brightness + WRITE_BYTE( 0 ); // speed + MESSAGE_END(); + */ +} + + +//------------------------------------------------------------------------------------------------------------ +void UTIL_DrawBeamPoints( Vector vecStart, Vector vecEnd, int iLifetime, byte bRed, byte bGreen, byte bBlue ) +{ + NDebugOverlay::Line( vecStart, vecEnd, bRed, bGreen, bBlue, true, 0.1f ); + + /* + MESSAGE_BEGIN( MSG_PVS, SVC_TEMPENTITY, vecStart ); + WRITE_BYTE( TE_BEAMPOINTS ); + WRITE_COORD( vecStart.x ); + WRITE_COORD( vecStart.y ); + WRITE_COORD( vecStart.z ); + WRITE_COORD( vecEnd.x ); + WRITE_COORD( vecEnd.y ); + WRITE_COORD( vecEnd.z ); + WRITE_SHORT( s_iBeamSprite ); + WRITE_BYTE( 0 ); // startframe + WRITE_BYTE( 0 ); // framerate + WRITE_BYTE( iLifetime ); // life + WRITE_BYTE( 10 ); // width + WRITE_BYTE( 0 ); // noise + WRITE_BYTE( bRed ); // r, g, b + WRITE_BYTE( bGreen ); // r, g, b + WRITE_BYTE( bBlue ); // r, g, b + WRITE_BYTE( 255 ); // brightness + WRITE_BYTE( 0 ); // speed + MESSAGE_END(); + */ +} + + +//------------------------------------------------------------------------------------------------------------ +void CONSOLE_ECHO( const char * pszMsg, ... ) +{ + va_list argptr; + static char szStr[1024]; + + va_start( argptr, pszMsg ); + vsprintf( szStr, pszMsg, argptr ); + va_end( argptr ); + + Msg( "%s", szStr ); +} + + +//------------------------------------------------------------------------------------------------------------ +void BotPrecache( void ) +{ + s_iBeamSprite = CBaseEntity::PrecacheModel( "sprites/smoke.spr" ); +} + +//------------------------------------------------------------------------------------------------------------ +#define COS_TABLE_SIZE 256 +static float cosTable[ COS_TABLE_SIZE ]; + +void InitBotTrig( void ) +{ + for( int i=0; i<COS_TABLE_SIZE; ++i ) + { + float angle = (float)(2.0f * M_PI * i / (float)(COS_TABLE_SIZE-1)); + cosTable[i] = (float)cos( angle ); + } +} + +float BotCOS( float angle ) +{ + angle = AngleNormalizePositive( angle ); + int i = (int)( angle * (COS_TABLE_SIZE-1) / 360.0f ); + return cosTable[i]; +} + +float BotSIN( float angle ) +{ + angle = AngleNormalizePositive( angle - 90 ); + int i = (int)( angle * (COS_TABLE_SIZE-1) / 360.0f ); + return cosTable[i]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Send a "hint" message to all players, dead or alive. + */ +void HintMessageToAllPlayers( const char *message ) +{ + hudtextparms_t textParms; + + textParms.x = -1.0f; + textParms.y = -1.0f; + textParms.fadeinTime = 1.0f; + textParms.fadeoutTime = 5.0f; + textParms.holdTime = 5.0f; + textParms.fxTime = 0.0f; + textParms.r1 = 100; + textParms.g1 = 255; + textParms.b1 = 100; + textParms.r2 = 255; + textParms.g2 = 255; + textParms.b2 = 255; + textParms.effect = 0; + textParms.channel = 0; + + UTIL_HudMessageAll( textParms, message ); +} + +//-------------------------------------------------------------------------------------------------------------------- +/** + * Return true if moving from "start" to "finish" will cross a player's line of fire. + * The path from "start" to "finish" is assumed to be a straight line. + * "start" and "finish" are assumed to be points on the ground. + */ +bool IsCrossingLineOfFire( const Vector &start, const Vector &finish, CBaseEntity *ignore, int ignoreTeam ) +{ + for ( int p=1; p <= gpGlobals->maxClients; ++p ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( p ) ); + + if (!IsEntityValid( player )) + continue; + + if (player == ignore) + continue; + + if (!player->IsAlive()) + continue; + + if (ignoreTeam && player->GetTeamNumber() == ignoreTeam) + continue; + + // compute player's unit aiming vector + Vector viewForward; + AngleVectors( player->EyeAngles() + player->GetPunchAngle(), &viewForward ); + + const float longRange = 5000.0f; + Vector playerOrigin = GetCentroid( player ); + Vector playerTarget = playerOrigin + longRange * viewForward; + + Vector result( 0, 0, 0 ); + if (IsIntersecting2D( start, finish, playerOrigin, playerTarget, &result )) + { + // simple check to see if intersection lies in the Z range of the path + float loZ, hiZ; + + if (start.z < finish.z) + { + loZ = start.z; + hiZ = finish.z; + } + else + { + loZ = finish.z; + hiZ = start.z; + } + + if (result.z >= loZ && result.z <= hiZ + HumanHeight) + return true; + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Performs a simple case-insensitive string comparison, honoring trailing * wildcards +*/ +bool WildcardMatch( const char *query, const char *test ) +{ + if ( !query || !test ) + return false; + + while ( *test && *query ) + { + char nameChar = *test; + char queryChar = *query; + if ( tolower(nameChar) != tolower(queryChar) ) // case-insensitive + break; + ++test; + ++query; + } + + if ( *query == 0 && *test == 0 ) + return true; + + // Support trailing * + if ( *query == '*' ) + return true; + + return false; +} + + + diff --git a/game/shared/cstrike/bot/bot_util.h b/game/shared/cstrike/bot/bot_util.h new file mode 100644 index 0000000..572b1db --- /dev/null +++ b/game/shared/cstrike/bot/bot_util.h @@ -0,0 +1,167 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef BOT_UTIL_H +#define BOT_UTIL_H + + +#include "convar.h" +#include "util.h" + +//-------------------------------------------------------------------------------------------------------------- +enum PriorityType +{ + PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH, PRIORITY_UNINTERRUPTABLE +}; + + +extern ConVar cv_bot_traceview; +extern ConVar cv_bot_stop; +extern ConVar cv_bot_show_nav; +extern ConVar cv_bot_walk; +extern ConVar cv_bot_difficulty; +extern ConVar cv_bot_debug; +extern ConVar cv_bot_debug_target; +extern ConVar cv_bot_quota; +extern ConVar cv_bot_quota_mode; +extern ConVar cv_bot_prefix; +extern ConVar cv_bot_allow_rogues; +extern ConVar cv_bot_allow_pistols; +extern ConVar cv_bot_allow_shotguns; +extern ConVar cv_bot_allow_sub_machine_guns; +extern ConVar cv_bot_allow_rifles; +extern ConVar cv_bot_allow_machine_guns; +extern ConVar cv_bot_allow_grenades; +extern ConVar cv_bot_allow_snipers; +extern ConVar cv_bot_allow_shield; +extern ConVar cv_bot_join_team; +extern ConVar cv_bot_join_after_player; +extern ConVar cv_bot_auto_vacate; +extern ConVar cv_bot_zombie; +extern ConVar cv_bot_defer_to_human; +extern ConVar cv_bot_chatter; +extern ConVar cv_bot_profile_db; +extern ConVar cv_bot_dont_shoot; +extern ConVar cv_bot_eco_limit; +extern ConVar cv_bot_auto_follow; +extern ConVar cv_bot_flipout; + +#define RAD_TO_DEG( deg ) ((deg) * 180.0 / M_PI) +#define DEG_TO_RAD( rad ) ((rad) * M_PI / 180.0) + +#define SIGN( num ) (((num) < 0) ? -1 : 1) +#define ABS( num ) (SIGN(num) * (num)) + + +#define CREATE_FAKE_CLIENT ( *g_engfuncs.pfnCreateFakeClient ) +#define GET_USERINFO ( *g_engfuncs.pfnGetInfoKeyBuffer ) +#define SET_KEY_VALUE ( *g_engfuncs.pfnSetKeyValue ) +#define SET_CLIENT_KEY_VALUE ( *g_engfuncs.pfnSetClientKeyValue ) + +class BotProfile; + +extern void BotPrecache( void ); +extern int UTIL_ClientsInGame( void ); + +extern bool UTIL_IsNameTaken( const char *name, bool ignoreHumans = false ); ///< return true if given name is already in use by another player + +#define IS_ALIVE true +extern int UTIL_HumansOnTeam( int teamID, bool isAlive = false ); + +extern int UTIL_BotsInGame( void ); +extern bool UTIL_IsTeamAllBots( int team ); +extern void UTIL_DrawBeamFromEnt( int iIndex, Vector vecEnd, int iLifetime, byte bRed, byte bGreen, byte bBlue ); +extern void UTIL_DrawBeamPoints( Vector vecStart, Vector vecEnd, int iLifetime, byte bRed, byte bGreen, byte bBlue ); +extern CBasePlayer *UTIL_GetClosestPlayer( const Vector &pos, float *distance = NULL ); +extern CBasePlayer *UTIL_GetClosestPlayer( const Vector &pos, int team, float *distance = NULL ); +extern bool UTIL_KickBotFromTeam( int kickTeam ); ///< kick a bot from the given team. If no bot exists on the team, return false. + +extern bool UTIL_IsVisibleToTeam( const Vector &spot, int team ); ///< return true if anyone on the given team can see the given spot + +/// return true if moving from "start" to "finish" will cross a player's line of fire. +extern bool IsCrossingLineOfFire( const Vector &start, const Vector &finish, CBaseEntity *ignore = NULL, int ignoreTeam = 0 ); + +extern void UTIL_ConstructBotNetName(char *name, int nameLength, const BotProfile *bot); ///< constructs a complete name including prefix + +/** + * Echos text to the console, and prints it on the client's screen. This is NOT tied to the developer cvar. + * If you are adding debugging output in cstrike, use UTIL_DPrintf() (debug.h) instead. + */ +extern void CONSOLE_ECHO( PRINTF_FORMAT_STRING const char * pszMsg, ... ); + +extern void InitBotTrig( void ); +extern float BotCOS( float angle ); +extern float BotSIN( float angle ); + +extern void HintMessageToAllPlayers( const char *message ); + +bool WildcardMatch( const char *query, const char *test ); ///< Performs a simple case-insensitive string comparison, honoring trailing * wildcards + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the given entity is valid + */ +inline bool IsEntityValid( CBaseEntity *entity ) +{ + if (entity == NULL) + return false; + + if (FNullEnt( entity->edict() )) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given two line segments: startA to endA, and startB to endB, return true if they intesect + * and put the intersection point in "result". + * Note that this computes the intersection of the 2D (x,y) projection of the line segments. + */ +inline bool IsIntersecting2D( const Vector &startA, const Vector &endA, + const Vector &startB, const Vector &endB, + Vector *result = NULL ) +{ + float denom = (endA.x - startA.x) * (endB.y - startB.y) - (endA.y - startA.y) * (endB.x - startB.x); + if (denom == 0.0f) + { + // parallel + return false; + } + + float numS = (startA.y - startB.y) * (endB.x - startB.x) - (startA.x - startB.x) * (endB.y - startB.y); + if (numS == 0.0f) + { + // coincident + return true; + } + + float numT = (startA.y - startB.y) * (endA.x - startA.x) - (startA.x - startB.x) * (endA.y - startA.y); + + float s = numS / denom; + if (s < 0.0f || s > 1.0f) + { + // intersection is not within line segment of startA to endA + return false; + } + + float t = numT / denom; + if (t < 0.0f || t > 1.0f) + { + // intersection is not within line segment of startB to endB + return false; + } + + // compute intesection point + if (result) + *result = startA + s * (endA - startA); + + return true; +} + + +#endif diff --git a/game/shared/cstrike/bot/improv_locomotor.h b/game/shared/cstrike/bot/improv_locomotor.h new file mode 100644 index 0000000..1705461 --- /dev/null +++ b/game/shared/cstrike/bot/improv_locomotor.h @@ -0,0 +1,57 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// improv_locomotor.h +// Interface for moving Improvs along computed paths +// Author: Michael Booth, July 2004 + +#ifndef _IMPROV_LOCOMOTOR_H_ +#define _IMPROV_LOCOMOTOR_H_ + +// TODO: Remove duplicate methods from CImprov, and update CImprov to use this class + +/** + * A locomotor owns the movement of an Improv + */ +class CImprovLocomotor +{ +public: + virtual const Vector &GetCentroid( void ) const = 0; + virtual const Vector &GetFeet( void ) const = 0; ///< return position of "feet" - point below centroid of improv at feet level + virtual const Vector &GetEyes( void ) const = 0; + virtual float GetMoveAngle( void ) const = 0; ///< return direction of movement + + virtual CNavArea *GetLastKnownArea( void ) const = 0; + virtual bool GetSimpleGroundHeightWithFloor( const Vector &pos, float *height, Vector *normal = NULL ) = 0; ///< find "simple" ground height, treating current nav area as part of the floor + + virtual void Crouch( void ) = 0; + virtual void StandUp( void ) = 0; ///< "un-crouch" + virtual bool IsCrouching( void ) const = 0; + + virtual void Jump( void ) = 0; ///< initiate a jump + virtual bool IsJumping( void ) const = 0; + + virtual void Run( void ) = 0; ///< set movement speed to running + virtual void Walk( void ) = 0; ///< set movement speed to walking + virtual bool IsRunning( void ) const = 0; + + virtual void StartLadder( const CNavLadder *ladder, NavTraverseType how, const Vector &approachPos, const Vector &departPos ) = 0; ///< invoked when a ladder is encountered while following a path + virtual bool TraverseLadder( const CNavLadder *ladder, NavTraverseType how, const Vector &approachPos, const Vector &departPos, float deltaT ) = 0; ///< traverse given ladder + virtual bool IsUsingLadder( void ) const = 0; + + enum MoveToFailureType + { + FAIL_INVALID_PATH, + FAIL_STUCK, + FAIL_FELL_OFF, + }; + virtual void TrackPath( const Vector &pathGoal, float deltaT ) = 0; ///< move along path by following "pathGoal" + virtual void OnMoveToSuccess( const Vector &goal ) { } ///< invoked when an improv reaches its MoveTo goal + virtual void OnMoveToFailure( const Vector &goal, MoveToFailureType reason ) { } ///< invoked when an improv fails to reach a MoveTo goal +}; + +#endif // _IMPROV_LOCOMOTOR_H_ diff --git a/game/shared/cstrike/bot/nav_path.cpp b/game/shared/cstrike/bot/nav_path.cpp new file mode 100644 index 0000000..92285bc --- /dev/null +++ b/game/shared/cstrike/bot/nav_path.cpp @@ -0,0 +1,1208 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_path.cpp +// Encapsulation of a path through space +// Author: Michael S. Booth ([email protected]), November 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "cs_player.h" + +#include "nav_mesh.h" +#include "nav_path.h" +#include "bot_util.h" +#include "improv_locomotor.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef _WIN32 +#pragma warning (disable:4701) // disable warning that variable *may* not be initialized +#endif + + +#define DrawLine( from, to, duration, red, green, blue ) NDebugOverlay::Line( from, to, red, green, blue, true, 0.1f ) + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine actual path positions + */ +bool CNavPath::ComputePathPositions( void ) +{ + if (m_segmentCount == 0) + return false; + + // 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; + + for( int i=1; i<m_segmentCount; ++i ) + { + const PathSegment *from = &m_path[ i-1 ]; + PathSegment *to = &m_path[ i ]; + + if (to->how <= GO_WEST) // walk along the floor to the next area + { + to->ladder = NULL; + + // compute next point, keeping path as straight as possible + from->area->ComputeClosestPointInPortal( to->area, (NavDirType)to->how, from->pos, &to->pos ); + + // move goal position into the goal area a bit + const float stepInDist = 5.0f; // how far to "step into" an area - must be less than min area size + AddDirectionVector( &to->pos, (NavDirType)to->how, stepInDist ); + + // 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 + if (to->area->IsConnected( from->area, NUM_DIRECTIONS ) == false) + { + // this is a "jump down" link + + // 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 pushDist = 25.0f; + to->pos.x += pushDist * dir.x; + to->pos.y += pushDist * dir.y; + + // 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 = to->pos.x + pushDist * dir.x; + m_path[i].pos.y = to->pos.y + pushDist * dir.y; + + // put this one at the bottom of the fall + m_path[i].pos.z = to->area->GetZ( m_path[i].pos ); + } + } + } + else if (to->how == GO_LADDER_UP) // to get to next area, must go up a ladder + { + // find our ladder + const NavLadderConnectList *list = from->area->GetLadderList( CSNavLadder::LADDER_UP ); + int it; + for( it = list->Head(); it != list->InvalidIndex(); it = list->Next(it)) + { + CSNavLadder *ladder = (*list)[ 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; + break; + } + } + + if (it == list->InvalidIndex()) + { + //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 NavLadderConnectList *list = from->area->GetLadderList( CSNavLadder::LADDER_DOWN ); + int it; + for( it = list->Head(); it != list->InvalidIndex(); it = list->Next(it)) + { + CSNavLadder *ladder = (*list)[ 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; + break; + } + } + + if (it == list->InvalidIndex()) + { + //PrintIfWatched( "ERROR: Can't find ladder in path\n" ); + return false; + } + } + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if position is at the end of the path + */ +bool CNavPath::IsAtEnd( const Vector &pos ) const +{ + if (!IsValid()) + return false; + + const float epsilon = 20.0f; + return (pos - GetEndpoint()).IsLengthLessThan( epsilon ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return length of path from start to finish + */ +float CNavPath::GetLength( void ) const +{ + float length = 0.0f; + for( int i=1; i<GetSegmentCount(); ++i ) + { + length += (m_path[i].pos - m_path[i-1].pos).Length(); + } + + return length; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return point a given distance along the path - if distance is out of path bounds, point is clamped to start/end + * @todo Be careful of returning "positions" along one-way drops, ladders, etc. + */ +bool CNavPath::GetPointAlongPath( float distAlong, Vector *pointOnPath ) const +{ + if (!IsValid() || pointOnPath == NULL) + return false; + + if (distAlong <= 0.0f) + { + *pointOnPath = m_path[0].pos; + return true; + } + + float lengthSoFar = 0.0f; + float segmentLength; + Vector dir; + for( int i=1; i<GetSegmentCount(); ++i ) + { + dir = m_path[i].pos - m_path[i-1].pos; + segmentLength = dir.Length(); + + if (segmentLength + lengthSoFar >= distAlong) + { + // desired point is on this segment of the path + float delta = distAlong - lengthSoFar; + float t = delta / segmentLength; + + *pointOnPath = m_path[i].pos + t * dir; + + return true; + } + + lengthSoFar += segmentLength; + } + + *pointOnPath = m_path[ GetSegmentCount()-1 ].pos; + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the node index closest to the given distance along the path without going over - returns (-1) if error + */ +int CNavPath::GetSegmentIndexAlongPath( float distAlong ) const +{ + if (!IsValid()) + return -1; + + if (distAlong <= 0.0f) + { + return 0; + } + + float lengthSoFar = 0.0f; + Vector dir; + for( int i=1; i<GetSegmentCount(); ++i ) + { + lengthSoFar += (m_path[i].pos - m_path[i-1].pos).Length(); + + if (lengthSoFar > distAlong) + { + return i-1; + } + } + + return GetSegmentCount()-1; +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute closest point on path to given point + * NOTE: This does not do line-of-sight tests, so closest point may be thru the floor, etc + */ +bool CNavPath::FindClosestPointOnPath( const Vector *worldPos, int startIndex, int endIndex, Vector *close ) const +{ + if (!IsValid() || close == NULL) + return false; + + Vector along, toWorldPos; + Vector pos; + const Vector *from, *to; + float length; + float closeLength; + float closeDistSq = 9999999999.9; + float distSq; + + for( int i=startIndex; i<=endIndex; ++i ) + { + from = &m_path[i-1].pos; + to = &m_path[i].pos; + + // compute ray along this path segment + along = *to - *from; + + // make it a unit vector along the path + length = along.NormalizeInPlace(); + + // compute vector from start of segment to our point + toWorldPos = *worldPos - *from; + + // find distance of closest point on ray + closeLength = DotProduct( toWorldPos, along ); + + // constrain point to be on path segment + if (closeLength <= 0.0f) + pos = *from; + else if (closeLength >= length) + pos = *to; + else + pos = *from + closeLength * along; + + distSq = (pos - *worldPos).LengthSqr(); + + // keep the closest point so far + if (distSq < closeDistSq) + { + closeDistSq = distSq; + *close = pos; + } + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Build trivial path when start and goal are in the same nav area + */ +bool CNavPath::BuildTrivialPath( const Vector &start, const Vector &goal ) +{ + m_segmentCount = 0; + + 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[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; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw the path for debugging. + */ +void CNavPath::Draw( const Vector &color ) +{ + if (!IsValid()) + return; + + for( int i=1; i<m_segmentCount; ++i ) + { + DrawLine( m_path[i-1].pos + Vector( 0, 0, HalfHumanHeight ), + m_path[i].pos + Vector( 0, 0, HalfHumanHeight ), 2, 255 * color.x, 255 * color.y, 255 * color.z ); + } +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * 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 CNavPath::FindNextOccludedNode( int anchor ) +{ + for( int i=anchor+1; i<m_segmentCount; ++i ) + { + // don't remove ladder nodes + if (m_path[i].ladder) + return i; + + if (!IsWalkableTraceLineClear( m_path[ anchor ].pos, m_path[ i ].pos )) + { + // cant see this node from anchor node + return i; + } + + Vector anchorPlusHalf = m_path[ anchor ].pos + Vector( 0, 0, HalfHumanHeight ); + Vector iPlusHalf = m_path[ i ].pos +Vector( 0, 0, HalfHumanHeight ); + if (!IsWalkableTraceLineClear( anchorPlusHalf, iPlusHalf) ) + { + // cant see this node from anchor node + return i; + } + + Vector anchorPlusFull = m_path[ anchor ].pos + Vector( 0, 0, HumanHeight ); + Vector iPlusFull = m_path[ i ].pos + Vector( 0, 0, HumanHeight ); + if (!IsWalkableTraceLineClear( anchorPlusFull, iPlusFull )) + { + // cant see this node from anchor node + return i; + } + } + + return m_segmentCount; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Smooth out path, removing redundant nodes + */ +void CNavPath::Optimize( void ) +{ +// DONT USE THIS: Optimizing the path results in cutting thru obstacles +return; + + if (m_segmentCount < 3) + return; + + int anchor = 0; + + while( anchor < m_segmentCount ) + { + int occluded = FindNextOccludedNode( 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; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- + +/** + * Constructor + */ +CNavPathFollower::CNavPathFollower( void ) +{ + m_improv = NULL; + m_path = NULL; + + m_segmentIndex = 0; + m_isLadderStarted = false; + + m_isDebug = false; +} + +void CNavPathFollower::Reset( void ) +{ + m_segmentIndex = 1; + m_isLadderStarted = false; + + m_stuckMonitor.Reset(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move improv along path + */ +void CNavPathFollower::Update( float deltaT, bool avoidObstacles ) +{ + if (m_path == NULL || m_path->IsValid() == false) + return; + + const CNavPath::PathSegment *node = (*m_path)[ m_segmentIndex ]; + + if (node == NULL) + { + m_improv->OnMoveToFailure( m_path->GetEndpoint(), CImprovLocomotor::FAIL_INVALID_PATH ); + m_path->Invalidate(); + return; + } + + // handle ladders + /* + if (node->ladder) + { + const Vector *approachPos = NULL; + const Vector *departPos = NULL; + + if (m_segmentIndex) + approachPos = &(*m_path)[ m_segmentIndex-1 ]->pos; + + if (m_segmentIndex < m_path->GetSegmentCount()-1) + departPos = &(*m_path)[ m_segmentIndex+1 ]->pos; + + if (!m_isLadderStarted) + { + // set up ladder movement + m_improv->StartLadder( node->ladder, node->how, approachPos, departPos ); + m_isLadderStarted = true; + } + + // move improv along ladder + if (m_improv->TraverseLadder( node->ladder, node->how, approachPos, departPos, deltaT )) + { + // completed ladder + ++m_segmentIndex; + } + return; + } + */ + + // reset ladder init flag + m_isLadderStarted = false; + + // + // Check if we reached the end of the path + // + const float closeRange = 20.0f; + if ((m_improv->GetFeet() - node->pos).IsLengthLessThan( closeRange )) + { + ++m_segmentIndex; + + if (m_segmentIndex >= m_path->GetSegmentCount()) + { + m_improv->OnMoveToSuccess( m_path->GetEndpoint() ); + m_path->Invalidate(); + return; + } + } + + + m_goal = node->pos; + + const float aheadRange = 300.0f; + m_segmentIndex = FindPathPoint( aheadRange, &m_goal, &m_behindIndex ); + if (m_segmentIndex >= m_path->GetSegmentCount()) + m_segmentIndex = m_path->GetSegmentCount()-1; + + + bool isApproachingJumpArea = false; + + // + // Crouching + // + if (!m_improv->IsUsingLadder()) + { + // because hostage crouching is not really supported by the engine, + // if we are standing in a crouch area, we must crouch to avoid collisions + if (m_improv->GetLastKnownArea() && + m_improv->GetLastKnownArea()->GetAttributes() & NAV_MESH_CROUCH && + !(m_improv->GetLastKnownArea()->GetAttributes() & NAV_MESH_JUMP)) + { + m_improv->Crouch(); + } + + // if we are approaching a crouch area, crouch + // if there are no crouch areas coming up, stand + const float crouchRange = 50.0f; + bool didCrouch = false; + for( int i=m_segmentIndex; i<m_path->GetSegmentCount(); ++i ) + { + const CNavArea *to = (*m_path)[i]->area; + + // if there is a jump area on the way to the crouch area, don't crouch as it messes up the jump + if (to->GetAttributes() & NAV_MESH_JUMP) + { + isApproachingJumpArea = true; + break; + } + + Vector close; + to->GetClosestPointOnArea( m_improv->GetCentroid(), &close ); + + if ((close - m_improv->GetFeet()).AsVector2D().IsLengthGreaterThan( crouchRange )) + break; + + if (to->GetAttributes() & NAV_MESH_CROUCH) + { + m_improv->Crouch(); + didCrouch = true; + break; + } + + } + + if (!didCrouch && !m_improv->IsJumping()) + { + // no crouch areas coming up + m_improv->StandUp(); + } + + } // end crouching logic + + + if (m_isDebug) + { + m_path->Draw(); + UTIL_DrawBeamPoints( m_improv->GetCentroid(), m_goal + Vector( 0, 0, StepHeight ), 1, 255, 0, 255 ); + UTIL_DrawBeamPoints( m_goal + Vector( 0, 0, StepHeight ), m_improv->GetCentroid(), 1, 255, 0, 255 ); + } + + // check if improv becomes stuck + m_stuckMonitor.Update( m_improv ); + + + // if improv has been stuck for too long, give up + const float giveUpTime = 2.0f; + if (m_stuckMonitor.GetDuration() > giveUpTime) + { + m_improv->OnMoveToFailure( m_path->GetEndpoint(), CImprovLocomotor::FAIL_STUCK ); + m_path->Invalidate(); + return; + } + + + // if our goal is high above us, we must have fallen + if (m_goal.z - m_improv->GetFeet().z > JumpCrouchHeight) + { + const float closeRange = 75.0f; + Vector2D to( m_improv->GetFeet().x - m_goal.x, m_improv->GetFeet().y - m_goal.y ); + if (to.IsLengthLessThan( closeRange )) + { + // we can't reach the goal position + // check if we can reach the next node, in case this was a "jump down" situation + const CNavPath::PathSegment *nextNode = (*m_path)[ m_behindIndex+1 ]; + if (m_behindIndex >=0 && nextNode) + { + if (nextNode->pos.z - m_improv->GetFeet().z > JumpCrouchHeight) + { + // the next node is too high, too - we really did fall of the path + m_improv->OnMoveToFailure( m_path->GetEndpoint(), CImprovLocomotor::FAIL_FELL_OFF ); + m_path->Invalidate(); + return; + } + } + else + { + // fell trying to get to the last node in the path + m_improv->OnMoveToFailure( m_path->GetEndpoint(), CImprovLocomotor::FAIL_FELL_OFF ); + m_path->Invalidate(); + return; + } + } + } + + + // avoid small obstacles + if (avoidObstacles && !isApproachingJumpArea && !m_improv->IsJumping() && m_segmentIndex < m_path->GetSegmentCount()-1) + { + FeelerReflexAdjustment( &m_goal ); + + // currently, this is only used for hostages, and their collision physics stinks + // do more feeler checks to avoid short obstacles + /* + const float inc = 0.25f; + for( float t = 0.5f; t < 1.0f; t += inc ) + { + FeelerReflexAdjustment( &m_goal, t * StepHeight ); + } + */ + + } + + // move improv along path + m_improv->TrackPath( m_goal, deltaT ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest point to our current position on our current path + * If "local" is true, only check the portion of the path surrounding m_pathIndex. + */ +int CNavPathFollower::FindOurPositionOnPath( Vector *close, bool local ) const +{ + if (!m_path->IsValid()) + return -1; + + Vector along, toFeet; + Vector feet = m_improv->GetFeet(); + Vector eyes = m_improv->GetEyes(); + Vector pos; + const Vector *from, *to; + float length; + float closeLength; + float closeDistSq = 9999999999.9; + int closeIndex = -1; + float distSq; + + int start, end; + + if (local) + { + start = m_segmentIndex - 3; + if (start < 1) + start = 1; + + end = m_segmentIndex + 3; + if (end > m_path->GetSegmentCount()) + end = m_path->GetSegmentCount(); + } + else + { + start = 1; + end = m_path->GetSegmentCount(); + } + + for( int i=start; i<end; ++i ) + { + from = &(*m_path)[i-1]->pos; + to = &(*m_path)[i]->pos; + + // compute ray along this path segment + along = *to - *from; + + // make it a unit vector along the path + length = along.NormalizeInPlace(); + + // compute vector from start of segment to our point + toFeet = feet - *from; + + // find distance of closest point on ray + closeLength = DotProduct( toFeet, along ); + + // constrain point to be on path segment + if (closeLength <= 0.0f) + pos = *from; + else if (closeLength >= length) + pos = *to; + else + pos = *from + closeLength * along; + + distSq = (pos - feet).LengthSqr(); + + // keep the closest point so far + if (distSq < closeDistSq) + { + // don't use points we cant see + Vector probe = pos + Vector( 0, 0, HalfHumanHeight ); + if (!IsWalkableTraceLineClear( eyes, probe, WALK_THRU_DOORS | WALK_THRU_BREAKABLES )) + continue; + + // don't use points we cant reach + //if (!IsStraightLinePathWalkable( &pos )) + // continue; + + closeDistSq = distSq; + if (close) + *close = pos; + closeIndex = i-1; + } + } + + return closeIndex; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute a point a fixed distance ahead along our path. + * Returns path index just after point. + */ +int CNavPathFollower::FindPathPoint( float aheadRange, Vector *point, int *prevIndex ) +{ + // find path index just past aheadRange + int afterIndex; + + // finds the closest point on local area of path, and returns the path index just prior to it + Vector close; + int startIndex = FindOurPositionOnPath( &close, true ); + + if (prevIndex) + *prevIndex = startIndex; + + if (startIndex <= 0) + { + // went off the end of the path + // or next point in path is unwalkable (ie: jump-down) + // keep same point + return m_segmentIndex; + } + + // if we are crouching, just follow the path exactly + if (m_improv->IsCrouching()) + { + // we want to move to the immediately next point along the path from where we are now + int index = startIndex+1; + if (index >= m_path->GetSegmentCount()) + index = m_path->GetSegmentCount()-1; + + *point = (*m_path)[ index ]->pos; + + // if we are very close to the next point in the path, skip ahead to the next one to avoid wiggling + // we must do a 2D check here, in case the goal point is floating in space due to jump down, etc + const float closeEpsilon = 20.0f; // 10 + while ((*point - close).AsVector2D().IsLengthLessThan( closeEpsilon )) + { + ++index; + + if (index >= m_path->GetSegmentCount()) + { + index = m_path->GetSegmentCount()-1; + break; + } + + *point = (*m_path)[ index ]->pos; + } + + return index; + } + + // make sure we use a node a minimum distance ahead of us, to avoid wiggling + while (startIndex < m_path->GetSegmentCount()-1) + { + Vector pos = (*m_path)[ startIndex+1 ]->pos; + + // we must do a 2D check here, in case the goal point is floating in space due to jump down, etc + const float closeEpsilon = 20.0f; + if ((pos - close).AsVector2D().IsLengthLessThan( closeEpsilon )) + { + ++startIndex; + } + else + { + break; + } + } + + // if we hit a ladder or jump area, must stop (dont use ladder behind us) + if (startIndex > m_segmentIndex && startIndex < m_path->GetSegmentCount() && + ((*m_path)[ startIndex ]->ladder || (*m_path)[ startIndex ]->area->GetAttributes() & NAV_MESH_JUMP)) + { + *point = (*m_path)[ startIndex ]->pos; + return startIndex; + } + + // we need the point just *ahead* of us + ++startIndex; + if (startIndex >= m_path->GetSegmentCount()) + startIndex = m_path->GetSegmentCount()-1; + + // if we hit a ladder or jump area, must stop + if (startIndex < m_path->GetSegmentCount() && + ((*m_path)[ startIndex ]->ladder || (*m_path)[ startIndex ]->area->GetAttributes() & NAV_MESH_JUMP)) + { + *point = (*m_path)[ startIndex ]->pos; + return startIndex; + } + + // note direction of path segment we are standing on + Vector initDir = (*m_path)[ startIndex ]->pos - (*m_path)[ startIndex-1 ]->pos; + initDir.NormalizeInPlace(); + + Vector feet = m_improv->GetFeet(); + Vector eyes = m_improv->GetEyes(); + float rangeSoFar = 0; + + // this flag is true if our ahead point is visible + bool visible = true; + + Vector prevDir = initDir; + + // step along the path until we pass aheadRange + bool isCorner = false; + int i; + for( i=startIndex; i<m_path->GetSegmentCount(); ++i ) + { + Vector pos = (*m_path)[i]->pos; + Vector to = pos - (*m_path)[i-1]->pos; + Vector dir = to; + dir.NormalizeInPlace(); + + // don't allow path to double-back from our starting direction (going upstairs, down curved passages, etc) + if (DotProduct( dir, initDir ) < 0.0f) // -0.25f + { + --i; + break; + } + + // if the path turns a corner, we want to move towards the corner, not into the wall/stairs/etc + if (DotProduct( dir, prevDir ) < 0.5f) + { + isCorner = true; + --i; + break; + } + prevDir = dir; + + // don't use points we cant see + Vector probe = pos + Vector( 0, 0, HalfHumanHeight ); + if (!IsWalkableTraceLineClear( eyes, probe, WALK_THRU_BREAKABLES )) + { + // presumably, the previous point is visible, so we will interpolate + visible = false; + break; + } + + // if we encounter a ladder or jump area, we must stop + if (i < m_path->GetSegmentCount() && + ((*m_path)[ i ]->ladder || (*m_path)[ i ]->area->GetAttributes() & NAV_MESH_JUMP)) + break; + + // Check straight-line path from our current position to this position + // Test for un-jumpable height change, or unrecoverable fall + //if (!IsStraightLinePathWalkable( &pos )) + //{ + // --i; + // break; + //} + + Vector along = (i == startIndex) ? (pos - feet) : (pos - (*m_path)[i-1]->pos); + rangeSoFar += along.Length2D(); + + // stop if we have gone farther than aheadRange + if (rangeSoFar >= aheadRange) + break; + } + + if (i < startIndex) + afterIndex = startIndex; + else if (i < m_path->GetSegmentCount()) + afterIndex = i; + else + afterIndex = m_path->GetSegmentCount()-1; + + + // compute point on the path at aheadRange + if (afterIndex == 0) + { + *point = (*m_path)[0]->pos; + } + else + { + // interpolate point along path segment + const Vector *afterPoint = &(*m_path)[ afterIndex ]->pos; + const Vector *beforePoint = &(*m_path)[ afterIndex-1 ]->pos; + + Vector to = *afterPoint - *beforePoint; + float length = to.Length2D(); + + float t = 1.0f - ((rangeSoFar - aheadRange) / length); + + if (t < 0.0f) + t = 0.0f; + else if (t > 1.0f) + t = 1.0f; + + *point = *beforePoint + t * to; + + // if afterPoint wasn't visible, slide point backwards towards beforePoint until it is + if (!visible) + { + const float sightStepSize = 25.0f; + float dt = sightStepSize / length; + + Vector probe = *point + Vector( 0, 0, HalfHumanHeight ); + while( t > 0.0f && !IsWalkableTraceLineClear( eyes, probe, WALK_THRU_BREAKABLES ) ) + { + t -= dt; + *point = *beforePoint + t * to; + } + + if (t <= 0.0f) + *point = *beforePoint; + } + } + + // if position found is too close to us, or behind us, force it farther down the path so we don't stop and wiggle + if (!isCorner) + { + const float epsilon = 50.0f; + Vector2D toPoint; + Vector2D centroid( m_improv->GetCentroid().x, m_improv->GetCentroid().y ); + + toPoint.x = point->x - centroid.x; + toPoint.y = point->y - centroid.y; + + if (DotProduct2D( toPoint, initDir.AsVector2D() ) < 0.0f || toPoint.IsLengthLessThan( epsilon )) + { + int i; + for( i=startIndex; i<m_path->GetSegmentCount(); ++i ) + { + toPoint.x = (*m_path)[i]->pos.x - centroid.x; + toPoint.y = (*m_path)[i]->pos.y - centroid.y; + if ((*m_path)[i]->ladder || (*m_path)[i]->area->GetAttributes() & NAV_MESH_JUMP || toPoint.IsLengthGreaterThan( epsilon )) + { + *point = (*m_path)[i]->pos; + startIndex = i; + break; + } + } + + if (i == m_path->GetSegmentCount()) + { + *point = m_path->GetEndpoint(); + startIndex = m_path->GetSegmentCount()-1; + } + } + } + + // m_pathIndex should always be the next point on the path, even if we're not moving directly towards it + if (startIndex < m_path->GetSegmentCount()) + return startIndex; + + return m_path->GetSegmentCount()-1; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do reflex avoidance movements if our "feelers" are touched + * @todo Parameterize feeler spacing + */ +void CNavPathFollower::FeelerReflexAdjustment( Vector *goalPosition, float height ) +{ + // if we are in a "precise" area, do not do feeler adjustments + if (m_improv->GetLastKnownArea() && m_improv->GetLastKnownArea()->GetAttributes() & NAV_MESH_PRECISE) + return; + + // use the direction towards the goal + Vector dir = *goalPosition - m_improv->GetFeet(); + dir.z = 0.0f; + dir.NormalizeInPlace(); + + Vector lat( -dir.y, dir.x, 0.0f ); + + const float feelerOffset = (m_improv->IsCrouching()) ? 15.0f : 20.0f; // 15, 20 + const float feelerLengthRun = 50.0f; // 100 - too long for tight hallways (cs_747) + const float feelerLengthWalk = 30.0f; + + const float feelerHeight = (height > 0.0f) ? height : StepHeight + 0.1f; // if obstacle is lower than StepHeight, we'll walk right over it + + float feelerLength = (m_improv->IsRunning()) ? feelerLengthRun : feelerLengthWalk; + + feelerLength = (m_improv->IsCrouching()) ? 20.0f : feelerLength; + + // + // Feelers must follow floor slope + // + float ground; + Vector normal; + if (m_improv->GetSimpleGroundHeightWithFloor( m_improv->GetEyes(), &ground, &normal ) == false) + return; + + // get forward vector along floor + dir = CrossProduct( lat, normal ); + + // correct the sideways vector + lat = CrossProduct( dir, normal ); + + + Vector feet = m_improv->GetFeet(); + feet.z += feelerHeight; + + Vector from = feet + feelerOffset * lat; + Vector to = from + feelerLength * dir; + + bool leftClear = IsWalkableTraceLineClear( from, to, WALK_THRU_DOORS | WALK_THRU_BREAKABLES ); + + // draw debug beams + if (m_isDebug) + { + if (leftClear) + UTIL_DrawBeamPoints( from, to, 1, 0, 255, 0 ); + else + UTIL_DrawBeamPoints( from, to, 1, 255, 0, 0 ); + } + + from = feet - feelerOffset * lat; + to = from + feelerLength * dir; + + bool rightClear = IsWalkableTraceLineClear( from, to, WALK_THRU_DOORS | WALK_THRU_BREAKABLES ); + + // draw debug beams + if (m_isDebug) + { + if (rightClear) + UTIL_DrawBeamPoints( from, to, 1, 0, 255, 0 ); + else + UTIL_DrawBeamPoints( from, to, 1, 255, 0, 0 ); + } + + + + const float avoidRange = (m_improv->IsCrouching()) ? 150.0f : 300.0f; + + if (!rightClear) + { + if (leftClear) + { + // right hit, left clear - veer left + *goalPosition = *goalPosition + avoidRange * lat; + //*goalPosition = m_improv->GetFeet() + avoidRange * lat; + + //m_improv->StrafeLeft(); + } + } + else if (!leftClear) + { + // right clear, left hit - veer right + *goalPosition = *goalPosition - avoidRange * lat; + //*goalPosition = m_improv->GetFeet() - avoidRange * lat; + + //m_improv->StrafeRight(); + } + +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset the stuck-checker. + */ +CStuckMonitor::CStuckMonitor( void ) +{ + m_isStuck = false; + m_avgVelIndex = 0; + m_avgVelCount = 0; +} + +/** + * Reset the stuck-checker. + */ +void CStuckMonitor::Reset( void ) +{ + m_isStuck = false; + m_avgVelIndex = 0; + m_avgVelCount = 0; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Test if the improv has become stuck + */ +void CStuckMonitor::Update( CImprovLocomotor *improv ) +{ + if (m_isStuck) + { + // improv is stuck - see if it has moved far enough to be considered unstuck + const float unstuckRange = 75.0f; + if ((improv->GetCentroid() - m_stuckSpot).IsLengthGreaterThan( unstuckRange )) + { + // no longer stuck + Reset(); + //PrintIfWatched( "UN-STUCK\n" ); + } + } + else + { + // check if improv has become stuck + + // compute average velocity over a short period (for stuck check) + Vector vel = improv->GetCentroid() - m_lastCentroid; + + // if we are jumping, ignore Z + //if (improv->IsJumping()) + // vel.z = 0.0f; + + // ignore Z unless we are on a ladder (which is only Z) + if (!improv->IsUsingLadder()) + vel.z = 0.0f; + + // cannot be Length2D, or will break ladder movement (they are only Z) + float moveDist = vel.Length(); + + float deltaT = gpGlobals->curtime - m_lastTime; + if (deltaT <= 0.0f) + return; + + m_lastTime = gpGlobals->curtime; + + // compute current velocity + m_avgVel[ m_avgVelIndex++ ] = moveDist/deltaT; + + if (m_avgVelIndex == MAX_VEL_SAMPLES) + m_avgVelIndex = 0; + + if (m_avgVelCount < MAX_VEL_SAMPLES) + { + ++m_avgVelCount; + } + else + { + // we have enough samples to know if we're stuck + + float avgVel = 0.0f; + for( int t=0; t<m_avgVelCount; ++t ) + avgVel += m_avgVel[t]; + + avgVel /= m_avgVelCount; + + // cannot make this velocity too high, or actors will get "stuck" when going down ladders + float stuckVel = (improv->IsUsingLadder()) ? 10.0f : 20.0f; + + if (avgVel < stuckVel) + { + // note when and where we initially become stuck + m_stuckTimer.Start(); + m_stuckSpot = improv->GetCentroid(); + m_isStuck = true; + } + } + } + + // always need to track this + m_lastCentroid = improv->GetCentroid(); +} + diff --git a/game/shared/cstrike/bot/nav_path.h b/game/shared/cstrike/bot/nav_path.h new file mode 100644 index 0000000..2542b84 --- /dev/null +++ b/game/shared/cstrike/bot/nav_path.h @@ -0,0 +1,246 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_path.h +// Navigation Path encapsulation +// Author: Michael S. Booth ([email protected]), November 2003 + +#ifndef _NAV_PATH_H_ +#define _NAV_PATH_H_ + +#include "cs_nav_area.h" +#include "bot_util.h" + +class CImprovLocomotor; + +//-------------------------------------------------------------------------------------------------------- +/** + * The CNavPath class encapsulates a path through space + */ +class CNavPath +{ +public: + CNavPath( void ) + { + m_segmentCount = 0; + } + + struct PathSegment + { + 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 + }; + + const PathSegment * operator[] ( int i ) const { return (i >= 0 && i < m_segmentCount) ? &m_path[i] : NULL; } + const PathSegment *GetSegment( int i ) const { return (i >= 0 && i < m_segmentCount) ? &m_path[i] : NULL; } + int GetSegmentCount( void ) const { return m_segmentCount; } + const Vector &GetEndpoint( void ) const { return m_path[ m_segmentCount-1 ].pos; } + bool IsAtEnd( const Vector &pos ) const; ///< return true if position is at the end of the path + + float GetLength( void ) const; ///< return length of path from start to finish + bool GetPointAlongPath( float distAlong, Vector *pointOnPath ) const; ///< return point a given distance along the path - if distance is out of path bounds, point is clamped to start/end + + /// return the node index closest to the given distance along the path without going over - returns (-1) if error + int GetSegmentIndexAlongPath( float distAlong ) const; + + bool IsValid( void ) const { return (m_segmentCount > 0); } + void Invalidate( void ) { m_segmentCount = 0; } + + void Draw( const Vector &color = Vector( 1.0f, 0.3f, 0 ) ); ///< draw the path for debugging + + /// compute closest point on path to given point + bool FindClosestPointOnPath( const Vector *worldPos, int startIndex, int endIndex, Vector *close ) const; + + void Optimize( void ); + + /** + * Compute shortest path from 'start' to 'goal' via A* algorithm. + * If returns true, path was build 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( const Vector &start, const Vector &goal, CostFunctor &costFunc ) + { + Invalidate(); + + CNavArea *startArea = TheNavMesh->GetNearestNavArea( start + Vector( 0.0f, 0.0f, 1.0f ) ); + if (startArea == NULL) + { + return false; + } + + CNavArea *goalArea = TheNavMesh->GetNavArea( goal ); + + // if we are already in the goal area, build trivial path + if (startArea == goalArea) + { + BuildTrivialPath( start, 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; + bool pathResult = NavAreaBuildPath( startArea, goalArea, &goal, costFunc, &closestArea ); + + // + // Build path by following parent links + // + + // get count + int count = 0; + CNavArea *area; + for( area = closestArea; area; area = area->GetParent() ) + { + ++count; + } + + // save room for endpoint + if (count > MAX_PATH_SEGMENTS-1) + { + count = MAX_PATH_SEGMENTS-1; + } + + if (count == 0) + { + return false; + } + + if (count == 1) + { + BuildTrivialPath( start, goal ); + return true; + } + + // build path + m_segmentCount = count; + for( area = closestArea; count && area; area = area->GetParent() ) + { + --count; + m_path[ count ].area = area; + m_path[ count ].how = area->GetParentHow(); + } + + // compute path positions + if (ComputePathPositions() == false) + { + //PrintIfWatched( "CNavPath::Compute: Error building path\n" ); + Invalidate(); + return false; + } + + // append path end 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_segmentCount; + + return pathResult; + } + +private: + enum { MAX_PATH_SEGMENTS = 256 }; + PathSegment m_path[ MAX_PATH_SEGMENTS ]; + int m_segmentCount; + + bool ComputePathPositions( void ); ///< determine actual path positions + bool BuildTrivialPath( const Vector &start, const Vector &goal ); ///< utility function for when start and goal are in the same area + + int FindNextOccludedNode( int anchor ); ///< used by Optimize() +}; + +//-------------------------------------------------------------------------------------------------------- +/** + * Monitor improv movement and determine if it becomes stuck + */ +class CStuckMonitor +{ +public: + CStuckMonitor( void ); + + void Reset( void ); + void Update( CImprovLocomotor *improv ); + bool IsStuck( void ) const { return m_isStuck; } + + float GetDuration( void ) const { return (m_isStuck) ? m_stuckTimer.GetElapsedTime() : 0.0f; } + +private: + bool m_isStuck; ///< if true, we are stuck + Vector m_stuckSpot; ///< the location where we became stuck + IntervalTimer m_stuckTimer; ///< how long we have been stuck + + enum { MAX_VEL_SAMPLES = 5 }; + float m_avgVel[ MAX_VEL_SAMPLES ]; + int m_avgVelIndex; + int m_avgVelCount; + Vector m_lastCentroid; + float m_lastTime; +}; + +//-------------------------------------------------------------------------------------------------------- +/** + * The CNavPathFollower class implements path following behavior + */ +class CNavPathFollower +{ +public: + CNavPathFollower( void ); + + void SetImprov( CImprovLocomotor *improv ) { m_improv = improv; } + void SetPath( CNavPath *path ) { m_path = path; } + + void Reset( void ); + + #define DONT_AVOID_OBSTACLES false + void Update( float deltaT, bool avoidObstacles = true ); ///< move improv along path + void Debug( bool status ) { m_isDebug = status; } ///< turn debugging on/off + + bool IsStuck( void ) const { return m_stuckMonitor.IsStuck(); } ///< return true if improv is stuck + void ResetStuck( void ) { m_stuckMonitor.Reset(); } + float GetStuckDuration( void ) const { return m_stuckMonitor.GetDuration(); } ///< return how long we've been stuck + + void FeelerReflexAdjustment( Vector *goalPosition, float height = -1.0f ); ///< adjust goal position if "feelers" are touched + +private: + CImprovLocomotor *m_improv; ///< who is doing the path following + + CNavPath *m_path; ///< the path being followed + + int m_segmentIndex; ///< the point on the path the improv is moving towards + int m_behindIndex; ///< index of the node on the path just behind us + Vector m_goal; ///< last computed follow goal + + bool m_isLadderStarted; + + bool m_isDebug; + + int FindOurPositionOnPath( Vector *close, bool local ) const; ///< return the closest point to our current position on current path + int FindPathPoint( float aheadRange, Vector *point, int *prevIndex ); ///< compute a point a fixed distance ahead along our path. + + CStuckMonitor m_stuckMonitor; +}; + + + +#endif // _NAV_PATH_H_ + diff --git a/game/shared/cstrike/bot/shared_util.cpp b/game/shared/cstrike/bot/shared_util.cpp new file mode 100644 index 0000000..4d66845 --- /dev/null +++ b/game/shared/cstrike/bot/shared_util.cpp @@ -0,0 +1,207 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: dll-agnostic routines (no dll dependencies here) +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Matthew D. Campbell ([email protected]), 2003 + +#include "cbase.h" + +#include <ctype.h> +#include "shared_util.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static char s_shared_token[ 1500 ]; +static char s_shared_quote = '\"'; + +//-------------------------------------------------------------------------------------------------------------- +char * SharedVarArgs(const char *format, ...) +{ + va_list argptr; + const int BufLen = 1024; + const int NumBuffers = 4; + static char string[NumBuffers][BufLen]; + static int curstring = 0; + + curstring = ( curstring + 1 ) % NumBuffers; + + va_start (argptr, format); + V_vsprintf_safe( string[curstring], format, argptr ); + va_end (argptr); + + return string[curstring]; +} + +//-------------------------------------------------------------------------------------------------------------- +char * BufPrintf(char *buf, int& len, const char *fmt, ...) +{ + if (len <= 0) + return NULL; + + va_list argptr; + + va_start(argptr, fmt); + _vsnprintf(buf, len, fmt, argptr); + buf[ len - 1 ] = 0; + va_end(argptr); + + len -= strlen(buf); + return buf + strlen(buf); +} + +//-------------------------------------------------------------------------------------------------------------- +wchar_t * BufWPrintf(wchar_t *buf, int& len, const wchar_t *fmt, ...) +{ + if (len <= 0) + return NULL; + + va_list argptr; + + va_start(argptr, fmt); +#ifdef WIN32 + _vsnwprintf(buf, len, fmt, argptr); +#else + vswprintf( buf, len, fmt, argptr ); +#endif + buf[ len - 1 ] = 0; + va_end(argptr); + + len -= wcslen(buf); + return buf + wcslen(buf); +} + +//-------------------------------------------------------------------------------------------------------------- +const wchar_t * NumAsWString( int val ) +{ + const int BufLen = 16; + static wchar_t buf[BufLen]; + int len = BufLen; + BufWPrintf( buf, len, L"%d", val ); + return buf; +} + +//-------------------------------------------------------------------------------------------------------------- +const char * NumAsString( int val ) +{ + const int BufLen = 16; + static char buf[BufLen]; + int len = BufLen; + BufPrintf( buf, len, "%d", val ); + return buf; +} + +//-------------------------------------------------------------------------------------------------------- +/** + * Returns the token parsed by SharedParse() + */ +char *SharedGetToken( void ) +{ + return s_shared_token; +} + +//-------------------------------------------------------------------------------------------------------- +/** + * Returns the token parsed by SharedParse() + */ +void SharedSetQuoteChar( char c ) +{ + s_shared_quote = c; +} + +//-------------------------------------------------------------------------------------------------------- +/** + * Parse a token out of a string + */ +const char *SharedParse( const char *data ) +{ + int c; + int len; + + len = 0; + s_shared_token[0] = 0; + + if (!data) + return NULL; + +// skip whitespace +skipwhite: + while ( (c = *data) <= ' ') + { + if (c == 0) + return NULL; // end of file; + data++; + } + +// skip // comments + if (c=='/' && data[1] == '/') + { + while (*data && *data != '\n') + data++; + goto skipwhite; + } + + +// handle quoted strings specially + if (c == s_shared_quote) + { + data++; + while (1) + { + c = *data++; + if (c==s_shared_quote || !c) + { + s_shared_token[len] = 0; + return data; + } + s_shared_token[len] = c; + len++; + } + } + +// parse single characters + if (c=='{' || c=='}'|| c==')'|| c=='(' || c=='\'' || c == ',' ) + { + s_shared_token[len] = c; + len++; + s_shared_token[len] = 0; + return data+1; + } + +// parse a regular word + do + { + s_shared_token[len] = c; + data++; + len++; + c = *data; + if (c=='{' || c=='}'|| c==')'|| c=='(' || c=='\'' || c == ',' ) + break; + } while (c>32); + + s_shared_token[len] = 0; + return data; +} + +//-------------------------------------------------------------------------------------------------------- +/** + * Returns true if additional data is waiting to be processed on this line + */ +bool SharedTokenWaiting( const char *buffer ) +{ + const char *p; + + p = buffer; + while ( *p && *p!='\n') + { + if ( !isspace( *p ) || isalnum( *p ) ) + return true; + + p++; + } + + return false; +} diff --git a/game/shared/cstrike/bot/shared_util.h b/game/shared/cstrike/bot/shared_util.h new file mode 100644 index 0000000..664dcd7 --- /dev/null +++ b/game/shared/cstrike/bot/shared_util.h @@ -0,0 +1,83 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: dll-agnostic routines (no dll dependencies here) +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Matthew D. Campbell ([email protected]), 2003 + +#ifndef SHARED_UTIL_H +#define SHARED_UTIL_H + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------- +/** + * Returns the token parsed by SharedParse() + */ +char *SharedGetToken( void ); + +//-------------------------------------------------------------------------------------------------------- +/** + * Sets the character used to delimit quoted strings. Default is '\"'. Be sure to set it back when done. + */ +void SharedSetQuoteChar( char c ); + +//-------------------------------------------------------------------------------------------------------- +/** + * Parse a token out of a string + */ +const char *SharedParse( const char *data ); + +//-------------------------------------------------------------------------------------------------------- +/** + * Returns true if additional data is waiting to be processed on this line + */ +bool SharedTokenWaiting( const char *buffer ); + +//-------------------------------------------------------------------------------------------------------- +/** + * Simple utility function to allocate memory and duplicate a string + */ +inline char *CloneString( const char *str ) +{ + char *cloneStr = new char [ strlen(str)+1 ]; + strcpy( cloneStr, str ); + return cloneStr; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * snprintf-alike that allows multiple prints into a buffer + */ +char * BufPrintf(char *buf, int& len, PRINTF_FORMAT_STRING const char *fmt, ...); + +//-------------------------------------------------------------------------------------------------------------- +/** + * wide char version of BufPrintf + */ +wchar_t * BufWPrintf(wchar_t *buf, int& len, PRINTF_FORMAT_STRING const wchar_t *fmt, ...); + +//-------------------------------------------------------------------------------------------------------------- +/** + * convenience function that prints an int into a static wchar_t* + */ +const wchar_t * NumAsWString( int val ); + +//-------------------------------------------------------------------------------------------------------------- +/** + * convenience function that prints an int into a static char* + */ +const char * NumAsString( int val ); + +//-------------------------------------------------------------------------------------------------------------- +/** + * convenience function that composes a string into a static char* + */ +char * SharedVarArgs(PRINTF_FORMAT_STRING const char *format, ...); + +#include "tier0/memdbgoff.h" + +#endif // SHARED_UTIL_H |